您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
大量超级有用的MWI的QoL功能
当前为
// ==UserScript== // @name Ranged Way Idle // @namespace http://tampermonkey.net/ // @version 4.0 // @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 = { // combat notifyCombatDeath: { type: "switch", value: true, trigger: ["ws", "init"], listenMessageTypes: ["new_battle", "battle_updated"] }, minimumNotifyCooldownSeconds: {type: "input_number", value: 0, trigger: [],}, // message 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: [],}, // info 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"]}, // UI 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"]}, // listing hookListingInfo: { type: "switch", value: true, trigger: ["ws"], listenMessageTypes: ["market_listings_updated", "init_character_data"], isHidden: true }, 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"] }, 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"] }, // other 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}, // testConfig: {type: "switch", value: true, trigger: [], isHidden: true, isSecret: false}, } 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: {}, language: "zh-cn", 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: {} }; unsafeWindow._rwivb = globalVariables; const I18NMap = { "ranged_way_idle_config_menu_title": {"zh-cn": "设置"}, "notifyCombatDeath": {"zh-cn": "战斗中角色死亡时,发出通知"}, "minimumNotifyCooldownSeconds": {"zh-cn": "角色死亡通知冷却时间(秒)"}, "notifyChatMessages": {"zh-cn": "聊天消息含有关键词时,发出声音提醒"}, "notifyChatMessagesVolume": {"zh-cn": "聊天消息声音提醒音量"}, "notifyChatMessagesByRegex": {"zh-cn": "聊天消息采用正则匹配"}, "updateLocalStorageMarketPrice": {"zh-cn": "更新localStorage中的市场价格"}, "showTaskValue": {"zh-cn": "显示任务期望收益(依赖 食用工具)"}, "trackLeaderBoardData": {"zh-cn": "跟踪排行榜数据"}, "autoClickTaskSortButton": {"zh-cn": "自动点击任务排序按钮(依赖 MWI TaskManager)"}, "showMarketAPIUpdateTime": {"zh-cn": "显示市场API更新时间"}, "forceUpdateAPIButton": {"zh-cn": "强制更新市场API按钮"}, "disableQueueUpgradeButton": {"zh-cn": "禁用各处队列升级按钮,以防跳转至牛铃商店"}, "disableActionQueueBar": {"zh-cn": "禁用行动队列提示框显示"}, "showTotalListingFunds": {"zh-cn": "显示市场挂单的总购买预付金/出售可获金/待领取金额"}, "showTotalListingFundsPrecise": {"zh-cn": "显示市场挂单的总购买预付金/出售可获金/待领取金额的精度"}, "showListingInfo": {"zh-cn": "显示各个挂单的价格、创建时间信息"}, "showListingPricePrecise": {"zh-cn": "各个挂单的购买预付金/出售可获金的价格精度"}, "showListingCreateTimeByLifespan": {"zh-cn": "显示挂单已存在时长,而非创建的时刻"}, "notifyListingFilled": {"zh-cn": "挂单完成时,发出声音提醒"}, "notifyListingFilledVolume": {"zh-cn": "挂单完成声音提醒音量"}, "estimateListingCreateTime": {"zh-cn": "估算挂单创建时间"}, "mournForMagicWayIdle": {"zh-cn": "在控制台为Magic Way Idle默哀"}, "optimizeDocumentObserver": {"zh-cn": "优化document监听器,减少性能开销(可能有bug,出现问题请关闭)"}, "debugPrintWSMessages": {"zh-cn": "打印WebSocket消息(不推荐打开)"}, "configNoteText": {"zh-cn": "部分设置可能需要刷新页面才能生效。如果完全无效,或者控制台大量报错,请尝试更新本插件或前置插件"}, "notifyChatMessagesAddRowButton": {"zh-cn": "添加聊天消息监听关键词"}, "taskExpectedValueText": {"zh-cn": "任务期望收益:"}, "trackLeaderBoardDataLeaderboardStoreButton": {"zh-cn": "记录当前排行榜数据"}, "trackLeaderBoardDataLeaderboardDeleteButton": {"zh-cn": "删除本地数据"}, "trackLeaderBoardDataLeaderboardRecordTimeText": {"zh-cn": "本地数据记录于:${recordTime}(${timeDelta}小时前)"}, "trackLeaderBoardDataLeaderboardNoRecordTimeText": {"zh-cn": "无本地数据记录"}, "trackLeaderBoardDataNoteText": {"zh-cn": "由于排行榜数据每20分钟记录一次,增速和超越时间有误差,仅供参考。"}, "trackLeaderBoardDataDifference": {"zh-cn": "增量"}, "trackLeaderBoardDataSpeed": {"zh-cn": "增速"}, "trackLeaderBoardDataCatchupTime": {"zh-cn": "超越时间"}, "trackLeaderBoardDataCatchupTimeNow": {"zh-cn": "现在!"}, "trackLeaderBoardDataNewRecordText": {"zh-cn": "新上榜"}, "showMarketAPIUpdateTimeText": {"zh-cn": "市场API更新时间于:"}, "forceUpdateAPIButtonText": {"zh-cn": "强制更新市场API"}, "forceUpdateAPIButtonTextSuccess": {"zh-cn": "更新成功。市场数据更新于"}, "forceUpdateAPIButtonTextError": {"zh-cn": "更新失败。请稍后重试。"}, "forceUpdateAPIButtonTextTimeout": {"zh-cn": "更新超时。请稍后重试。"}, "totalUnclaimedCoinsText": {"zh-cn": "待领取金额"}, "totalPrepaidCoinsText": {"zh-cn": "购买预付金"}, "totalSellResultCoinsText": {"zh-cn": "出售可获金"}, "showListingInfoCreateTimeAt": {"zh-cn": "创建于"}, "showListingInfoCreateTimeLifespan": {"zh-cn": "已存在"}, "showListingInfoTopOrderPriceText": {"zh-cn": "左一/右一 价格"}, "showListingInfoTotalPriceText": {"zh-cn": "购买预付金/出售可获金"}, "estimateListingCreateTimeText": {"zh-cn": "估计创建时间"}, "/chat_channel_types/general": {"zh-cn": "英语"}, "/chat_channel_types/chinese": {"zh-cn": "中文"}, "/chat_channel_types/ironcow": {"zh-cn": "铁牛"}, "/chat_channel_types/trade": {"zh-cn": "交易"}, "/chat_channel_types/recruit": {"zh-cn": "招募"}, "/chat_channel_types/beginner": {"zh-cn": "新手"}, "/chat_channel_types/guild": {"zh-cn": "公会"}, "/chat_channel_types/party": {"zh-cn": "队伍"}, "/chat_channel_types/whisper": {"zh-cn": "私聊"}, "/chat_channel_types/moderator": {"zh-cn": "管理员"}, "/chat_channel_types/arabic": {"zh-cn": "العربية"}, "/chat_channel_types/french": {"zh-cn": "Français"}, "/chat_channel_types/german": {"zh-cn": "Deutsch"}, "/chat_channel_types/hebrew": {"zh-cn": "עברית"}, "/chat_channel_types/hindi": {"zh-cn": "हिंदी"}, "/chat_channel_types/japanese": {"zh-cn": "日本語"}, "/chat_channel_types/korean": {"zh-cn": "한국어"}, "/chat_channel_types/portuguese": {"zh-cn": "Português"}, "/chat_channel_types/russian": {"zh-cn": "Русский"}, "/chat_channel_types/spanish": {"zh-cn": "Español"}, "/chat_channel_types/vietnamese": {"zh-cn": "Tiếng Việt"}, }; function initScript() { const allFunctionsObject = new AllFunctions(); for (const configName in configs) { if (configs[configName].trigger.length === 0) continue; if (!allFunctionsObject[configName]) { console.warn("No function found for config: " + configName); continue; } globalVariables.functionMap[configName] = allFunctionsObject[configName](); } globalVariables.functionMap["showConfigMenu"].loadLocalConfig(); hookWebSocket(); initDocumentObserver(); for (const configName in configs) { if (globalVariables.functionMap[configName] && configs[configName].trigger.includes('init')) { try { globalVariables.functionMap[configName].init(); } catch (err) { console.error(err); } } } function hookWebSocket() { // message processor globalVariables.webSocketMessageProcessor = function (message, type) { const obj = JSON.parse(message); if (configs.debugPrintWSMessages.value) console.log(type, obj); if (type !== 'get' || !obj) return; const messageType = obj.type; for (const configName in configs) { if (configs[configName].type !== 'switch' || !configs[configName].value) continue; if (globalVariables.functionMap[configName] && configs[configName].trigger.includes('ws') && configs[configName].listenMessageTypes && configs[configName].listenMessageTypes.includes(messageType)) { try { globalVariables.functionMap[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.optimizeDocumentObserver.enable ? mutationsList[0].target : document; for (const configName in configs) { if (configs[configName].type !== 'switch' || !configs[configName].value) continue; if (globalVariables.functionMap[configName] && configs[configName].trigger.includes('ob')) { try { globalVariables.functionMap[configName].ob(node); } catch (err) { console.error(err); } } } } globalVariables.documentObserver = new MutationObserver(globalVariables.documentObserverFunction); globalVariables.documentObserver.observe(document, {childList: true, subtree: true}); } } class AllFunctions { showConfigMenu() { function loadLocalConfig() { // delete old version config delete localStorage["ranged_way_idle_config"]; const localConfig = localStorage.getItem("ranged_way_idle_configs"); const localConfigObject = localConfig ? JSON.parse(localConfig) : {}; for (const configName in localConfigObject) { if (configs[configName]) { configs[configName].value = localConfigObject[configName]; } } } function saveLocalConfig() { const localConfig = localStorage.getItem("ranged_way_idle_configs"); const localConfigObject = localConfig ? JSON.parse(localConfig) : {}; for (const configName in configs) { localConfigObject[configName] = configs[configName].value; } localStorage.setItem("ranged_way_idle_configs", JSON.stringify(localConfigObject)); } function setConfig(configName, value) { // forbid changing hidden config if (configs[configName].isHidden) return; configs[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 if (Object.values(configs).some(config => config.isSecret)) { // 没错我就是有隐藏功能不给大伙用,不服你就憋着嘿嘿嘿 ᗜˬᗜ const secretTextNode = document.createElement("div"); secretTextNode.innerHTML = `<span style="color:#66CCFF">天依蓝</span>为内部功能,严禁外传!截图也不行!`; configMenuRootNode.appendChild(secretTextNode); } // body for (const configName in configs) { if (configs[configName].isHidden) continue; const divNode = document.createElement("div"); divNode.style.display = "flex"; divNode.style.alignItems = "center"; if (configs[configName].type === "switch") { const inputNode = document.createElement("input"); inputNode.type = "checkbox"; inputNode.checked = configs[configName].value; inputNode.addEventListener("change", () => { setConfig(configName, inputNode.checked); }); inputNode.id = configName; divNode.appendChild(inputNode); const textNode = document.createElement("span"); textNode.textContent = I18N(configName); if (configs[configName].isSecret) { textNode.style.color = "#66CCFF"; } divNode.appendChild(textNode); } else if (configs[configName].type === "input_number") { const textNode = document.createElement("span"); textNode.textContent = I18N(configName); if (configs[configName].isSecret) { textNode.style.color = "#66CCFF"; } divNode.appendChild(textNode); const inputNode = document.createElement("input"); inputNode.type = "number"; inputNode.value = configs[configName].value; inputNode.addEventListener("change", () => { setConfig(configName, Number(inputNode.value)); }); inputNode.id = configName; inputNode.style.width = "5rem"; divNode.appendChild(inputNode); } else if (configs[configName].type === "input_range") { const textNode = document.createElement("span"); textNode.textContent = I18N(configName); if (configs[configName].isSecret) { textNode.style.color = "#66CCFF"; } divNode.appendChild(textNode); const inputNode = document.createElement("input"); inputNode.type = "range"; inputNode.value = configs[configName].value; inputNode.min = configs[configName].min; inputNode.max = configs[configName].max; inputNode.step = configs[configName].step; inputNode.addEventListener("change", () => { setConfig(configName, Number(inputNode.value)); }); inputNode.id = configName; inputNode.style.width = "10rem"; divNode.appendChild(inputNode); } configMenuRootNode.appendChild(divNode); } // add to panel settingPanelNode.appendChild(configMenuRootNode); } return {loadLocalConfig: loadLocalConfig, ob: ob}; } 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.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}; } 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 (!listenObject[channel]) return; for (const listenText of listenObject[channel]) { if (configs.notifyChatMessages.value) { const regex = new RegExp(listenText, "g"); if (regex.test(text)) { globalVariables.notifyMessageAudio.volume = configs.notifyChatMessagesVolume.value; globalVariables.notifyMessageAudio.play(); break; } } else { if (text.includes(listenText)) { globalVariables.notifyMessageAudio.volume = configs.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}; } initCharacterData() { function ws(obj) { globalVariables.initCharacterData = obj; } return {ws: ws}; } 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}; } 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.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}; } 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}; } 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}; } 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; if (nowTime === lastTime) return; 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}; } forceUpdateAPIButton() { function ob(node) { const listingContainerNode = node.querySelector(".MarketplacePanel_listingCount__3nVY_"); if (!listingContainerNode) 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}; } 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}; } 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}; } 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 ws(obj) { if (obj.type === "init_character_data") { for (const listing of obj.myMarketListings) { handleListing(listing); } } else if (obj.type === "market_listings_updated") { for (const listing of obj.endMarketListings) { handleListing(listing); } } } return {ws: ws}; } 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.showTotalListingFundsPrecise.value); totalPrepaidCoinsNode.querySelector(".Item_count__1HVvv").textContent = formatItemCount(totalPrepaidCoins, configs.showTotalListingFundsPrecise.value); totalSellResultCoinsNode.querySelector(".Item_count__1HVvv").textContent = formatItemCount(totalSellResultCoins, configs.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} } 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); return I18N("showListingInfoCreateTimeLifespan") + " " + [hours, (minutes % 60).toString().padStart(2, '0'), (seconds % 60).toString().padStart(2, '0')].join(':'); } 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); 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.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.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.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"); const bodyNode = myListingTableNode.querySelector("tbody"); 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(); } } 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) { // console.error("Listings length not match!"); return; } myListingTableNode.classList.add("RangedWayIdleShowListingInfoSet"); handleTableHead(myListingTableNode.querySelector("thead tr")); addDataToRows(myListingTableNode.querySelector("tbody")); handleTableBody(myListingTableNode.querySelector("tbody")); } return {ws: ws, ob: ob}; } 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.notifyListingFilledVolume.value; globalVariables.notifyListingFilledAudio.play(); return; } } } } return {ws: ws}; } estimateListingCreateTime() { let lastMarketItemOrderBooks = null; 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(',', ''); } function getListingData() { // author's data const data = [ {id: 97888637, timestamp: 1760266805648}, {id: 98545826, timestamp: 1760496508616}, {id: 98724734, timestamp: 1760551920380}, {id: 98978743, timestamp: 1760637750329} ]; 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.timestamp - 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 ws(obj) { if (obj.type === "market_item_order_books_updated") { lastMarketItemOrderBooks = obj.marketItemOrderBooks; const targetItemNode = document.querySelector(".RangedWayIdleEstimateListingCreateTimeSet"); if (targetItemNode) { targetItemNode.classList.remove("RangedWayIdleEstimateListingCreateTimeSet"); } } } function ob(node) { const targetItemNode = node.querySelector(".MarketplacePanel_currentItem__3ercC"); if (!targetItemNode) return; if (targetItemNode.classList.contains("RangedWayIdleEstimateListingCreateTimeSet")) return; targetItemNode.classList.add("RangedWayIdleEstimateListingCreateTimeSet"); 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; 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; } // 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"); node.textContent = formatUTCTime(new Date(estimatedTime)); 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"); node.textContent = formatUTCTime(new Date(estimatedTime)); row.insertBefore(node, row.lastChild); bidIndex++; } } return {ws: ws, ob: ob}; } mournForMagicWayIdle() { function init() { console.log("为法师助手默哀"); } return {init: init}; } } function I18N(key, data) { const defaultLanguage = "zh-cn"; let i18nValue; if (!I18NMap[key]) { i18nValue = key; } else if (I18NMap[key][globalVariables.language]) { i18nValue = I18NMap[key][globalVariables.language]; } else if (I18NMap[key][defaultLanguage]) { i18nValue = I18NMap[key][defaultLanguage]; } 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(); })();