MWIAlchemyCalc

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

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