MWIAlchemyCalc

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

目前为 2025-04-26 提交的版本,查看 最新版本

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