Ranged Way Idle

死亡提醒、强制刷新MWITools的价格、私信提醒音、自动任务排序、显示购买预付金/出售可获金/待领取金额、显示任务价值、默哀法师助手

Від 17.05.2025. Дивіться остання версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Ranged Way Idle
// @namespace    http://tampermonkey.net/
// @version      1.11
// @description  死亡提醒、强制刷新MWITools的价格、私信提醒音、自动任务排序、显示购买预付金/出售可获金/待领取金额、显示任务价值、默哀法师助手
// @author       AlphB
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @grant        GM_notification
// @grant        GM_getValue
// @grant        GM_setValue
// @icon         https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
// @grant        none
// @license      CC-BY-NC-SA-4.0
// ==/UserScript==

(function () {
    const config = {
        notifyDeath: true,
        forceUpdateMarketPrice: true,
        notifyWhisperMessages: false,
        listenKeywordMessages: false,
        autoTaskSort: true,
        showMarketListingsFunds: true,
        mournForMagicWayIdle: true,
        showTaskValue: true,
        keywords: [],
    }
    const globalVariable = {
        battleData: {
            players: null
        },
        itemDetailMap: JSON.parse(localStorage.getItem("initClientData")).itemDetailMap,
        whisperAudio: new Audio(`https://upload.thbwiki.cc/d/d1/se_bonus2.mp3`),
        keywordAudio: new Audio(`https://upload.thbwiki.cc/c/c9/se_pldead00.mp3`),
        market: {
            hasFundsElement: false,
            sellValue: null,
            buyValue: null,
            unclaimedValue: null,
            sellListings: null,
            buyListings: null
        },
        task: {
            taskListElement: null,
            taskTokenValueData: null,
            hasTaskValueElement: false,
            taskValueElements: [],
            tokenValue: {
                Bid: null,
                Ask: null
            }
        }
    };


    init();

    function init() {
        // readConfig();
        if (!('Edible_Tools' in localStorage)) {
            config.showTaskValue = false;
        }
        globalVariable.whisperAudio.volume = 0.4;
        globalVariable.keywordAudio.volume = 0.4;
        let observer = new MutationObserver(function () {
            if (config.showMarketListingsFunds) showMarketListingsFunds();
            if (config.autoTaskSort) autoClickTaskSortButton();
            if (config.showTaskValue) showTaskValue();
        });
        observer.observe(document, {childList: true, subtree: true});
        const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
        const oriGet = dataProperty.get;
        dataProperty.get = hookedGet;
        Object.defineProperty(MessageEvent.prototype, "data", dataProperty);


        globalVariable.task.taskTokenValueData = getTaskTokenValue();

        if (config.mournForMagicWayIdle) {
            console.log("为法师助手默哀");
        }

        function hookedGet() {
            const socket = this.currentTarget;
            if (!(socket instanceof WebSocket)) {
                return oriGet.call(this);
            }
            if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) {
                return oriGet.call(this);
            }
            const message = oriGet.call(this);
            Object.defineProperty(this, "data", {value: message});
            return handleMessage(message);
        }

        // function readConfig() {
        //     const localConfig = localStorage.getItem("ranged_way_idle_config");
        //     if (localConfig) {
        //         const localConfigObj = JSON.parse(localConfig);
        //         for (let key in localConfigObj) {
        //             if (config.hasOwnProperty(key)) {
        //                 config[key] = localConfigObj[key];
        //             }
        //         }
        //     }
        // }
    }


    function handleMessage(message) {
        const obj = JSON.parse(message);
        if (!obj) return message;
        switch (obj.type) {
            case "init_character_data":
                globalVariable.market.sellListings = {};
                globalVariable.market.buyListings = {};
                config.keywords.push(obj.character.name.toLowerCase());
                updateMarketListings(obj.myMarketListings);
                break;
            case "market_listings_updated":
                updateMarketListings(obj.endMarketListings);
                break;
            case "new_battle":
                if (config.notifyDeath) initBattle(obj);
                break;
            case "battle_updated":
                if (config.notifyDeath) checkDeath(obj);
                break;
            case "market_item_order_books_updated":
                if (config.forceUpdateMarketPrice) marketPriceUpdate(obj);
                break;
            case "quests_updated":
                for (let e of globalVariable.task.taskValueElements) {
                    e.remove();
                }
                globalVariable.task.taskValueElements = [];
                globalVariable.task.hasTaskValueElement = false;
                break;
            case "chat_message_received":
                handleChatMessage(obj);
                break;
        }
        return message;
    }

    function notifyDeath(name) {
        new Notification('🎉🎉🎉喜报🎉🎉🎉', {body: `${name} 死了!`});
    }

    function initBattle(obj) {
        globalVariable.battleData.players = [];
        for (let player of obj.players) {
            globalVariable.battleData.players.push({
                name: player.name, isAlive: player.currentHitpoints > 0,
            });
            if (player.currentHitpoints === 0) {
                notifyDeath(player.name);
            }
        }
    }

    function checkDeath(obj) {
        if (!globalVariable.battleData.players) return;
        for (let key in obj.pMap) {
            const index = parseInt(key);
            if (globalVariable.battleData.players[index].isAlive && obj.pMap[key].cHP === 0) {
                globalVariable.battleData.players[index].isAlive = false;
                notifyDeath(globalVariable.battleData.players[index].name);
            } else if (!globalVariable.battleData.players[index].isAlive && obj.pMap[key].cHP > 0) {
                globalVariable.battleData.players[index].isAlive = true;
            }
        }
    }

    function marketPriceUpdate(obj) {
        globalVariable.task.taskTokenValueData = getTaskTokenValue();
        // 本函数的代码复制自Magic Way Idle
        let itemDetailMap = globalVariable.itemDetailMap;
        let itemName = itemDetailMap[obj.marketItemOrderBooks.itemHrid].name;
        let ask = -1;
        let bid = -1;
        // 读取ask最低报价
        if (obj.marketItemOrderBooks.orderBooks[0].asks && obj.marketItemOrderBooks.orderBooks[0].asks.length > 0) {
            ask = obj.marketItemOrderBooks.orderBooks[0].asks[0].price;
        }
        // 读取bid最高报价
        if (obj.marketItemOrderBooks.orderBooks[0].bids && obj.marketItemOrderBooks.orderBooks[0].bids.length > 0) {
            bid = obj.marketItemOrderBooks.orderBooks[0].bids[0].price;
        }
        // 读取所有物品价格
        let jsonObj = JSON.parse(localStorage.getItem("MWITools_marketAPI_json"));
        // 修改当前查看物品价格
        if (jsonObj.market[itemName]) {
            jsonObj.market[itemName].ask = ask;
            jsonObj.market[itemName].bid = bid;
        }
        // 将修改后结果写回marketAPI缓存,完成对marketAPI价格的强制修改
        localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(jsonObj));
    }

    function handleChatMessage(obj) {
        if (obj.message.chan === "/chat_channel_types/whisper") {
            if (config.notifyWhisperMessages) {
                globalVariable.whisperAudio.play();
            }
        } else if (obj.message.chan === "/chat_channel_types/chinese") {
            if (config.listenKeywordMessages) {
                for (let keyword of config.keywords) {
                    if (obj.message.m.toLowerCase().includes(keyword)) {
                        globalVariable.keywordAudio.play();
                    }
                }
            }
        }
    }

    function autoClickTaskSortButton() {
        const targetElement = document.querySelector('#TaskSort');
        if (targetElement && targetElement.textContent !== '手动排序') {
            targetElement.click();
            targetElement.textContent = '手动排序';
        }
    }

    function formatCoinValue(num) {
        if (num >= 1e13) {
            return Math.floor(num / 1e12) + "T";
        } else if (num >= 1e10) {
            return Math.floor(num / 1e9) + "B";
        } else if (num >= 1e7) {
            return Math.floor(num / 1e6) + "M";
        } else if (num >= 1e4) {
            return Math.floor(num / 1e3) + "K";
        }
        return num.toString();
    }

    function updateMarketListings(obj) {
        for (let listing of obj) {
            if (listing.status === "/market_listing_status/cancelled") {
                delete globalVariable.market[listing.isSell ? "sellListings" : "buyListings"][listing.id];
                continue
            }
            globalVariable.market[listing.isSell ? "sellListings" : "buyListings"][listing.id] = {
                itemHrid: listing.itemHrid,
                price: (listing.orderQuantity - listing.filledQuantity) * (listing.isSell ? Math.ceil(listing.price * 0.98) : listing.price),
                unclaimedCoinCount: listing.unclaimedCoinCount,
            }
        }
        globalVariable.market.buyValue = 0;
        globalVariable.market.sellValue = 0;
        globalVariable.market.unclaimedValue = 0;
        for (let id in globalVariable.market.buyListings) {
            const listing = globalVariable.market.buyListings[id];
            globalVariable.market.buyValue += listing.price;
            globalVariable.market.unclaimedValue += listing.unclaimedCoinCount;
        }
        for (let id in globalVariable.market.sellListings) {
            const listing = globalVariable.market.sellListings[id];
            globalVariable.market.sellValue += listing.price;
            globalVariable.market.unclaimedValue += listing.unclaimedCoinCount;
        }
        globalVariable.market.hasFundsElement = false;
    }

    function showMarketListingsFunds() {
        if (globalVariable.market.hasFundsElement) return;
        const coinStackElement = document.querySelector("div.MarketplacePanel_coinStack__1l0UD");
        if (coinStackElement) {
            coinStackElement.style.top = "0px";
            coinStackElement.style.left = "0px";
            let fundsElement = coinStackElement.parentNode.querySelector("div.fundsElement");
            while (fundsElement) {
                fundsElement.remove();
                fundsElement = coinStackElement.parentNode.querySelector("div.fundsElement");
            }
            makeNode("购买预付金", globalVariable.market.buyValue, ["125px", "0px"]);
            makeNode("出售可获金", globalVariable.market.sellValue, ["125px", "22px"]);
            makeNode("待领取金额", globalVariable.market.unclaimedValue, ["0px", "22px"]);
            globalVariable.market.hasFundsElement = true;
        }

        function makeNode(text, value, style) {
            let node = coinStackElement.cloneNode(true);
            node.classList.add("fundsElement");
            const countNode = node.querySelector("div.Item_count__1HVvv");
            const textNode = node.querySelector("div.Item_name__2C42x");
            if (countNode) countNode.textContent = formatCoinValue(value);
            if (textNode) textNode.innerHTML = `<span style="color: rgb(102,204,255); font-weight: bold;">${text}</span>`;
            node.style.left = style[0];
            node.style.top = style[1];
            coinStackElement.parentNode.insertBefore(node, coinStackElement.nextSibling);
        }
    }

    function getTaskTokenValue() {
        const chestDropData = JSON.parse(localStorage.getItem("Edible_Tools")).Chest_Drop_Data;
        const lootsName = ["大陨石舱", "大工匠匣", "大宝箱"];
        const bidValueList = [
            parseFloat(chestDropData["Large Meteorite Cache"]["期望产出Bid"]),
            parseFloat(chestDropData["Large Artisan's Crate"]["期望产出Bid"]),
            parseFloat(chestDropData["Large Treasure Chest"]["期望产出Bid"]),
        ]
        const askValueList = [
            parseFloat(chestDropData["Large Meteorite Cache"]["期望产出Ask"]),
            parseFloat(chestDropData["Large Artisan's Crate"]["期望产出Ask"]),
            parseFloat(chestDropData["Large Treasure Chest"]["期望产出Ask"]),
        ]
        const res = {
            bidValue: Math.max(...bidValueList),
            askValue: Math.max(...askValueList)
        }
        res.bidLoots = lootsName[bidValueList.indexOf(res.bidValue)];
        res.askLoots = lootsName[askValueList.indexOf(res.askValue)];
        res.bidValue = Math.round(res.bidValue / 30);
        res.askValue = Math.round(res.askValue / 30);
        res.giftValueBid = Math.round(parseFloat(chestDropData["Purple's Gift"]["期望产出Bid"]));
        res.giftValueAsk = Math.round(parseFloat(chestDropData["Purple's Gift"]["期望产出Ask"]));
        if (config.forceUpdateMarketPrice) {
            const marketJSON = JSON.parse(localStorage.getItem("MWITools_marketAPI_json"));
            marketJSON.market["Task Token"].ask = res.askValue;
            marketJSON.market["Task Token"].bid = res.bidValue;
            localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(marketJSON));
        }
        res.rewardValueBid = res.bidValue + res.giftValueBid / 50;
        res.rewardValueAsk = res.askValue + res.giftValueAsk / 50;
        return res;
    }

    function showTaskValue() {
        globalVariable.task.taskListElement = document.querySelector("div.TasksPanel_taskList__2xh4k");
        if (!globalVariable.task.taskListElement) {
            globalVariable.task.taskValueElements = [];
            globalVariable.task.hasTaskValueElement = false;
            globalVariable.task.taskListElement = null;
            return;
        }
        if (globalVariable.task.hasTaskValueElement) return;
        globalVariable.task.hasTaskValueElement = true;
        const taskNodes = [...globalVariable.task.taskListElement.querySelectorAll("div.RandomTask_randomTask__3B9fA")];

        function convertKEndStringToNumber(str) {
            if (str.endsWith('K') || str.endsWith('k')) {
                return Number(str.slice(0, -1)) * 1000;
            } else {
                return Number(str);
            }
        }

        taskNodes.forEach(function (node) {
            const reward = node.querySelector("div.RandomTask_rewards__YZk7D");
            const coin = convertKEndStringToNumber(reward.querySelectorAll("div.Item_count__1HVvv")[0].innerText);
            const tokenCount = Number(reward.querySelectorAll("div.Item_count__1HVvv")[1].innerText);
            const newDiv = document.createElement("div");
            newDiv.textContent = `奖励期望收益: 
            ${formatCoinValue(coin + tokenCount * globalVariable.task.taskTokenValueData.rewardValueAsk)} / 
            ${formatCoinValue(coin + tokenCount * globalVariable.task.taskTokenValueData.rewardValueBid)}`;
            newDiv.style.color = "rgb(248,0,248)";
            node.querySelector("div.RandomTask_action__3eC6o").appendChild(newDiv);
            globalVariable.task.taskValueElements.push(newDiv);
        });
    }
})();