Ranged Way Idle

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

当前为 2025-05-16 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Ranged Way Idle
// @namespace    http://tampermonkey.net/
// @version      1.10
// @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("#root > div > div > div.GamePage_gamePanel__3uNKN > div.GamePage_contentPanel__Zx4FH > div.GamePage_middlePanel__uDts7.GamePage_chatCollapsed__3pV19 > div.GamePage_mainPanel__2njyb > div > div:nth-child(2) > div > div.TasksPanel_tabsComponentContainer__3Q2EX > div > div.TabsComponent_tabPanelsContainer__26mzo > div:nth-child(1) > div > 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);
        });
    }
})();