MWIAlchemyCalc

显示炼金收益和产出统计 milkywayidle 银河奶牛放置

  1. // ==UserScript==
  2. // @name MWIAlchemyCalc
  3.  
  4. // @namespace http://tampermonkey.net/
  5. // @version 20250507.4
  6. // @description 显示炼金收益和产出统计 milkywayidle 银河奶牛放置
  7.  
  8. // @author IOMisaka
  9. // @match https://www.milkywayidle.com/*
  10. // @icon https://www.milkywayidle.com/favicon.svg
  11. // @grant none
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17. if (!window.mwi) {
  18. console.error("MWIAlchemyCalc需要安装mooket才能使用");
  19. return;
  20. }
  21.  
  22. ////////////////code//////////////////
  23. function hookWS() {
  24. const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
  25. const oriGet = dataProperty.get;
  26. dataProperty.get = hookedGet;
  27. Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
  28.  
  29. function hookedGet() {
  30. const socket = this.currentTarget;
  31. if (!(socket instanceof WebSocket)) {
  32. return oriGet.call(this);
  33. }
  34. if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1) {
  35. return oriGet.call(this);
  36. }
  37. const message = oriGet.call(this);
  38. Object.defineProperty(this, "data", { value: message }); // Anti-loop
  39. handleMessage(message);
  40. return message;
  41. }
  42. }
  43.  
  44. let characterData = null;
  45. let alchemyActionIndex = 0;
  46. function handleMessage(message) {
  47. let obj = JSON.parse(message);
  48. if (obj) {
  49. if (obj.type === "init_character_data") {
  50. characterData = obj;
  51. } else if (obj.type === "action_type_consumable_slots_updated") {//更新饮料和食物槽数据
  52. characterData.actionTypeDrinkSlotsMap = obj.actionTypeDrinkSlotsMap;
  53. characterData.actionTypeFoodSlotsMap = obj.actionTypeFoodSlotsMap;
  54.  
  55. handleAlchemyDetailChanged();
  56. } else if (obj.type === "consumable_buffs_updated") {
  57. characterData.consumableActionTypeBuffsMap = obj.consumableActionTypeBuffsMap;
  58. handleAlchemyDetailChanged();
  59. } else if (obj.type === "community_buffs_updated") {
  60. characterData.communityActionTypeBuffsMap = obj.communityActionTypeBuffsMap;
  61. handleAlchemyDetailChanged();
  62. } else if (obj.type === "equipment_buffs_updated") {//装备buff
  63. characterData.equipmentActionTypeBuffsMap = obj.equipmentActionTypeBuffsMap;
  64. characterData.equipmentTaskActionBuffs = obj.equipmentTaskActionBuffs;
  65. handleAlchemyDetailChanged();
  66. } else if (obj.type === "house_rooms_updated") {//房屋更新
  67. characterData.characterHouseRoomMap = obj.characterHouseRoomMap;
  68. characterData.houseActionTypeBuffsMap = obj.houseActionTypeBuffsMap;
  69. }
  70. else if (obj.type === "actions_updated") {
  71. //延迟检测
  72. setTimeout(() => {
  73. let firstAction = mwi.game?.state?.characterActions[0];
  74. if (firstAction && firstAction.actionHrid.startsWith("/actions/alchemy")) {
  75. updateAlchemyAction(firstAction);
  76. }
  77. }, 100);
  78.  
  79.  
  80. }
  81. else if (obj.type === "action_completed") {//更新技能等级和经验
  82. if (obj.endCharacterItems) {//道具更新
  83. //炼金统计
  84. try {
  85. if (obj.endCharacterAction.actionHrid.startsWith("/actions/alchemy")) {//炼金统计
  86. updateAlchemyAction(obj.endCharacterAction);
  87.  
  88. let outputHashCount = {};
  89. let inputHashCount = {};
  90. let tempItems = {};
  91. obj.endCharacterItems.forEach(
  92. item => {
  93.  
  94. let existItem = tempItems[item.id] || characterData.characterItems.find(x => x.id === item.id);
  95.  
  96. //console.log("炼金(old):",existItem.id,existItem.itemHrid, existItem.count);
  97. //console.log("炼金(new):", item.id,item.itemHrid, item.count);
  98.  
  99. let delta = (item.count - (existItem?.count || 0));//计数
  100. if (delta < 0) {//数量减少
  101. inputHashCount[item.hash] = (inputHashCount[item.hash] || 0) + delta;//可能多次发送同一个物品
  102. tempItems[item.id] = item;//替换旧的物品计数
  103. } else if (delta > 0) {//数量增加
  104. outputHashCount[item.hash] = (outputHashCount[item.hash] || 0) + delta;//可能多次发送同一个物品
  105. tempItems[item.id] = item;//替换旧的物品计数
  106. } else {
  107. console.log("炼金统计出错?不应该为0", item);
  108. }
  109. }
  110. );
  111. let index = [
  112. "/actions/alchemy/coinify",
  113. "/actions/alchemy/decompose",
  114. "/actions/alchemy/transmute"
  115. ].findIndex(x => x === obj.endCharacterAction.actionHrid);
  116. countAlchemyOutput(inputHashCount, outputHashCount, index);
  117. } else {
  118. alchemyActionIndex = -1;//不是炼金
  119. }
  120. } catch (e) { }
  121.  
  122. let newIds = obj.endCharacterItems.map(i => i.id);
  123. characterData.characterItems = characterData.characterItems.filter(e => !newIds.includes(e.id));//移除存在的物品
  124. characterData.characterItems.push(...mergeObjectsById(obj.endCharacterItems));//放入新物品
  125. }
  126. if (obj.endCharacterSkills) {
  127. for (let newSkill of obj.endCharacterSkills) {
  128. let oldSkill = characterData.characterSkills.find(skill => skill.skillHrid === newSkill.skillHrid);
  129.  
  130. oldSkill.level = newSkill.level;
  131. oldSkill.experience = newSkill.experience;
  132. }
  133. }
  134. } else if (obj.type === "items_updated") {
  135. if (obj.endCharacterItems) {//道具更新
  136. let newIds = obj.endCharacterItems.map(i => i.id);
  137. characterData.characterItems = characterData.characterItems.filter(e => !newIds.includes(e.id));//移除存在的物品
  138. characterData.characterItems.push(...mergeObjectsById(obj.endCharacterItems));//放入新物品
  139. }
  140. }
  141. }
  142. return message;
  143. }
  144. function mergeObjectsById(list) {
  145. return Object.values(list.reduce((acc, obj) => {
  146. const id = obj.id;
  147. acc[id] = { ...acc[id], ...obj }; // 后面的对象会覆盖前面的
  148. return acc;
  149. }, {}));
  150. }
  151. /////////辅助函数,角色动态数据///////////
  152. // skillHrid = "/skills/alchemy"
  153. function getSkillLevel(skillHrid, withBuff = false) {
  154. let skill = characterData.characterSkills.find(skill => skill.skillHrid === skillHrid);
  155. let level = skill?.level || 0;
  156.  
  157. if (withBuff) {//计算buff加成
  158. level += getBuffValueByType(
  159. skillHrid.replace("/skills/", "/action_types/"),
  160. skillHrid.replace("/skills/", "/buff_types/") + "_level"
  161. );
  162. }
  163. return level;
  164. }
  165.  
  166. /// actionTypeHrid = "/action_types/alchemy"
  167. /// buffTypeHrid = "/buff_types/alchemy_level"
  168. function getBuffValueByType(actionTypeHrid, buffTypeHrid) {
  169. let returnValue = 0;
  170. //社区buff
  171.  
  172. for (let buff of characterData.communityActionTypeBuffsMap[actionTypeHrid] || []) {
  173. if (buff.typeHrid === buffTypeHrid) returnValue += buff.flatBoost;
  174. }
  175. //装备buff
  176. for (let buff of characterData.equipmentActionTypeBuffsMap[actionTypeHrid] || []) {
  177. if (buff.typeHrid === buffTypeHrid) returnValue += buff.flatBoost;
  178. }
  179. //房屋buff
  180. for (let buff of characterData.houseActionTypeBuffsMap[actionTypeHrid] || []) {
  181. if (buff.typeHrid === buffTypeHrid) returnValue += buff.flatBoost;
  182. }
  183. //茶饮buff
  184. for (let buff of characterData.consumableActionTypeBuffsMap[actionTypeHrid] || []) {
  185. if (buff.typeHrid === buffTypeHrid) returnValue += buff.flatBoost;
  186. }
  187. return returnValue;
  188. }
  189. /**
  190. * 获取角色ID
  191. *
  192. * @returns {string|null} 角色ID,如果不存在则返回null
  193. */
  194. function getCharacterId() {
  195. return characterData?.character.id;
  196. }
  197. /**
  198. * 获取指定物品的数量
  199. *
  200. * @param itemHrid 物品的唯一标识
  201. * @param enhancementLevel 物品强化等级,默认为0
  202. * @returns 返回指定物品的数量,如果未找到该物品则返回0
  203. */
  204. function getItemCount(itemHrid, enhancementLevel = 0) {
  205. return characterData.characterItems.find(item => item.itemHrid === itemHrid && item.itemLocationHrid === "/item_locations/inventory" && item.enhancementLevel === enhancementLevel)?.count || 0;//背包里面的物品
  206. }
  207. //获取饮料状态,传入类型/action_types/brewing,返回列表
  208.  
  209. function getDrinkSlots(actionTypeHrid) {
  210. return characterData.actionTypeDrinkSlotsMap[actionTypeHrid]
  211. }
  212. /////////游戏静态数据////////////
  213. //中英文都有可能
  214. function getItemHridByShowName(showName) {
  215. return window.mwi.ensureItemHrid(showName)
  216. }
  217. //类似这样的名字/items/blackberry_donut,/items/knights_ingot
  218. function getItemDataByHrid(itemHrid) {
  219. return mwi.initClientData.itemDetailMap[itemHrid];
  220. }
  221. function getOpenableItems(itemHrid) {
  222. let items = [];
  223. for (let openItem of mwi.initClientData.openableLootDropMap[itemHrid]) {
  224. items.push({
  225. itemHrid: openItem.itemHrid,
  226. count: (openItem.minCount + openItem.maxCount) / 2 * openItem.dropRate
  227. });
  228. }
  229. return items;
  230. }
  231. ////////////观察节点变化/////////////
  232. function observeNode(nodeSelector, rootSelector, addFunc = null, updateFunc = null, removeFunc = null) {
  233. const rootNode = document.querySelector(rootSelector);
  234. if (!rootNode) {
  235. //console.error(`Root node with selector "${rootSelector}" not found.wait for 1s to try again...`);
  236. setTimeout(() => observeNode(nodeSelector, rootSelector, addFunc, updateFunc, removeFunc), 1000);
  237. return;
  238. }
  239. console.info(`observing "${rootSelector}"`);
  240.  
  241. function delayCall(func, observer, delay = 200) {
  242. //判断func是function类型
  243. if (typeof func !== 'function') return;
  244. // 延迟执行,如果再次调用则在原有基础上继续延时
  245. func.timeout && clearTimeout(func.timeout);
  246. func.timeout = setTimeout(() => func(observer), delay);
  247. }
  248.  
  249. const observer = new MutationObserver((mutationsList, observer) => {
  250.  
  251. mutationsList.forEach((mutation) => {
  252. mutation.addedNodes.forEach((addedNode) => {
  253. if (addedNode.matches && addedNode.matches(nodeSelector)) {
  254. addFunc?.(observer);
  255. }
  256. });
  257.  
  258. mutation.removedNodes.forEach((removedNode) => {
  259. if (removedNode.matches && removedNode.matches(nodeSelector)) {
  260. removeFunc?.(observer);
  261. }
  262. });
  263.  
  264. // 处理子节点变化
  265. if (mutation.type === 'childList') {
  266. let node = mutation.target?.matches(nodeSelector) ? mutation.target : mutation.target.closest(nodeSelector);
  267. if (node) {
  268. delayCall(updateFunc, observer); // 延迟 100ms 合并变动处理,避免频繁触发
  269. }
  270.  
  271. } else if (mutation.type === 'characterData') {
  272. // 文本内容变化(如文本节点修改)
  273. let node = document.querySelector(nodeSelector);
  274. let targetNode = mutation.target;
  275. while (targetNode) {
  276. if (targetNode == node) {
  277. delayCall(updateFunc, observer);
  278. break;
  279. }
  280. targetNode = targetNode.parentNode;
  281. }
  282. }
  283. });
  284. });
  285.  
  286.  
  287. const config = {
  288. childList: true,
  289. subtree: true,
  290. characterData: true
  291. };
  292. observer.reobserve = function () {
  293. observer.observe(rootNode, config);
  294. }//重新观察
  295. observer.observe(rootNode, config);
  296. return observer;
  297. }
  298. hookWS();//hook收到角色信息
  299. //返回[买,卖]
  300. function getPrice(itemHrid, enhancementLevel = 0) {
  301. return mwi.coreMarket.getItemPrice(itemHrid, enhancementLevel);
  302. }
  303. let includeRare = false;
  304. let priceMode = "ab";//左买右卖
  305. //计算每次的收益
  306. function calculateProfit(data, isIroncow = false, isCoinify = false) {
  307. let profit = 0;
  308. let input = 0;
  309. let output = 0;
  310. let essence = 0;
  311. let rare = 0;
  312. let tea = 0;
  313. let catalyst = 0;
  314. let tax = isIroncow ? 1 : 0.98;//铁牛不扣税
  315.  
  316. const mode = {
  317. "ab": ["ask", "bid"],
  318. "ba": ["bid", "ask"],
  319. "aa": ["ask", "ask"],
  320. "bb": ["bid", "bid"],
  321. };
  322. let [buyPrice, sellPrice] = mode[priceMode];
  323.  
  324. for (let item of data.inputItems) {//消耗物品每次必定消耗
  325.  
  326. input -= getPrice(item.itemHrid, item.enhancementLevel)[buyPrice] * item.count;//买入材料价格*数量
  327.  
  328. }
  329. for (let item of data.teaUsage) {//茶每次必定消耗
  330. tea -= getPrice(item.itemHrid)[buyPrice] * item.count;//买入材料价格*数量
  331. }
  332.  
  333. for (let item of data.outputItems) {//产出物品每次不一定产出,需要计算成功率
  334. output += getPrice(item.itemHrid)[sellPrice] * item.count * data.successRate * tax;//卖出产出价格*数量*成功率*税后
  335.  
  336. }
  337. if (data.inputItems[0].itemHrid !== "/items/task_crystal") {//任务水晶有问题,暂时不计算
  338. for (let item of data.essenceDrops) {//精华和宝箱与成功率无关 消息id,10211754失败出精华!
  339. essence += getPrice(item.itemHrid)[sellPrice] * item.count * tax;//采集数据的地方已经算进去了
  340. }
  341. if (includeRare) {//排除宝箱,因为几率过低,严重影响收益显示
  342. for (let item of data.rareDrops) {//宝箱也是按自己的几率出!
  343. // getOpenableItems(item.itemHrid).forEach(openItem => {
  344. // rare += getPrice(openItem.itemHrid).bid * openItem.count * item.count;//已折算
  345. // });
  346. rare += getPrice(item.itemHrid)[sellPrice] * item.count * tax;//失败要出箱子,消息id,2793104转化,工匠茶失败出箱子了
  347. }
  348. }
  349. }
  350. //催化剂
  351. for (let item of data.catalystItems) {//催化剂,成功才会用
  352. catalyst -= getPrice(item.itemHrid)[buyPrice] * item.count * data.successRate;//买入材料价格*数量
  353. }
  354.  
  355. let description = "";
  356. if (isIroncow && isCoinify) {//铁牛点金不计算输入
  357. profit = tea + output + essence + rare + catalyst;
  358. description = `
  359. (${mwi.isZh ? "税" : "tax"}${isIroncow ? "0" : "2%"})
  360. (${mwi.isZh ? "效率" : "effeciency"}+${(data.effeciency * 100).toFixed(2)}%)
  361. ${mwi.isZh ? "每次收益" : "each"}:${profit}=
  362. \t${mwi.isZh ? "材料" : "material"}(${input})[${mwi.isZh ? "铁牛点金不计入" : "not included for ironcowinify"}]
  363. \t${mwi.isZh ? "茶" : "tea"}(${tea})
  364. \t${mwi.isZh ? "催化剂" : "catalyst"}(${catalyst})
  365. \t${mwi.isZh ? "产出" : "output"}(${output})
  366. \t${mwi.isZh ? "精华" : "essence"}(${essence})
  367. \t${mwi.isZh ? "稀有" : "rare"}(${rare})`;
  368.  
  369. } else {
  370. profit = input + tea + output + essence + rare + catalyst;
  371. description = `
  372. (${mwi.isZh ? "税" : "tax"}${isIroncow ? "0" : "2%"})
  373. (${mwi.isZh ? "效率" : "effeciency"}+${(data.effeciency * 100).toFixed(2)}%)
  374. ${mwi.isZh ? "每次收益" : "each"}:${profit}=
  375. \t${mwi.isZh ? "材料" : "material"}(${input})
  376. \t${mwi.isZh ? "茶" : "tea"}(${tea})
  377. \t${mwi.isZh ? "催化剂" : "catalyst"}(${catalyst})
  378. \t${mwi.isZh ? "产出" : "output"}(${output})
  379. \t${mwi.isZh ? "精华" : "essence"}(${essence})
  380. \t${mwi.isZh ? "稀有" : "rare"}(${rare})`;
  381. }
  382.  
  383. //console.info(description);
  384. return [profit, description];//再乘以次数
  385. }
  386.  
  387. function showNumber(num) {
  388. if (isNaN(num)) return num;
  389. if (num === 0) return "0";// 单独处理0的情况
  390.  
  391. const sign = num > 0 ? '+' : '';
  392. const absNum = Math.abs(num);
  393.  
  394. return absNum >= 1e10 ? `${sign}${(num / 1e9).toFixed(1)}B` :
  395. absNum >= 1e7 ? `${sign}${(num / 1e6).toFixed(1)}M` :
  396. absNum >= 1e5 ? `${sign}${Math.floor(num / 1e3)}K` :
  397. `${sign}${Math.floor(num)}`;
  398. }
  399. function parseNumber(str) {
  400. return parseInt(str.replaceAll("/", "").replaceAll(",", "").replaceAll(" ", ""));
  401. }
  402. let predictPerDay = {};
  403. function handleAlchemyDetailChanged(observer) {
  404. let inputItems = [];
  405. let outputItems = [];
  406. let essenceDrops = [];
  407. let rareDrops = [];
  408. let teaUsage = [];
  409. let catalystItems = [];
  410.  
  411. let costNodes = document.querySelector(".AlchemyPanel_skillActionDetailContainer__o9SsW .SkillActionDetail_itemRequirements__3SPnA");
  412. if (!costNodes) return;//没有炼金详情就不处理
  413.  
  414. let costs = Array.from(costNodes.children);
  415. //每三个元素取textContent拼接成一个字符串,用空格和/分割
  416. for (let i = 0; i < costs.length; i += 3) {
  417.  
  418. let need = parseNumber(costs[i + 1].textContent);
  419. let nameArr = costs[i + 2].textContent.split("+");
  420. let itemHrid = getItemHridByShowName(nameArr[0]);
  421. let enhancementLevel = nameArr.length > 1 ? parseNumber(nameArr[1]) : 0;
  422.  
  423. inputItems.push({ itemHrid: itemHrid, enhancementLevel: enhancementLevel, count: need });
  424. }
  425.  
  426. //炼金输出
  427. for (let line of document.querySelectorAll(".SkillActionDetail_alchemyOutput__6-92q .SkillActionDetail_drop__26KBZ")) {
  428. let count = parseFloat(line.children[0].textContent.replaceAll(",", ""));
  429. let itemName = line.children[1].textContent;
  430. let rate = line.children[2].textContent ? parseFloat(line.children[2].textContent.substring(1, line.children[2].textContent.length - 1) / 100.0) : 1;//默认1
  431. outputItems.push({ itemHrid: getItemHridByShowName(itemName), count: count * rate });
  432. }
  433. //精华输出
  434. for (let line of document.querySelectorAll(".SkillActionDetail_essenceDrops__2skiB .SkillActionDetail_drop__26KBZ")) {
  435. let count = parseFloat(line.children[0].textContent);
  436. let itemName = line.children[1].textContent;
  437. let rate = line.children[2].textContent ? parseFloat(line.children[2].textContent.substring(1, line.children[2].textContent.length - 1) / 100.0) : 1;//默认1
  438. essenceDrops.push({ itemHrid: getItemHridByShowName(itemName), count: count * rate });
  439. }
  440. //稀有输出
  441. for (let line of document.querySelectorAll(".SkillActionDetail_rareDrops__3OTzu .SkillActionDetail_drop__26KBZ")) {
  442. let count = parseFloat(line.children[0].textContent);
  443. let itemName = line.children[1].textContent;
  444. let rate = line.children[2].textContent ? parseFloat(line.children[2].textContent.substring(1, line.children[2].textContent.length - 1) / 100.0) : 1;//默认1
  445. rareDrops.push({ itemHrid: getItemHridByShowName(itemName), count: count * rate });
  446. }
  447. //成功率
  448. let successRateStr = document.querySelector(".SkillActionDetail_successRate__2jPEP .SkillActionDetail_value__dQjYH").textContent;
  449. let successRate = parseFloat(successRateStr.substring(0, successRateStr.length - 1)) / 100.0;
  450.  
  451. //消耗时间
  452. let costTimeStr = document.querySelector(".SkillActionDetail_timeCost__1jb2x .SkillActionDetail_value__dQjYH").textContent;
  453. let costSeconds = parseFloat(costTimeStr.substring(0, costTimeStr.length - 1));//秒,有分再改
  454.  
  455.  
  456.  
  457. //催化剂
  458. let catalystItem = document.querySelector(".SkillActionDetail_catalystItemInput__2ERjq .Icon_icon__2LtL_") || document.querySelector(".SkillActionDetail_catalystItemInputContainer__5zmou .Item_iconContainer__5z7j4 .Icon_icon__2LtL_");//过程中是另一个框
  459. if (catalystItem) {
  460. catalystItems = [{ itemHrid: getItemHridByShowName(catalystItem.getAttribute("aria-label")), count: 1 }];
  461. }
  462.  
  463. //计算效率
  464. let effeciency = getBuffValueByType("/action_types/alchemy", "/buff_types/efficiency");
  465. let skillLevel = getSkillLevel("/skills/alchemy", true);
  466. let mainItem = getItemDataByHrid(inputItems[0].itemHrid);
  467. if (mainItem.itemLevel) {
  468. effeciency += Math.max(0, skillLevel - mainItem.itemLevel) / 100;//等级加成
  469. }
  470.  
  471. //costSeconds = costSeconds * (1 - effeciency);//效率,相当于减少每次的时间
  472. costSeconds = costSeconds / (1 + effeciency);
  473. //茶饮,茶饮的消耗就减少了
  474. let teas = getDrinkSlots("/action_types/alchemy");//炼金茶配置
  475. for (let tea of teas) {
  476. if (tea) {//有可能空位
  477. teaUsage.push({ itemHrid: tea.itemHrid, count: costSeconds / 300 });//300秒消耗一个茶
  478. }
  479. }
  480. console.info("效率", effeciency);
  481.  
  482.  
  483. //返回结果
  484. let ret = {
  485. inputItems: inputItems,
  486. outputItems: outputItems,
  487. essenceDrops: essenceDrops,
  488. rareDrops: rareDrops,
  489. successRate: successRate,
  490. costTime: costSeconds,
  491. teaUsage: teaUsage,
  492. catalystItems: catalystItems,
  493. effeciency: effeciency,
  494. }
  495. const buttons = document.querySelectorAll(".AlchemyPanel_tabsComponentContainer__1f7FY .MuiButtonBase-root.MuiTab-root.MuiTab-textColorPrimary.css-1q2h7u5");
  496. const selectedIndex = Array.from(buttons).findIndex(button =>
  497. button.classList.contains('Mui-selected')
  498. );
  499. let isCowinify = (selectedIndex == 0 || (selectedIndex == 3 && alchemyActionIndex == 0));//点金模式
  500.  
  501. //次数,收益
  502. let result = calculateProfit(ret, mwi.character?.gameMode === "ironcow", isCowinify);
  503. let profit = result[0];
  504. let desc = result[1];
  505.  
  506. let timesPerHour = 3600 / costSeconds;//加了效率相当于增加了次数
  507. let profitPerHour = profit * timesPerHour;
  508.  
  509. let timesPerDay = 24 * timesPerHour;
  510. let profitPerDay = profit * timesPerDay;
  511.  
  512. predictPerDay[selectedIndex] = profitPerDay;//记录第几个对应的每日收益
  513.  
  514. observer?.disconnect();//断开观察
  515.  
  516. //显示位置
  517. let showParent = document.querySelector(".SkillActionDetail_notes__2je2F");
  518. let label = showParent.querySelector("#alchemoo");
  519. if (!label) {
  520. label = document.createElement("div");
  521. label.id = "alchemoo";
  522. showParent.appendChild(label);
  523. }
  524.  
  525. let color = "white";
  526. if (profitPerHour > 0) {
  527. color = "lime";
  528. } else if (profitPerHour < 0) {
  529. color = "red";
  530. }
  531. label.innerHTML = `
  532. <div id="alchemoo" style="color: ${color};">
  533. <div>
  534. <span title="${desc}">${mwi.isZh ? "预估收益" : "Profit"}ℹ️:</span><input type="checkbox" id="alchemoo_includeRare"/><label for="alchemoo_includeRare">${mwi.isZh ? "稀有" : "Rares"}</label>
  535. <select id="alchemoo_selectMode">
  536. <option value="ab">${mwi.isZh ? "左买右卖" : "ask in,bid out"}</option>
  537. <option value="ba">${mwi.isZh ? "右买左卖" : "bid in,ask out"}</option>
  538. <option value="aa">${mwi.isZh ? "左买左卖" : "ask in,ask out"}</option>
  539. <option value="bb">${mwi.isZh ? "右买右卖" : "bid in,bid out"}</option>
  540. </select>
  541. </div>
  542. <div>
  543. <svg width="14px" height="14px" style="display:inline-block"><use href="/static/media/items_sprite.6d12eb9d.svg#coin"></use></svg>
  544. <span>${showNumber(profit)}/${mwi.isZh ? "次" : "each"}</span>
  545. </div>
  546. <div>
  547. <svg width="14px" height="14px" style="display:inline-block"><use href="/static/media/items_sprite.6d12eb9d.svg#coin"></use></svg>
  548. <span title="${showNumber(timesPerHour)}${mwi.isZh ? "" : "times"}">${showNumber(profitPerHour)}/${mwi.isZh ? "时" : "hour"}</span>
  549. </div>
  550. <div>
  551. <svg width="14px" height="14px" style="display:inline-block"><use href="/static/media/items_sprite.6d12eb9d.svg#coin"></use></svg>
  552. <span title="${showNumber(timesPerDay)}${mwi.isZh ? "" : "times"}">${showNumber(profitPerDay)}/${mwi.isZh ? "天" : "day"}</span>
  553. </div>
  554. </div>`;
  555. document.querySelector("#alchemoo_includeRare").checked = includeRare;
  556. document.querySelector("#alchemoo_includeRare").onchange = function () {
  557. includeRare = this.checked;
  558. handleAlchemyDetailChanged();//重新计算
  559. };
  560. document.querySelector("#alchemoo_selectMode").value = priceMode;
  561. document.querySelector("#alchemoo_selectMode").onchange = function () {
  562. priceMode = this.value;
  563. handleAlchemyDetailChanged();//重新计算
  564. };
  565.  
  566. //console.log(ret);
  567. observer?.reobserve();
  568. }
  569.  
  570. observeNode(".SkillActionDetail_alchemyComponent__1J55d", "body", handleAlchemyDetailChanged, handleAlchemyDetailChanged);
  571.  
  572. let alchemyStartTime = Date.now();
  573. let lastAction = null;
  574. let alchemyHistory = [];//历史记录
  575. let alchemyIndex = 0;//第几次炼金
  576. //统计功能
  577. function countAlchemyOutput(inputHashCount, outputHashCount, index) {
  578. let currentInput = alchemyHistory[alchemyHistory.length-1].input;
  579. let currentOutput = alchemyHistory[alchemyHistory.length-1].output;
  580. alchemyActionIndex = index;
  581. for (let itemHash in inputHashCount) {
  582. currentInput[itemHash] = (currentInput[itemHash] || 0) + inputHashCount[itemHash];
  583. }
  584. for (let itemHash in outputHashCount) {
  585. currentOutput[itemHash] = (currentOutput[itemHash] || 0) + outputHashCount[itemHash];
  586. }
  587. showOutput();
  588. }
  589.  
  590. function updateAlchemyAction(action) {
  591. if ((!lastAction) || (lastAction.id != action.id)) {//新动作,重置统计信息
  592. alchemyHistory.push({//记录新动作
  593. input: {},
  594. output: {},
  595. });
  596. alchemyIndex=alchemyHistory.length - 1;
  597. lastAction = action;
  598. alchemyStartTime = Date.now();//重置开始时间
  599. }
  600.  
  601. showOutput();
  602. }
  603. function calcChestPrice(itemHrid) {
  604. let total = 0;
  605. const mode = {
  606. "ab": ["ask", "bid"],
  607. "ba": ["bid", "ask"],
  608. "aa": ["ask", "ask"],
  609. "bb": ["bid", "bid"],
  610. };
  611. let [buyPrice, sellPrice] = mode[priceMode];
  612.  
  613. getOpenableItems(itemHrid).forEach(openItem => {
  614.  
  615. total += getPrice(openItem.itemHrid)[sellPrice] * openItem.count;
  616. });
  617. return total;
  618. }
  619. function calcPrice(items, buy) {
  620. let total = 0;
  621. const mode = {
  622. "ab": ["ask", "bid"],
  623. "ba": ["bid", "ask"],
  624. "aa": ["ask", "ask"],
  625. "bb": ["bid", "bid"],
  626. };
  627. let [buyPrice, sellPrice] = mode[priceMode];
  628. let priceType = buy ? buyPrice : sellPrice;
  629.  
  630. for (let item of items) {
  631.  
  632. if (item.itemHrid === "/items/task_crystal") {//任务水晶有问题,暂时不计算
  633. }
  634. else if (getItemDataByHrid(item.itemHrid)?.categoryHrid === "/item_categories/loot") {//箱子必定是卖
  635. total += calcChestPrice(item.itemHrid) * item.count * 0.98;//税
  636. } else {
  637.  
  638. total += getPrice(item.itemHrid, item.enhancementLevel ?? 0)[priceType] * item.count * (buy ? 1 : 0.98);//买入材料价格*数量
  639. }
  640.  
  641. }
  642. return total;
  643. }
  644. function itemHashToItem(itemHash) {
  645. let item = {};
  646. let arr = itemHash.split("::");
  647. item.itemHrid = arr[2];
  648. item.enhancementLevel = arr[3];
  649. return item;
  650. }
  651. function getItemNameByHrid(itemHrid) {
  652. return mwi.isZh ?
  653. mwi.lang.zh.translation.itemNames[itemHrid] : mwi.lang.en.translation.itemNames[itemHrid];
  654. }
  655. function secondsToHms(seconds) {
  656. seconds = Number(seconds);
  657. const h = Math.floor(seconds / 3600);
  658. const m = Math.floor((seconds % 3600) / 60);
  659. const s = Math.floor(seconds % 60);
  660.  
  661. return [
  662. h.toString().padStart(2, '0'),
  663. m.toString().padStart(2, '0'),
  664. s.toString().padStart(2, '0')
  665. ].join(':');
  666. }
  667. function showOutput() {
  668. let alchemyContainer = document.querySelector(".SkillActionDetail_alchemyComponent__1J55d");
  669. if (!alchemyContainer) return;
  670.  
  671. if (!document.querySelector("#alchemoo_result")) {
  672. let outputContainer = document.createElement("div");
  673. outputContainer.id = "alchemoo_result";
  674. outputContainer.style.fontSize = "13px";
  675. outputContainer.style.lineHeight = "16px";
  676. outputContainer.style.maxWidth = "220px";
  677. outputContainer.innerHTML = `
  678. <div id="alchemoo_title" style="font-weight: bold; margin-bottom: 10px; text-align: center; color: var(--color-space-300);">${mwi.isZh ? "炼金统计" : "Alchemy Result"}</div>
  679. <div>
  680. <select id="alchemoo_current">
  681. </select>
  682. </div>
  683. <div id="alchemoo_cost" style="display: flex; flex-wrap: wrap; gap: 4px;"></div>
  684. <div id="alchemoo_rate"></div>
  685. <div id="alchemoo_output" style="display: flex; flex-wrap: wrap; gap: 4px;"></div>
  686. <div id="alchemoo_essence"></div>
  687. <div id="alchemoo_rare"></div>
  688. <div id="alchemoo_exp"></div>
  689. <div id="alchemoo_time"></div>
  690. <div id="alchemoo_total" style="font-weight:bold;font-size:16px;border:1px solid var(--color-space-300);border-radius:4px;padding:1px 5px;display: flex; flex-direction: column; align-items: flex-start; gap: 4px;"></div>
  691. `;
  692. outputContainer.style.flex = "0 0 auto";
  693. alchemyContainer.appendChild(outputContainer);
  694. }
  695. let selector = document.querySelector("#alchemoo_current");
  696. selector.innerHTML = "";
  697. for (let i = 0; i < alchemyHistory.length; i++) {
  698. let option = document.createElement("option");
  699. option.value = i;
  700. option.text = (i == alchemyHistory.length - 1) ? (mwi.isZh ? "当前" : "current") : `#${i + 1}`;
  701. selector.add(option);
  702. }
  703. selector.selectedIndex = alchemyIndex;
  704. selector.onchange = function () {
  705. alchemyIndex = this.selectedIndex;
  706. showOutput();
  707. };
  708. let currentInput = alchemyHistory[alchemyIndex].input;
  709. let currentOutput = alchemyHistory[alchemyIndex].output;
  710.  
  711. let cost = calcPrice(Object.entries(currentInput).map(
  712. ([itemHash, count]) => {
  713. let arr = itemHash.split("::");
  714. return { "itemHrid": arr[2], "enhancementLevel": parseInt(arr[3]), "count": count }
  715. })
  716. , true);
  717. let gain = calcPrice(Object.entries(currentOutput).map(
  718. ([itemHash, count]) => {
  719. let arr = itemHash.split("::");
  720. return { "itemHrid": arr[2], "enhancementLevel": parseInt(arr[3]), "count": count }
  721. })
  722. , false);
  723. if (alchemyActionIndex == 0 && mwi.character?.gameMode === "ironcow") { cost = 0 };//铁牛点金,不计算成本
  724. let total = cost + gain;
  725. const mode = {
  726. "ab": ["ask", "bid"],
  727. "ba": ["bid", "ask"],
  728. "aa": ["ask", "ask"],
  729. "bb": ["bid", "bid"],
  730. };
  731. let [buyPrice, sellPrice] = mode[priceMode];
  732.  
  733. let text = "";
  734. //消耗
  735. Object.entries(currentInput).forEach(([itemHash, count]) => {
  736. let item = itemHashToItem(itemHash);
  737. let price = getPrice(item.itemHrid);
  738. text += `
  739. <div title="in:${price[buyPrice]}" style="display: inline-flex;border:1px solid var(--color-space-300);border-radius:4px;padding:1px 5px;">
  740. <svg width="14px" height="14px" style="display:inline-block"><use href="/static/media/items_sprite.6d12eb9d.svg#${item.itemHrid.replace("/items/", "")}"></use></svg>
  741. <span style="display:inline-block">${getItemNameByHrid(item.itemHrid)}</span>
  742. <span style="color:red;display:inline-block;font-size:14px;">${showNumber(count).replace("-", "*")}</span>
  743. </div>
  744. `;
  745. });
  746. if (cost < 0) {//0不显示.已经是负数了
  747. text += `<div style="display: inline-block;border:1px solid var(--color-space-300);border-radius:4px;padding:1px 5px;"><span style="color:red;font-size:16px;">${showNumber(cost)}</span></div>`;
  748. }
  749. document.querySelector("#alchemoo_cost").innerHTML = text;
  750.  
  751. document.querySelector("#alchemoo_rate").innerHTML = `<br/>`;//成功率
  752.  
  753. text = "";
  754. Object.entries(currentOutput).forEach(([itemHash, count]) => {
  755. let item = itemHashToItem(itemHash);
  756. let price = getPrice(item.itemHrid);
  757. text += `
  758. <div title="out:${price[sellPrice]}" style="display: inline-flex;border:1px solid var(--color-space-300);border-radius:4px;padding:1px 5px;">
  759. <svg width="14px" height="14px" style="display:inline-block"><use href="/static/media/items_sprite.6d12eb9d.svg#${item.itemHrid.replace("/items/", "")}"></use></svg>
  760. <span style="display:inline-block">${getItemNameByHrid(item.itemHrid)}</span>
  761. <span style="color:lime;display:inline-block;font-size:14px;">${showNumber(count).replace("+", "*")}</span>
  762. </div>
  763. `;
  764. });
  765. if (gain > 0) {//0不显示
  766. text += `<div style="display: inline-block;border:1px solid var(--color-space-300);border-radius:4px;padding:1px 5px;"><span style="color:lime;font-size:16px;">${showNumber(gain)}</span></div>`;
  767. }
  768. document.querySelector("#alchemoo_output").innerHTML = text;//产出
  769.  
  770. //document.querySelector("#alchemoo_essence").innerHTML = `<br/>`;//精华
  771. //document.querySelector("#alchemoo_rare").innerHTML = `<br/>`;//稀有
  772. document.querySelector("#alchemoo_exp").innerHTML = `<br/>`;//经验
  773. let time = (Date.now() - alchemyStartTime) / 1000;
  774. //document.querySelector("#alchemoo_time").innerHTML = `<span>耗时:${secondsToHms(time)}</span>`;//时间
  775. let perDay = (86400 / time) * total;
  776.  
  777. let profitPerDay = predictPerDay[alchemyActionIndex] || 0;
  778. let timeElapsedStr =`<span>${mwi.isZh ? "耗时" : "Time Elapsed"}:${secondsToHms(time)}</span>`;
  779. let totalProfitStr = `<div>${mwi.isZh ? "累计收益" : "Gain"}:<span style="color:${total > 0 ? "lime" : "red"}">${showNumber(total)}</span></div>`;
  780. let perdayProfitStr = `<div>${mwi.isZh ? "每日收益" : "Daily"}:<span style="color:${perDay > profitPerDay ? "lime" : "red"}">${showNumber(total * (86400 / time)).replace("+", "")}</span></div>`
  781.  
  782. let totalStr = "";
  783. if(alchemyIndex==alchemyHistory.length-1){
  784. totalStr += timeElapsedStr;
  785. totalStr += totalProfitStr;
  786. totalStr += perdayProfitStr;
  787. }else{
  788. totalStr += totalProfitStr;
  789. }
  790.  
  791. //总收益
  792. document.querySelector("#alchemoo_total").innerHTML = totalStr;
  793. }
  794. //mwi.hookMessage("action_completed", countAlchemyOutput);
  795. //mwi.hookMessage("action_updated", updateAlchemyAction)
  796. })();