Ranged Way Idle

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

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址