Ranged Way Idle

一些超级有用的MWI的QoL功能

// ==UserScript==
// @name         Ranged Way Idle
// @namespace    http://tampermonkey.net/
// @version      5.2
// @description  一些超级有用的MWI的QoL功能
// @author       AlphB
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @grant        GM_notification
// @grant        GM.xmlHttpRequest
// @connect      https://www.milkywayidle.com/*
// @connect      https://test.milkywayidle.com/*
// @icon         https://tupian.li/images/2025/09/30/68dae3cf1fa7e.png
// @license      CC-BY-NC-SA-4.0
// ==/UserScript==

(function () {
    const configs = {
        combatClass: {
            notifyCombatDeath: {
                type: "switch",
                value: true,
                trigger: ["ws", "init"],
                listenMessageTypes: ["new_battle", "battle_updated"]
            }, minimumNotifyCooldownSeconds: {type: "input_number", value: 5, trigger: [],}
        },

        messageClass: {
            notifyChatMessages: {
                type: "switch",
                value: true,
                trigger: ["ws", "ob", "init"],
                listenMessageTypes: ["chat_message_received"]
            },
            notifyChatMessagesVolume: {type: "input_range", value: 0.5, trigger: [], min: 0, max: 1, step: 0.01},
            notifyChatMessagesByRegex: {type: "switch", value: false, trigger: []},
            notifyChatMessagesFilterSelf: {type: "switch", value: true, trigger: []}
        },

        gameInfoClass: {
            initCharacterData: {
                type: "switch",
                value: true,
                trigger: ["ws"],
                listenMessageTypes: ["init_character_data"],
                isHidden: true
            }, updateLocalStorageMarketPrice: {
                type: "switch", value: true, trigger: ["ws"], listenMessageTypes: ["market_item_order_books_updated"]
            }, showTaskValue: {
                type: "switch", value: true, trigger: ["ws", "ob", "init"], listenMessageTypes: ["quests_updated"]
            }, trackLeaderBoardData: {type: "switch", value: true, trigger: ["ob"]},
        },

        gameUIClass: {
            autoClickTaskSortButton: {type: "switch", value: true, trigger: ["ob"]},
            showMarketAPIUpdateTime: {type: "switch", value: true, trigger: ["ob"]},
            forceUpdateAPIButton: {type: "switch", value: true, trigger: ["ob"]},
            disableQueueUpgradeButton: {type: "switch", value: false, trigger: ["ob"]},
            disableActionQueueBar: {type: "switch", value: false, trigger: ["ob"]},
            hideSideBarButton: {
                type: "switch", value: false, trigger: ["ob", "ws"], listenMessageTypes: ["init_character_data"]
            },
            hideTrainRubbishButton: {type: "switch", value: false, trigger: ["ob"]},
            alwaysHideTrainRubbish: {type: "switch", value: false, trigger: []},
        },

        listingClass: {
            hookListingInfo: {
                type: "switch",
                value: true,
                trigger: ["ws"],
                listenMessageTypes: ["market_listings_updated", "init_character_data"],
                isHidden: true
            },
            saveListingInfoToLocalStorage: {type: "switch", value: true, trigger: []},
            saveListingInfoToLocalStorageMaxDays: {type: "input_number", value: 30, trigger: []},
            showTotalListingFunds: {
                type: "switch", value: true, trigger: ["ws", "ob"], listenMessageTypes: ["market_listings_updated"]
            },
            showTotalListingFundsPrecise: {type: "input_number", value: 0, trigger: []},
            showListingInfo: {
                type: "switch",
                value: true,
                trigger: ["ws", "ob"],
                listenMessageTypes: ["market_listings_updated", "market_item_order_books_updated"]
            },
            listingSortCompatible: {type: "switch", value: true, trigger: []},
            showListingPricePrecise: {type: "input_number", value: 2, trigger: []},
            showListingCreateTimeByLifespan: {type: "switch", value: false, trigger: []},
            listingSortTools: {type: "switch", value: false, isHidden: true, trigger: ["ob"]}, // TO DO
            notifyListingFilled: {
                type: "switch", value: false, trigger: ["ws"], listenMessageTypes: ["market_listings_updated"]
            },
            notifyListingFilledVolume: {type: "input_range", value: 0.5, trigger: [], min: 0, max: 1, step: 0.01},
            estimateListingCreateTime: {
                type: "switch",
                value: true,
                trigger: ["ws", "ob"],
                listenMessageTypes: ["market_item_order_books_updated"]
            },
            estimateListingCreateTimeColorByAccuracy: {type: "switch", value: false, trigger: []},
            estimateListingCreateTimeColorByLifespan: {type: "switch", value: false, trigger: []},
            estimateListingCreateTimeByLifespan: {type: "switch", value: false, trigger: []},
        },

        otherClass: {
            scriptLanguage: {type: "select", value: "zh-cn", trigger: [], options: ["zh-cn", "en-us"]},
            showSponsor: {type: "switch", value: false, trigger: ["ob"]},
            mournForMagicWayIdle: {type: "switch", value: true, trigger: ["init"]},
            optimizeDocumentObserver: {type: "switch", value: false, trigger: []},
            debugPrintWSMessages: {type: "switch", value: false, trigger: [], listenMessageTypes: []},
            showConfigMenu: {type: "switch", value: true, trigger: ["ob"], isHidden: true}
        }
    };

    const globalVariables = {
        marketAPIUrl: document.URL.includes("test.milkywayidle.com") ? "https://test.milkywayidle.com/game_data/marketplace.json" : "https://www.milkywayidle.com/game_data/marketplace.json",
        initCharacterData: null,
        documentObserver: null,
        documentObserverFunction: null,
        webSocketMessageProcessor: null,
        functionMap: {},
        notifyMessageAudio: new Audio("https://upload.thbwiki.cc/d/d1/se_bonus2.mp3"),
        notifyListingFilledAudio: new Audio("https://upload.thbwiki.cc/f/ff/se_trophy.mp3"),
        allListings: {},
        configs: configs
    };
    unsafeWindow._rwivb = globalVariables;

    const I18NMap = {
        "combatClass": {"zh-cn": "战斗功能", "en-us": "Combat Functions"},
        "messageClass": {"zh-cn": "聊天功能", "en-us": "Message Functions"},
        "gameInfoClass": {"zh-cn": "游戏信息设置", "en-us": "Game Info"},
        "gameUIClass": {"zh-cn": "游戏界面设置", "en-us": "Game UI"},
        "listingClass": {"zh-cn": "挂单功能设置", "en-us": "Listing Functions"},
        "otherClass": {"zh-cn": "其他设置", "en-us": "Other Functions"},

        "ranged_way_idle_config_menu_title": {"zh-cn": "设置", "en-us": "Config"},
        "notifyCombatDeath": {"zh-cn": "战斗中角色死亡时,发出通知", "en-us": "Notify when a character dies in combat"},
        "minimumNotifyCooldownSeconds": {
            "zh-cn": "角色死亡通知冷却时间(秒)",
            "en-us": "Minimum cooldown time for notifying when a character dies in combat (seconds)"
        },
        "notifyChatMessages": {
            "zh-cn": "聊天消息含有关键词时,发出声音提醒", "en-us": "Notify when chat messages contain preset keywords"
        },
        "notifyChatMessagesVolume": {"zh-cn": "聊天消息声音提醒音量", "en-us": "Chat message notify sound volume"},
        "notifyChatMessagesByRegex": {"zh-cn": "聊天消息采用正则匹配", "en-us": "Use regex to match chat messages"},
        "notifyChatMessagesFilterSelf": {
            "zh-cn": "不提醒自己发送的聊天消息", "en-us": "Filter out chat messages sent by yourself"
        },
        "updateLocalStorageMarketPrice": {
            "zh-cn": "更新localStorage中的市场价格", "en-us": "Update localStorage market price while click in market"
        },
        "showTaskValue": {
            "zh-cn": "显示任务期望收益(依赖 食用工具)", "en-us": "Show task expected value (requires TaskManager)"
        },
        "trackLeaderBoardData": {"zh-cn": "跟踪排行榜数据", "en-us": "Track leaderboard data"},
        "autoClickTaskSortButton": {
            "zh-cn": "自动点击任务排序按钮(依赖 MWI TaskManager)",
            "en-us": "Auto-click task sort button (requires TaskManager)"
        },
        "showMarketAPIUpdateTime": {"zh-cn": "显示市场API更新时间", "en-us": "Show market API update time"},
        "forceUpdateAPIButton": {"zh-cn": "强制更新市场API按钮", "en-us": "Force update market API button"},
        "disableQueueUpgradeButton": {
            "zh-cn": "禁用各处队列升级按钮,以防跳转至牛铃商店",
            "en-us": "Disable queue upgrade buttons to prevent redirect to cowbell shop"
        },
        "disableActionQueueBar": {"zh-cn": "禁用行动队列提示框显示", "en-us": "Disable action queue bar display"},
        "hideSideBarButton": {"zh-cn": "隐藏左侧边栏的部分按钮", "en-us": "Hide some buttons in left sidebar"},
        "hideTrainRubbishButton": {
            "zh-cn": "允许隐藏背包里的火车垃圾(无强化等级的奶酪、木制、皮革或布料装备等)",
            "en-us": "Allow hiding train rubbish in inventory (with no enhancement level)"
        },
        "alwaysHideTrainRubbish": {
            "zh-cn": "总是自动隐藏背包里的火车垃圾", "en-us": "Always hide train rubbish in inventory"
        },
        "saveListingInfoToLocalStorage": {
            "zh-cn": "保存挂单信息到localStorage", "en-us": "Save listing info to localStorage"
        },
        "saveListingInfoToLocalStorageMaxDays": {
            "zh-cn": "挂单信息本地保存时间(天)", "en-us": "Max days to save listing info to localStorage"
        },
        "showTotalListingFunds": {
            "zh-cn": "显示市场挂单的总购买预付金/出售可获金/待领取金额",
            "en-us": "Show total listing funds (purchase prepaid coins/sell result coins/unclaimed coins)"
        },
        "showTotalListingFundsPrecise": {
            "zh-cn": "显示市场挂单的总购买预付金/出售可获金/待领取金额的精度", "en-us": "Precise of total listing funds"
        },
        "showListingInfo": {"zh-cn": "显示各个挂单的价格、创建时间信息", "en-us": "Show listing price/create time"},
        "listingSortCompatible": {"zh-cn": "尝试兼容挂单排序", "en-us": "Try to compatible listing sort"},
        "showListingPricePrecise": {
            "zh-cn": "各个挂单的购买预付金/出售可获金的价格精度", "en-us": "Precise of listing price"
        },
        "showListingCreateTimeByLifespan": {
            "zh-cn": "显示挂单已存在时长,而非创建的时刻", "en-us": "Show listing lifespan instead of create time"
        },
        "notifyListingFilled": {"zh-cn": "挂单完成时,发出声音提醒", "en-us": "Notify when a listing is filled"},
        "notifyListingFilledVolume": {"zh-cn": "挂单完成声音提醒音量", "en-us": "Listing filled notify sound volume"},
        "estimateListingCreateTime": {
            "zh-cn": "依据挂单ID线性估算挂单创建时间", "en-us": "Linearly estimate listing create time by listing ID"
        },
        "estimateListingCreateTimeColorByAccuracy": {
            "zh-cn": "依据精度为挂单创建时间着色(越偏向绿色 精度越高)该项为真时,覆盖下一选项设置",
            "en-us": "Color listing create time by accuracy (green for high accuracy). while this option is true, it overrides the next option setting"
        },
        "estimateListingCreateTimeColorByLifespan": {
            "zh-cn": "依据存在时间为挂单创建时间着色(越偏向绿色 创建时间越短)",
            "en-us": "Color listing create time by lifespan (green for short lifespan)"
        },
        "estimateListingCreateTimeByLifespan": {
            "zh-cn": "估算结果显示为挂单已存在时长,而非创建的时刻",
            "en-us": "Show estimate listing create time by lifespan"
        },
        "scriptLanguage": {"zh-cn": "语言 🌏", "en-us": "Language 🌏"},
        "showSponsor": {"zh-cn": "赞助作者", "en-us": "Buy me a coffee"},
        "mournForMagicWayIdle": {"zh-cn": "在控制台为Magic Way Idle默哀", "en-us": "Mourn for Magic Way Idle"},
        "optimizeDocumentObserver": {
            "zh-cn": "优化document监听器,减少性能开销(可能有bug,出现问题请关闭)",
            "en-us": "Optimize document observer to reduce performance overhead (may have bugs, please disable this option if any problem occurs)"
        },
        "debugPrintWSMessages": {
            "zh-cn": "打印WebSocket消息(不推荐打开)", "en-us": "Print WebSocket messages (not recommended)"
        },

        "configNoteText": {
            "zh-cn": "部分设置可能需要刷新页面才能生效。如果完全无效,或者控制台大量报错,请尝试更新本插件或前置插件",
            "en-us": "Some settings may not take effect until page refresh. If not working, or console is spammed with errors, try updating this script or its pre-requisites."
        },
        "notifyChatMessagesAddRowButton": {"zh-cn": "添加聊天消息监听关键词", "en-us": "Add chat message keyword"},
        "taskExpectedValueText": {"zh-cn": "任务期望收益:", "en-us": "Task expected value:"},
        "trackLeaderBoardDataLeaderboardStoreButton": {"zh-cn": "记录当前排行榜数据", "en-us": "Record current data"},
        "trackLeaderBoardDataLeaderboardDeleteButton": {"zh-cn": "删除本地数据", "en-us": "Delete local data"},
        "trackLeaderBoardDataLeaderboardRecordTimeText": {
            "zh-cn": "本地数据记录于:${recordTime}(${timeDelta}小时前)",
            "en-us": "Local data recorded at: ${recordTime} (${timeDelta} hours ago)"
        },
        "trackLeaderBoardDataLeaderboardNoRecordTimeText": {
            "zh-cn": "无本地数据记录", "en-us": "No local data recorded"
        },
        "trackLeaderBoardDataNoteText": {
            "zh-cn": "由于排行榜数据每20分钟记录一次,增速和超越时间有误差,仅供参考。",
            "en-us": "Due to the leaderboard update every 20 minutes, speed and catchup time may be inaccurate. This is for reference only."
        },
        "trackLeaderBoardDataDifference": {"zh-cn": "增量", "en-us": "Difference"},
        "trackLeaderBoardDataSpeed": {"zh-cn": "增速", "en-us": "Speed"},
        "trackLeaderBoardDataCatchupTime": {"zh-cn": "超越时间", "en-us": "Catchup time"},
        "trackLeaderBoardDataCatchupTimeNow": {"zh-cn": "现在!", "en-us": "Now!"},
        "trackLeaderBoardDataNewRecordText": {"zh-cn": "新上榜", "en-us": "New in LB"},
        "showMarketAPIUpdateTimeText": {"zh-cn": "市场API更新时间于:", "en-us": "Market API update time:"},
        "forceUpdateAPIButtonText": {"zh-cn": "强制更新市场API", "en-us": "Force update market API"},
        "forceUpdateAPIButtonTextSuccess": {
            "zh-cn": "更新成功。市场数据更新于", "en-us": "Update success. Market data updated at:"
        },
        "forceUpdateAPIButtonTextError": {
            "zh-cn": "更新失败。请稍后重试。", "en-us": "Update failed. Please try again later."
        },
        "forceUpdateAPIButtonTextTimeout": {
            "zh-cn": "更新超时。请稍后重试。", "en-us": "Update timeout. Please try again later."
        },
        "hideSidebarText": {"zh-cn": "隐藏左侧边栏按钮配置", "en-us": "Hide sidebar buttons config"},
        "hideTrainRubbishButtonText": {"zh-cn": "隐藏火车垃圾", "en-us": "Hide train rubbish"},
        "showTrainRubbishButtonText": {"zh-cn": "显示火车垃圾", "en-us": "Show train rubbish"},
        "totalUnclaimedCoinsText": {"zh-cn": "待领取金额", "en-us": "Unclaimed"},
        "totalPrepaidCoinsText": {"zh-cn": "购买预付金", "en-us": "Purchase prepaid"},
        "totalSellResultCoinsText": {"zh-cn": "出售可获金", "en-us": "Sell result"},
        "showListingInfoCreateTimeAt": {"zh-cn": "创建于", "en-us": "Created at"},
        "showListingInfoCreateTimeLifespan": {
            "zh-cn": "已存在 ${days}天${hours}时${minutes}分${seconds}秒",
            "en-us": "Lifespan: ${days}d ${hours}h ${minutes}m ${seconds}s"
        },
        "showListingInfoTopOrderPriceText": {"zh-cn": "左一/右一 价格", "en-us": "Top order price"},
        "showListingInfoTotalPriceText": {"zh-cn": "购买预付金/出售可获金", "en-us": "Purchase prepaid / Sell result"},
        "estimateListingCreateTimeText": {"zh-cn": "估计创建时间", "en-us": "Estimated create time"},
        "estimateListingCreateTimeLifespan": {
            "zh-cn": "${days}天${hours}时${minutes}分", "en-us": "${days}d ${hours}h ${minutes}m"
        },
        "sponsorText": {"zh-cn": "赞助作者", "en-us": "Buy me a coffee"},
        "sponsorAlertText": {
            "zh-cn": "本赞助为纯自愿捐赠,不包含任何本脚本的额外隐藏功能或服务。如果您愿意,你可以在备注中写上你的ID,作者将会把您的名字加入到赞助名单中。",
            "en-us": "This sponsorship is purely voluntary and does not include any additional hidden features or services for this script. If you wish, you can write your ID in the note and the author will add your name to the sponsor list."
        },
        "zh-cn": {"zh-cn": "中文", "en-us": "中文"},
        "en-us": {"zh-cn": "English", "en-us": "English"},

        "/chat_channel_types/general": {"zh-cn": "英语", "en-us": "English"},
        "/chat_channel_types/chinese": {"zh-cn": "中文", "en-us": "Chinese"},
        "/chat_channel_types/ironcow": {"zh-cn": "铁牛", "en-us": "Ironcow"},
        "/chat_channel_types/trade": {"zh-cn": "交易", "en-us": "Trade"},
        "/chat_channel_types/recruit": {"zh-cn": "招募", "en-us": "Recruit"},
        "/chat_channel_types/beginner": {"zh-cn": "新手", "en-us": "Beginner"},
        "/chat_channel_types/guild": {"zh-cn": "公会", "en-us": "Guild"},
        "/chat_channel_types/party": {"zh-cn": "队伍", "en-us": "Party"},
        "/chat_channel_types/whisper": {"zh-cn": "私聊", "en-us": "Whisper"},
        "/chat_channel_types/moderator": {"zh-cn": "管理员", "en-us": "Moderator"},

        "/chat_channel_types/arabic": {"zh-cn": "العربية", "en-us": "العربية"},
        "/chat_channel_types/french": {"zh-cn": "Français", "en-us": "Français"},
        "/chat_channel_types/german": {"zh-cn": "Deutsch", "en-us": "Deutsch"},
        "/chat_channel_types/hebrew": {"zh-cn": "עברית", "en-us": "עברית"},
        "/chat_channel_types/hindi": {"zh-cn": "हिंदी", "en-us": "हिंदी"},
        "/chat_channel_types/japanese": {"zh-cn": "日本語", "en-us": "日本語"},
        "/chat_channel_types/korean": {"zh-cn": "한국어", "en-us": "한국어"},
        "/chat_channel_types/portuguese": {"zh-cn": "Português", "en-us": "Português"},
        "/chat_channel_types/russian": {"zh-cn": "Русский", "en-us": "Русский"},
        "/chat_channel_types/spanish": {"zh-cn": "Español", "en-us": "Español"},
        "/chat_channel_types/vietnamese": {"zh-cn": "Tiếng Việt", "en-us": "Tiếng Việt"},

    };

    function initScript() {
        const allFunctionsObject = new AllFunctions();
        for (const configClass in configs) {
            globalVariables.functionMap[configClass] = {};
            const functionClassObject = allFunctionsObject[configClass]();
            for (const configName in configs[configClass]) {
                if (configs[configClass][configName].trigger.length === 0) continue;
                if (!functionClassObject[configName]) {
                    console.warn("No function found for config: " + configName);
                    continue;
                }
                globalVariables.functionMap[configClass][configName] = functionClassObject[configName]();
            }
        }
        globalVariables.functionMap.otherClass["showConfigMenu"].loadLocalConfig();

        hookWebSocket();
        initDocumentObserver();

        for (const configClass in configs) {
            for (const configName in configs[configClass]) {
                if (configs[configClass][configName].type === 'switch' && configs[configClass][configName].value &&
                    globalVariables.functionMap[configClass][configName] && configs[configClass][configName].trigger.includes('init')) {
                    try {
                        globalVariables.functionMap[configClass][configName].init();
                    } catch (err) {
                        console.error(err);
                    }
                }
            }
        }

        function hookWebSocket() {
            // message processor
            globalVariables.webSocketMessageProcessor = function (message, type) {
                const obj = JSON.parse(message);
                if (configs.otherClass.debugPrintWSMessages.value) console.log(type, obj);
                if (type !== 'get' || !obj) return;
                const messageType = obj.type;
                for (const configClass in configs) {
                    for (const configName in configs[configClass]) {
                        if (configs[configClass][configName].type === 'switch' && configs[configClass][configName].value &&
                            globalVariables.functionMap[configClass][configName] && configs[configClass][configName].trigger.includes('ws') &&
                            configs[configClass][configName].listenMessageTypes && configs[configClass][configName].listenMessageTypes.includes(messageType)) {
                            try {
                                globalVariables.functionMap[configClass][configName].ws(obj);
                            } catch (err) {
                                console.error(err);
                            }
                        }
                    }
                }
            };

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

            function hookedGet() {
                const socket = this.currentTarget;
                if (!(socket instanceof WebSocket) || !socket.url) {
                    return oriGet.call(this);
                }
                const message = oriGet.call(this);
                try {
                    globalVariables.webSocketMessageProcessor(message, 'get')
                } catch (err) {
                    console.error(err);
                }
                return message;
            }

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

            // send
            const originalSend = WebSocket.prototype.send;

            WebSocket.prototype.send = function (message) {
                try {
                    globalVariables.webSocketMessageProcessor(message, 'send');
                } catch (err) {
                    console.error(err);
                }
                return originalSend.call(this, message);
            };
        }

        function initDocumentObserver() {
            globalVariables.documentObserverFunction = function documentObserverFunction(mutationsList, observer) {
                const node = configs.otherClass.optimizeDocumentObserver.enable ? mutationsList[0].target : document;
                for (const configClass in configs) {
                    for (const configName in configs[configClass]) {
                        if (configs[configClass][configName].type === 'switch' && configs[configClass][configName].value &&
                            globalVariables.functionMap[configClass][configName] && configs[configClass][configName].trigger.includes('ob')) {
                            try {
                                globalVariables.functionMap[configClass][configName].ob(node);
                            } catch (err) {
                                console.error(err);
                            }
                        }
                    }
                }
            }
            globalVariables.documentObserver = new MutationObserver(globalVariables.documentObserverFunction);
            globalVariables.documentObserver.observe(document, {childList: true, subtree: true});
        }
    }

    class AllFunctions {
        combatClass() {
            function notifyCombatDeath() {
                const players = [];
                let lastNotificationTime = 0;

                function newBattle(obj) {
                    players.length = 0;
                    for (const player of obj.players) {
                        players.push({
                            name: player.name, isAlive: player.currentHitpoints > 0
                        });
                        if (player.currentHitpoints === 0) {
                            new Notification('战斗提醒', {body: `${player.name} 死了!`});
                        }
                    }
                }

                function battleUpdated(obj) {
                    for (const playerIndex in obj.pMap) {
                        const player = players[playerIndex];
                        if (player.isAlive && obj.pMap[playerIndex].cHP === 0 && Date.now() - lastNotificationTime > 1000 * configs.combatClass.minimumNotifyCooldownSeconds.value) {
                            new Notification('战斗提醒', {body: `${player.name} 死了!`});
                            lastNotificationTime = Date.now();
                        }
                        player.isAlive = obj.pMap[playerIndex].cHP > 0;
                    }
                }

                function ws(obj) {
                    if (obj.type === "new_battle") {
                        newBattle(obj);
                    } else if (obj.type === "battle_updated") {
                        battleUpdated(obj);
                    }
                }

                function init() {
                    Notification.requestPermission();
                }

                return {ws: ws, init: init};
            }

            return {notifyCombatDeath: notifyCombatDeath};
        }

        messageClass() {
            function notifyChatMessages() {
                const allChannels = ["/chat_channel_types/chinese", "/chat_channel_types/general", "/chat_channel_types/ironcow", "/chat_channel_types/trade", "/chat_channel_types/recruit", "/chat_channel_types/beginner", "/chat_channel_types/guild", "/chat_channel_types/party", "/chat_channel_types/whisper", "/chat_channel_types/moderator",

                    "/chat_channel_types/arabic", "/chat_channel_types/french", "/chat_channel_types/german", "/chat_channel_types/hebrew", "/chat_channel_types/hindi", "/chat_channel_types/japanese", "/chat_channel_types/korean", "/chat_channel_types/portuguese", "/chat_channel_types/russian", "/chat_channel_types/spanish", "/chat_channel_types/vietnamese",];
                let listenObject = {};
                let messageListerMenuRootNode;

                function createNewRow(selectedChannel = "", inputText = "") {
                    const listenRow = document.createElement("div");
                    listenRow.classList.add("RangedWayIdleMessageListenRow");

                    // channel select
                    const selectNode = document.createElement('select');
                    allChannels.forEach(channel => {
                        const option = document.createElement('option');
                        option.value = channel;
                        option.textContent = I18N(channel);
                        if (channel === selectedChannel) {
                            option.selected = true;
                        }
                        selectNode.appendChild(option);
                    });
                    selectNode.addEventListener('change', updateListenObject);

                    // input text
                    const inputNode = document.createElement('input');
                    inputNode.type = 'text';
                    inputNode.value = inputText;
                    inputNode.addEventListener('input', updateListenObject);

                    // delete button
                    const deleteButton = document.createElement('button');
                    deleteButton.textContent = "×";
                    deleteButton.addEventListener('click', function () {
                        listenRow.remove();
                        updateListenObject();
                    });
                    deleteButton.style.backgroundColor = "#F44444";

                    // add to row
                    listenRow.appendChild(selectNode);
                    listenRow.appendChild(inputNode);
                    listenRow.appendChild(deleteButton);

                    return listenRow;
                }

                function updateListenObject() {
                    const newListenObject = {};
                    for (const channel of allChannels) {
                        newListenObject[channel] = [];
                    }

                    // collect channel and text from rows
                    for (const row of messageListerMenuRootNode.querySelectorAll('.RangedWayIdleMessageListenRow')) {
                        const channel = row.querySelector('select').value;
                        const text = row.querySelector('input').value.trim();
                        newListenObject[channel].push(text);
                    }

                    listenObject = newListenObject;
                    localStorage.setItem("ranged_way_idle_listen_chat_messages", JSON.stringify(listenObject));
                }

                function ws(obj) {
                    if (obj.type === "chat_message_received") {
                        const channel = obj.message.chan;
                        const text = obj.message.m;
                        if (configs.messageClass.notifyChatMessagesFilterSelf.value && obj.message.cId === globalVariables.initCharacterData.character.id) return;
                        if (!listenObject[channel]) return;
                        for (const listenText of listenObject[channel]) {
                            if (configs.messageClass.notifyChatMessagesByRegex.value) {
                                const regex = new RegExp(listenText, "g");
                                if (regex.test(text)) {
                                    globalVariables.notifyMessageAudio.volume = configs.messageClass.notifyChatMessagesVolume.value;
                                    globalVariables.notifyMessageAudio.play();
                                    break;
                                }
                            } else {
                                if (text.includes(listenText)) {
                                    globalVariables.notifyMessageAudio.volume = configs.messageClass.notifyChatMessagesVolume.value;
                                    globalVariables.notifyMessageAudio.play();
                                    break;
                                }
                            }
                        }

                    }
                }

                function ob(node) {
                    // add this after config menu
                    const configMenuRootNode = node.querySelector(".RangedWayIdleConfigMenuRoot");
                    if (!configMenuRootNode) return;
                    if (node.querySelector(".RangedWayIdleMessageListerMenu")) return;
                    messageListerMenuRootNode = document.createElement("div");
                    messageListerMenuRootNode.classList.add("RangedWayIdleMessageListerMenu");

                    // new row button
                    const addNewRowButton = document.createElement("button");
                    addNewRowButton.textContent = I18N("notifyChatMessagesAddRowButton");
                    addNewRowButton.addEventListener("click", () => {
                        messageListerMenuRootNode.appendChild(createNewRow());
                    });
                    addNewRowButton.style.backgroundColor = "#66CCFF";
                    addNewRowButton.style.color = "#000000";
                    messageListerMenuRootNode.appendChild(addNewRowButton);

                    // load local listeners
                    for (const channel of allChannels) {
                        if (listenObject[channel]) {
                            for (const text of listenObject[channel]) {
                                messageListerMenuRootNode.appendChild(createNewRow(channel, text));
                            }
                        }
                    }

                    configMenuRootNode.insertAdjacentElement("afterend", messageListerMenuRootNode);
                }

                function init() {
                    const localListenObject = localStorage.getItem("ranged_way_idle_listen_chat_messages");
                    if (localListenObject) {
                        listenObject = JSON.parse(localListenObject);
                    }
                }

                return {ws: ws, ob: ob, init: init};
            }

            return {notifyChatMessages: notifyChatMessages};
        }

        gameInfoClass() {
            function initCharacterData() {
                function ws(obj) {
                    globalVariables.initCharacterData = obj;
                }

                return {ws: ws};
            }

            function updateLocalStorageMarketPrice() {
                function ws(obj) {
                    if (obj.type === "market_item_order_books_updated") {
                        const localMarketAPIJson = JSON.parse(localStorage.getItem('MWITools_marketAPI_json'));
                        const itemHrid = obj.marketItemOrderBooks.itemHrid;
                        const orderBooks = obj.marketItemOrderBooks.orderBooks;
                        for (let enhanceLevel = 0; enhanceLevel <= 20; enhanceLevel++) {
                            if (orderBooks[enhanceLevel]) {
                                // 如果左右至少有一个挂单,则需要更新为该价格
                                let askValue = -1;
                                const ask = orderBooks[enhanceLevel].asks;
                                if (ask && ask.length) {
                                    askValue = Math.min(...ask.map(listing => listing.price));
                                }
                                let bidValue = -1;
                                const bid = orderBooks[enhanceLevel].bids;
                                if (bid && bid.length) {
                                    bidValue = Math.max(...bid.map(listing => listing.price));
                                }

                                if (askValue !== -1 || bidValue !== -1) {
                                    localMarketAPIJson.marketData[itemHrid][enhanceLevel] = {
                                        a: askValue, b: bidValue
                                    };
                                }
                            } else if (enhanceLevel === 0) {
                                // 左右都没有,强化等级为+0,记录为-1
                                localMarketAPIJson.marketData[itemHrid][enhanceLevel] = {
                                    a: -1, b: -1
                                }
                            } else {
                                // 左右都没有,强化等级不为+0,删除记录
                                delete localMarketAPIJson.marketData[itemHrid][enhanceLevel];
                            }
                        }
                        // 将修改后结果写回marketAPI缓存,完成对marketAPI价格的强制修改
                        localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(localMarketAPIJson));
                    }
                }

                return {ws: ws};
            }

            function showTaskValue() {
                let taskValueObject;

                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"]));

                    res.rewardValueBid = res.bidValue + res.giftValueBid / 50;
                    res.rewardValueAsk = res.askValue + res.giftValueAsk / 50;
                    return res;
                }

                function updateTaskValueNode(node) {
                    const taskListNode = node.querySelector(".TasksPanel_taskList__2xh4k");
                    if (!taskListNode) return;
                    if (taskListNode.querySelector(".RangedWayIdleTaskValue")) return;

                    for (const taskNode of taskListNode.querySelectorAll(".RandomTask_taskInfo__1uasf")) {
                        const rewardsNode = taskNode.querySelector(".RandomTask_rewards__YZk7D");
                        let coinCount = 0;
                        let taskTokenCount = 0;
                        for (const itemContainerNode of rewardsNode.querySelectorAll(".Item_itemContainer__x7kH1")) {
                            if (itemContainerNode.querySelector("use").href.baseVal.includes("coin")) {
                                coinCount = parseItemCount(itemContainerNode.querySelector(".Item_count__1HVvv").textContent);
                            } else if (itemContainerNode.querySelector("use").href.baseVal.includes("task_token")) {
                                taskTokenCount = parseItemCount(itemContainerNode.querySelector(".Item_count__1HVvv").textContent);
                            }
                        }

                        const askValue = taskTokenCount * taskValueObject.rewardValueAsk + coinCount;
                        const bidValue = taskTokenCount * taskValueObject.rewardValueBid + coinCount;

                        const taskValueDivNode = document.createElement("div");
                        taskValueDivNode.classList.add("RangedWayIdleTaskValue");
                        taskValueDivNode.textContent = I18N("taskExpectedValueText") + `${formatItemCount(askValue)} / ${formatItemCount(bidValue)}`;
                        taskValueDivNode.style.color = "#66CCFF";
                        taskValueDivNode.style.fontSize = "0.75rem";
                        taskNode.querySelector(".RandomTask_action__3eC6o").appendChild(taskValueDivNode);
                    }
                }

                function updateTaskShopItemValue(node) {
                    const taskShopPanelNode = node.querySelector(".TasksPanel_taskShop__q5sHL");
                    if (!taskShopPanelNode) return;
                    if (taskShopPanelNode.classList.contains("RangedWayIdleTaskShopValueSet")) return;
                    const chestDropData = JSON.parse(localStorage.getItem("Edible_Tools")).Chest_Drop_Data;
                    taskShopPanelNode.classList.add("RangedWayIdleTaskShopValueSet");
                    const nameMap = {
                        "large_meteorite_cache": "Large Meteorite Cache",
                        "large_artisans_crate": "Large Artisan's Crate",
                        "large_treasure_chest": "Large Treasure Chest"
                    }
                    for (const taskShopItemNode of taskShopPanelNode.querySelectorAll(".TasksPanel_item__DWSpv")) {
                        const item = taskShopItemNode.querySelector(".TasksPanel_iconContainer__2JGVN use").href.baseVal.split("#")[1];
                        if (!Object.keys(nameMap).includes(item)) {
                            continue;
                        }
                        const name = nameMap[item];
                        const askValue = parseFloat(chestDropData[name]["期望产出" + "Ask"]);
                        const bidValue = parseFloat(chestDropData[name]["期望产出" + "Bid"]);
                        const divNode = document.createElement("div");
                        divNode.textContent = `${formatItemCount(askValue)} / ${formatItemCount(bidValue)}`;
                        divNode.style.color = "#66CCFF";
                        taskShopItemNode.insertBefore(divNode, taskShopItemNode.lastChild);
                    }
                }

                function ws(obj) {
                    if (obj.type === "quests_updated") {
                        // remove old task value nodes
                        document.querySelectorAll(".RangedWayIdleTaskValue").forEach(node => {
                            node.remove();
                        });
                    }
                }

                function ob(node) {
                    // set task expected value
                    updateTaskValueNode(node);

                    // set task shop item value
                    updateTaskShopItemValue(node);
                }

                function init() {
                    taskValueObject = getTaskTokenValue();
                    if (configs.gameInfoClass.updateLocalStorageMarketPrice.value) {
                        const localMarketAPIJson = JSON.parse(localStorage.getItem("MWITools_marketAPI_json"));
                        localMarketAPIJson.marketData["/items/task_token"] = {
                            "0": {
                                a: taskValueObject.askValue, b: taskValueObject.bidValue
                            }
                        };
                        localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(localMarketAPIJson));
                    }
                }

                return {ws: ws, ob: ob, init: init};
            }

            function trackLeaderBoardData() {
                function getCurrentKey() {
                    const selectedTabs = document.querySelectorAll(".LeaderboardPanel_tabsComponentContainer__mIgnw .Mui-selected");
                    if (selectedTabs.length === 0) return;
                    const selectedText = Array.from(selectedTabs).map((tab) => tab.textContent);
                    return selectedText.join("-");
                }

                function createNoteAndButton(noteNode) {
                    const keyString = getCurrentKey();

                    // store data button
                    const storeButton = document.createElement("button");
                    storeButton.textContent = I18N("trackLeaderBoardDataLeaderboardStoreButton");
                    storeButton.style.backgroundColor = "#66CCFF";
                    storeButton.addEventListener("click", function () {
                        // get data
                        const leaderBoardData = {};
                        const tableNode = document.querySelector(".LeaderboardPanel_leaderboardTable__3JLvu");
                        for (const row of tableNode.querySelectorAll("tbody tr")) {
                            const characterNameNode = row.querySelector(".LeaderboardPanel_name__3hpvo").querySelector("span");
                            const guildNameNode = row.querySelector(".LeaderboardPanel_guildName__2RYcC");
                            const name = characterNameNode ? characterNameNode.textContent : guildNameNode.textContent;
                            const valueNode1 = row.querySelector(".LeaderboardPanel_valueColumn1__2HFDb");
                            const valueNode2 = row.querySelector(".LeaderboardPanel_valueColumn2__1ejF2");
                            const value = Number((valueNode2 ? valueNode2.textContent : valueNode1.textContent).replaceAll(",", ""));
                            leaderBoardData[name] = value || 0;
                        }

                        // store data
                        const localData = JSON.parse(localStorage.getItem("ranged_way_idle_leaderboard_data") || "{}");
                        localData[keyString] = {
                            data: leaderBoardData, timestamp: new Date().getTime()
                        };
                        localStorage.setItem("ranged_way_idle_leaderboard_data", JSON.stringify(localData));
                    });
                    noteNode.appendChild(storeButton);

                    // delete data button
                    const deleteDataButton = document.createElement("button");
                    deleteDataButton.textContent = I18N("trackLeaderBoardDataLeaderboardDeleteButton");
                    deleteDataButton.style.backgroundColor = "#F44444";
                    deleteDataButton.addEventListener("click", function () {
                        const localData = JSON.parse(localStorage.getItem("ranged_way_idle_leaderboard_data") || "{}");
                        delete localData[keyString];
                        localStorage.setItem("ranged_way_idle_leaderboard_data", JSON.stringify(localData));
                    });
                    noteNode.appendChild(deleteDataButton);

                    // record time text node
                    const localData = JSON.parse(localStorage.getItem("ranged_way_idle_leaderboard_data") || "{}");
                    const recordTimeTextNode = document.createElement("div");
                    if (localData[keyString]) {
                        const recordTime = new Date(localData[keyString].timestamp);
                        const timeDelta = (new Date().getTime() - localData[keyString].timestamp) / 3600000;
                        recordTimeTextNode.textContent = I18N("trackLeaderBoardDataLeaderboardRecordTimeText", {
                            recordTime: recordTime.toLocaleString(), timeDelta: timeDelta.toFixed(2)
                        });
                    } else {
                        recordTimeTextNode.textContent = I18N("trackLeaderBoardDataLeaderboardNoRecordTimeText");
                    }
                    noteNode.appendChild(recordTimeTextNode);

                    // hint text node
                    const noteTextNode = document.createElement("div");
                    noteTextNode.textContent = I18N("trackLeaderBoardDataNoteText");
                    noteNode.appendChild(noteTextNode);
                }

                function showDifference(leaderBoardContentNode) {
                    const keyString = getCurrentKey();

                    const allStoreData = JSON.parse(localStorage.getItem("ranged_way_idle_leaderboard_data") || "{}");
                    if (!allStoreData || !allStoreData[keyString]) {
                        return;
                    }
                    // expand panel
                    leaderBoardContentNode.style.maxWidth = '60rem';

                    // get current data
                    const localData = allStoreData[keyString].data;
                    const timeDelta = (new Date().getTime() - allStoreData[keyString].timestamp) / 1000;
                    const hourDelta = timeDelta / 3600;

                    const tableNode = leaderBoardContentNode.querySelector(".LeaderboardPanel_leaderboardTable__3JLvu");

                    // head
                    const headNode = tableNode.querySelector("thead").firstChild;
                    const diffNode = document.createElement("th");
                    diffNode.textContent = I18N("trackLeaderBoardDataDifference");
                    headNode.appendChild(diffNode);
                    const speedNode = document.createElement("th");
                    speedNode.textContent = I18N("trackLeaderBoardDataSpeed");
                    headNode.appendChild(speedNode);
                    const catchupTimeNode = document.createElement("th");
                    catchupTimeNode.textContent = I18N("trackLeaderBoardDataCatchupTime");
                    headNode.appendChild(catchupTimeNode);

                    // body
                    let previousRowValue = null;
                    let previousRowSpeed = null;
                    let maxSpeedValue = 0.0;
                    let personalRow = null;
                    let personalName = null;

                    // calculate max speed for set color
                    for (const row of tableNode.querySelectorAll("tbody tr")) {
                        const characterNameNode = row.querySelector(".LeaderboardPanel_name__3hpvo").querySelector("span");
                        const guildNameNode = row.querySelector(".LeaderboardPanel_guildName__2RYcC");
                        const name = characterNameNode ? characterNameNode.textContent : guildNameNode.textContent;
                        const valueNode1 = row.querySelector(".LeaderboardPanel_valueColumn1__2HFDb");
                        const valueNode2 = row.querySelector(".LeaderboardPanel_valueColumn2__1ejF2");
                        const value = Number((valueNode2 ? valueNode2.textContent : valueNode1.textContent).replaceAll(",", ""));
                        if (localData[name]) {
                            const diffValue = value - localData[name];
                            maxSpeedValue = Math.max(maxSpeedValue, diffValue / hourDelta);
                        }
                        if (row.classList.contains("LeaderboardPanel_personal__DZ7Nr")) {
                            personalRow = row;
                            personalName = name;
                        }
                    }

                    for (const row of tableNode.querySelectorAll("tbody tr")) {
                        const characterNameNode = row.querySelector(".LeaderboardPanel_name__3hpvo").querySelector("span");
                        const guildNameNode = row.querySelector(".LeaderboardPanel_guildName__2RYcC");
                        const name = characterNameNode ? characterNameNode.textContent : guildNameNode.textContent;
                        const valueNode1 = row.querySelector(".LeaderboardPanel_valueColumn1__2HFDb");
                        const valueNode2 = row.querySelector(".LeaderboardPanel_valueColumn2__1ejF2");
                        const value = Number((valueNode2 ? valueNode2.textContent : valueNode1.textContent).replaceAll(",", ""));

                        const diffValueNode = document.createElement("td");
                        diffValueNode.classList.add("RangedWayIdleLeaderBoardDiffValue");
                        const speedValueNode = document.createElement("td");
                        speedValueNode.classList.add("RangedWayIdleLeaderBoardSpeedValue");
                        const catchupTimeValueNode = document.createElement("td");
                        catchupTimeValueNode.classList.add("RangedWayIdleLeaderBoardCatchupTimeValue");

                        if (localData[name]) {
                            const diffValue = value - localData[name];
                            diffValueNode.textContent = diffValue.toLocaleString();
                            const speedValue = diffValue / hourDelta;
                            speedValueNode.textContent = formatItemCount(speedValue, 2) + "/h";

                            const k1 = Math.log(1 + (Math.E - 1) * speedValue / maxSpeedValue);
                            diffValueNode.style.color = `rgb(${255 - k1 * 255}, ${k1 * 255}, 0)`;
                            speedValueNode.style.color = `rgb(${255 - k1 * 255}, ${k1 * 255}, 0)`;

                            if (previousRowValue === null || previousRowSpeed === null) {
                                catchupTimeValueNode.textContent = "?????";
                                catchupTimeValueNode.style.color = "#66CCFF";
                            } else {
                                const deltaSpeed = speedValue - previousRowSpeed;
                                if (deltaSpeed === 0) {
                                    if (previousRowValue === value) {
                                        catchupTimeValueNode.textContent = I18N("trackLeaderBoardDataCatchupTimeNow");
                                        catchupTimeValueNode.style.color = "#00FF00";
                                    } else {
                                        catchupTimeValueNode.textContent = "∞";
                                        catchupTimeValueNode.style.color = "#FF0000";
                                    }
                                } else {
                                    const catchupTimeValue = (previousRowValue - value) / deltaSpeed;
                                    if (catchupTimeValue > 0) {
                                        catchupTimeValueNode.textContent = formatItemCount(catchupTimeValue, 2) + "h";
                                        const k2 = 10000 / (10000 + catchupTimeValue * catchupTimeValue);
                                        catchupTimeValueNode.style.color = `rgb(${255 - k2 * 255}, ${k2 * 255}, 0)`;
                                    } else if (catchupTimeValue === 0) {
                                        catchupTimeValueNode.textContent = "?????";
                                        catchupTimeValueNode.style.color = "#66CCFF";
                                    } else {
                                        catchupTimeValueNode.textContent = "∞";
                                        catchupTimeValueNode.style.color = "#FF0000";
                                    }
                                }
                            }
                            previousRowSpeed = speedValue;
                        } else {
                            diffValueNode.textContent = I18N("trackLeaderBoardDataNewRecordText");
                            speedValueNode.textContent = I18N("trackLeaderBoardDataNewRecordText");
                            catchupTimeValueNode.textContent = I18N("trackLeaderBoardDataNewRecordText");
                            diffValueNode.style.color = "#66CCFF";
                            speedValueNode.style.color = "#66CCFF";
                            catchupTimeValueNode.style.color = "#66CCFF";
                            previousRowSpeed = null;
                        }
                        previousRowValue = value;

                        // personal row
                        if (row.classList.contains("LeaderboardPanel_personal__DZ7Nr")) {
                            previousRowValue = null;
                            previousRowSpeed = null;
                        }

                        row.appendChild(diffValueNode);
                        row.appendChild(speedValueNode);
                        row.appendChild(catchupTimeValueNode);

                        if (personalRow && personalName === name) {
                            personalRow.querySelector(".RangedWayIdleLeaderBoardCatchupTimeValue").textContent = catchupTimeValueNode.textContent;
                            personalRow.querySelector(".RangedWayIdleLeaderBoardCatchupTimeValue").style.color = catchupTimeValueNode.style.color;
                        }
                    }
                }

                function ob(node) {
                    const leaderBoardRootNode = node.querySelector(".LeaderboardPanel_leaderboardPanel__19U0W");
                    if (!leaderBoardRootNode) return;
                    const noteNode = leaderBoardRootNode.querySelector(".LeaderboardPanel_note__z4OpJ");
                    if (!noteNode) return;

                    // make note and buttons
                    if (noteNode.classList.contains("RangedWayIdleLeaderBoardNote")) return;
                    noteNode.classList.add("RangedWayIdleLeaderBoardNote");
                    createNoteAndButton(noteNode);

                    // show difference
                    const leaderBoardContentNode = leaderBoardRootNode.querySelector(".LeaderboardPanel_content__p_WNw");
                    showDifference(leaderBoardContentNode);
                }

                return {ob: ob};
            }

            return {
                initCharacterData: initCharacterData,
                updateLocalStorageMarketPrice: updateLocalStorageMarketPrice,
                showTaskValue: showTaskValue,
                trackLeaderBoardData: trackLeaderBoardData
            }
        }

        gameUIClass() {
            function autoClickTaskSortButton() {
                function ob(node) {
                    const buttonNode = node.querySelector('#TaskSort');
                    if (!buttonNode || buttonNode.classList.contains("RangedWayIdleAutoClicked")) return;
                    buttonNode.click();
                    buttonNode.classList.add("RangedWayIdleAutoClicked");
                }

                return {ob: ob};
            }

            function showMarketAPIUpdateTime() {
                let lastTime = 0;

                function ob(node) {
                    const buttonContainerNode = node.querySelector(".MarketplacePanel_buttonContainer__vJQud");
                    if (!buttonContainerNode) return;
                    const nowTime = JSON.parse(localStorage.getItem('MWITools_marketAPI_json')).timestamp;
                    const lastNode = buttonContainerNode.querySelector(".RangedWayIdleShowMarketAPIUpdateTime");
                    if (nowTime === lastTime) return;
                    if (lastNode) lastNode.remove();
                    lastTime = nowTime;
                    const divNode = document.createElement("div");
                    divNode.textContent = I18N("showMarketAPIUpdateTimeText") + " " + new Date(nowTime * 1000).toLocaleString();
                    divNode.style.color = "rgb(102,204,255)";
                    divNode.classList.add("RangedWayIdleShowMarketAPIUpdateTime");
                    buttonContainerNode.insertBefore(divNode, buttonContainerNode.lastChild);
                }

                return {ob: ob};
            }

            function forceUpdateAPIButton() {
                function ob(node) {
                    const listingContainerNode = node.querySelector(".MarketplacePanel_listingCount__3nVY_");
                    if (!listingContainerNode || !listingContainerNode.querySelector("button")) return;
                    if (listingContainerNode.querySelector(".RangedWayIdleForceUpdateAPIButton")) return;
                    const buttonNode = listingContainerNode.querySelector("button").cloneNode(true);
                    buttonNode.classList.add("RangedWayIdleForceUpdateAPIButton");
                    buttonNode.textContent = I18N("forceUpdateAPIButtonText");
                    buttonNode.addEventListener("click", async function () {
                        if (GM && GM.xmlHttpRequest) {
                            GM.xmlHttpRequest({
                                method: 'GET', url: globalVariables.marketAPIUrl, onload: function (response) {
                                    const text = response.responseText;
                                    localStorage.setItem("MWITools_marketAPI_json", text);
                                    alert(I18N("forceUpdateAPIButtonTextSuccess") + new Date(JSON.parse(text).timestamp * 1000).toLocaleString());
                                }, onerror: function (err) {
                                    alert(I18N("forceUpdateAPIButtonTextError"));
                                    console.error(err);
                                }, ontimeout: function () {
                                    alert(I18N("forceUpdateAPIButtonTextTimeout"));
                                    console.error('timeout');
                                }
                            });
                        } else {
                            const resp = await fetch(globalVariable.marketURL);
                            const text = await resp.text();
                            localStorage.setItem("MWITools_marketAPI_json", text);
                            alert(I18N("forceUpdateAPIButtonTextSuccess") + new Date(JSON.parse(text).timestamp * 1000).toLocaleString());
                        }
                    });
                    listingContainerNode.appendChild(buttonNode);
                }

                return {ob: ob};
            }

            function disableQueueUpgradeButton() {
                const disabledButtons = [];

                function ob(node) {
                    const buttons = node.querySelectorAll("button");
                    for (const button of buttons) {
                        if ((button.textContent === "Upgrade Queue Capacity" || button.textContent === "升级行动队列") && !button.disabled) {
                            button.disabled = true;
                            disabledButtons.push(button);
                        }
                    }
                    for (let i = disabledButtons.length - 1; i >= 0; i--) {
                        const button = disabledButtons[i];
                        if (!button.isConnected || (button.textContent !== "Upgrade Queue Capacity" && button.textContent !== "升级行动队列")) {
                            button.disabled = false;
                            disabledButtons.splice(i, 1);
                        }
                    }
                }

                return {ob: ob};
            }

            function disableActionQueueBar() {
                function ob(node) {
                    const actionQueueBarNode = node.querySelector(".QueuedActions_queuedActionsEditMenu__3OoQH");
                    if (!actionQueueBarNode) return;
                    const buttonNode = node.querySelector(".QueuedActions_queuedActions__2xerL ");
                    buttonNode.click();
                }

                return {ob: ob};
            }

            function hideSideBarButton() {
                let hideConfigs = null;
                let hasInit = false;

                function hideSideBar() {
                    const sideBarRootNode = document.querySelector(".NavigationBar_navigationLinks__1XSSb");
                    if (!sideBarRootNode) return false;
                    for (const sideBarNode of sideBarRootNode.querySelectorAll(".NavigationBar_navigationLink__3eAHA ")) {
                        for (const useNode of sideBarNode.querySelectorAll("use")) {
                            if (hideConfigs[useNode.href.baseVal] !== undefined) {
                                sideBarNode.style.display = hideConfigs[useNode.href.baseVal] ? "none" : "block";
                                break;
                            }
                        }
                    }
                    return true;
                }

                function showConfigMenu(node) {
                    // add this after config menu
                    const configMenuRootNode = node.querySelector(".RangedWayIdleConfigMenuRoot");
                    if (!configMenuRootNode) return;
                    if (configMenuRootNode.parentNode.querySelector(".RangedWayIdleHideSidebar")) return;
                    const divRootNode = document.createElement("div");
                    divRootNode.appendChild(document.createElement("div"));
                    divRootNode.firstChild.textContent = I18N("hideSidebarText");
                    divRootNode.firstChild.style.fontSize = "1.5rem";
                    divRootNode.classList.add("RangedWayIdleHideSidebar");

                    for (const key in hideConfigs) {
                        const svgNode = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                        const useNode = document.createElementNS('http://www.w3.org/2000/svg', 'use');
                        useNode.setAttributeNS('http://www.w3.org/1999/xlink', 'href', key);
                        svgNode.appendChild(useNode);
                        svgNode.style.width = "2.5rem";
                        svgNode.style.height = "2.5rem";
                        svgNode.style.opacity = hideConfigs[key] ? "0.25" : "1";
                        svgNode.onclick = function () {
                            hideConfigs[key] = !hideConfigs[key];
                            svgNode.style.opacity = hideConfigs[key] ? "0.25" : "1";
                            localStorage.setItem("ranged_way_idle_hide_sidebar_config", JSON.stringify(hideConfigs));
                            hideSideBar();
                        };
                        divRootNode.appendChild(svgNode);
                    }
                    configMenuRootNode.insertAdjacentElement("afterend", divRootNode);
                }

                function ob(node) {
                    // init hide svg
                    if (hideConfigs === null) {
                        const sideBarRootNode = node.querySelector(".NavigationBar_navigationLinks__1XSSb");
                        if (!sideBarRootNode) return;
                        const localConfigs = JSON.parse(localStorage.getItem("ranged_way_idle_hide_sidebar_config") || "{}");
                        hideConfigs = {};
                        for (const sideBarNode of sideBarRootNode.querySelectorAll(".NavigationBar_navigationLink__3eAHA ")) {
                            const useNode = sideBarNode.querySelector("use");
                            if (useNode.href.baseVal.includes("triangle_")) {
                                // combat
                                const link = "/static/media/misc_sprite.6fa5e97c.svg#combat";
                                hideConfigs[link] = localConfigs[link] || false;
                            } else if (!useNode.href.baseVal.includes("settings")) {
                                // cannot hide settings
                                hideConfigs[useNode.href.baseVal] = localConfigs[useNode.href.baseVal] || false;
                            }
                        }
                    }
                    showConfigMenu(node);
                    if (!hasInit) hasInit = hideSideBar();
                }

                function ws(obj) {
                    if (obj.type === "init_character_data") {
                        hasInit = false;
                    }
                }

                return {ob: ob, ws: ws};
            }

            function hideTrainRubbishButton() {
                const rubbishNames = [];
                for (const a of ['cheese', 'verdant', 'azure', 'burble', 'crimson', 'rainbow']) {
                    for (const b of ['brush', 'shears', 'hatchet', 'hammer', 'chisel', 'needle', 'spatula', 'pot', 'alembic', 'enhancer', 'sword', 'spear', 'mace', 'bulwark', 'buckler', 'boots', 'gauntlets', 'helmet', 'plate_legs', 'plate_body',]) {
                        rubbishNames.push(`${a}_${b}`);
                    }
                }
                for (const a of ['wooden', 'birch', 'cedar', 'purpleheart', 'ginkgo', 'redwood']) {
                    for (const b of ['crossbow', 'bow', 'water_staff', 'nature_staff', 'fire_staff', 'shield']) {
                        rubbishNames.push(`${a}_${b}`);
                    }
                }
                for (const a of ['rough', 'reptile', 'gobo', 'beast']) {
                    for (const b of ['boots', 'bracers', 'hood', 'chaps', 'tunic']) {
                        rubbishNames.push(`${a}_${b}`);
                    }
                }
                for (const a of ['cotton', 'linen', 'bamboo', 'silk']) {
                    for (const b of ['boots', 'gloves', 'hat', 'robe_bottoms', 'robe_top']) {
                        rubbishNames.push(`${a}_${b}`);
                    }
                }

                function hide(inventoryNode) {
                    for (const itemContainerNode of inventoryNode.querySelectorAll(".Item_itemContainer__x7kH1")) {
                        const itemName = itemContainerNode.querySelector("use").href.baseVal.split("#")[1];
                        const isNotEnhanced = !itemContainerNode.querySelector(".Item_enhancementLevel__19g-e");
                        if (rubbishNames.includes(itemName) && isNotEnhanced) {
                            itemContainerNode.style.display = "none";
                        }
                    }
                }

                function show(inventoryNode) {
                    if (configs.gameUIClass.alwaysHideTrainRubbish.value) return;
                    for (const itemContainerNode of inventoryNode.querySelectorAll(".Item_itemContainer__x7kH1")) {
                        itemContainerNode.style.display = "block";
                    }
                }

                function ob(node) {
                    for (const inventoryNode of node.querySelectorAll(".Inventory_inventory__17CH2")) {
                        if (configs.gameUIClass.alwaysHideTrainRubbish.value) hide(inventoryNode);
                        if (inventoryNode.querySelector(".RangedWayIdleHideTrainRubbishButton")) continue;
                        const hideButtonNode = document.createElement("button");
                        hideButtonNode.textContent = I18N("hideTrainRubbishButtonText");
                        hideButtonNode.style.backgroundColor = "#66CCFF";
                        hideButtonNode.classList.add("RangedWayIdleHideTrainRubbishButton");
                        hideButtonNode.addEventListener("click", () => hide(inventoryNode));

                        const showButtonNode = document.createElement("button");
                        showButtonNode.textContent = I18N("showTrainRubbishButtonText");
                        showButtonNode.style.backgroundColor = "#66CCFF";
                        showButtonNode.addEventListener("click", () => show(inventoryNode));

                        inventoryNode.insertBefore(showButtonNode, inventoryNode.firstChild);
                        inventoryNode.insertBefore(hideButtonNode, inventoryNode.firstChild);
                    }
                }

                return {ob: ob};

            }

            return {
                autoClickTaskSortButton: autoClickTaskSortButton,
                showMarketAPIUpdateTime: showMarketAPIUpdateTime,
                forceUpdateAPIButton: forceUpdateAPIButton,
                disableQueueUpgradeButton: disableQueueUpgradeButton,
                disableActionQueueBar: disableActionQueueBar,
                hideSideBarButton: hideSideBarButton,
                hideTrainRubbishButton: hideTrainRubbishButton,
            }
        }

        listingClass() {
            function hookListingInfo() {
                function handleListing(listing) {
                    if (listing.status === "/market_listing_status/cancelled" || (listing.status === "/market_listing_status/filled" && listing.unclaimedItemCount === 0 && listing.unclaimedCoinCount === 0)) {
                        delete globalVariables.allListings[listing.id];
                        return;
                    }
                    globalVariables.allListings[listing.id] = {
                        id: listing.id,
                        isSell: listing.isSell,
                        itemHrid: listing.itemHrid,
                        enhancementLevel: listing.enhancementLevel,
                        orderQuantity: listing.orderQuantity,
                        filledQuantity: listing.filledQuantity,
                        price: listing.price,
                        coinsAvailable: listing.coinsAvailable,
                        unclaimedItemCount: listing.unclaimedItemCount,
                        unclaimedCoinCount: listing.unclaimedCoinCount,
                        createdTimestamp: listing.createdTimestamp,
                    }
                }

                function saveListings() {
                    const obj = JSON.parse(localStorage.getItem('ranged_way_idle_market_listings') || "{}");
                    const characterId = globalVariables.initCharacterData.character.id;
                    if (!obj[characterId]) obj[characterId] = {};
                    for (const listingId in globalVariables.allListings) {
                        if (obj[characterId][listingId]) continue;
                        const listing = globalVariables.allListings[listingId];
                        obj[characterId][listingId] = {
                            id: listing.id,
                            isSell: listing.isSell,
                            itemHrid: listing.itemHrid,
                            enhancementLevel: listing.enhancementLevel,
                            orderQuantity: listing.orderQuantity,
                            filledQuantity: listing.filledQuantity,
                            price: listing.price,
                            createdTimestamp: listing.createdTimestamp,
                        }
                    }
                    const nowTime = new Date().getTime();
                    for (const listingId in obj[characterId]) {
                        const listing = obj[characterId][listingId];
                        if (nowTime - new Date(listing.createdTimestamp).getTime() > configs.listingClass.saveListingInfoToLocalStorageMaxDays * 24 * 60 * 60 * 1000) {
                            delete obj[characterId][listingId];
                        }
                    }
                    localStorage.setItem('ranged_way_idle_market_listings', JSON.stringify(obj));
                }

                function ws(obj) {
                    if (obj.type === "init_character_data") {
                        for (const listing of obj.myMarketListings) {
                            handleListing(listing);
                        }
                        if (configs.listingClass.saveListingInfoToLocalStorage.value) {
                            saveListings();
                        }
                    } else if (obj.type === "market_listings_updated") {
                        for (const listing of obj.endMarketListings) {
                            handleListing(listing);
                        }
                        if (configs.listingClass.saveListingInfoToLocalStorage.value) {
                            saveListings();
                        }
                    }
                }

                return {ws: ws};
            }

            function showTotalListingFunds() {
                function ws(obj) {
                    if (obj.type === "market_listings_updated") {
                        document.querySelectorAll(".RangedWayIdleTotalListingFunds").forEach(node => {
                            node.remove();
                        });
                    }
                }

                function ob(node) {
                    const marketplacePanelNode = node.querySelector(".MarketplacePanel_marketplacePanel__21b7o");
                    if (!marketplacePanelNode) return;
                    if (marketplacePanelNode.querySelector(".RangedWayIdleTotalListingFunds")) return;

                    let totalUnclaimedCoins = 0;
                    let totalPrepaidCoins = 0;
                    let totalSellResultCoins = 0;

                    for (const listing of Object.values(globalVariables.allListings)) {
                        totalUnclaimedCoins += listing.unclaimedCoinCount;
                        totalPrepaidCoins += listing.coinsAvailable;
                        if (listing.isSell) {
                            const tax = listing.itemHrid === "/items/bag_of_10_cowbells" ? 0.82 : 0.98;
                            totalSellResultCoins += (listing.orderQuantity - listing.filledQuantity) * Math.floor(listing.price * tax)
                        }
                    }

                    const currentCoinNode = marketplacePanelNode.querySelector(".MarketplacePanel_coinStack__1l0UD");

                    const totalUnclaimedCoinsNode = currentCoinNode.cloneNode(true);
                    const totalPrepaidCoinsNode = currentCoinNode.cloneNode(true);
                    const totalSellResultCoinsNode = currentCoinNode.cloneNode(true);

                    totalUnclaimedCoinsNode.querySelector(".Item_count__1HVvv").textContent = formatItemCount(totalUnclaimedCoins, configs.listingClass.showTotalListingFundsPrecise.value);
                    totalPrepaidCoinsNode.querySelector(".Item_count__1HVvv").textContent = formatItemCount(totalPrepaidCoins, configs.listingClass.showTotalListingFundsPrecise.value);
                    totalSellResultCoinsNode.querySelector(".Item_count__1HVvv").textContent = formatItemCount(totalSellResultCoins, configs.listingClass.showTotalListingFundsPrecise.value);

                    totalUnclaimedCoinsNode.querySelector(".Item_name__2C42x").textContent = I18N("totalUnclaimedCoinsText");
                    totalPrepaidCoinsNode.querySelector(".Item_name__2C42x").textContent = I18N("totalPrepaidCoinsText");
                    totalSellResultCoinsNode.querySelector(".Item_name__2C42x").textContent = I18N("totalSellResultCoinsText");

                    totalUnclaimedCoinsNode.querySelector(".Item_name__2C42x").style.color = "#66CCFF";
                    totalPrepaidCoinsNode.querySelector(".Item_name__2C42x").style.color = "#66CCFF";
                    totalSellResultCoinsNode.querySelector(".Item_name__2C42x").style.color = "#66CCFF";

                    currentCoinNode.style.left = "0rem";
                    currentCoinNode.style.top = "0rem";
                    totalUnclaimedCoinsNode.style.left = "0rem";
                    totalUnclaimedCoinsNode.style.top = "1.5rem";
                    totalPrepaidCoinsNode.style.left = "8rem";
                    totalPrepaidCoinsNode.style.top = "0rem";
                    totalSellResultCoinsNode.style.left = "8rem";
                    totalSellResultCoinsNode.style.top = "1.5rem";

                    totalUnclaimedCoinsNode.classList.add("RangedWayIdleTotalListingFunds");
                    totalPrepaidCoinsNode.classList.add("RangedWayIdleTotalListingFunds");
                    totalSellResultCoinsNode.classList.add("RangedWayIdleTotalListingFunds");

                    marketplacePanelNode.insertBefore(totalUnclaimedCoinsNode, currentCoinNode.nextSibling);
                    marketplacePanelNode.insertBefore(totalPrepaidCoinsNode, currentCoinNode.nextSibling);
                    marketplacePanelNode.insertBefore(totalSellResultCoinsNode, currentCoinNode.nextSibling);
                }

                return {ws: ws, ob: ob}
            }

            function showListingInfo() {
                const allCreateTimeNodes = [];
                let intervalId = null;

                function formatUTCTime(date) {
                    return I18N("showListingInfoCreateTimeAt") + " " + date.toLocaleString('en-US', {
                        month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false
                    }).replace(/\//g, '-').replace(',', '');
                }

                function formatLifespan(date) {
                    const diffMs = new Date() - date;
                    const seconds = Math.floor(diffMs / 1000);
                    const minutes = Math.floor(seconds / 60);
                    const hours = Math.floor(minutes / 60);
                    const days = Math.floor(hours / 24);
                    return I18N("showListingInfoCreateTimeLifespan", {
                        days: days, hours: hours % 24, minutes: minutes % 60, seconds: seconds % 60
                    });
                }

                function handleTableHead(trNode) {
                    const topOrderPriceNode = document.createElement("th");
                    topOrderPriceNode.classList.add("RangedWayIdleShowListingInfo");
                    const totalPriceNode = document.createElement("th");
                    totalPriceNode.classList.add("RangedWayIdleShowListingInfo");

                    topOrderPriceNode.textContent = I18N("showListingInfoTopOrderPriceText");
                    totalPriceNode.textContent = I18N("showListingInfoTotalPriceText");

                    trNode.insertBefore(topOrderPriceNode, trNode.children[4]);
                    trNode.insertBefore(totalPriceNode, trNode.children[5]);
                }

                function addDataToRows(bodyNode) {
                    let index = Object.keys(globalVariables.allListings).length - 1;
                    for (const listingId in globalVariables.allListings) {
                        const trNode = bodyNode.childNodes[index];
                        for (const key in globalVariables.allListings[listingId]) {
                            trNode.dataset[key] = globalVariables.allListings[listingId][key];
                        }
                        trNode.dataset.originalIndex = index;
                        index--;
                    }
                }

                function handleTableBody(tbodyNode) {
                    const localMarketAPIJson = JSON.parse(localStorage.getItem('MWITools_marketAPI_json'));
                    for (const trNode of tbodyNode.querySelectorAll("tr")) {
                        const dataSet = trNode.dataset;

                        // top order price
                        const topOrderPriceNode = document.createElement("td");
                        topOrderPriceNode.classList.add("RangedWayIdleShowListingInfo");
                        const topOrderPriceSpanNode = document.createElement("span");
                        topOrderPriceSpanNode.classList.add("RangedWayIdleShowListingInfo");
                        const itemHrid = dataSet.itemHrid;
                        const enhancementLevel = Number(dataSet.enhancementLevel);
                        const isSell = dataSet.isSell === 'true';
                        const price = Number(dataSet.price);
                        let localPrice = null;
                        try {
                            localPrice = localMarketAPIJson.marketData[itemHrid][enhancementLevel][isSell ? "a" : "b"];
                        } catch (e) {
                        }
                        if (localPrice === -1) localPrice = null;
                        topOrderPriceSpanNode.textContent = formatItemCount(localPrice, 1);
                        if (localPrice === null) {
                            topOrderPriceSpanNode.style.color = "#004FFF";
                        } else if (isSell) {
                            topOrderPriceSpanNode.style.color = localPrice < price ? "#FF0000" : "#00FF00";
                        } else {
                            topOrderPriceSpanNode.style.color = localPrice > price ? "#FF0000" : "#00FF00";
                        }
                        topOrderPriceNode.appendChild(topOrderPriceSpanNode);
                        trNode.insertBefore(topOrderPriceNode, trNode.children[4]);

                        // total price
                        const totalPriceNode = document.createElement("td");
                        totalPriceNode.classList.add("RangedWayIdleShowListingInfo");
                        const totalPriceSpanNode = document.createElement("span");
                        totalPriceSpanNode.classList.add("RangedWayIdleShowListingInfo");
                        const orderQuantity = Number(dataSet.orderQuantity);
                        const filledQuantity = Number(dataSet.filledQuantity);
                        const tax = isSell ? (itemHrid === "/items/bag_of_10_cowbells" ? 0.82 : 0.98) : 1.0;
                        const totalPrice = (orderQuantity - filledQuantity) * Math.floor(price * tax);
                        totalPriceSpanNode.textContent = formatItemCount(totalPrice, configs.listingClass.showListingPricePrecise.value);
                        totalPriceSpanNode.style.color = itemCountColorMap(totalPrice);
                        totalPriceNode.appendChild(totalPriceSpanNode);
                        trNode.insertBefore(totalPriceNode, trNode.children[5]);

                        // add create time
                        const createTimeNode = document.createElement("div");
                        createTimeNode.classList.add("RangedWayIdleShowListingInfo");
                        createTimeNode.style.fontSize = '0.75rem';
                        if (configs.listingClass.showListingCreateTimeByLifespan.value) {
                            createTimeNode.textContent = formatLifespan(new Date(dataSet.createdTimestamp));
                            allCreateTimeNodes.push(createTimeNode);
                        } else {
                            createTimeNode.textContent = formatUTCTime(new Date(dataSet.createdTimestamp));
                        }
                        createTimeNode.style.color = "gray";
                        trNode.firstChild.appendChild(createTimeNode);
                    }
                }

                function updateLifespan() {
                    if (!configs.listingClass.showListingCreateTimeByLifespan.value) {
                        allCreateTimeNodes.length = 0;
                        if (intervalId !== null) {
                            resetAll();
                            clearInterval(intervalId);
                            intervalId = null;
                        }
                        return;
                    }
                    allCreateTimeNodes.forEach(node => {
                        if (!node.isConnected) {
                            allCreateTimeNodes.splice(allCreateTimeNodes.indexOf(node), 1);
                            node.remove();
                            return;
                        }
                        const newText = formatLifespan(new Date(node.parentNode.parentNode.dataset.createdTimestamp));
                        if (newText !== node.textContent) {
                            node.textContent = newText;
                        }
                    });
                    if (intervalId === null) {
                        resetAll();
                        intervalId = setInterval(updateLifespan, 250);
                    }
                }

                function resetAll() {
                    const myListingTableNode = document.querySelector(".MarketplacePanel_myListingsTable__3P1aT");
                    if (!myListingTableNode) return;
                    const bodyNode = myListingTableNode.querySelector("tbody");
                    if (!bodyNode) return;
                    if (!configs.listingClass.listingSortCompatible.value) {
                        const sortedChildren = Array.from(bodyNode.childNodes).sort((a, b) => parseInt(b.dataset.id) - parseInt(a.dataset.id));
                        sortedChildren.forEach(node => bodyNode.appendChild(node));
                    }
                    myListingTableNode.classList.remove("RangedWayIdleShowListingInfoSet");
                    document.querySelectorAll(".RangedWayIdleShowListingInfo").forEach(node => {
                        node.remove();
                    });
                }

                function ws(obj) {
                    if (obj.type === "market_listings_updated") {
                        resetAll();
                    } else if (obj.type === "market_item_order_books_updated") {
                        resetAll();
                    }
                }

                function ob(node) {
                    updateLifespan();
                    const myListingTableNode = node.querySelector(".MarketplacePanel_myListingsTable__3P1aT");
                    if (!myListingTableNode) return;
                    if (myListingTableNode.classList.contains("RangedWayIdleShowListingInfoSet")) return;
                    if (myListingTableNode.querySelectorAll("tbody tr").length !== Object.keys(globalVariables.allListings).length) {
                        return;
                    }
                    myListingTableNode.classList.add("RangedWayIdleShowListingInfoSet");

                    handleTableHead(myListingTableNode.querySelector("thead tr"));
                    addDataToRows(myListingTableNode.querySelector("tbody"));
                    handleTableBody(myListingTableNode.querySelector("tbody"));
                }

                return {ws: ws, ob: ob};
            }

            function notifyListingFilled() {
                function ws(obj) {
                    if (obj.type === "market_listings_updated") {
                        for (const listing of obj.endMarketListings) {
                            if (listing.status === "/market_listing_status/filled" && (listing.unclaimedCoinCount || listing.unclaimedItemCount)) {
                                globalVariables.notifyListingFilledAudio.volume = configs.listingClass.notifyListingFilledVolume.value;
                                globalVariables.notifyListingFilledAudio.play();
                                return;
                            }
                        }
                    }
                }

                return {ws: ws};
            }

            function estimateListingCreateTime() {
                let lastMarketItemOrderBooks = null;

                function formatLifespan(date) {
                    const diffMs = new Date() - date;
                    const seconds = Math.floor(diffMs / 1000);
                    const minutes = Math.floor(seconds / 60);
                    const hours = Math.floor(minutes / 60);
                    const days = Math.floor(hours / 24);
                    return I18N("estimateListingCreateTimeLifespan", {
                        days: days, hours: hours % 24, minutes: minutes % 60
                    });
                }

                function formatUTCTime(date) {

                    return date.toLocaleString('en-US', {
                        month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false
                    }).replace(/\//g, '-').replace(',', '').replace('24:', '00:');
                }

                function getListingData() {
                    // author's data
                    const data = [{id: 97888637, timestamp: 1760266805648}, {
                        id: 98545826,
                        timestamp: 1760496508616
                    }, {id: 98724734, timestamp: 1760551920380}, {id: 98978743, timestamp: 1760637750329}];
                    const localListings = JSON.parse(localStorage.getItem('ranged_way_idle_market_listings'));
                    if (localListings) {
                        for (const characterId in localListings) {
                            for (const listingId in localListings[characterId]) {
                                const listing = localListings[characterId][listingId];
                                data.push({id: listing.id, timestamp: new Date(listing.createdTimestamp).getTime()});
                            }
                        }
                    } else {
                        for (const listing of Object.values(globalVariables.allListings)) {
                            data.push({id: listing.id, timestamp: new Date(listing.createdTimestamp).getTime()});
                        }
                    }
                    return [...data].sort((a, b) => a.id - b.id);
                }

                function estimateCreateTime(sortedData, id) {
                    const minId = sortedData[0].id;
                    const maxId = sortedData[sortedData.length - 1].id;
                    if (minId <= id && id <= maxId) {
                        return linearInterpolationEstimate();
                    } else {
                        return linearRegressionEstimate();
                    }

                    function linearInterpolationEstimate() {
                        let leftIndex = 0;
                        let rightIndex = sortedData.length - 1;
                        for (let i = 0; i < sortedData.length; i++) {
                            if (sortedData[i].id === id) {
                                return sortedData[i].timestamp;
                            }
                        }
                        for (let i = 0; i < sortedData.length - 1; i++) {
                            if (id >= sortedData[i].id && id <= sortedData[i + 1].id) {
                                leftIndex = i;
                                rightIndex = i + 1;
                                break;
                            }
                        }
                        const left = sortedData[leftIndex];
                        const right = sortedData[rightIndex];
                        const rightLeftDistance = right.id - left.id;
                        const leftDistance = id - left.id;
                        const k = leftDistance / rightLeftDistance;
                        return (1 - k) * left.timestamp + k * right.timestamp;
                    }

                    function linearRegressionEstimate() {
                        let sumX = 0, sumY = 0;
                        for (const point of sortedData) {
                            sumX += point.id;
                            sumY += point.timestamp;
                        }
                        const meanX = sumX / sortedData.length;
                        const meanY = sumY / sortedData.length;
                        let numerator = 0;
                        let denominator = 0;
                        for (const datum of sortedData) {
                            numerator += (datum.id - meanX) * (datum.timestamp - meanY);
                            denominator += (datum.id - meanX) * (datum.id - meanX);
                        }
                        const slope = numerator / denominator;
                        if (id > maxId) {
                            return slope * (id - maxId) + sortedData[sortedData.length - 1].timestamp;
                        } else {
                            return slope * (id - minId) + sortedData[0].timestamp;
                        }
                    }
                }

                function colorByAccuracy(sortedData, timestamp) {
                    const timeDelta = Math.min(...sortedData.map(item => Math.abs(item.timestamp - timestamp)));
                    return Math.max(1 - timeDelta / 43200_000, 0.0);
                }

                function colorByLifespan(sortedData, timestamp) {
                    const timeDelta = Math.max(new Date().getTime() - timestamp, 0);
                    const meanTime = 172800_000;
                    return (meanTime * meanTime) / (meanTime * meanTime + timeDelta * timeDelta);
                }

                function ws(obj) {
                    if (obj.type === "market_item_order_books_updated") {
                        const shouldRemove = lastMarketItemOrderBooks ? obj.marketItemOrderBooks.itemHrid === lastMarketItemOrderBooks.itemHrid : false;
                        lastMarketItemOrderBooks = obj.marketItemOrderBooks;
                        if (shouldRemove) {
                            document.querySelectorAll(".RangedWayIdleEstimateListingCreateTimeSet").forEach(node => node.classList.remove("RangedWayIdleEstimateListingCreateTimeSet"));
                        }
                    }
                }

                function ob(node) {
                    const targetItemNode = node.querySelector(".MarketplacePanel_currentItem__3ercC");
                    if (!targetItemNode) return;
                    if (node.querySelector(".RangedWayIdleEstimateListingCreateTimeSet")) return;
                    document.querySelectorAll(".RangedWayIdleEstimateListingCreateTime").forEach(node => {
                        node.remove();
                    });

                    const itemHrid = "/items/" + targetItemNode.querySelector("use").href.baseVal.split('#')[1];
                    const enhanceLevelNode = targetItemNode.querySelector(".Item_enhancementLevel__19g-e");
                    const enhanceLevel = enhanceLevelNode ? Number(enhanceLevelNode.textContent.substring(1)) : 0;
                    if (itemHrid !== lastMarketItemOrderBooks.itemHrid) return;

                    const listingContainer = node.querySelector(".MarketplacePanel_orderBooksContainer__B4YE-");
                    const askContainer = listingContainer ? listingContainer.childNodes[0] : node.querySelectorAll(".MarketplacePanel_orderBookTableContainer__hUu-X")[0];
                    const bidContainer = listingContainer ? listingContainer.childNodes[1] : node.querySelectorAll(".MarketplacePanel_orderBookTableContainer__hUu-X")[1];
                    if (!askContainer || !bidContainer) return;
                    if (!askContainer || !bidContainer) return;
                    const askTable = askContainer.querySelector("table");
                    const bidTable = bidContainer.querySelector("table");
                    if (!askTable || !bidTable) return;
                    if (askTable.querySelector("tbody").childNodes.length !== lastMarketItemOrderBooks.orderBooks[enhanceLevel].asks.length || bidTable.querySelector("tbody").childNodes.length !== lastMarketItemOrderBooks.orderBooks[enhanceLevel].bids.length) {
                        return;
                    }
                    askContainer.classList.add("RangedWayIdleEstimateListingCreateTimeSet");
                    bidContainer.classList.add("RangedWayIdleEstimateListingCreateTimeSet");

                    // head
                    const askTimeHead = document.createElement("th");
                    askTimeHead.classList.add("RangedWayIdleEstimateListingCreateTime");
                    const bidTimeHead = document.createElement("th");
                    bidTimeHead.classList.add("RangedWayIdleEstimateListingCreateTime");
                    askTimeHead.textContent = I18N("estimateListingCreateTimeText");
                    bidTimeHead.textContent = I18N("estimateListingCreateTimeText");
                    askTable.querySelector("thead tr").insertBefore(askTimeHead, askTable.querySelector("thead tr").lastChild);
                    bidTable.querySelector("thead tr").insertBefore(bidTimeHead, bidTable.querySelector("thead tr").lastChild);


                    // body
                    const sortedData = getListingData();
                    let askIndex = 0, bidIndex = 0;
                    for (const row of askTable.querySelectorAll("tbody tr")) {
                        const listingId = lastMarketItemOrderBooks.orderBooks[enhanceLevel].asks[askIndex].listingId;
                        const estimatedTime = estimateCreateTime(sortedData, listingId);
                        const node = document.createElement("td");
                        node.classList.add("RangedWayIdleEstimateListingCreateTime");
                        if (configs.listingClass.estimateListingCreateTimeByLifespan.value) {
                            node.textContent = formatLifespan(new Date(estimatedTime));
                        } else {
                            node.textContent = formatUTCTime(new Date(estimatedTime));
                        }
                        if (configs.listingClass.estimateListingCreateTimeColorByAccuracy.value) {
                            const k = colorByAccuracy(sortedData, estimatedTime);
                            node.style.color = `rgb(${255 - k * 255}, ${k * 255}, 0)`;
                        } else if (configs.listingClass.estimateListingCreateTimeColorByLifespan.value) {
                            const k = colorByLifespan(sortedData, estimatedTime);
                            node.style.color = `rgb(${255 - k * 255}, ${k * 255}, 0)`;
                        }
                        row.insertBefore(node, row.lastChild);
                        askIndex++;
                    }
                    for (const row of bidTable.querySelectorAll("tbody tr")) {
                        const listingId = lastMarketItemOrderBooks.orderBooks[enhanceLevel].bids[bidIndex].listingId;
                        const estimatedTime = estimateCreateTime(sortedData, listingId)
                        const node = document.createElement("td");
                        node.classList.add("RangedWayIdleEstimateListingCreateTime");
                        if (configs.listingClass.estimateListingCreateTimeByLifespan.value) {
                            node.textContent = formatLifespan(new Date(estimatedTime));
                        } else {
                            node.textContent = formatUTCTime(new Date(estimatedTime));
                        }
                        if (configs.listingClass.estimateListingCreateTimeColorByAccuracy.value) {
                            const k = colorByAccuracy(sortedData, estimatedTime);
                            node.style.color = `rgb(${255 - k * 255}, ${k * 255}, 0)`;
                        } else if (configs.listingClass.estimateListingCreateTimeColorByLifespan.value) {
                            const k = colorByLifespan(sortedData, estimatedTime);
                            node.style.color = `rgb(${255 - k * 255}, ${k * 255}, 0)`;
                        }
                        row.insertBefore(node, row.lastChild);
                        bidIndex++;
                    }

                }

                return {ws: ws, ob: ob};
            }

            return {
                hookListingInfo: hookListingInfo,
                showTotalListingFunds: showTotalListingFunds,
                showListingInfo: showListingInfo,
                notifyListingFilled: notifyListingFilled,
                estimateListingCreateTime: estimateListingCreateTime
            }
        }

        otherClass() {
            function showSponsor() {
                const imageURL = "https://tupian.li/images/2025/10/26/68fdddfbe6b75.png";

                function showImage() {
                    const img = document.createElement('img');
                    img.src = imageURL;
                    img.style.position = 'fixed';
                    img.style.top = '50%';
                    img.style.left = '50%';
                    img.style.transform = 'translate(-50%, -50%)';
                    img.style.maxWidth = '90%';
                    img.style.maxHeight = '90%';
                    img.style.zIndex = '1000';

                    const overlay = document.createElement('div');
                    overlay.style.position = 'fixed';
                    overlay.style.top = '0';
                    overlay.style.left = '0';
                    overlay.style.width = '100%';
                    overlay.style.height = '100%';
                    overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
                    overlay.style.zIndex = '999';
                    img.addEventListener('click', () => {
                        img.remove();
                        overlay.remove();
                    });
                    overlay.addEventListener('click', () => {
                        img.remove();
                        overlay.remove();
                    });
                    document.body.appendChild(overlay);
                    document.body.appendChild(img);

                    alert(I18N("sponsorAlertText"));
                }

                function ob(node) {
                    const configMenuRootNode = node.querySelector(".RangedWayIdleConfigMenuRoot");
                    if (!configMenuRootNode) return;
                    if (configMenuRootNode.parentNode.querySelector(".RangedWayIdleSponsorButton")) return;
                    const sponsorButton = document.createElement("button");
                    sponsorButton.classList.add("RangedWayIdleSponsorButton");
                    sponsorButton.textContent = I18N("sponsorText");
                    sponsorButton.style.backgroundColor = "#66CCFF"
                    sponsorButton.addEventListener("click", showImage);
                    configMenuRootNode.insertAdjacentElement("afterend", sponsorButton);
                }

                return {ob: ob};
            }

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

                return {init: init};
            }

            function showConfigMenu() {
                function loadLocalConfig() {
                    const localConfig = localStorage.getItem("ranged_way_idle_configs");
                    let localConfigObject = localConfig ? JSON.parse(localConfig) : {};
                    if (localConfigObject.version !== 'v5') localConfigObject = {};
                    for (const configClass in configs) {
                        if (!localConfigObject[configClass]) localConfigObject[configClass] = {};
                        for (const configName in configs[configClass]) {
                            if (localConfigObject[configClass][configName] !== undefined) {
                                configs[configClass][configName].value = localConfigObject[configClass][configName];
                            }
                        }
                    }
                }

                function saveLocalConfig() {
                    const localConfig = localStorage.getItem("ranged_way_idle_configs");
                    const localConfigObject = localConfig ? JSON.parse(localConfig) : {};
                    for (const configClass in configs) {
                        if (!localConfigObject[configClass]) localConfigObject[configClass] = {};
                        for (const configName in configs[configClass]) {
                            localConfigObject[configClass][configName] = configs[configClass][configName].value;
                        }
                    }
                    localConfigObject.version = 'v5';
                    localStorage.setItem("ranged_way_idle_configs", JSON.stringify(localConfigObject));
                }

                function setConfig(configClass, configName, value) {
                    // forbid changing hidden config
                    if (configs[configClass][configName].isHidden) return;
                    configs[configClass][configName].value = value;
                    saveLocalConfig();
                }

                function ob(node) {
                    const settingPanelNode = node.querySelector(".SettingsPanel_profileTab__214Bj");
                    if (!settingPanelNode) return;
                    if (settingPanelNode.querySelector(".RangedWayIdleConfigMenuRoot")) return;
                    const configMenuRootNode = document.createElement("div");
                    configMenuRootNode.classList.add("RangedWayIdleConfigMenuRoot");
                    configMenuRootNode.style.display = "flex";
                    configMenuRootNode.style.flexDirection = "column";

                    // head
                    const headNode = document.createElement("div");
                    const headSpanNode1 = document.createElement("span");
                    headSpanNode1.textContent = "Ranged Way Idle ";
                    headSpanNode1.style.fontSize = "1.5rem";
                    headSpanNode1.style.color = "#66CCFF";
                    headNode.appendChild(headSpanNode1);
                    const headSpanNode2 = document.createElement("span");
                    headSpanNode2.textContent = I18N("ranged_way_idle_config_menu_title");
                    headSpanNode2.style.fontSize = "1.5rem";
                    headNode.appendChild(headSpanNode2);
                    configMenuRootNode.appendChild(headNode);

                    // note text
                    const noteTextNode = document.createElement("div");
                    noteTextNode.textContent = I18N("configNoteText");
                    configMenuRootNode.appendChild(noteTextNode);

                    // if contains secret setting, add additional text
                    let hasSecretSetting = false;
                    for (const configClass in configs) {
                        for (const configName in configs[configClass]) {
                            if (configs[configClass][configName].isSecret) {
                                hasSecretSetting = true;
                                break;
                            }
                        }
                        if (hasSecretSetting) break;
                    }
                    if (hasSecretSetting) {
                        // 没错我就是有隐藏功能不给大伙用,不服你就憋着嘿嘿嘿 ᗜˬᗜ
                        const secretTextNode = document.createElement("div");
                        secretTextNode.innerHTML = `<span style="color:#66CCFF">天依蓝</span>为内部功能,严禁外传!截图也不行!`;
                        configMenuRootNode.appendChild(secretTextNode);
                    }

                    // body
                    for (const configClass in configs) {
                        const classDivNode = document.createElement("div");
                        classDivNode.style.display = "flex";
                        classDivNode.style.alignItems = "center";
                        classDivNode.style.fontSize = "1.2rem";
                        classDivNode.style.color = "#F800F8";
                        classDivNode.textContent = I18N(configClass);
                        configMenuRootNode.appendChild(classDivNode);
                        for (const configName in configs[configClass]) {
                            if (configs[configClass][configName].isHidden) continue;
                            const divNode = document.createElement("div");
                            divNode.style.display = "flex";
                            divNode.style.alignItems = "center";
                            if (configs[configClass][configName].type === "switch") {
                                const inputNode = document.createElement("input");
                                inputNode.type = "checkbox";
                                inputNode.checked = configs[configClass][configName].value;
                                inputNode.addEventListener("change", () => {
                                    setConfig(configClass, configName, inputNode.checked);
                                });
                                inputNode.id = configName;
                                divNode.appendChild(inputNode);

                                const textNode = document.createElement("span");
                                textNode.textContent = I18N(configName);
                                if (configs[configClass][configName].isSecret) {
                                    textNode.style.color = "#66CCFF";
                                }
                                divNode.appendChild(textNode);

                            } else if (configs[configClass][configName].type === "input_number") {
                                const textNode = document.createElement("span");
                                textNode.textContent = I18N(configName);
                                if (configs[configClass][configName].isSecret) {
                                    textNode.style.color = "#66CCFF";
                                }
                                divNode.appendChild(textNode);

                                const inputNode = document.createElement("input");
                                inputNode.type = "number";
                                inputNode.value = configs[configClass][configName].value;
                                inputNode.addEventListener("change", () => {
                                    setConfig(configClass, configName, Number(inputNode.value));
                                });
                                inputNode.id = configName;
                                inputNode.style.width = "5rem";
                                divNode.appendChild(inputNode);
                            } else if (configs[configClass][configName].type === "input_range") {
                                const textNode = document.createElement("span");
                                textNode.textContent = I18N(configName);
                                if (configs[configClass][configName].isSecret) {
                                    textNode.style.color = "#66CCFF";
                                }
                                divNode.appendChild(textNode);

                                const inputNode = document.createElement("input");
                                inputNode.type = "range";
                                inputNode.min = configs[configClass][configName].min;
                                inputNode.max = configs[configClass][configName].max;
                                inputNode.step = configs[configClass][configName].step;
                                inputNode.value = configs[configClass][configName].value;
                                inputNode.addEventListener("change", () => {
                                    setConfig(configClass, configName, Number(inputNode.value));
                                });
                                inputNode.id = configName;
                                inputNode.style.width = "10rem";
                                divNode.appendChild(inputNode);
                            } else if (configs[configClass][configName].type === "select") {
                                const textNode = document.createElement("span");
                                textNode.textContent = I18N(configName);
                                if (configs[configClass][configName].isSecret) {
                                    textNode.style.color = "#66CCFF";
                                }
                                divNode.appendChild(textNode);

                                const selectNode = document.createElement("select");
                                for (const option of configs[configClass][configName].options) {
                                    const optionNode = document.createElement("option");
                                    optionNode.value = option;
                                    optionNode.textContent = I18N(option);
                                    if (option.value === configs[configClass][configName].value) optionNode.selected = true;
                                    selectNode.appendChild(optionNode);
                                }
                                selectNode.value = configs[configClass][configName].value;
                                selectNode.addEventListener("change", () => {
                                    setConfig(configClass, configName, selectNode.value);
                                });
                                divNode.appendChild(selectNode);
                            }
                            configMenuRootNode.appendChild(divNode);
                        }
                    }

                    // add to panel
                    settingPanelNode.appendChild(configMenuRootNode);
                }


                return {loadLocalConfig: loadLocalConfig, ob: ob};
            }

            return {
                showSponsor: showSponsor, mournForMagicWayIdle: mournForMagicWayIdle, showConfigMenu: showConfigMenu
            }
        }
    }

    function I18N(key, data) {
        let i18nValue;
        if (!I18NMap[key]) {
            i18nValue = key;
        } else if (I18NMap[key][configs.otherClass.scriptLanguage.value]) {
            i18nValue = I18NMap[key][configs.otherClass.scriptLanguage.value];
        } else {
            i18nValue = key;
        }
        return fillTemplate(i18nValue, data || {});

        function fillTemplate(template, data) {
            return template.replace(/\$\{(\w+)}/g, (match, key) => {
                return data[key] !== undefined ? data[key] : match;
            });
        }
    }

    function formatItemCount(num, precise = 0) {
        if (num === null) return "NULL";
        num = Number(num);
        if (isNaN(num)) {
            return "NULL";
        }
        const divisorMap = [{threshold: 1e13, divisor: 1e12, unit: "T"}, {
            threshold: 1e10,
            divisor: 1e9,
            unit: "B"
        }, {threshold: 1e7, divisor: 1e6, unit: "M"}, {threshold: 1e4, divisor: 1e3, unit: "K"}];
        for (const {threshold, divisor, unit} of divisorMap) {
            if (Math.abs(num) >= threshold) {
                const value = Math.floor(num / divisor * Math.pow(10, precise)) / Math.pow(10, precise);
                return value + unit;
            }
        }
        return Math.floor(num * Math.pow(10, precise)) / Math.pow(10, precise);
    }

    function parseItemCount(str) {
        const unitMap = {
            "T": 1e12, "B": 1e9, "M": 1e6, "K": 1e3
        }
        for (const unit in unitMap) {
            if (str.endsWith(unit)) {
                const value = Number(str.slice(0, -1));
                return value * unitMap[unit];
            }
        }
        return Number(str);
    }

    function itemCountColorMap(num) {
        if (Math.abs(num) < 1e5) {
            return "#FFFFFF";
        }
        if (Math.abs(num) < 1e7) {
            return "#FDDAA5";
        }
        if (Math.abs(num) < 1e10) {
            return "#82DCCA";
        }
        if (Math.abs(num) < 1e13) {
            return "#77BAEC";
        }
        if (Math.abs(num) < 1e16) {
            return "#AC8FD4";
        }
        return "#F800F8";
    }

    initScript();
})();