// ==UserScript==
// @name Ranged Way Idle
// @namespace http://tampermonkey.net/
// @version 4.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 = {
// combat
notifyCombatDeath: {
type: "switch",
value: true,
trigger: ["ws", "init"],
listenMessageTypes: ["new_battle", "battle_updated"]
},
minimumNotifyCooldownSeconds: {type: "input_number", value: 5, 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: []},
notifyChatMessagesFilterSelf: {type: "switch", value: true, 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"]
},
estimateListingCreateTimeColorByAccuracy: {type: "switch", value: false, trigger: []},
estimateListingCreateTimeColorByLifespan: {type: "switch", value: false, trigger: []},
// 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": "聊天消息采用正则匹配"},
"notifyChatMessagesFilterSelf": {"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": "依据挂单ID线性估算挂单创建时间"},
"estimateListingCreateTimeColorByAccuracy": {"zh-cn": "依据精度为挂单创建时间着色(越偏向绿色 精度越高)"},
"estimateListingCreateTimeColorByLifespan": {"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 (configs.notifyChatMessagesFilterSelf.value && obj.message.cId === globalVariables.initCharacterData.character.id) return;
if (!listenObject[channel]) return;
for (const listenText of listenObject[channel]) {
if (configs.notifyChatMessagesByRegex.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 || !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};
}
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 colorByAccuracy(sortedData, timestamp) {
const timeDelta = Math.min(...sortedData.map(item => Math.abs(item.timestamp - timestamp)));
return Math.max(1 - timeDelta / 86400_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") {
lastMarketItemOrderBooks = obj.marketItemOrderBooks;
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;
askContainer.classList.add("RangedWayIdleEstimateListingCreateTimeSet");
bidContainer.classList.add("RangedWayIdleEstimateListingCreateTimeSet");
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));
if (configs.estimateListingCreateTimeColorByAccuracy.value) {
const k = colorByAccuracy(sortedData, estimatedTime);
node.style.color = `rgb(${255 - k * 255}, ${k * 255}, 0)`;
} else if (configs.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");
node.textContent = formatUTCTime(new Date(estimatedTime));
if (configs.estimateListingCreateTimeColorByAccuracy.value) {
const k = colorByAccuracy(sortedData, estimatedTime);
node.style.color = `rgb(${255 - k * 255}, ${k * 255}, 0)`;
} else if (configs.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};
}
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();
})();