您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
动作面板增强
// ==UserScript== // @name MWI_Toolkit_ActionDetailPlus // @namespace http://tampermonkey.net/ // @version 5.1.4 // @description 动作面板增强 // @author zqzhang1996 // @icon https://www.milkywayidle.com/favicon.svg // @match https://www.milkywayidle.com/* // @match https://test.milkywayidle.com/* // @require https://cdn.jsdelivr.net/npm/[email protected]/libs/lz-string.min.js // @require https://update.greasyfork.org/scripts/550719/1677027/MWI_Toolkit.js // @grant none // @run-at document-body // @license MIT // ==/UserScript== (function () { 'use strict'; //#region 工具类 class Utils { static formatNumber(num) { if (typeof num !== 'number' || isNaN(num)) return '0'; if (num < 0) num = 0; if (num < 100) { // 整数部分<=2位,保留1位小数,但如果小数为0则只显示整数 const fixed = num.toFixed(1); if (fixed.endsWith('.0')) { return Math.round(num).toString(); } return fixed; } else if (num < 100000) { // 整数部分<=5位,向上取整 return Math.ceil(num).toString(); } else if (num < 10_000_000) { // 10,000,000~9,999,999 显示xxxK return Math.floor(num / 1000) + 'K'; } else if (num < 10_000_000_000) { // 10,000,000~9,999,999,999 显示xxxM return Math.floor(num / 1_000_000) + 'M'; } else { // 更大的数值显示NaN return 'NaN'; } } static getIconHrefByItemHrid(itemHrid) { return '/static/media/items_sprite.d4d08849.svg#' + itemHrid.split('/').pop(); } static getItemDisplayName(itemHrid) { return window.MWI_Toolkit?.i18n?.getItemName(itemHrid, MWI_Toolkit_Calculator_App.Language); } static reactInputTriggerHack(inputElem) { let lastValue = inputElem.value; let event = new Event("input", { bubbles: true }); event.simulated = true; let tracker = inputElem._valueTracker; if (tracker) { tracker.setValue(lastValue === '' ? ' ' : ''); // 触发变更 } inputElem.dispatchEvent(event); } } //#endregion //#region 主应用程序 class MWI_Toolkit_ActionDetailPlus_App { static Language = 'zh'; constructor() { this.processableItemMap = { "/items/milk": "/items/cheese", "/items/verdant_milk": "/items/verdant_cheese", "/items/azure_milk": "/items/azure_cheese", "/items/burble_milk": "/items/burble_cheese", "/items/crimson_milk": "/items/crimson_cheese", "/items/rainbow_milk": "/items/rainbow_cheese", "/items/holy_milk": "/items/holy_cheese", "/items/log": "/items/lumber", "/items/birch_log": "/items/birch_lumber", "/items/cedar_log": "/items/cedar_lumber", "/items/purpleheart_log": "/items/purpleheart_lumber", "/items/ginkgo_log": "/items/ginkgo_lumber", "/items/redwood_log": "/items/redwood_lumber", "/items/arcane_log": "/items/arcane_lumber", "/items/cotton": "/items/cotton_fabric", "/items/flax": "/items/linen_fabric", "/items/bamboo_branch": "/items/bamboo_fabric", "/items/cocoon": "/items/silk_fabric", "/items/radiant_fiber": "/items/radiant_fabric" }; } enhanceSkillActionDetail() { const { upgradeItemHrid, // itemHrid inputItems, // [{itemHrid, count}] outputItems // [{itemHrid, count}] } = this.calculateActionDetail(); const missingUpgradeItemCountComponent = {}; if (upgradeItemHrid) { const missingCountContainer = document.querySelector('[class^="SkillActionDetail_upgradeItemSelectorInput"]')?.parentElement?.previousElementSibling; if (missingCountContainer) { const newTextSpan = document.createElement('span'); newTextSpan.textContent = missingCountContainer.textContent; newTextSpan.style.height = window.getComputedStyle(document.querySelector('[class*="SkillActionDetail_levelRequirement"]')).height; missingCountContainer.innerHTML = ''; missingCountContainer.appendChild(newTextSpan); const missingCountComponent = document.createElement('div'); missingCountComponent.style.display = 'flex'; missingCountComponent.style.alignItems = 'flex-end'; missingCountComponent.style.flexDirection = 'column'; const missingCountSpan = document.createElement('span'); missingCountSpan.style.display = 'flex'; missingCountSpan.style.alignItems = 'center'; missingCountSpan.style.color = '#faa21e'; missingCountComponent.appendChild(missingCountSpan); missingUpgradeItemCountComponent.itemHrid = upgradeItemHrid; missingUpgradeItemCountComponent.missingCountSpan = missingCountSpan; missingUpgradeItemCountComponent.count = 1; // 升级物品固定需求1个 missingCountContainer.appendChild(missingCountComponent); } } // [{itemHrid, missingCountSpan, inventoryCountSpan, inputCountSpan, count}] const inputItemComponents = []; if (inputItems) { const inputItemComponentContainer = document.querySelector('[class^="SkillActionDetail_itemRequirements"]'); const missingCountContainer = inputItemComponentContainer?.parentElement?.previousElementSibling; if (missingCountContainer) { const newTextSpan = document.createElement('span'); newTextSpan.textContent = missingCountContainer.textContent; newTextSpan.style.height = window.getComputedStyle(document.querySelector('[class*="SkillActionDetail_levelRequirement"]')).height; missingCountContainer.innerHTML = ''; missingCountContainer.appendChild(newTextSpan); const missingCountComponent = document.createElement('div'); missingCountComponent.style.display = 'flex'; missingCountComponent.style.alignItems = 'flex-end'; missingCountComponent.style.flexDirection = 'column'; const inventoryCountSpans = inputItemComponentContainer?.querySelectorAll('[class*="SkillActionDetail_inventoryCount"]'); const inputCountSpans = inputItemComponentContainer?.querySelectorAll('[class*="SkillActionDetail_inputCount"]'); const itemContainers = inputItemComponentContainer?.querySelectorAll('[class*="Item_itemContainer"]'); for (let i = 0; i < itemContainers.length; i++) { inputCountSpans[i].style.color = '#E7E7E7'; const inputItemHrid = '/items/' + itemContainers[i].querySelector('svg use').getAttribute('href').split('#').pop(); const inputItemCount = inputItems.find(item => item.itemHrid === inputItemHrid)?.count || 0; const missingCountSpan = document.createElement('span'); missingCountSpan.style.height = window.getComputedStyle(itemContainers[i]).height; missingCountSpan.style.display = 'flex'; missingCountSpan.style.alignItems = 'center'; missingCountSpan.style.color = '#faa21e'; missingCountComponent.appendChild(missingCountSpan); inputItemComponents.push({ itemHrid: inputItemHrid, missingCountSpan, inventoryCountSpan: inventoryCountSpans[i], inputCountSpan: inputCountSpans[i], count: inputItemCount }); } missingCountContainer.appendChild(missingCountComponent); } } // [{itemHrid, input, count}] const outputItemComponents = []; let lastOutputItemComponent = document.querySelector('[class^="SkillActionDetail_maxActionCountInput"]'); const outputItemComponentContainer = lastOutputItemComponent.parentElement; const skillActionTimeInput = lastOutputItemComponent.querySelector('input'); const skillActionTimeButtons = lastOutputItemComponent.querySelectorAll('button'); for (const outputItem of outputItems) { if (outputItem.count === 1 && outputItems.length === 1) break; // 仅有一个产出且数量为1时不创建额外输入框 const { component: newOutputItemComponent, input: newInput } = this.createOutputItemComponent(outputItem.itemHrid); if (newOutputItemComponent && newInput) { outputItemComponentContainer.insertBefore(newOutputItemComponent, lastOutputItemComponent.nextSibling); outputItemComponents.push({ itemHrid: outputItem.itemHrid, input: newInput, count: outputItem.count }); lastOutputItemComponent = newOutputItemComponent; } } // 联动 let linking = false; function updateSkillActionDetail(e) { if (linking) return; linking = true; const idx = outputItemComponents.findIndex(input => input.input === e.target); const targetValue = parseInt(e.target.value, 10); if (idx !== -1) { skillActionTimeInput.value = (isNaN(targetValue)) ? '∞' : Math.ceil(targetValue / outputItemComponents[idx].count); Utils.reactInputTriggerHack(skillActionTimeInput); } const skillActionTimes = parseInt(skillActionTimeInput.value, 10); outputItemComponents.forEach(({ itemHrid, input, count }) => { if (input !== e.target) input.value = (isNaN(skillActionTimes)) ? '∞' : Math.ceil(skillActionTimes * count); }); inputItemComponents.forEach(({ itemHrid, missingCountSpan, inventoryCountSpan, inputCountSpan, count }) => { const inventoryCount = window.MWI_Toolkit.characterItems.getCount(itemHrid); const requiredCount = count * skillActionTimes; if (isNaN(skillActionTimes)) { missingCountSpan.textContent = ''; inventoryCountSpan.style.color = ''; } else { if (requiredCount > inventoryCount) { missingCountSpan.textContent = Utils.formatNumber(requiredCount - inventoryCount); inventoryCountSpan.style.color = '#f44336'; } else { missingCountSpan.textContent = ' '; inventoryCountSpan.style.color = '#E7E7E7'; } } inputCountSpan.textContent = '\u00A0/ ' + Utils.formatNumber(count * ((isNaN(skillActionTimes) ? 1 : skillActionTimes))) + '\u00A0'; }); if (missingUpgradeItemCountComponent.missingCountSpan) { if (isNaN(skillActionTimes)) { missingUpgradeItemCountComponent.missingCountSpan.textContent = ''; } else { const requiredCount = missingUpgradeItemCountComponent.count * skillActionTimes; const inventoryCount = window.MWI_Toolkit.characterItems.getCount(missingUpgradeItemCountComponent.itemHrid); if (requiredCount > inventoryCount) { missingUpgradeItemCountComponent.missingCountSpan.textContent = Utils.formatNumber(requiredCount - inventoryCount); } else { missingUpgradeItemCountComponent.missingCountSpan.textContent = ' '; } } } linking = false; } skillActionTimeInput.addEventListener('input', updateSkillActionDetail); outputItemComponents.forEach(({ input }) => { input.addEventListener('input', updateSkillActionDetail); }); skillActionTimeButtons.forEach(btn => { btn.addEventListener('click', () => { setTimeout(() => { updateSkillActionDetail({ target: skillActionTimeInput }); }, 20); }); }); // 初次填充 setTimeout(() => { updateSkillActionDetail({ target: skillActionTimeInput }); }, 20); } //#region 数据计算 // 数据计算,返回 {upgradeItemHrid, inputItems, outputItems} calculateActionDetail() { const actionDetail = this.getActionDetail(); const actionType = this.getActionType(); if (!actionDetail || !actionType) { console.warn('MWI_Toolkit_ActionDetailPlus: 无法获取动作详情'); return { upgradeItemHrid: null, inputItems: null, outputItems: null }; } // console.log('MWI_Toolkit_ActionDetailPlus: 获取到动作详情', actionDetail); const drinkSlots = this.getActionTypeDrinkSlots(); const drinkConcentration = this.getDrinkConcentration(); // console.log('MWI_Toolkit_ActionDetailPlus: 获取到茶列表', drinkSlots, drinkConcentration); const upgradeItemHrid = actionDetail.upgradeItemHrid; const inputItems = actionDetail.inputItems; const outputItems = actionDetail.outputItems || []; // 检查采集数量加成 const gatheringBuff = (drinkSlots?.some(slot => slot && slot.itemHrid === '/items/gathering_tea') ? 0.15 * drinkConcentration : 0) + this.getEquipmentGatheringBuff() + this.getCommunityGatheringBuff(); // 检查加工茶加成 const processingBuff = (drinkSlots?.some(slot => slot && slot.itemHrid === '/items/processing_tea') ? 0.15 * drinkConcentration : 0); // 检查美食茶加成 const gourmetBuff = (drinkSlots?.some(slot => slot && slot.itemHrid === '/items/gourmet_tea') ? 0.12 * drinkConcentration : 0); // 检查工匠茶加成 const artisanBuff = (drinkSlots?.some(slot => slot && slot.itemHrid === '/items/artisan_tea') ? 0.1 * drinkConcentration : 0); if (['milking', 'foraging', 'woodcutting', /*'cheesesmithing', 'crafting', 'tailoring', 'cooking', 'brewing'*/].includes(actionType)) { const dropTable = actionDetail.dropTable; for (const dropItem of dropTable) { const averageCount = dropItem.dropRate * (dropItem.minCount + dropItem.maxCount) / 2 * (1 + gatheringBuff); const processedItemHrid = this.processableItemMap[dropItem.itemHrid]; if (processedItemHrid) { outputItems.push({ itemHrid: dropItem.itemHrid, count: averageCount * (1 - processingBuff), }); outputItems.push({ itemHrid: processedItemHrid, count: averageCount * (1 - processingBuff) / 2 / (1 - artisanBuff) + averageCount * processingBuff / 2, }); } else { outputItems.push({ itemHrid: dropItem.itemHrid, count: averageCount, }); } } } if ([/*'milking', 'foraging', 'woodcutting', 'cheesesmithing', 'crafting', 'tailoring',*/ 'cooking', 'brewing'].includes(actionType)) { for (const outputItem of outputItems) { outputItem.count = outputItem.count * (1 + gourmetBuff); } } const newInputItems = []; if ([/*'milking', 'foraging', 'woodcutting',*/ 'cheesesmithing', 'crafting', 'tailoring', 'cooking', 'brewing'].includes(actionType)) { for (const inputItem of inputItems) { newInputItems.push({ itemHrid: inputItem.itemHrid, count: inputItem.count * (1 - artisanBuff) }); } } return { upgradeItemHrid, inputItems: newInputItems, outputItems }; } //#endregion //#region UI创建 // 创建output数量栏,返回 [{component, input}] createOutputItemComponent(itemHrid) { const origComponent = document.querySelector('[class^="SkillActionDetail_maxActionCountInput"]'); if (!origComponent) return null; // 克隆外层div(不带子内容) const newComponent = origComponent.cloneNode(false); const originalActionLabel = document.querySelector('[class^="SkillActionDetail_actionContainer"] [class^="SkillActionDetail_label"]'); if (Object.values(this.processableItemMap).includes(itemHrid)) { const tab = originalActionLabel.cloneNode(false); tab.style.width = window.getComputedStyle(originalActionLabel).width; tab.className = 'SkillActionDetail_tab'; tab.textContent = '┗'; newComponent.appendChild(tab); } // 物品图标 const itemIcon = document.createElement('div'); itemIcon.style.width = window.getComputedStyle(originalActionLabel).width; itemIcon.style.height = window.getComputedStyle(originalActionLabel).height; itemIcon.style.marginRight = '2px'; itemIcon.style.display = 'flex'; itemIcon.style.alignItems = 'center'; itemIcon.style.justifyContent = 'center'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '20px'); svg.setAttribute('height', '20px'); svg.style.display = 'block'; const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', Utils.getIconHrefByItemHrid(itemHrid)); svg.appendChild(use); itemIcon.appendChild(svg); newComponent.appendChild(itemIcon); // 输入框 const origInputWrap = origComponent.querySelector('[class^="SkillActionDetail_input"]'); const inputWrap = origInputWrap.cloneNode(true); const origInput = origInputWrap.querySelector('input'); const input = inputWrap.querySelector('input'); input.addEventListener('focus', function () { setTimeout(() => { input.select(); }, 0); }); input.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { if (origInput) { const event = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', keyCode: 13, which: 13 }); origInput.dispatchEvent(event); } } }); newComponent.appendChild(inputWrap); if (!Object.values(this.processableItemMap).includes(itemHrid)) { // 快捷填充按钮 const btns = [ { val: 1000, txt: '1k' }, { val: 2000, txt: '2k' }, { val: 5000, txt: '5k' } ]; const origButtons = origComponent.querySelectorAll('button'); let buttonClass = ''; if (origButtons.length > 0) buttonClass = origButtons[0].className; btns.forEach(({ val, txt }) => { const btn = document.createElement('button'); btn.className = buttonClass; btn.textContent = txt; btn.addEventListener('click', () => { input.value = val; input.dispatchEvent(new Event('input', { bubbles: true })); }); newComponent.appendChild(btn); }); } return { component: newComponent, input }; } //#endregion //#region 数据获取函数 getActionHrid() { const actionNameDiv = document.querySelector('[class^="SkillActionDetail_name"]'); const actionName = actionNameDiv ? actionNameDiv.textContent : ''; return window.MWI_Toolkit.i18n.getHridByName(actionName, 'actionNames', MWI_Toolkit_ActionDetailPlus_App.Language); } getActionDetail() { const actionHrid = this.getActionHrid(); return window.MWI_Toolkit?.init_client_data?.actionDetailMap?.[`${actionHrid}`]; } getActionType() { // const actionType = document // .querySelector('[class*="SkillActionDetail_value"][class*="SkillActionDetail_levelRequirement"]') // ?.querySelector('svg use') // ?.getAttribute('href') // ?.split('#') // ?.pop() // || ''; const actionDetail = this.getActionDetail(); const actionType = actionDetail?.type?.split('/').pop() || ''; // 仅支持八种常规类型 if (['milking', 'foraging', 'woodcutting', 'cheesesmithing', 'crafting', 'tailoring', 'cooking', 'brewing'].includes(actionType)) { return actionType; } return null; } // 获取茶列表 getActionTypeDrinkSlots() { const actionType = this.getActionType(); const drinkSlots = window.MWI_Toolkit?.init_character_data?.actionTypeDrinkSlotsMap?.[`/action_types/${actionType}`]; // if (['milking', 'foraging', 'woodcutting', /*'cheesesmithing', 'crafting', 'tailoring', 'cooking', 'brewing'*/].includes(actionType)) // 对三采添加对应的工匠茶数据用于计算加工数量 const processActionType = { milking: 'cheesesmithing', foraging: 'tailoring', woodcutting: 'crafting' }[actionType] || null; if (processActionType) { const processDrinkSlots = window.MWI_Toolkit?.init_character_data?.actionTypeDrinkSlotsMap?.[`/action_types/${processActionType}`]; processDrinkSlots.forEach(drink => { if (drink && drink.itemHrid == '/items/artisan_tea') { drinkSlots.push(drink); } }); } return drinkSlots; } // 获取饮料浓度系数 getDrinkConcentration() { let drinkConcentration = 1; if (window.MWI_Toolkit?.init_client_data && window.MWI_Toolkit?.characterItems) { const enhancementLevel = window.MWI_Toolkit.characterItems.getMaxEnhancementLevel("/items/guzzling_pouch"); if (enhancementLevel != -1) { drinkConcentration = 1 + window.MWI_Toolkit.init_client_data.itemDetailMap?.[`/items/guzzling_pouch`].equipmentDetail.noncombatStats.drinkConcentration + window.MWI_Toolkit.init_client_data.itemDetailMap?.[`/items/guzzling_pouch`].equipmentDetail.noncombatEnhancementBonuses.drinkConcentration * window.MWI_Toolkit.init_client_data.enhancementLevelTotalBonusMultiplierTable[enhancementLevel]; } } return drinkConcentration; } // 获取装备采集数量buff getEquipmentGatheringBuff() { let equipmentGatheringBuff = 0; if (window.MWI_Toolkit?.init_client_data && window.MWI_Toolkit?.characterItems) { const philosophers_earrings_enhancementLevel = window.MWI_Toolkit.characterItems.getMaxEnhancementLevel("/items/philosophers_earrings"); const earrings_of_gathering_enhancementLevel = window.MWI_Toolkit.characterItems.getMaxEnhancementLevel("/items/earrings_of_gathering"); const philosophers_ring_enhancementLevel = window.MWI_Toolkit.characterItems.getMaxEnhancementLevel("/items/philosophers_ring"); const ring_of_gathering_enhancementLevel = window.MWI_Toolkit.characterItems.getMaxEnhancementLevel("/items/ring_of_gathering"); if (philosophers_earrings_enhancementLevel != -1) { equipmentGatheringBuff = equipmentGatheringBuff + window.MWI_Toolkit.init_client_data.itemDetailMap?.[`/items/philosophers_earrings`].equipmentDetail.noncombatStats.gatheringQuantity + window.MWI_Toolkit.init_client_data.itemDetailMap?.[`/items/philosophers_earrings`].equipmentDetail.noncombatEnhancementBonuses.gatheringQuantity * window.MWI_Toolkit.init_client_data.enhancementLevelTotalBonusMultiplierTable[philosophers_earrings_enhancementLevel]; } else if (earrings_of_gathering_enhancementLevel != -1) { equipmentGatheringBuff = equipmentGatheringBuff + window.MWI_Toolkit.init_client_data.itemDetailMap?.[`/items/earrings_of_gathering`].equipmentDetail.noncombatStats.gatheringQuantity + window.MWI_Toolkit.init_client_data.itemDetailMap?.[`/items/earrings_of_gathering`].equipmentDetail.noncombatEnhancementBonuses.gatheringQuantity * window.MWI_Toolkit.init_client_data.enhancementLevelTotalBonusMultiplierTable[earrings_of_gathering_enhancementLevel]; } if (philosophers_ring_enhancementLevel != -1) { equipmentGatheringBuff = equipmentGatheringBuff + window.MWI_Toolkit.init_client_data.itemDetailMap?.[`/items/philosophers_ring`].equipmentDetail.noncombatStats.gatheringQuantity + window.MWI_Toolkit.init_client_data.itemDetailMap?.[`/items/philosophers_ring`].equipmentDetail.noncombatEnhancementBonuses.gatheringQuantity * window.MWI_Toolkit.init_client_data.enhancementLevelTotalBonusMultiplierTable[philosophers_ring_enhancementLevel]; } else if (ring_of_gathering_enhancementLevel != -1) { equipmentGatheringBuff = equipmentGatheringBuff + window.MWI_Toolkit.init_client_data.itemDetailMap?.[`/items/ring_of_gathering`].equipmentDetail.noncombatStats.gatheringQuantity + window.MWI_Toolkit.init_client_data.itemDetailMap?.[`/items/ring_of_gathering`].equipmentDetail.noncombatEnhancementBonuses.gatheringQuantity * window.MWI_Toolkit.init_client_data.enhancementLevelTotalBonusMultiplierTable[ring_of_gathering_enhancementLevel]; } } return equipmentGatheringBuff; } // 获取社区采集数量buff getCommunityGatheringBuff() { const buffDivs = document.querySelectorAll('[class^="CommunityBuff_communityBuff"]'); for (const buffDiv of buffDivs) { const useEl = buffDiv.querySelector('svg use'); if (!useEl) continue; const href = useEl.getAttribute('href') || ''; if (href.includes('gathering')) { const levelDiv = buffDiv.querySelector('[class^="CommunityBuff_level"]'); if (levelDiv) { const match = levelDiv.textContent.match(/Lv\.(\d+)/); if (match) { return parseInt(match[1], 10) * 0.005 + 0.195; } } } } return 0; } //#endregion // 监听页面变化 initialize() { let lastPanel = null; const observer = new MutationObserver(() => { const panel = document.querySelector('[class^="SkillActionDetail_regularComponent"]'); if (panel && panel !== lastPanel) { lastPanel = panel; setTimeout(() => { this.enhanceSkillActionDetail(); // 箭头函数保证 this 正确 }, 50); } }); observer.observe(document.body, { childList: true, subtree: true }); } } //#endregion // 创建并启动应用程序实例 const app = new MWI_Toolkit_ActionDetailPlus_App(); app.initialize(); })();