MWITools

Tools for MilkyWayIdle. Shows total action time. Shows market prices. Shows action number quick inputs. Shows skill exp percentages. Shows total networth.

当前为 2024-05-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MWITools
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.3
  5. // @description Tools for MilkyWayIdle. Shows total action time. Shows market prices. Shows action number quick inputs. Shows skill exp percentages. Shows total networth.
  6. // @author bot7420
  7. // @match https://www.milkywayidle.com/*
  8. // @grant GM_xmlhttpRequest
  9. // @connect raw.githubusercontent.com
  10. // @connect 43.129.194.214
  11. // ==/UserScript==
  12.  
  13. (() => {
  14. "use strict";
  15.  
  16. let initData_characterSkills = null;
  17. let initData_characterItems = null;
  18. let initData_characterHouseRoomMap = null;
  19. let initData_actionTypeDrinkSlotsMap = null;
  20. let initData_actionDetailMap = null;
  21. let initData_levelExperienceTable = null;
  22. let initData_itemDetailMap = null;
  23.  
  24. let currentActionsHridList = [];
  25.  
  26. hookWS();
  27.  
  28. fetchMarketJSON(true);
  29.  
  30. function hookWS() {
  31. const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
  32. const oriGet = dataProperty.get;
  33.  
  34. dataProperty.get = hookedGet;
  35. Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
  36.  
  37. function hookedGet() {
  38. const socket = this.currentTarget;
  39. if (!(socket instanceof WebSocket)) {
  40. return oriGet.call(this);
  41. }
  42. if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1) {
  43. return oriGet.call(this);
  44. }
  45.  
  46. const message = oriGet.call(this);
  47. Object.defineProperty(this, "data", { value: message }); // Anti-loop
  48.  
  49. return handleMessage(message);
  50. }
  51. }
  52.  
  53. function handleMessage(message) {
  54. let obj = JSON.parse(message);
  55. if (obj && obj.type === "init_character_data") {
  56. console.log(obj.characterItems);
  57. initData_characterSkills = obj.characterSkills;
  58. initData_characterItems = obj.characterItems;
  59. initData_characterHouseRoomMap = obj.characterHouseRoomMap;
  60. initData_actionTypeDrinkSlotsMap = obj.actionTypeDrinkSlotsMap;
  61. currentActionsHridList = [...obj.characterActions];
  62. calculateNetworth();
  63. } else if (obj && obj.type === "init_client_data") {
  64. console.log(obj.itemDetailMap);
  65. initData_actionDetailMap = obj.actionDetailMap;
  66. initData_levelExperienceTable = obj.levelExperienceTable;
  67. initData_itemDetailMap = obj.itemDetailMap;
  68. } else if (obj && obj.type === "actions_updated") {
  69. for (const action of obj.endCharacterActions) {
  70. if (action.isDone === false) {
  71. let o = {};
  72. o.id = action.id;
  73. o.actionHrid = action.actionHrid;
  74. currentActionsHridList.push(o);
  75. } else {
  76. currentActionsHridList = currentActionsHridList.filter((o) => {
  77. return o.id !== action.id;
  78. });
  79. }
  80. }
  81. console.log(currentActionsHridList);
  82. } else if (obj && obj.type === "battle_unit_fetched") {
  83. console.log(obj);
  84. handleBattleSummary(obj);
  85. }
  86. return message;
  87. }
  88.  
  89. /* 计算Networth */
  90. async function calculateNetworth() {
  91. const marketAPIJson = await fetchMarketJSON();
  92. let networthAsk = 0;
  93. let networthBid = 0;
  94. for (const item of initData_characterItems) {
  95. const itemName = initData_itemDetailMap[item.itemHrid].name;
  96. const marketPrices = marketAPIJson.market[itemName];
  97. if (marketPrices) {
  98. networthAsk += item.count * (marketPrices.ask > 0 ? marketPrices.ask : 0);
  99. networthBid += item.count * (marketPrices.bid > 0 ? marketPrices.bid : 0);
  100. }
  101. }
  102.  
  103. const waitForHeader = () => {
  104. const targetNode = document.querySelector("div.Header_totalLevel__8LY3Q");
  105. if (targetNode) {
  106. targetNode.insertAdjacentHTML("afterend", `<div style="text-align: left;">Networth: ${numberFormatter(networthAsk)} / ${numberFormatter(networthBid)}</div>`);
  107. } else {
  108. setTimeout(waitForHeader, 200);
  109. }
  110. };
  111. waitForHeader();
  112. }
  113.  
  114. /* 显示当前动作总时间 */
  115. const showTotalActionTime = () => {
  116. const targetNode = document.querySelector("div.Header_actionName__31-L2 > div.Header_actionName__31-L2");
  117. if (targetNode) {
  118. calculateTotalTime(targetNode);
  119. new MutationObserver((mutationsList) =>
  120. mutationsList.forEach((mutation) => {
  121. if (mutation.type === "characterData") {
  122. calculateTotalTime();
  123. }
  124. })
  125. ).observe(targetNode, { characterData: true, subtree: true });
  126. } else {
  127. setTimeout(showTotalActionTime, 200);
  128. }
  129. };
  130. showTotalActionTime();
  131.  
  132. function calculateTotalTime() {
  133. const targetNode = document.querySelector("div.Header_actionName__31-L2 > div.Header_actionName__31-L2");
  134. const textNode = [...targetNode.childNodes]
  135. .filter((child) => child.nodeType === Node.TEXT_NODE)
  136. .filter((child) => child.textContent.trim())
  137. .map((textNode) => textNode)[0];
  138. if (textNode.textContent.includes("[")) {
  139. return;
  140. }
  141.  
  142. let totalTimeStr = "Error";
  143. if (targetNode.childNodes.length === 1) {
  144. totalTimeStr = " [" + timeReadable(0) + "]";
  145. } else if (targetNode.childNodes.length === 2) {
  146. const content = targetNode.innerText;
  147. const match = content.match(/\((\d+)\)/);
  148. if (match) {
  149. const numOfTimes = +match[1];
  150. const timePerActionSec = +document.querySelector(".ProgressBar_text__102Yn").textContent.match(/[\d\.]+/)[0];
  151. const actionHrid = currentActionsHridList[0].actionHrid;
  152. const effBuff = 1 + getTotalEffiPercentage(actionHrid) / 100;
  153. const actualNumberOfTimes = Math.round(numOfTimes / effBuff);
  154. totalTimeStr = " [" + timeReadable(actualNumberOfTimes * timePerActionSec) + "]";
  155. } else {
  156. totalTimeStr = " [∞]";
  157. }
  158. }
  159. textNode.textContent += totalTimeStr;
  160. }
  161.  
  162. function timeReadable(sec) {
  163. if (sec >= 86400) {
  164. return Number(sec / 86400).toFixed(1) + " 天";
  165. }
  166. const d = new Date(Math.round(sec * 1000));
  167. function pad(i) {
  168. return ("0" + i).slice(-2);
  169. }
  170. let str = d.getUTCHours() + "h " + pad(d.getUTCMinutes()) + "m " + pad(d.getUTCSeconds()) + "s";
  171. return str;
  172. }
  173.  
  174. /* 物品 ToolTips */
  175. const tooltipObserver = new MutationObserver(async function (mutations) {
  176. for (const mutation of mutations) {
  177. for (const added of mutation.addedNodes) {
  178. if (added.classList.contains("MuiTooltip-popper")) {
  179. if (added.querySelector("div.ItemTooltipText_name__2JAHA")) {
  180. await handleTooltipItem(added);
  181. }
  182. }
  183. }
  184. }
  185. });
  186. tooltipObserver.observe(document.body, { attributes: false, childList: true, characterData: false });
  187.  
  188. const actionHridToToolsSpeedBuffNamesMap = {
  189. "/action_types/brewing": "brewingSpeed",
  190. "/action_types/cheesesmithing": "cheesesmithingSpeed",
  191. "/action_types/cooking": "cookingSpeed",
  192. "/action_types/crafting": "craftingSpeed",
  193. "/action_types/foraging": "foragingSpeed",
  194. "/action_types/milking": "milkingSpeed",
  195. "/action_types/tailoring": "tailoringSpeed",
  196. "/action_types/woodcutting": "woodcuttingSpeed",
  197. };
  198.  
  199. const actionHridToHouseNamesMap = {
  200. "/action_types/brewing": "/house_rooms/brewery",
  201. "/action_types/cheesesmithing": "/house_rooms/forge",
  202. "/action_types/cooking": "/house_rooms/kitchen",
  203. "/action_types/crafting": "/house_rooms/workshop",
  204. "/action_types/foraging": "/house_rooms/garden",
  205. "/action_types/milking": "/house_rooms/dairy_barn",
  206. "/action_types/tailoring": "/house_rooms/sewing_parlor",
  207. "/action_types/woodcutting": "/house_rooms/log_shed",
  208. };
  209.  
  210. const itemEnhanceLevelToBuffBonusMap = {
  211. 0: 0,
  212. 1: 2,
  213. 2: 4.2,
  214. 3: 6.6,
  215. 4: 9.2,
  216. 5: 12.0,
  217. 6: 15.0,
  218. 7: 18.2,
  219. 8: 21.6,
  220. 9: 25.2,
  221. 10: 29.0,
  222. 11: 33.0,
  223. 12: 37.2,
  224. 13: 41.6,
  225. 14: 46.2,
  226. 15: 51.0,
  227. 16: 56.0,
  228. 17: 61.2,
  229. 18: 66.6,
  230. 19: 72.2,
  231. 20: 78.0,
  232. };
  233.  
  234. function getToolsSpeedBuffByActionHrid(actionHrid) {
  235. let buff = 0;
  236. for (const item of initData_characterItems) {
  237. if (item.itemLocationHrid.includes("_tool")) {
  238. const buffName = actionHridToToolsSpeedBuffNamesMap[initData_actionDetailMap[actionHrid].type];
  239. const enhanceBonus = 1 + itemEnhanceLevelToBuffBonusMap[item.enhancementLevel] / 100;
  240. buff += initData_itemDetailMap[item.itemHrid].equipmentDetail.noncombatStats[buffName] * enhanceBonus;
  241. }
  242. }
  243. return Number(buff * 100).toFixed(1);
  244. }
  245.  
  246. function getItemEffiBuffByActionHrid(actionHrid) {
  247. let buff = 0;
  248. const propertyName = initData_actionDetailMap[actionHrid].type.replace("/action_types/", "") + "Efficiency";
  249. for (const item of initData_characterItems) {
  250. const itemDetail = initData_itemDetailMap[item.itemHrid];
  251. const stat = itemDetail?.equipmentDetail?.noncombatStats[propertyName];
  252. if (stat && stat > 0) {
  253. let enhanceBonus = 1;
  254. if (item.itemLocationHrid.includes("earrings") || item.itemLocationHrid.includes("ring") || item.itemLocationHrid.includes("neck")) {
  255. enhanceBonus = 1 + (itemEnhanceLevelToBuffBonusMap[item.enhancementLevel] * 5) / 100;
  256. } else {
  257. enhanceBonus = 1 + itemEnhanceLevelToBuffBonusMap[item.enhancementLevel] / 100;
  258. }
  259. buff += stat * enhanceBonus;
  260. }
  261. }
  262. return Number(buff * 100).toFixed(1);
  263. }
  264.  
  265. function getHousesEffBuffByActionHrid(actionHrid) {
  266. const houseName = actionHridToHouseNamesMap[initData_actionDetailMap[actionHrid].type];
  267. if (!houseName) {
  268. return 0;
  269. }
  270. const house = initData_characterHouseRoomMap[houseName];
  271. if (!house) {
  272. return 0;
  273. }
  274. return house.level * 1.5;
  275. }
  276.  
  277. function getTeaBuffsByActionHrid(actionHrid) {
  278. // YES Gathering (+15% quantity) — milking, foraging, woodcutting
  279. // TODO Processing (+15% chance to convert product into processed material) — milking, foraging, woodcutting
  280. // YES Gourmet (+12% to produce free product) — cooking, brewing
  281. // YES Artisan (-10% less resources used, but treat as -5 levels) — cheesesmithing, crafting, tailoring, cooking, brewing
  282. // NO Wisdom (+12% XP) — all
  283. // YES Efficiency (+10% chance to repeat action) — all (except enhancing)
  284. // YES S.Skill (treat as +3 or +6 levels, different names) — all
  285. let teaBuffs = {
  286. efficiency: 0,
  287. quantity: 0,
  288. upgradedProduct: 0,
  289. lessResource: 0,
  290. };
  291.  
  292. const teaList = initData_actionTypeDrinkSlotsMap[initData_actionDetailMap[actionHrid].type];
  293. for (const tea of teaList) {
  294. if (!tea || !tea.itemHrid) {
  295. continue;
  296. }
  297. if (tea.itemHrid === "/items/efficiency_tea") {
  298. teaBuffs.efficiency += 10;
  299. continue;
  300. }
  301. const teaBuffDetail = initData_itemDetailMap[tea.itemHrid]?.consumableDetail?.buffs[0];
  302. if (teaBuffDetail && teaBuffDetail.typeHrid.includes("_level")) {
  303. teaBuffs.efficiency += teaBuffDetail.flatBoost;
  304. continue;
  305. }
  306. if (tea.itemHrid === "/items/artisan_tea") {
  307. teaBuffs.lessResource += 10;
  308. continue;
  309. }
  310. if (tea.itemHrid === "/items/gathering_tea") {
  311. teaBuffs.quantity += 15;
  312. continue;
  313. }
  314. if (tea.itemHrid === "/items/gourmet_tea") {
  315. teaBuffs.quantity += 12;
  316. continue;
  317. }
  318. if (tea.itemHrid === "/items/processing_tea") {
  319. teaBuffs.upgradedProduct += 15;
  320. continue;
  321. }
  322. }
  323. return teaBuffs;
  324. }
  325.  
  326. async function handleTooltipItem(tooltip) {
  327. const itemName = tooltip.querySelector("div.ItemTooltipText_name__2JAHA").textContent;
  328. const amountSpan = tooltip.querySelectorAll("span")[1];
  329. const amount = +amountSpan.textContent.split(": ")[1].replaceAll(",", "");
  330.  
  331. const jsonObj = await fetchMarketJSON();
  332. if (!jsonObj) {
  333. amountSpan.parentNode.insertAdjacentHTML(
  334. "afterend",
  335. `
  336. <div style="color: DarkGreen;"">获取市场API失败</div>
  337. `
  338. );
  339. return;
  340. }
  341. if (!jsonObj.market) {
  342. amountSpan.parentNode.insertAdjacentHTML(
  343. "afterend",
  344. `
  345. <div style="color: DarkGreen;"">市场API格式错误</div>
  346. `
  347. );
  348. return;
  349. }
  350. if (!jsonObj.market[itemName]) {
  351. console.error("itemName not found in market API json: " + itemName);
  352. }
  353.  
  354. let appendHTMLStr = "";
  355.  
  356. // 市场价格
  357. const ask = jsonObj?.market[itemName]?.ask;
  358. const bid = jsonObj?.market[itemName]?.bid;
  359. appendHTMLStr += `
  360. <div style="color: DarkGreen;"">日均价: ${numberFormatter(ask)} / ${numberFormatter(bid)} (${ask && ask > 0 ? numberFormatter(ask * amount) : ""} / ${
  361. bid && bid > 0 ? numberFormatter(bid * amount) : ""
  362. })</div>
  363. `;
  364.  
  365. if (
  366. getActionHridFromItemName(itemName) &&
  367. initData_actionDetailMap[getActionHridFromItemName(itemName)].inputItems &&
  368. initData_actionDetailMap[getActionHridFromItemName(itemName)].inputItems.length > 0 &&
  369. initData_actionDetailMap &&
  370. initData_itemDetailMap
  371. ) {
  372. // 制造类技能
  373. const actionHrid = getActionHridFromItemName(itemName);
  374. const inputItems = JSON.parse(JSON.stringify(initData_actionDetailMap[actionHrid].inputItems));
  375. let totalAskPrice = 0;
  376. let totalBidPrice = 0;
  377. for (let item of inputItems) {
  378. item.name = initData_itemDetailMap[item.itemHrid].name;
  379. item.perAskPrice = jsonObj?.market[item.name]?.ask;
  380. item.perBidPrice = jsonObj?.market[item.name]?.bid;
  381. totalAskPrice += item.perAskPrice * item.count;
  382. totalBidPrice += item.perBidPrice * item.count;
  383. }
  384.  
  385. appendHTMLStr += `<div style="color: DarkGreen; font-size: 10px;">原料价: ${numberFormatter(totalAskPrice)} / ${numberFormatter(totalBidPrice)}</div>`;
  386. for (const item of inputItems) {
  387. appendHTMLStr += `
  388. <div style="color: DarkGreen; font-size: 10px;"> ${item.name} x${item.count}: ${numberFormatter(item.perAskPrice)} / ${numberFormatter(item.perBidPrice)}</div>
  389. `;
  390. }
  391.  
  392. // 基础每小时生产数量
  393. let produceItemPerHour = 3600000 / (initData_actionDetailMap[actionHrid].baseTimeCost / 1000000);
  394. // 基础掉率
  395. let droprate = 1;
  396. // 工具提高速度
  397. let toolPercent = getToolsSpeedBuffByActionHrid(actionHrid);
  398. produceItemPerHour *= 1 + toolPercent / 100;
  399. // 等级碾压提高效率
  400. const requiredLevel = initData_actionDetailMap[actionHrid].levelRequirement.level;
  401. let currentLevel = requiredLevel;
  402. for (const skill of initData_characterSkills) {
  403. if (skill.skillHrid === initData_actionDetailMap[actionHrid].levelRequirement.skillHrid) {
  404. currentLevel = skill.level;
  405. break;
  406. }
  407. }
  408. const levelEffBuff = currentLevel - requiredLevel > 0 ? currentLevel - requiredLevel : 0;
  409. // 房子效率
  410. const houseEffBuff = getHousesEffBuffByActionHrid(actionHrid);
  411. // 茶效率
  412. const teaBuffs = getTeaBuffsByActionHrid(actionHrid);
  413. // 特殊装备效率
  414. const itemEffiBuff = Number(getItemEffiBuffByActionHrid(actionHrid));
  415. // 总效率
  416. produceItemPerHour *= 1 + (levelEffBuff + houseEffBuff + teaBuffs.efficiency + itemEffiBuff) / 100;
  417. // 茶额外数量
  418. let extraQuantityPerHour = (produceItemPerHour * teaBuffs.quantity) / 100;
  419.  
  420. appendHTMLStr += `<div style="color: DarkGreen; font-size: 10px;">生产利润(卖单价进、买单价出;不包括Processing Tea、社区buff、稀有掉落;刷新网页更新人物数据):</div>`;
  421. appendHTMLStr += `<div style="color: DarkGreen; font-size: 10px;">x${droprate}基础掉率 +${toolPercent}%工具速度 +${levelEffBuff}%等级效率 +${houseEffBuff}%房子效率 +${teaBuffs.efficiency}%茶效率 +${itemEffiBuff}%装备效率 +${teaBuffs.quantity}%茶额外数量 +${teaBuffs.lessResource}%茶减少消耗</div>`;
  422. appendHTMLStr += `<div style="color: DarkGreen; font-size: 10px;">每小时生产 ${Number(produceItemPerHour + extraQuantityPerHour).toFixed(1)} 个</div>`;
  423. appendHTMLStr += `<div style="color: DarkGreen;">利润: ${numberFormatter(bid - totalAskPrice * (1 - teaBuffs.lessResource / 100))}/个, ${numberFormatter(
  424. produceItemPerHour * (bid - totalAskPrice * (1 - teaBuffs.lessResource / 100)) + extraQuantityPerHour * bid
  425. )}/小时, ${numberFormatter(24 * produceItemPerHour * (bid - totalAskPrice * (1 - teaBuffs.lessResource / 100)) + extraQuantityPerHour * bid)}/天</div>`;
  426. } else if (getActionHridFromItemName(itemName) && initData_actionDetailMap[getActionHridFromItemName(itemName)].inputItems === null && initData_actionDetailMap && initData_itemDetailMap) {
  427. // 采集类技能
  428. const actionHrid = getActionHridFromItemName(itemName);
  429. // 基础每小时生产数量
  430. let produceItemPerHour = 3600000 / (initData_actionDetailMap[actionHrid].baseTimeCost / 1000000);
  431. // 基础掉率
  432. let droprate = (initData_actionDetailMap[actionHrid].dropTable[0].minCount + initData_actionDetailMap[actionHrid].dropTable[0].maxCount) / 2;
  433. produceItemPerHour *= droprate;
  434. // 工具提高速度
  435. let toolPercent = getToolsSpeedBuffByActionHrid(actionHrid);
  436. produceItemPerHour *= 1 + toolPercent / 100;
  437. // 等级碾压效率
  438. const requiredLevel = initData_actionDetailMap[actionHrid].levelRequirement.level;
  439. let currentLevel = requiredLevel;
  440. for (const skill of initData_characterSkills) {
  441. if (skill.skillHrid === initData_actionDetailMap[actionHrid].levelRequirement.skillHrid) {
  442. currentLevel = skill.level;
  443. break;
  444. }
  445. }
  446. const levelEffBuff = currentLevel - requiredLevel > 0 ? currentLevel - requiredLevel : 0;
  447. // 房子效率
  448. const houseEffBuff = getHousesEffBuffByActionHrid(actionHrid);
  449. // 茶效率
  450. const teaBuffs = getTeaBuffsByActionHrid(actionHrid);
  451. // 特殊装备效率
  452. const itemEffiBuff = Number(getItemEffiBuffByActionHrid(actionHrid));
  453. // 总效率
  454. produceItemPerHour *= 1 + (levelEffBuff + houseEffBuff + teaBuffs.efficiency + itemEffiBuff) / 100;
  455. // 茶额外数量
  456. let extraQuantityPerHour = (produceItemPerHour * teaBuffs.quantity) / 100;
  457.  
  458. appendHTMLStr += `<div style="color: DarkGreen; font-size: 10px;">生产利润(卖单价进、买单价出;不包括Processing Tea、社区buff、稀有掉落;刷新网页更新人物数据):</div>`;
  459. appendHTMLStr += `<div style="color: DarkGreen; font-size: 10px;">x${droprate}基础掉率 +${toolPercent}%工具速度 +${levelEffBuff}%等级效率 +${houseEffBuff}%房子效率 +${teaBuffs.efficiency}%茶效率 +${itemEffiBuff}%装备效率 +${teaBuffs.quantity}%茶额外数量 +${teaBuffs.lessResource}%茶减少消耗</div>`;
  460. appendHTMLStr += `<div style="color: DarkGreen; font-size: 10px;">每小时生产 ${Number(produceItemPerHour + extraQuantityPerHour).toFixed(1)} 个</div>`;
  461. appendHTMLStr += `<div style="color: DarkGreen;">利润: ${numberFormatter(bid)}/个, ${numberFormatter(produceItemPerHour * bid + extraQuantityPerHour * bid)}/小时, ${numberFormatter(
  462. 24 * produceItemPerHour * bid + extraQuantityPerHour * bid
  463. )}/天</div>`;
  464. }
  465.  
  466. amountSpan.parentNode.nextSibling.insertAdjacentHTML("afterend", appendHTMLStr);
  467. }
  468.  
  469. async function fetchMarketJSON(forceFetch = false) {
  470. if (!forceFetch && localStorage.getItem("MWITools_marketAPI_timestamp") && Date.now() - localStorage.getItem("MWITools_marketAPI_timestamp") < 900000) {
  471. return JSON.parse(localStorage.getItem("MWITools_marketAPI_json"));
  472. }
  473.  
  474. console.log("fetchMarketJSON fetch");
  475. let jsonStr = null;
  476. jsonStr = await new Promise((resolve, reject) => {
  477. GM.xmlHttpRequest({
  478. url: `https://raw.githubusercontent.com/holychikenz/MWIApi/main/medianmarket.json`,
  479. method: "GET",
  480. synchronous: true,
  481. onload: async (response) => {
  482. if (response.status == 200) {
  483. console.log("fetchMarketJSON github fetch success 200");
  484. resolve(response.responseText);
  485. } else {
  486. console.error("MWITools: fetchMarketJSON github onload with HTTP status " + response.status);
  487. resolve(null);
  488. }
  489. },
  490. onabort: () => {
  491. console.error("MWITools: fetchMarketJSON github onabort");
  492. resolve(null);
  493. },
  494. onerror: () => {
  495. console.error("MWITools: fetchMarketJSON github onerror");
  496. resolve(null);
  497. },
  498. ontimeout: () => {
  499. console.error("MWITools: fetchMarketJSON github ontimeout");
  500. resolve(null);
  501. },
  502. });
  503. });
  504.  
  505. if (jsonStr === null) {
  506. console.log("MWITools: fetchMarketJSON try fetch cache start");
  507. jsonStr = await new Promise((resolve, reject) => {
  508. GM.xmlHttpRequest({
  509. url: `http://43.129.194.214:5000/apijson`,
  510. method: "GET",
  511. synchronous: true,
  512. onload: async (response) => {
  513. if (response.status == 200) {
  514. console.log("fetchMarketJSON cache fetch success 200");
  515. resolve(response.responseText);
  516. } else {
  517. console.error("MWITools: fetchMarketJSON cache onload with HTTP status " + response.status);
  518. resolve(null);
  519. }
  520. },
  521. onabort: () => {
  522. console.error("MWITools: fetchMarketJSON cache onabort");
  523. resolve(null);
  524. },
  525. onerror: () => {
  526. console.error("MWITools: fetchMarketJSON cache onerror");
  527. resolve(null);
  528. },
  529. ontimeout: () => {
  530. console.error("MWITools: fetchMarketJSON cache ontimeout");
  531. resolve(null);
  532. },
  533. });
  534. });
  535. }
  536.  
  537. const jsonObj = JSON.parse(jsonStr);
  538. if (jsonObj && jsonObj.time && jsonObj.market) {
  539. jsonObj.market.Coin.ask = 1;
  540. jsonObj.market.Coin.bid = 1;
  541. console.log(jsonObj);
  542. localStorage.setItem("MWITools_marketAPI_timestamp", Date.now());
  543. localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(jsonObj));
  544. return jsonObj;
  545. }
  546. console.error("MWITools: fetchMarketJSON JSON.parse error");
  547. localStorage.setItem("MWITools_marketAPI_timestamp", 0);
  548. localStorage.setItem("MWITools_marketAPI_json", "");
  549. return null;
  550. }
  551.  
  552. function numberFormatter(num, digits = 1) {
  553. if (num === null || num === undefined) {
  554. return null;
  555. }
  556. if (num < 0) {
  557. return "-" + numberFormatter(-num);
  558. }
  559. const lookup = [
  560. { value: 1, symbol: "" },
  561. { value: 1e3, symbol: "k" },
  562. { value: 1e6, symbol: "M" },
  563. { value: 1e9, symbol: "B" },
  564. ];
  565. const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
  566. var item = lookup
  567. .slice()
  568. .reverse()
  569. .find(function (item) {
  570. return num >= item.value;
  571. });
  572. return item ? (num / item.value).toFixed(digits).replace(rx, "$1") + item.symbol : "0";
  573. }
  574.  
  575. function getActionHridFromItemName(name) {
  576. let newName = name.replace("Milk", "Cow");
  577. newName = newName.replace("Log", "Tree");
  578. newName = newName.replace("Cowing", "Milking");
  579. newName = newName.replace("Rainbow Cow", "Unicow");
  580. if (!initData_actionDetailMap) {
  581. console.error("getActionHridFromItemName no initData_actionDetailMap: " + name);
  582. return null;
  583. }
  584. for (const action of Object.values(initData_actionDetailMap)) {
  585. if (action.name === newName) {
  586. return action.hrid;
  587. }
  588. }
  589. return null;
  590. }
  591.  
  592. /* 动作面板 */
  593. const waitForActionPanelParent = () => {
  594. const targetNode = document.querySelector("div.GamePage_mainPanel__2njyb");
  595. if (targetNode) {
  596. const actionPanelObserver = new MutationObserver(async function (mutations) {
  597. for (const mutation of mutations) {
  598. for (const added of mutation.addedNodes) {
  599. if (added && added.classList && added.classList.contains("Modal_modalContainer__3B80m") && added.querySelector("div.SkillActionDetail_nonenhancingComponent__1Y-ZY")) {
  600. handleActionPanel(added.querySelector("div.SkillActionDetail_nonenhancingComponent__1Y-ZY"));
  601. }
  602. }
  603. }
  604. });
  605. actionPanelObserver.observe(targetNode, { attributes: false, childList: true, subtree: true });
  606. } else {
  607. setTimeout(waitForActionPanelParent, 200);
  608. }
  609. };
  610. waitForActionPanelParent();
  611.  
  612. async function handleActionPanel(panel) {
  613. const actionName = panel.querySelector("div.SkillActionDetail_name__3erHV").textContent;
  614. const exp = Number(panel.querySelector("div.SkillActionDetail_expGain__F5xHu").textContent);
  615. const duration = Number(panel.querySelectorAll("div.SkillActionDetail_value__dQjYH")[4].textContent.replace("s", ""));
  616. const inputElem = panel.querySelector("div.SkillActionDetail_maxActionCountInput__1C0Pw input");
  617.  
  618. const actionHrid = initData_actionDetailMap[getActionHridFromItemName(actionName)].hrid;
  619. const effBuff = 1 + getTotalEffiPercentage(actionHrid, false) / 100;
  620.  
  621. // 显示总时间
  622. let hTMLStr = `<div id="showTotalTime" style="color: Green; text-align: left;">${getTotalTimeStr(inputElem.value, duration, effBuff)}</div>`;
  623. inputElem.parentNode.insertAdjacentHTML("afterend", hTMLStr);
  624. const showTotalTimeDiv = panel.querySelector("div#showTotalTime");
  625.  
  626. panel.addEventListener("click", function (evt) {
  627. setTimeout(() => {
  628. showTotalTimeDiv.textContent = getTotalTimeStr(inputElem.value, duration, effBuff);
  629. }, 50);
  630. });
  631. inputElem.addEventListener("keyup", function (evt) {
  632. showTotalTimeDiv.textContent = getTotalTimeStr(inputElem.value, duration, effBuff);
  633. });
  634.  
  635. // 显示快捷按钮
  636. hTMLStr = `<div id="quickInputButtons" style="color: Green; text-align: left;">做 </div>`;
  637. showTotalTimeDiv.insertAdjacentHTML("afterend", hTMLStr);
  638. const quickInputButtonsDiv = panel.querySelector("div#quickInputButtons");
  639.  
  640. const presetHours = [0.5, 1, 2, 3, 4, 5, 6, 10, 12, 24];
  641. for (const value of presetHours) {
  642. const btn = document.createElement("button");
  643. btn.style.backgroundColor = "white";
  644. btn.style.padding = "1px 6px 1px 6px";
  645. btn.style.margin = "1px";
  646. btn.innerText = value === 0.5 ? 0.5 : numberFormatter(value);
  647. btn.onclick = () => {
  648. reactInputTriggerHack(inputElem, Math.round((value * 60 * 60 * effBuff) / duration));
  649. };
  650. quickInputButtonsDiv.append(btn);
  651. }
  652. quickInputButtonsDiv.append(document.createTextNode(" 小时"));
  653.  
  654. quickInputButtonsDiv.append(document.createElement("div"));
  655. quickInputButtonsDiv.append(document.createTextNode("做 "));
  656. const presetTimes = [10, 20, 50, 100, 200, 500, 1000, 2000];
  657. for (const value of presetTimes) {
  658. const btn = document.createElement("button");
  659. btn.style.backgroundColor = "white";
  660. btn.style.padding = "1px 6px 1px 6px";
  661. btn.style.margin = "1px";
  662. btn.innerText = numberFormatter(value);
  663. btn.onclick = () => {
  664. reactInputTriggerHack(inputElem, value);
  665. };
  666. quickInputButtonsDiv.append(btn);
  667. }
  668. quickInputButtonsDiv.append(document.createTextNode(" 次"));
  669.  
  670. // 还有多久到多少技能等级
  671. const skillHrid = initData_actionDetailMap[getActionHridFromItemName(actionName)].experienceGain.skillHrid;
  672. let currentExp = null;
  673. let currentLevel = null;
  674. for (const skill of initData_characterSkills) {
  675. if (skill.skillHrid === skillHrid) {
  676. currentExp = skill.experience;
  677. currentLevel = skill.level;
  678. break;
  679. }
  680. }
  681. if (currentExp && currentLevel) {
  682. let targetLevel = currentLevel + 1;
  683. let needExp = initData_levelExperienceTable[targetLevel] - currentExp;
  684. let needNumOfActions = Math.round(needExp / exp);
  685. let needTime = timeReadable((needNumOfActions / effBuff) * duration);
  686.  
  687. hTMLStr = `<div id="tillLevel" style="color: Green; text-align: left;">到 <input id="tillLevelInput" type="number" value="${targetLevel}" min="${targetLevel}" max="200"> 级还需做 <span id="tillLevelNumber">${needNumOfActions} 次[${needTime}] (刷新网页更新当前等级)</span></div>`;
  688. quickInputButtonsDiv.insertAdjacentHTML("afterend", hTMLStr);
  689. const tillLevelInput = panel.querySelector("input#tillLevelInput");
  690. const tillLevelNumber = panel.querySelector("span#tillLevelNumber");
  691. tillLevelInput.onchange = () => {
  692. let targetLevel = Number(tillLevelInput.value);
  693. if (targetLevel > currentLevel && targetLevel <= 200) {
  694. let needExp = initData_levelExperienceTable[targetLevel] - currentExp;
  695. let needNumOfActions = Math.round(needExp / exp);
  696. let needTime = timeReadable((needNumOfActions / effBuff) * duration);
  697. tillLevelNumber.textContent = `${needNumOfActions} [${needTime}] (刷新网页更新当前等级)`;
  698. } else {
  699. tillLevelNumber.textContent = "Error";
  700. }
  701. };
  702. tillLevelInput.addEventListener("keyup", function (evt) {
  703. let targetLevel = Number(tillLevelInput.value);
  704. if (targetLevel > currentLevel && targetLevel <= 200) {
  705. let needExp = initData_levelExperienceTable[targetLevel] - currentExp;
  706. let needNumOfActions = Math.round(needExp / exp);
  707. let needTime = timeReadable((needNumOfActions / effBuff) * duration);
  708. tillLevelNumber.textContent = `${needNumOfActions} [${needTime}] (刷新网页更新当前等级)`;
  709. } else {
  710. tillLevelNumber.textContent = "Error";
  711. }
  712. });
  713. }
  714.  
  715. // 显示每小时经验
  716. panel
  717. .querySelector("div#tillLevel")
  718. .insertAdjacentHTML(
  719. "afterend",
  720. `<div id="expPerHour" style="color: Green; text-align: left;">每小时经验: ${numberFormatter(Math.round((3600 / duration) * exp * effBuff))} (+${Number((effBuff - 1) * 100).toFixed(
  721. 1
  722. )}%效率)</div>`
  723. );
  724.  
  725. // 显示Foraging最后一个图综合收益
  726. if (panel.querySelector("div.SkillActionDetail_dropTable__3ViVp").children.length > 1) {
  727. const jsonObj = await fetchMarketJSON();
  728. const actionHrid = "/actions/foraging/" + actionName.toLowerCase().replaceAll(" ", "_");
  729. let numOfActionsPerHour = 3600000 / (initData_actionDetailMap[actionHrid].baseTimeCost / 1000000);
  730. let dropTable = initData_actionDetailMap[actionHrid].dropTable;
  731. let virtualItemBid = 0;
  732. for (const drop of dropTable) {
  733. const bid = jsonObj?.market[initData_itemDetailMap[drop.itemHrid].name]?.bid;
  734. const amount = drop.dropRate * ((drop.minCount + drop.maxCount) / 2);
  735. virtualItemBid += bid * amount;
  736. }
  737.  
  738. // 工具提高速度
  739. let toolPercent = getToolsSpeedBuffByActionHrid(actionHrid);
  740. numOfActionsPerHour *= 1 + toolPercent / 100;
  741. // 等级碾压效率
  742. const requiredLevel = initData_actionDetailMap[actionHrid].levelRequirement.level;
  743. let currentLevel = requiredLevel;
  744. for (const skill of initData_characterSkills) {
  745. if (skill.skillHrid === initData_actionDetailMap[actionHrid].levelRequirement.skillHrid) {
  746. currentLevel = skill.level;
  747. break;
  748. }
  749. }
  750. const levelEffBuff = currentLevel - requiredLevel;
  751. // 房子效率
  752. const houseEffBuff = getHousesEffBuffByActionHrid(actionHrid);
  753. // 茶
  754. const teaBuffs = getTeaBuffsByActionHrid(actionHrid);
  755. // 总效率
  756. numOfActionsPerHour *= 1 + (levelEffBuff + houseEffBuff + teaBuffs.efficiency) / 100;
  757. // 茶额外数量
  758. let extraQuantityPerHour = (numOfActionsPerHour * teaBuffs.quantity) / 100;
  759.  
  760. let htmlStr = `<div id="totalProfit" style="color: Green; text-align: left;">综合利润: ${numberFormatter(
  761. numOfActionsPerHour * virtualItemBid + extraQuantityPerHour * virtualItemBid
  762. )}/小时, ${numberFormatter(24 * numOfActionsPerHour * virtualItemBid + extraQuantityPerHour * virtualItemBid)}/天</div>`;
  763. panel.querySelector("div#expPerHour").insertAdjacentHTML("afterend", htmlStr);
  764. }
  765. }
  766.  
  767. function getTotalEffiPercentage(actionHrid, debug = false) {
  768. if (debug) {
  769. console.log("----- getTotalEffiPercentage " + actionHrid);
  770. }
  771. // 等级碾压效率
  772. const requiredLevel = initData_actionDetailMap[actionHrid].levelRequirement.level;
  773. let currentLevel = requiredLevel;
  774. for (const skill of initData_characterSkills) {
  775. if (skill.skillHrid === initData_actionDetailMap[actionHrid].levelRequirement.skillHrid) {
  776. currentLevel = skill.level;
  777. break;
  778. }
  779. }
  780. const levelEffBuff = currentLevel - requiredLevel > 0 ? currentLevel - requiredLevel : 0;
  781. if (debug) {
  782. console.log("等级碾压 " + levelEffBuff);
  783. }
  784. // 房子效率
  785. const houseEffBuff = getHousesEffBuffByActionHrid(actionHrid);
  786. if (debug) {
  787. console.log("房子 " + houseEffBuff);
  788. }
  789. // 茶
  790. const teaBuffs = getTeaBuffsByActionHrid(actionHrid);
  791. if (debug) {
  792. console.log("茶 " + teaBuffs.efficiency);
  793. }
  794. // 特殊装备
  795. const itemEffiBuff = getItemEffiBuffByActionHrid(actionHrid);
  796. if (debug) {
  797. console.log("特殊装备 " + itemEffiBuff);
  798. }
  799. // 总效率
  800. const total = levelEffBuff + houseEffBuff + teaBuffs.efficiency + Number(itemEffiBuff);
  801. if (debug) {
  802. console.log("总计 " + total);
  803. }
  804. return total;
  805. }
  806.  
  807. function getTotalTimeStr(input, duration, effBuff) {
  808. if (input === "unlimited") {
  809. return "[∞]";
  810. } else if (isNaN(input)) {
  811. return "Error";
  812. }
  813. return "[" + timeReadable(Math.round(input / effBuff) * duration) + "]";
  814. }
  815.  
  816. function reactInputTriggerHack(inputElem, value) {
  817. let lastValue = inputElem.value;
  818. inputElem.value = value;
  819. let event = new Event("input", { bubbles: true });
  820. event.simulated = true;
  821. let tracker = inputElem._valueTracker;
  822. if (tracker) {
  823. tracker.setValue(lastValue);
  824. }
  825. inputElem.dispatchEvent(event);
  826. }
  827.  
  828. /* 左侧栏显示技能百分比 */
  829. const waitForProgressBar = () => {
  830. const elements = document.querySelectorAll(".NavigationBar_currentExperience__3GDeX");
  831. if (elements.length) {
  832. removeInsertedDivs();
  833. elements.forEach((element) => {
  834. let text = element.style.width;
  835. text = Number(text.replace("%", "")).toFixed(2) + "%";
  836.  
  837. const span = document.createElement("span");
  838. span.textContent = text;
  839. span.classList.add("insertedSpan");
  840. span.style.fontSize = "13px";
  841. span.style.color = "green";
  842.  
  843. element.parentNode.parentNode.querySelector("span.NavigationBar_level__3C7eR").style.width = "auto";
  844.  
  845. const insertParent = element.parentNode.parentNode.children[0];
  846. insertParent.insertBefore(span, insertParent.children[1]);
  847. });
  848. } else {
  849. setTimeout(waitForProgressBar, 200);
  850. }
  851. };
  852.  
  853. const removeInsertedDivs = () => document.querySelectorAll("span.insertedSpan").forEach((div) => div.parentNode.removeChild(div));
  854.  
  855. window.setInterval(() => {
  856. removeInsertedDivs();
  857. waitForProgressBar();
  858. }, 1000);
  859.  
  860. /* 战斗总结 */
  861. async function handleBattleSummary(message) {
  862. const marketJson = await fetchMarketJSON();
  863. if (!marketJson) {
  864. console.error("handleBattleSummary failed because of null marketAPI");
  865. return;
  866. }
  867. let totalPriceAsk = 0;
  868. let totalPriceAskBid = 0;
  869.  
  870. for (const loot of Object.values(message.unit.totalLootMap)) {
  871. const itemName = initData_itemDetailMap[loot.itemHrid].name;
  872. const itemCount = loot.count;
  873. if (marketJson.market[itemName]) {
  874. totalPriceAsk += marketJson.market[itemName].ask * itemCount;
  875. totalPriceAskBid += marketJson.market[itemName].bid * itemCount;
  876. } else {
  877. console.error("handleBattleSummary failed to read price of " + loot.itemHrid);
  878. }
  879. }
  880.  
  881. let totalSkillsExp = 0;
  882. for (const exp of Object.values(message.unit.totalSkillExperienceMap)) {
  883. totalSkillsExp += exp;
  884. }
  885.  
  886. let tryTimes = 0;
  887. findElem();
  888. function findElem() {
  889. tryTimes++;
  890. let elem = document.querySelector(".BattlePanel_gainedExp__3SaCa");
  891. if (elem) {
  892. // 战斗时长和次数
  893. let battleDurationSec = null;
  894. const combatInfoElement = document.querySelector(".BattlePanel_combatInfo__sHGCe");
  895. if (combatInfoElement) {
  896. let matches = combatInfoElement.innerHTML.match(
  897. /(战斗时长|Combat Duration): (?:(\d+)d\s*)?(?:(\d+)h\s*)?(?:(\d+)m\s*)?(?:(\d+)s).*?(战斗|Battles): (\d+).*?(死亡次数|Deaths): (\d+)/
  898. );
  899. if (matches) {
  900. let days = parseInt(matches[2], 10) || 0;
  901. let hours = parseInt(matches[3], 10) || 0;
  902. let minutes = parseInt(matches[4], 10) || 0;
  903. let seconds = parseInt(matches[5], 10) || 0;
  904. let battles = parseInt(matches[7], 10);
  905. battleDurationSec = days * 86400 + hours * 3600 + minutes * 60 + seconds;
  906. let efficiencyPerHour = ((battles / battleDurationSec) * 3600).toFixed(1);
  907. elem.insertAdjacentHTML("afterend", `<div id="script_battleNumbers" style="color: Green;">平均每小时战斗 ${efficiencyPerHour} 次</div>`);
  908. }
  909. }
  910. // 总收入
  911. document
  912. .querySelector("div#script_battleNumbers")
  913. .insertAdjacentHTML("afterend", `<div id="script_totalIncome" style="color: Green;">总收入: ${numberFormatter(totalPriceAsk)} / ${numberFormatter(totalPriceAskBid)}</div>`);
  914. // 平均收入
  915. if (battleDurationSec) {
  916. document
  917. .querySelector("div#script_totalIncome")
  918. .insertAdjacentHTML(
  919. "afterend",
  920. `<div id="script_averageIncome" style="color: Green;">平均每小时收入: ${numberFormatter(totalPriceAsk / (battleDurationSec / 60 / 60))} / ${numberFormatter(
  921. totalPriceAskBid / (battleDurationSec / 60 / 60)
  922. )}</div>`
  923. );
  924. } else {
  925. console.error("handleBattleSummary unable to display average income due to null battleDurationSec");
  926. }
  927. // 总经验
  928. document
  929. .querySelector("div#script_averageIncome")
  930. .insertAdjacentHTML("afterend", `<div id="script_totalSkillsExp" style="color: Green;">总经验: ${numberFormatter(totalSkillsExp)}</div>`);
  931. // 平均经验
  932. if (battleDurationSec) {
  933. document
  934. .querySelector("div#script_totalSkillsExp")
  935. .insertAdjacentHTML(
  936. "afterend",
  937. `<div id="script_averageSkillsExp" style="color: Green;">平均每小时经验: ${numberFormatter(totalSkillsExp / (battleDurationSec / 60 / 60))}</div>`
  938. );
  939. } else {
  940. console.error("handleBattleSummary unable to display average exp due to null battleDurationSec");
  941. }
  942. } else if (tryTimes <= 10) {
  943. setTimeout(findElem, 200);
  944. } else {
  945. console.log("handleBattleSummary: Elem not found after 10 tries.");
  946. }
  947. }
  948. }
  949. })();