Ranged Way Idle

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

当前为 2025-05-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Ranged Way Idle
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.12
  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: true,
  20. forceUpdateMarketPrice: true,
  21. notifyWhisperMessages: false,
  22. listenKeywordMessages: false,
  23. autoTaskSort: true,
  24. showMarketListingsFunds: true,
  25. mournForMagicWayIdle: true,
  26. showTaskValue: true,
  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. if (!('Edible_Tools' in localStorage)) {
  63. config.showTaskValue = false;
  64. }
  65. globalVariable.whisperAudio.volume = 0.4;
  66. globalVariable.keywordAudio.volume = 0.4;
  67. let observer = new MutationObserver(function () {
  68. if (config.showMarketListingsFunds) showMarketListingsFunds();
  69. if (config.autoTaskSort) autoClickTaskSortButton();
  70. if (config.showTaskValue) showTaskValue();
  71. });
  72. observer.observe(document, {childList: true, subtree: true});
  73. const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
  74. const oriGet = dataProperty.get;
  75. dataProperty.get = hookedGet;
  76. Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
  77.  
  78.  
  79. globalVariable.task.taskTokenValueData = getTaskTokenValue();
  80.  
  81. if (config.mournForMagicWayIdle) {
  82. console.log("为法师助手默哀");
  83. }
  84.  
  85. function hookedGet() {
  86. const socket = this.currentTarget;
  87. if (!(socket instanceof WebSocket)) {
  88. return oriGet.call(this);
  89. }
  90. if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) {
  91. return oriGet.call(this);
  92. }
  93. const message = oriGet.call(this);
  94. Object.defineProperty(this, "data", {value: message});
  95. return handleMessage(message);
  96. }
  97.  
  98. // function readConfig() {
  99. // const localConfig = localStorage.getItem("ranged_way_idle_config");
  100. // if (localConfig) {
  101. // const localConfigObj = JSON.parse(localConfig);
  102. // for (let key in localConfigObj) {
  103. // if (config.hasOwnProperty(key)) {
  104. // config[key] = localConfigObj[key];
  105. // }
  106. // }
  107. // }
  108. // }
  109. }
  110.  
  111.  
  112. function handleMessage(message) {
  113. const obj = JSON.parse(message);
  114. if (!obj) return message;
  115. switch (obj.type) {
  116. case "init_character_data":
  117. globalVariable.market.sellListings = {};
  118. globalVariable.market.buyListings = {};
  119. config.keywords.push(obj.character.name.toLowerCase());
  120. updateMarketListings(obj.myMarketListings);
  121. break;
  122. case "market_listings_updated":
  123. updateMarketListings(obj.endMarketListings);
  124. break;
  125. case "new_battle":
  126. if (config.notifyDeath) initBattle(obj);
  127. break;
  128. case "battle_updated":
  129. if (config.notifyDeath) checkDeath(obj);
  130. break;
  131. case "market_item_order_books_updated":
  132. if (config.forceUpdateMarketPrice) marketPriceUpdate(obj);
  133. break;
  134. case "quests_updated":
  135. for (let e of globalVariable.task.taskValueElements) {
  136. e.remove();
  137. }
  138. globalVariable.task.taskValueElements = [];
  139. globalVariable.task.hasTaskValueElement = false;
  140. break;
  141. case "chat_message_received":
  142. handleChatMessage(obj);
  143. break;
  144. }
  145. return message;
  146. }
  147.  
  148. function notifyDeath(name) {
  149. const nowTime = Date.now();
  150. if (nowTime - globalVariable.battleData.lastNotifyTime < 60000) return;
  151. globalVariable.battleData.lastNotifyTime = nowTime;
  152. new Notification('🎉🎉🎉喜报🎉🎉🎉', {body: `${name} 死了!`});
  153. }
  154.  
  155. function initBattle(obj) {
  156. globalVariable.battleData.players = [];
  157. for (let player of obj.players) {
  158. globalVariable.battleData.players.push({
  159. name: player.name, isAlive: player.currentHitpoints > 0,
  160. });
  161. if (player.currentHitpoints === 0) {
  162. notifyDeath(player.name);
  163. }
  164. }
  165. }
  166.  
  167. function checkDeath(obj) {
  168. if (!globalVariable.battleData.players) return;
  169. for (let key in obj.pMap) {
  170. const index = parseInt(key);
  171. if (globalVariable.battleData.players[index].isAlive && obj.pMap[key].cHP === 0) {
  172. globalVariable.battleData.players[index].isAlive = false;
  173. notifyDeath(globalVariable.battleData.players[index].name);
  174. } else if (!globalVariable.battleData.players[index].isAlive && obj.pMap[key].cHP > 0) {
  175. globalVariable.battleData.players[index].isAlive = true;
  176. }
  177. }
  178. }
  179.  
  180. function marketPriceUpdate(obj) {
  181. globalVariable.task.taskTokenValueData = getTaskTokenValue();
  182. // 本函数的代码复制自Magic Way Idle
  183. let itemDetailMap = globalVariable.itemDetailMap;
  184. let itemName = itemDetailMap[obj.marketItemOrderBooks.itemHrid].name;
  185. let ask = -1;
  186. let bid = -1;
  187. // 读取ask最低报价
  188. if (obj.marketItemOrderBooks.orderBooks[0].asks && obj.marketItemOrderBooks.orderBooks[0].asks.length > 0) {
  189. ask = obj.marketItemOrderBooks.orderBooks[0].asks[0].price;
  190. }
  191. // 读取bid最高报价
  192. if (obj.marketItemOrderBooks.orderBooks[0].bids && obj.marketItemOrderBooks.orderBooks[0].bids.length > 0) {
  193. bid = obj.marketItemOrderBooks.orderBooks[0].bids[0].price;
  194. }
  195. // 读取所有物品价格
  196. let jsonObj = JSON.parse(localStorage.getItem("MWITools_marketAPI_json"));
  197. // 修改当前查看物品价格
  198. if (jsonObj.market[itemName]) {
  199. jsonObj.market[itemName].ask = ask;
  200. jsonObj.market[itemName].bid = bid;
  201. }
  202. // 将修改后结果写回marketAPI缓存,完成对marketAPI价格的强制修改
  203. localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(jsonObj));
  204. }
  205.  
  206. function handleChatMessage(obj) {
  207. if (obj.message.chan === "/chat_channel_types/whisper") {
  208. if (config.notifyWhisperMessages) {
  209. globalVariable.whisperAudio.play();
  210. }
  211. } else if (obj.message.chan === "/chat_channel_types/chinese") {
  212. if (config.listenKeywordMessages) {
  213. for (let keyword of config.keywords) {
  214. if (obj.message.m.toLowerCase().includes(keyword)) {
  215. globalVariable.keywordAudio.play();
  216. }
  217. }
  218. }
  219. }
  220. }
  221.  
  222. function autoClickTaskSortButton() {
  223. const targetElement = document.querySelector('#TaskSort');
  224. if (targetElement && targetElement.textContent !== '手动排序') {
  225. targetElement.click();
  226. targetElement.textContent = '手动排序';
  227. }
  228. }
  229.  
  230. function formatCoinValue(num) {
  231. if (num >= 1e13) {
  232. return Math.floor(num / 1e12) + "T";
  233. } else if (num >= 1e10) {
  234. return Math.floor(num / 1e9) + "B";
  235. } else if (num >= 1e7) {
  236. return Math.floor(num / 1e6) + "M";
  237. } else if (num >= 1e4) {
  238. return Math.floor(num / 1e3) + "K";
  239. }
  240. return num.toString();
  241. }
  242.  
  243. function updateMarketListings(obj) {
  244. for (let listing of obj) {
  245. if (listing.status === "/market_listing_status/cancelled") {
  246. delete globalVariable.market[listing.isSell ? "sellListings" : "buyListings"][listing.id];
  247. continue
  248. }
  249. globalVariable.market[listing.isSell ? "sellListings" : "buyListings"][listing.id] = {
  250. itemHrid: listing.itemHrid,
  251. price: (listing.orderQuantity - listing.filledQuantity) * (listing.isSell ? Math.ceil(listing.price * 0.98) : listing.price),
  252. unclaimedCoinCount: listing.unclaimedCoinCount,
  253. }
  254. }
  255. globalVariable.market.buyValue = 0;
  256. globalVariable.market.sellValue = 0;
  257. globalVariable.market.unclaimedValue = 0;
  258. for (let id in globalVariable.market.buyListings) {
  259. const listing = globalVariable.market.buyListings[id];
  260. globalVariable.market.buyValue += listing.price;
  261. globalVariable.market.unclaimedValue += listing.unclaimedCoinCount;
  262. }
  263. for (let id in globalVariable.market.sellListings) {
  264. const listing = globalVariable.market.sellListings[id];
  265. globalVariable.market.sellValue += listing.price;
  266. globalVariable.market.unclaimedValue += listing.unclaimedCoinCount;
  267. }
  268. globalVariable.market.hasFundsElement = false;
  269. }
  270.  
  271. function showMarketListingsFunds() {
  272. if (globalVariable.market.hasFundsElement) return;
  273. const coinStackElement = document.querySelector("div.MarketplacePanel_coinStack__1l0UD");
  274. if (coinStackElement) {
  275. coinStackElement.style.top = "0px";
  276. coinStackElement.style.left = "0px";
  277. let fundsElement = coinStackElement.parentNode.querySelector("div.fundsElement");
  278. while (fundsElement) {
  279. fundsElement.remove();
  280. fundsElement = coinStackElement.parentNode.querySelector("div.fundsElement");
  281. }
  282. makeNode("购买预付金", globalVariable.market.buyValue, ["125px", "0px"]);
  283. makeNode("出售可获金", globalVariable.market.sellValue, ["125px", "22px"]);
  284. makeNode("待领取金额", globalVariable.market.unclaimedValue, ["0px", "22px"]);
  285. globalVariable.market.hasFundsElement = true;
  286. }
  287.  
  288. function makeNode(text, value, style) {
  289. let node = coinStackElement.cloneNode(true);
  290. node.classList.add("fundsElement");
  291. const countNode = node.querySelector("div.Item_count__1HVvv");
  292. const textNode = node.querySelector("div.Item_name__2C42x");
  293. if (countNode) countNode.textContent = formatCoinValue(value);
  294. if (textNode) textNode.innerHTML = `<span style="color: rgb(102,204,255); font-weight: bold;">${text}</span>`;
  295. node.style.left = style[0];
  296. node.style.top = style[1];
  297. coinStackElement.parentNode.insertBefore(node, coinStackElement.nextSibling);
  298. }
  299. }
  300.  
  301. function getTaskTokenValue() {
  302. const chestDropData = JSON.parse(localStorage.getItem("Edible_Tools")).Chest_Drop_Data;
  303. const lootsName = ["大陨石舱", "大工匠匣", "大宝箱"];
  304. const bidValueList = [
  305. parseFloat(chestDropData["Large Meteorite Cache"]["期望产出Bid"]),
  306. parseFloat(chestDropData["Large Artisan's Crate"]["期望产出Bid"]),
  307. parseFloat(chestDropData["Large Treasure Chest"]["期望产出Bid"]),
  308. ]
  309. const askValueList = [
  310. parseFloat(chestDropData["Large Meteorite Cache"]["期望产出Ask"]),
  311. parseFloat(chestDropData["Large Artisan's Crate"]["期望产出Ask"]),
  312. parseFloat(chestDropData["Large Treasure Chest"]["期望产出Ask"]),
  313. ]
  314. const res = {
  315. bidValue: Math.max(...bidValueList),
  316. askValue: Math.max(...askValueList)
  317. }
  318. res.bidLoots = lootsName[bidValueList.indexOf(res.bidValue)];
  319. res.askLoots = lootsName[askValueList.indexOf(res.askValue)];
  320. res.bidValue = Math.round(res.bidValue / 30);
  321. res.askValue = Math.round(res.askValue / 30);
  322. res.giftValueBid = Math.round(parseFloat(chestDropData["Purple's Gift"]["期望产出Bid"]));
  323. res.giftValueAsk = Math.round(parseFloat(chestDropData["Purple's Gift"]["期望产出Ask"]));
  324. if (config.forceUpdateMarketPrice) {
  325. const marketJSON = JSON.parse(localStorage.getItem("MWITools_marketAPI_json"));
  326. marketJSON.market["Task Token"].ask = res.askValue;
  327. marketJSON.market["Task Token"].bid = res.bidValue;
  328. localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(marketJSON));
  329. }
  330. res.rewardValueBid = res.bidValue + res.giftValueBid / 50;
  331. res.rewardValueAsk = res.askValue + res.giftValueAsk / 50;
  332. return res;
  333. }
  334.  
  335. function showTaskValue() {
  336. globalVariable.task.taskListElement = document.querySelector("div.TasksPanel_taskList__2xh4k");
  337. if (!globalVariable.task.taskListElement) {
  338. globalVariable.task.taskValueElements = [];
  339. globalVariable.task.hasTaskValueElement = false;
  340. globalVariable.task.taskListElement = null;
  341. return;
  342. }
  343. if (globalVariable.task.hasTaskValueElement) return;
  344. globalVariable.task.hasTaskValueElement = true;
  345. const taskNodes = [...globalVariable.task.taskListElement.querySelectorAll("div.RandomTask_randomTask__3B9fA")];
  346.  
  347. function convertKEndStringToNumber(str) {
  348. if (str.endsWith('K') || str.endsWith('k')) {
  349. return Number(str.slice(0, -1)) * 1000;
  350. } else {
  351. return Number(str);
  352. }
  353. }
  354.  
  355. taskNodes.forEach(function (node) {
  356. const reward = node.querySelector("div.RandomTask_rewards__YZk7D");
  357. const coin = convertKEndStringToNumber(reward.querySelectorAll("div.Item_count__1HVvv")[0].innerText);
  358. const tokenCount = Number(reward.querySelectorAll("div.Item_count__1HVvv")[1].innerText);
  359. const newDiv = document.createElement("div");
  360. newDiv.textContent = `奖励期望收益:
  361. ${formatCoinValue(coin + tokenCount * globalVariable.task.taskTokenValueData.rewardValueAsk)} /
  362. ${formatCoinValue(coin + tokenCount * globalVariable.task.taskTokenValueData.rewardValueBid)}`;
  363. newDiv.style.color = "rgb(248,0,248)";
  364. node.querySelector("div.RandomTask_action__3eC6o").appendChild(newDiv);
  365. globalVariable.task.taskValueElements.push(newDiv);
  366. });
  367. }
  368. })();