Ranged Way Idle

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

当前为 2025-06-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Ranged Way Idle
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.2
  5. // @description 死亡提醒、强制刷新MWITools的价格、私信提醒音、自动任务排序、显示购买预付金/出售可获金/待领取金额、显示任务价值、默哀法师助手
  6. // @author AlphB
  7. // @match https://www.milkywayidle.com/*
  8. // @match https://test.milkywayidle.com/*
  9. // @grant GM_notification
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
  13. // @grant none
  14. // @license CC-BY-NC-SA-4.0
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. const config = {
  19. notifyDeath: {enable: true, desc: "战斗中角色死亡时发送通知"},
  20. forceUpdateMarketPrice: {enable: true, desc: "进入市场时,强制更新MWITools的市场价格"},
  21. notifyWhisperMessages: {enable: false, desc: "接受到私信时播放提醒音"},
  22. listenKeywordMessages: {enable: false, desc: "中文频道消息含有关键词时播放提醒音"},
  23. autoTaskSort: {enable: true, desc: "自动点击MWI TaskManager的任务排序按钮"},
  24. showMarketListingsFunds: {enable: true, desc: "显示购买预付金/出售可获金/待领取金额"},
  25. mournForMagicWayIdle: {enable: true, desc: "在控制台默哀法师助手"},
  26. showTaskValue: {enable: true, desc: "显示任务代币的价值"},
  27. keywords: [],
  28. }
  29. const globalVariable = {
  30. battleData: {
  31. players: null,
  32. lastNotifyTime: 0,
  33. },
  34. itemDetailMap: JSON.parse(localStorage.getItem("initClientData")).itemDetailMap,
  35. whisperAudio: new Audio(`https://upload.thbwiki.cc/d/d1/se_bonus2.mp3`),
  36. keywordAudio: new Audio(`https://upload.thbwiki.cc/c/c9/se_pldead00.mp3`),
  37. market: {
  38. hasFundsElement: false,
  39. sellValue: null,
  40. buyValue: null,
  41. unclaimedValue: null,
  42. sellListings: null,
  43. buyListings: null
  44. },
  45. task: {
  46. taskListElement: null,
  47. taskTokenValueData: null,
  48. hasTaskValueElement: false,
  49. taskValueElements: [],
  50. tokenValue: {
  51. Bid: null,
  52. Ask: null
  53. }
  54. }
  55. };
  56.  
  57.  
  58. init();
  59.  
  60. function init() {
  61. readConfig();
  62.  
  63. // 任务代币计算功能需要食用工具
  64. if (!('Edible_Tools' in localStorage)) {
  65. config.showTaskValue.enable = false;
  66. }
  67.  
  68. // 更新市场价格需要MWITools支持
  69. if (!('MWITools_marketAPI_json' in localStorage)) {
  70. config.forceUpdateMarketPrice.enable = false;
  71. }
  72. globalVariable.whisperAudio.volume = 0.4;
  73. globalVariable.keywordAudio.volume = 0.4;
  74. let observer = new MutationObserver(function () {
  75. if (config.showMarketListingsFunds.enable) showMarketListingsFunds();
  76. if (config.autoTaskSort.enable) autoClickTaskSortButton();
  77. if (config.showTaskValue.enable) showTaskValue();
  78. showConfigMenu();
  79. });
  80. observer.observe(document, {childList: true, subtree: true});
  81.  
  82. globalVariable.task.taskTokenValueData = getTaskTokenValue();
  83. if (config.mournForMagicWayIdle.enable) {
  84. console.log("为法师助手默哀");
  85. }
  86.  
  87. const oriGet = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data").get;
  88.  
  89. function hookedGet() {
  90. const socket = this.currentTarget;
  91. if (!(socket instanceof WebSocket) || !socket.url ||
  92. (socket.url.indexOf("api.milkywayidle.com/ws") === -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") === -1)) {
  93. return oriGet.call(this);
  94. }
  95. const message = oriGet.call(this);
  96. return handleMessage(message);
  97. }
  98.  
  99. Object.defineProperty(MessageEvent.prototype, "data", {
  100. get: hookedGet,
  101. configurable: true,
  102. enumerable: true
  103. });
  104. }
  105.  
  106. function readConfig() {
  107. const localConfig = localStorage.getItem("ranged_way_idle_config");
  108. if (localConfig) {
  109. const localConfigObj = JSON.parse(localConfig);
  110. for (let key in localConfigObj) {
  111. if (config.hasOwnProperty(key) && key !== 'keywords') {
  112. config[key].enable = localConfigObj[key];
  113. }
  114. }
  115. config.keywords = localConfigObj.keywords;
  116. }
  117. }
  118.  
  119. function saveConfig() {
  120. // 仅保存enable开关和keywords
  121. const saveConfigObj = {};
  122. const configMenu = document.querySelectorAll("div#ranged_way_idle_config_menu input");
  123. if (configMenu.length === 0) return;
  124. for (const checkbox of configMenu) {
  125. config[checkbox.id].isTrue = checkbox.checked;
  126. saveConfigObj[checkbox.id] = checkbox.checked;
  127. }
  128. saveConfigObj.keywords = config.keywords;
  129. localStorage.setItem("ranged_way_idle_config", JSON.stringify(saveConfigObj));
  130. }
  131.  
  132. function showConfigMenu() {
  133. const targetNode = document.querySelector("div.SettingsPanel_profileTab__214Bj");
  134. if (targetNode) {
  135. if (!targetNode.querySelector("#ranged_way_idle_config_menu")) {
  136. // enable开关部分
  137. targetNode.insertAdjacentHTML("beforeend", `<div id="ranged_way_idle_config_menu"></div>`);
  138. const insertElem = targetNode.querySelector("div#ranged_way_idle_config_menu");
  139. insertElem.insertAdjacentHTML(
  140. "beforeend",
  141. `<div style="float: left;" id="ranged_way_idle_config">${
  142. "Ranged Way Idle 设置"
  143. }</div></br>`
  144. );
  145. for (let key in config) {
  146. if (key === 'keywords') continue;
  147. insertElem.insertAdjacentHTML(
  148. "beforeend",
  149. `<div style="float: left;">
  150. <input type="checkbox" id="${key}" ${config[key].enable ? "checked" : ""}>${config[key].desc}
  151. </div></br>`
  152. );
  153. }
  154. insertElem.addEventListener("change", saveConfig);
  155.  
  156. // 控制 keywords 列表
  157. const container = document.createElement('div');
  158. container.style.marginTop = '20px';
  159. container.classList.add("ranged_way_idle_keywords_config_menu")
  160. const input = document.createElement('input');
  161. input.type = 'text';
  162. input.style.width = '200px';
  163. input.placeholder = 'Ranged Way Idle 监听关键词';
  164. const button = document.createElement('button');
  165. button.textContent = '添加';
  166. const listContainer = document.createElement('div');
  167. listContainer.style.marginTop = '10px';
  168. container.appendChild(input);
  169. container.appendChild(button);
  170. container.appendChild(listContainer);
  171. targetNode.insertBefore(container, targetNode.nextSibling);
  172.  
  173. function renderList() {
  174. listContainer.innerHTML = '';
  175. config.keywords.forEach((item, index) => {
  176. const itemDiv = document.createElement('div');
  177. itemDiv.textContent = item;
  178. itemDiv.style.margin = 'auto';
  179. itemDiv.style.width = '200px';
  180. itemDiv.style.cursor = 'pointer';
  181. itemDiv.addEventListener('click', () => {
  182. config.keywords.splice(index, 1);
  183. renderList();
  184. });
  185. listContainer.appendChild(itemDiv);
  186. });
  187. saveConfig();
  188. }
  189.  
  190. renderList();
  191. button.addEventListener('click', () => {
  192. const newItem = input.value.trim();
  193. if (newItem) {
  194. config.keywords.push(newItem);
  195. input.value = '';
  196. saveConfig();
  197. renderList();
  198. }
  199. });
  200. }
  201. }
  202. }
  203.  
  204. function handleMessage(message) {
  205. try {
  206. const obj = JSON.parse(message);
  207. if (!obj) return message;
  208. switch (obj.type) {
  209. case "init_character_data":
  210. globalVariable.market.sellListings = {};
  211. globalVariable.market.buyListings = {};
  212. updateMarketListings(obj.myMarketListings);
  213. break;
  214. case "market_listings_updated":
  215. updateMarketListings(obj.endMarketListings);
  216. break;
  217. case "new_battle":
  218. if (config.notifyDeath.enable) initBattle(obj);
  219. break;
  220. case "battle_updated":
  221. if (config.notifyDeath.enable) checkDeath(obj);
  222. break;
  223. case "market_item_order_books_updated":
  224. if (config.forceUpdateMarketPrice.enable) marketPriceUpdate(obj);
  225. break;
  226. case "quests_updated":
  227. for (let e of globalVariable.task.taskValueElements) {
  228. e.remove();
  229. }
  230. globalVariable.task.taskValueElements = [];
  231. globalVariable.task.hasTaskValueElement = false;
  232. break;
  233. case "chat_message_received":
  234. handleChatMessage(obj);
  235. break;
  236. }
  237. } catch (e) {
  238. console.error(e);
  239. }
  240. return message;
  241. }
  242.  
  243. function notifyDeath(name) {
  244. // 如果间隔小于60秒,强制不播报
  245. const nowTime = Date.now();
  246. if (nowTime - globalVariable.battleData.lastNotifyTime < 60000) return;
  247. globalVariable.battleData.lastNotifyTime = nowTime;
  248. new Notification('🎉🎉🎉喜报🎉🎉🎉', {body: `${name} 死了!`});
  249. }
  250.  
  251. function initBattle(obj) {
  252. // 处理战斗中各个玩家的角色名,供播报死亡信息
  253. globalVariable.battleData.players = [];
  254. for (let player of obj.players) {
  255. globalVariable.battleData.players.push({
  256. name: player.name, isAlive: player.currentHitpoints > 0,
  257. });
  258. if (player.currentHitpoints === 0) {
  259. notifyDeath(player.name);
  260. }
  261. }
  262. }
  263.  
  264. function checkDeath(obj) {
  265. // 检查玩家是否死亡
  266. if (!globalVariable.battleData.players) return;
  267. for (let key in obj.pMap) {
  268. const index = parseInt(key);
  269. if (globalVariable.battleData.players[index].isAlive && obj.pMap[key].cHP === 0) {
  270. // 角色 活->死 时发送提醒
  271. globalVariable.battleData.players[index].isAlive = false;
  272. notifyDeath(globalVariable.battleData.players[index].name);
  273. } else if (obj.pMap[key].cHP > 0) {
  274. globalVariable.battleData.players[index].isAlive = true;
  275. }
  276. }
  277. }
  278.  
  279. function marketPriceUpdate(obj) {
  280. globalVariable.task.taskTokenValueData = getTaskTokenValue();
  281. // 本函数的代码复制自Magic Way Idle
  282. let itemDetailMap = globalVariable.itemDetailMap;
  283. let itemName = itemDetailMap[obj.marketItemOrderBooks.itemHrid].name;
  284. let ask = -1;
  285. let bid = -1;
  286. // 读取ask最低报价
  287. if (obj.marketItemOrderBooks.orderBooks[0].asks && obj.marketItemOrderBooks.orderBooks[0].asks.length > 0) {
  288. ask = obj.marketItemOrderBooks.orderBooks[0].asks[0].price;
  289. }
  290. // 读取bid最高报价
  291. if (obj.marketItemOrderBooks.orderBooks[0].bids && obj.marketItemOrderBooks.orderBooks[0].bids.length > 0) {
  292. bid = obj.marketItemOrderBooks.orderBooks[0].bids[0].price;
  293. }
  294. // 读取所有物品价格
  295. let jsonObj = JSON.parse(localStorage.getItem("MWITools_marketAPI_json"));
  296. // 修改当前查看物品价格
  297. if (jsonObj.marketData[itemName]) {
  298. jsonObj.marketData[itemName].ask = ask;
  299. jsonObj.marketData[itemName].bid = bid;
  300. }
  301. // 将修改后结果写回marketAPI缓存,完成对marketAPI价格的强制修改
  302. localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(jsonObj));
  303. }
  304.  
  305. function handleChatMessage(obj) {
  306. // 处理聊天信息
  307. if (obj.message.chan === "/chat_channel_types/whisper") {
  308. if (config.notifyWhisperMessages.enable) {
  309. globalVariable.whisperAudio.play();
  310. }
  311. } else if (obj.message.chan === "/chat_channel_types/chinese") {
  312. if (config.listenKeywordMessages.enable) {
  313. for (let keyword of config.keywords) {
  314. if (obj.message.m.includes(keyword)) {
  315. globalVariable.keywordAudio.play();
  316. }
  317. }
  318. }
  319. }
  320. }
  321.  
  322. function autoClickTaskSortButton() {
  323. // 点击MWI TaskManager的任务排序按钮
  324. const targetElement = document.querySelector('#TaskSort');
  325. if (targetElement && targetElement.textContent !== '手动排序') {
  326. targetElement.click();
  327. targetElement.textContent = '手动排序';
  328. }
  329. }
  330.  
  331. function formatCoinValue(num) {
  332. if (isNaN(num)) return "NaN";
  333. if (num >= 1e13) {
  334. return Math.floor(num / 1e12) + "T";
  335. } else if (num >= 1e10) {
  336. return Math.floor(num / 1e9) + "B";
  337. } else if (num >= 1e7) {
  338. return Math.floor(num / 1e6) + "M";
  339. } else if (num >= 1e4) {
  340. return Math.floor(num / 1e3) + "K";
  341. }
  342. return num.toString();
  343. }
  344.  
  345. function updateMarketListings(obj) {
  346. // 更新市场价格
  347. for (let listing of obj) {
  348. if (listing.status === "/market_listing_status/cancelled") {
  349. delete globalVariable.market[listing.isSell ? "sellListings" : "buyListings"][listing.id];
  350. continue
  351. }
  352. globalVariable.market[listing.isSell ? "sellListings" : "buyListings"][listing.id] = {
  353. itemHrid: listing.itemHrid,
  354. price: (listing.orderQuantity - listing.filledQuantity) * (listing.isSell ? Math.ceil(listing.price * 0.98) : listing.price),
  355. unclaimedCoinCount: listing.unclaimedCoinCount,
  356. }
  357. }
  358. globalVariable.market.buyValue = 0;
  359. globalVariable.market.sellValue = 0;
  360. globalVariable.market.unclaimedValue = 0;
  361. for (let id in globalVariable.market.buyListings) {
  362. const listing = globalVariable.market.buyListings[id];
  363. globalVariable.market.buyValue += listing.price;
  364. globalVariable.market.unclaimedValue += listing.unclaimedCoinCount;
  365. }
  366. for (let id in globalVariable.market.sellListings) {
  367. const listing = globalVariable.market.sellListings[id];
  368. globalVariable.market.sellValue += listing.price;
  369. globalVariable.market.unclaimedValue += listing.unclaimedCoinCount;
  370. }
  371. globalVariable.market.hasFundsElement = false;
  372. }
  373.  
  374. function showMarketListingsFunds() {
  375. // 如果已经存在节点,不必更新
  376. if (globalVariable.market.hasFundsElement) return;
  377. const coinStackElement = document.querySelector("div.MarketplacePanel_coinStack__1l0UD");
  378. // 不在市场面板,不必更新
  379. if (coinStackElement) {
  380. coinStackElement.style.top = "0px";
  381. coinStackElement.style.left = "0px";
  382. let fundsElement = coinStackElement.parentNode.querySelector("div.fundsElement");
  383. while (fundsElement) {
  384. fundsElement.remove();
  385. fundsElement = coinStackElement.parentNode.querySelector("div.fundsElement");
  386. }
  387. makeNode("购买预付金", globalVariable.market.buyValue, ["125px", "0px"]);
  388. makeNode("出售可获金", globalVariable.market.sellValue, ["125px", "22px"]);
  389. makeNode("待领取金额", globalVariable.market.unclaimedValue, ["0px", "22px"]);
  390. globalVariable.market.hasFundsElement = true;
  391. }
  392.  
  393. function makeNode(text, value, style) {
  394. let node = coinStackElement.cloneNode(true);
  395. node.classList.add("fundsElement");
  396. const countNode = node.querySelector("div.Item_count__1HVvv");
  397. const textNode = node.querySelector("div.Item_name__2C42x");
  398. if (countNode) countNode.textContent = formatCoinValue(value);
  399. if (textNode) textNode.innerHTML = `<span style="color: rgb(102,204,255); font-weight: bold;">${text}</span>`;
  400. node.style.left = style[0];
  401. node.style.top = style[1];
  402. coinStackElement.parentNode.insertBefore(node, coinStackElement.nextSibling);
  403. }
  404. }
  405.  
  406. function getTaskTokenValue() {
  407. const chestDropData = JSON.parse(localStorage.getItem("Edible_Tools")).Chest_Drop_Data;
  408. const lootsName = ["大陨石舱", "大工匠匣", "大宝箱"];
  409. const bidValueList = [
  410. parseFloat(chestDropData["Large Meteorite Cache"]["期望产出Bid"]),
  411. parseFloat(chestDropData["Large Artisan's Crate"]["期望产出Bid"]),
  412. parseFloat(chestDropData["Large Treasure Chest"]["期望产出Bid"]),
  413. ]
  414. const askValueList = [
  415. parseFloat(chestDropData["Large Meteorite Cache"]["期望产出Ask"]),
  416. parseFloat(chestDropData["Large Artisan's Crate"]["期望产出Ask"]),
  417. parseFloat(chestDropData["Large Treasure Chest"]["期望产出Ask"]),
  418. ]
  419. const res = {
  420. bidValue: Math.max(...bidValueList),
  421. askValue: Math.max(...askValueList)
  422. }
  423. // bid和ask的最佳兑换选项
  424. res.bidLoots = lootsName[bidValueList.indexOf(res.bidValue)];
  425. res.askLoots = lootsName[askValueList.indexOf(res.askValue)];
  426. // bid和ask的任务代币价值
  427. res.bidValue = Math.round(res.bidValue / 30);
  428. res.askValue = Math.round(res.askValue / 30);
  429. // 小紫牛的礼物的额外价值计算
  430. res.giftValueBid = Math.round(parseFloat(chestDropData["Purple's Gift"]["期望产出Bid"]));
  431. res.giftValueAsk = Math.round(parseFloat(chestDropData["Purple's Gift"]["期望产出Ask"]));
  432. if (config.forceUpdateMarketPrice.enable) {
  433. const marketJSON = JSON.parse(localStorage.getItem("MWITools_marketAPI_json"));
  434. marketJSON.marketData["/items/task_token"].ask = res.askValue;
  435. marketJSON.marketData["/items/task_token"].bid = res.bidValue;
  436. localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(marketJSON));
  437. }
  438. res.rewardValueBid = res.bidValue + res.giftValueBid / 50;
  439. res.rewardValueAsk = res.askValue + res.giftValueAsk / 50;
  440. return res;
  441. }
  442.  
  443. function showTaskValue() {
  444. globalVariable.task.taskListElement = document.querySelector("div.TasksPanel_taskList__2xh4k");
  445. // 如果不在任务面板,则销毁显示任务价值的元素
  446. if (!globalVariable.task.taskListElement) {
  447. globalVariable.task.taskValueElements = [];
  448. globalVariable.task.hasTaskValueElement = false;
  449. globalVariable.task.taskListElement = null;
  450. return;
  451. }
  452. // 如果已经存在任务价值的元素,不再更新
  453. if (globalVariable.task.hasTaskValueElement) return;
  454. globalVariable.task.hasTaskValueElement = true;
  455. const taskNodes = [...globalVariable.task.taskListElement.querySelectorAll("div.RandomTask_randomTask__3B9fA")];
  456.  
  457. function convertKEndStringToNumber(str) {
  458. if (str.endsWith('K') || str.endsWith('k')) {
  459. return Number(str.slice(0, -1)) * 1000;
  460. } else {
  461. return Number(str);
  462. }
  463. }
  464.  
  465. taskNodes.forEach(function (node) {
  466. const reward = node.querySelector("div.RandomTask_rewards__YZk7D");
  467. const coin = convertKEndStringToNumber(reward.querySelectorAll("div.Item_count__1HVvv")[0].innerText);
  468. const tokenCount = Number(reward.querySelectorAll("div.Item_count__1HVvv")[1].innerText);
  469. const newDiv = document.createElement("div");
  470. newDiv.textContent = `奖励期望收益:
  471. ${formatCoinValue(coin + tokenCount * globalVariable.task.taskTokenValueData.rewardValueAsk)} /
  472. ${formatCoinValue(coin + tokenCount * globalVariable.task.taskTokenValueData.rewardValueBid)}`;
  473. newDiv.style.color = "rgb(248,0,248)";
  474. newDiv.classList.add("rewardValue");
  475. node.querySelector("div.RandomTask_action__3eC6o").appendChild(newDiv);
  476. globalVariable.task.taskValueElements.push(newDiv);
  477. });
  478. }
  479. })();