Ranged Way Idle

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

目前為 2025-05-14 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Ranged Way Idle
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  死亡提醒、强制刷新MWITools的价格、私信提醒音、自动任务排序、显示购买预付金/出售可获金/待领取金额、显示任务价值、默哀法师助手
// @author       AlphB
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @grant        GM_notification
// @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
    }
    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`),
        keywords: [],
        market: {
            hasFundsElement: false,
            sellValue: null,
            buyValue: null,
            unclaimedValue: null,
            sellListings: null,
            buyListings: null
        },
        task: {
            taskTokenValueData: null,
            hasTaskValueElement: false,
            taskValueElements: [],
        }
    };
    init();

    function init() {
        if (!('Edible_Tools' in localStorage)) {
            config.showTaskValue = false;
        }
        globalVariable.whisperAudio.volume = 0.4;
        globalVariable.keywordAudio.volume = 0.4;
        let observer = new MutationObserver(function (mutationsList, observer) {
            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);

        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);
        }

        globalVariable.task.taskTokenValueData = getTaskTokenValue();

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


    function handleMessage(message) {
        const obj = JSON.parse(message);
        console.log(obj.type);
        if (!obj) return message;
        switch (obj.type) {
            case "init_character_data":
                globalVariable.market.sellListings = {};
                globalVariable.market.buyListings = {};
                globalVariable.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.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) {
        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) {
        // 本函数的代码复制自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 globalVariable.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() {
        function makeNode(innerHTML, value, targetNode, style) {
            let node = targetNode.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 = innerHTML;
            node.style.left = style[0];
            node.style.top = style[1];
            targetNode.parentNode.insertBefore(node, targetNode.nextSibling);
        }

        if (globalVariable.market.hasFundsElement) return;
        const targetNode = document.querySelector("div.MarketplacePanel_coinStack__1l0UD");
        if (targetNode) {
            targetNode.style.top = "0px";
            targetNode.style.left = "0px";
            let fundsElement = document.querySelector("div.fundsElement");
            while (fundsElement) {
                fundsElement.remove();
                fundsElement = document.querySelector("div.fundsElement");
            }
            makeNode('<span style="color: rgb(102,204,255); font-weight: bold;">购买预付金</span>', globalVariable.market.buyValue, targetNode, ["125px", "0px"]);
            makeNode('<span style="color: rgb(102,204,255); font-weight: bold;">出售可获金</span>', globalVariable.market.sellValue, targetNode, ["125px", "22px"]);
            makeNode('<span style="color: rgb(102,204,255); font-weight: bold;">待领取金额</span>', globalVariable.market.unclaimedValue, targetNode, ["0px", "22px"]);
            globalVariable.market.hasFundsElement = true;
        }
    }

    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));
        }
        return res;
    }

    function showTaskValue() {
        const isInTask = document.querySelector("div.TasksPanel_taskSlotCount__nfhgS");
        if (!isInTask) {
            for (let e of globalVariable.task.taskValueElements) {
                e.remove();
            }
            globalVariable.task.hasTaskValueElement = false;
            return;
        }
        if (globalVariable.task.hasTaskValueElement) return;
        globalVariable.task.hasTaskValueElement = true;
        const list = document.querySelector("div.TasksPanel_taskList__2xh4k");
        const taskNodes = [...list.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);
            }
        }

        const tokenValueBid = globalVariable.task.taskTokenValueData.bidValue + globalVariable.task.taskTokenValueData.giftValueBid / 50;
        const tokenValueAsk = globalVariable.task.taskTokenValueData.askValue + globalVariable.task.taskTokenValueData.giftValueAsk / 50;

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