MWIAlchemyCalc

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

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