Ranged Way Idle

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

// ==UserScript==
// @name         Ranged Way Idle
// @namespace    http://tampermonkey.net/
// @version      2.0
// @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: {enable: true, desc: "战斗中角色死亡时发送通知"},
        forceUpdateMarketPrice: {enable: true, desc: "进入市场时,强制更新MWITools的市场价格"},
        notifyWhisperMessages: {enable: false, desc: "接受到私信时播放提醒音"},
        listenKeywordMessages: {enable: false, desc: "中文频道消息含有关键词时播放提醒音"},
        autoTaskSort: {enable: true, desc: "自动点击MWI TaskManager的任务排序按钮"},
        showMarketListingsFunds: {enable: true, desc: "显示购买预付金/出售可获金/待领取金额"},
        mournForMagicWayIdle: {enable: true, desc: "在控制台默哀法师助手"},
        showTaskValue: {enable: true, desc: "显示任务代币的价值"},
        keywords: [],
    }
    const globalVariable = {
        battleData: {
            players: null,
            lastNotifyTime: 0,
        },
        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.enable = false;
        }

        // 更新市场价格需要MWITools支持
        if (!('MWITools_marketAPI_json' in localStorage)) {
            config.forceUpdateMarketPrice.enable = false;
        }
        globalVariable.whisperAudio.volume = 0.4;
        globalVariable.keywordAudio.volume = 0.4;
        let observer = new MutationObserver(function () {
            if (config.showMarketListingsFunds.enable) showMarketListingsFunds();
            if (config.autoTaskSort.enable) autoClickTaskSortButton();
            if (config.showTaskValue.enable) showTaskValue();
            showConfigMenu();
        });
        observer.observe(document, {childList: true, subtree: true});

        globalVariable.task.taskTokenValueData = getTaskTokenValue();
        if (config.mournForMagicWayIdle.enable) {
            console.log("为法师助手默哀");
        }

        const oriGet = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data").get;

        function hookedGet() {
            const socket = this.currentTarget;
            if (!(socket instanceof WebSocket) || !socket.url ||
                (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);
            return handleMessage(message);
        }

        Object.defineProperty(MessageEvent.prototype, "data", {
            get: hookedGet,
            configurable: true,
            enumerable: true
        });
    }

    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) && key !== 'keywords') {
                    config[key].enable = localConfigObj[key];
                }
            }
            config.keywords = localConfigObj.keywords;
        }
    }

    function saveConfig() {
        // 仅保存enable开关和keywords
        const saveConfigObj = {};
        const configMenu = document.querySelectorAll("div#ranged_way_idle_config_menu input");
        if (configMenu.length === 0) return;
        for (const checkbox of configMenu) {
            config[checkbox.id].isTrue = checkbox.checked;
            saveConfigObj[checkbox.id] = checkbox.checked;
        }
        saveConfigObj.keywords = config.keywords;
        localStorage.setItem("ranged_way_idle_config", JSON.stringify(saveConfigObj));
    }

    function showConfigMenu() {
        const targetNode = document.querySelector("div.SettingsPanel_profileTab__214Bj");
        if (targetNode) {
            if (!targetNode.querySelector("#ranged_way_idle_config_menu")) {
                // enable开关部分
                targetNode.insertAdjacentHTML("beforeend", `<div id="ranged_way_idle_config_menu"></div>`);
                const insertElem = targetNode.querySelector("div#ranged_way_idle_config_menu");
                insertElem.insertAdjacentHTML(
                    "beforeend",
                    `<div style="float: left;">${
                        "Ranged Way Idle 设置"
                    }</div></br>`
                );
                for (let key in config) {
                    if (key === 'keywords') continue;
                    insertElem.insertAdjacentHTML(
                        "beforeend",
                        `<div style="float: left;">
                                   <input type="checkbox" id="${key}" ${config[key].enable ? "checked" : ""}>${config[key].desc}
                               </div></br>`
                    );
                }
                insertElem.addEventListener("change", saveConfig);

                // 控制 keywords 列表
                const container = document.createElement('div');
                container.style.marginTop = '20px';
                const input = document.createElement('input');
                input.type = 'text';
                input.style.width = '200px';
                input.placeholder = 'Ranged Way Idle 监听关键词';
                const button = document.createElement('button');
                button.textContent = '添加';
                const listContainer = document.createElement('div');
                listContainer.style.marginTop = '10px';
                container.appendChild(input);
                container.appendChild(button);
                container.appendChild(listContainer);
                targetNode.parentNode.insertBefore(container, targetNode.nextSibling);

                function renderList() {
                    listContainer.innerHTML = '';
                    config.keywords.forEach((item, index) => {
                        const itemDiv = document.createElement('div');
                        itemDiv.textContent = item;
                        itemDiv.style.margin = 'auto';
                        itemDiv.style.width = '200px';
                        itemDiv.style.cursor = 'pointer';
                        itemDiv.addEventListener('click', () => {
                            config.keywords.splice(index, 1);
                            renderList();
                        });
                        listContainer.appendChild(itemDiv);
                    });
                    saveConfig();
                }

                renderList();
                button.addEventListener('click', () => {
                    const newItem = input.value.trim();
                    if (newItem) {
                        config.keywords.push(newItem);
                        input.value = '';
                        saveConfig();
                        renderList();
                    }
                });
            }
        }
    }

    function handleMessage(message) {
        try {
            const obj = JSON.parse(message);
            if (!obj) return message;
            switch (obj.type) {
                case "init_character_data":
                    globalVariable.market.sellListings = {};
                    globalVariable.market.buyListings = {};
                    updateMarketListings(obj.myMarketListings);
                    break;
                case "market_listings_updated":
                    updateMarketListings(obj.endMarketListings);
                    break;
                case "new_battle":
                    if (config.notifyDeath.enable) initBattle(obj);
                    break;
                case "battle_updated":
                    if (config.notifyDeath.enable) checkDeath(obj);
                    break;
                case "market_item_order_books_updated":
                    if (config.forceUpdateMarketPrice.enable) 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;
            }
        } catch (e) {
            console.error(e);
        }
        return message;
    }

    function notifyDeath(name) {
        // 如果间隔小于60秒,强制不播报
        const nowTime = Date.now();
        if (nowTime - globalVariable.battleData.lastNotifyTime < 60000) return;
        globalVariable.battleData.lastNotifyTime = nowTime;
        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 (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.enable) {
                globalVariable.whisperAudio.play();
            }
        } else if (obj.message.chan === "/chat_channel_types/chinese") {
            if (config.listenKeywordMessages.enable) {
                for (let keyword of config.keywords) {
                    if (obj.message.m.includes(keyword)) {
                        globalVariable.keywordAudio.play();
                    }
                }
            }
        }
    }

    function autoClickTaskSortButton() {
        // 点击MWI TaskManager的任务排序按钮
        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)
        }
        // bid和ask的最佳兑换选项
        res.bidLoots = lootsName[bidValueList.indexOf(res.bidValue)];
        res.askLoots = lootsName[askValueList.indexOf(res.askValue)];
        // bid和ask的任务代币价值
        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.enable) {
            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);
        });
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址