Show item time cost and related helper info in Milky Way Idle.
// ==UserScript== // @name ICTime // @name:en ICTime // @name:zh-CN ICTime 时间计算 // @namespace http://tampermonkey.net/ // @version 1.0.2 // @description Show item time cost and related helper info in Milky Way Idle. // @description:en Show item time cost and related helper info in Milky Way Idle. // @description:zh-CN 在 Milky Way Idle 中显示物品时间成本及相关辅助信息。 // @author dakonglong // @license MIT // @match https://www.milkywayidle.com/* // @match https://test.milkywayidle.com/* // @match https://www.milkywayidlecn.com/* // @match https://test.milkywayidlecn.com/* // @match https://shykai.github.io/MWICombatSimulatorTest/dist/* // @require https://cdn.jsdelivr.net/npm/[email protected]/libs/lz-string.min.js // @grant GM_getValue // @grant GM_setValue // @run-at document-start // ==/UserScript== (function () { "use strict"; const SIMULATOR_IMPORT_STORAGE_KEY = "ICTime_SimulatorImport_v1"; const SIMULATOR_IMPORT_REQUEST_KEY = "ICTime_SimulatorImport_Request_v1"; const SIMULATOR_SNAPSHOT_EVENT = "__ICTIME_SIMULATOR_SNAPSHOT__"; async function sharedGetValue(key, fallbackValue) { try { if (typeof GM_getValue === "function") { const value = GM_getValue(key, fallbackValue); return value instanceof Promise ? await value : value; } } catch (_error) { // Fall back to localStorage. } try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : fallbackValue; } catch (_error) { return fallbackValue; } } async function sharedSetValue(key, value) { try { if (typeof GM_setValue === "function") { const result = GM_setValue(key, value); if (result instanceof Promise) { await result; } return; } } catch (_error) { // Fall back to localStorage. } localStorage.setItem(key, JSON.stringify(value)); } function dispatchNativeChange(element) { if (!element) { return; } element.dispatchEvent(new Event("input", { bubbles: true })); element.dispatchEvent(new Event("change", { bubbles: true })); } function findSimulatorResultRoot() { const heading = Array.from(document.querySelectorAll("div,span,b,h1,h2,h3,h4,h5,h6,button")) .find((node) => (node.textContent || "").trim() === "模拟结果"); let node = heading instanceof HTMLElement ? heading.parentElement : null; let depth = 0; while (node && depth < 6) { const text = (node.textContent || "").replace(/\s+/g, " "); if (text.includes("每小时使用的消耗品") && (text.includes("非随机掉落物") || text.includes("掉落物合计"))) { return node; } node = node.parentElement; depth += 1; } return Array.from(document.querySelectorAll("div")).find((candidate) => { const text = (candidate.textContent || "").replace(/\s+/g, " "); return text.includes("模拟结果") && text.includes("每小时使用的消耗品") && (text.includes("非随机掉落物") || text.includes("掉落物合计")); }) || null; } function findSimulatorSelectByOptions(expectedOptions) { return Array.from(document.querySelectorAll("select")).find((select) => { const texts = Array.from(select.options).map((option) => (option.textContent || "").trim()); return expectedOptions.every((optionText) => texts.includes(optionText)); }) || null; } function findLabeledValue(root, labelText) { const labelNode = Array.from(root?.querySelectorAll("div") || []).find((node) => (node.textContent || "").trim() === labelText); if (!labelNode?.parentElement) { return ""; } const siblings = Array.from(labelNode.parentElement.children).filter((node) => node instanceof HTMLElement); const valueNode = siblings[siblings.length - 1]; return valueNode && valueNode !== labelNode ? (valueNode.textContent || "").trim() : ""; } function parseSimulatorConsumables(root) { const labelNode = Array.from(root?.querySelectorAll("div") || []).find((node) => (node.textContent || "").trim() === "每小时使用的消耗品"); const section = labelNode?.nextElementSibling; if (!section) { return []; } return Array.from(section.children || []) .map((row) => { const children = Array.from(row.children || []); const name = (children[0]?.textContent || "").trim(); const perHour = Number((children[1]?.textContent || "").trim() || 0); return name ? { name, perHour } : null; }) .filter(Boolean); } function findSimulatorDurationHours() { const input = Array.from(document.querySelectorAll('input[type="number"]')).find((element) => { const nearby = ((element.parentElement?.textContent || "") + " " + (element.closest("div")?.textContent || "")) .replace(/\s+/g, " ") .trim(); return nearby === "小时" || nearby.startsWith("小时 "); }); return parseNonNegativeDecimal(input?.value || 24) || 24; } function parseSimulatorNonRandomDrops(root) { const heading = Array.from(root?.querySelectorAll("h1, h2, h3, h4, h5, h6, button, div, span, b") || []) .find((node) => (node.textContent || "").trim() === "非随机掉落物"); const accordionItem = heading?.closest(".accordion-item"); const body = accordionItem?.querySelector(".accordion-body"); if (!body) { return []; } const rows = body.querySelectorAll("#noRngDrops > .row, #noRngDrops .row"); return Array.from(rows || []) .map((row) => { const children = Array.from(row.children || []).filter((node) => node instanceof HTMLElement); const name = (children[0]?.textContent || "").trim(); const count = parseNonNegativeDecimal(children[1]?.textContent || 0); return name ? { name, count } : null; }) .filter(Boolean); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function installSimulatorPageBridge() { if (document.getElementById("ictime-simulator-page-bridge")) { return; } const script = document.createElement("script"); script.id = "ictime-simulator-page-bridge"; script.textContent = ` (function () { if (window.__ICTIME_SIMULATOR_PAGE_BRIDGE__) { return; } window.__ICTIME_SIMULATOR_PAGE_BRIDGE__ = true; const EVENT_NAME = ${JSON.stringify(SIMULATOR_SNAPSHOT_EVENT)}; function parseNumber(value) { const text = String(value == null ? "" : value).trim(); if (!text) { return 0; } let normalized = text.replace(/\\s+/g, ""); if (normalized.includes(",") && normalized.includes(".")) { normalized = normalized.replace(/,/g, ""); } else if (normalized.includes(",")) { normalized = normalized.replace(/,/g, "."); } const number = Number(normalized); return Number.isFinite(number) ? number : 0; } function parseNoRngDropsFromDom() { const rows = document.querySelectorAll("#noRngDrops > .row, #noRngDrops .row"); return Array.from(rows || []).map((row) => { const cells = Array.from(row.children || []).filter((node) => node instanceof HTMLElement); const name = (cells[0]?.textContent || "").trim(); const count = parseNumber(cells[1]?.textContent || 0); return name ? { name, count } : null; }).filter(Boolean); } function computeAverageMinutes(simResult) { try { if (simResult?.isDungeon) { const completed = parseNumber(simResult.dungeonsCompleted || 0); if (completed <= 0) { return 0; } const totalTime = parseNumber(simResult.lastDungeonFinishTime || 0) > 0 ? parseNumber(simResult.lastDungeonFinishTime) : parseNumber(simResult.simulatedTime || 0); return (totalTime / ONE_HOUR) * 60 / completed; } const encounters = parseNumber(simResult?.encounters || 0); if (encounters <= 0) { return 0; } const totalTime = parseNumber(simResult.lastEncounterFinishTime || 0) > 0 ? parseNumber(simResult.lastEncounterFinishTime) : parseNumber(simResult.simulatedTime || 0); return (totalTime / ONE_HOUR) * 60 / encounters; } catch (_error) { return 0; } } function parseDungeonTier(simResult, dungeonName) { const numericCandidates = [ simResult?.dungeonTier, simResult?.tier, simResult?.rewardTier, simResult?.zoneTier, simResult?.difficultyTier, ]; for (const candidate of numericCandidates) { const numeric = Number(candidate); if (Number.isFinite(numeric)) { return numeric >= 2 ? 2 : numeric >= 1 ? 1 : 0; } } const textCandidates = [ simResult?.tierName, simResult?.difficultyName, simResult?.zoneName, dungeonName, ]; for (const textCandidate of textCandidates) { const match = String(textCandidate || "").match(/T\\s*([012])/i); if (match) { const numeric = Number(match[1]); return numeric >= 2 ? 2 : numeric >= 1 ? 1 : 0; } } return 0; } function buildSnapshot() { try { if (typeof currentSimResults === "undefined" || !currentSimResults || !Object.keys(currentSimResults).length) { return null; } const simResult = currentSimResults; const itemMap = typeof itemDetailMap !== "undefined" ? itemDetailMap : {}; const tabEntries = Array.from(document.querySelectorAll("#playerTab .nav-link")).map((tab, index) => ({ playerKey: "player" + (index + 1), name: (tab.textContent || "").trim(), })).filter((entry) => entry.name); const durationHours = Math.max(0, parseNumber(simResult.simulatedTime || 0) / ONE_HOUR); const averageMinutes = computeAverageMinutes(simResult); const nonRandomDrops = parseNoRngDropsFromDom(); const selectedCharacterName = (document.querySelector("#playerTab .nav-link.active")?.textContent || "").trim(); const dungeonName = String(simResult.zoneName || document.querySelector("#selectZone")?.selectedOptions?.[0]?.textContent || "").trim(); const dungeonTier = parseDungeonTier(simResult, dungeonName); const characters = tabEntries.map((entry) => { const consumablesUsed = simResult.consumablesUsed?.[entry.playerKey] || {}; const consumables = Object.entries(consumablesUsed).map(([itemHrid, amount]) => ({ itemHrid, name: itemMap[itemHrid]?.name || itemHrid, perHour: durationHours > 0 ? parseNumber(amount) / durationHours : 0, })).sort((left, right) => right.perHour - left.perHour); return { id: entry.playerKey, name: entry.name, averageMinutes, durationHours: durationHours || 24, consumables, nonRandomDrops, }; }); return { dungeonName, dungeonTier, selectedCharacterName, characters, capturedAt: Date.now(), }; } catch (_error) { return null; } } function dispatchSnapshot() { const snapshot = buildSnapshot(); if (!snapshot?.characters?.length) { return; } window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: snapshot })); } function wrapFunction(name) { const original = window[name]; if (typeof original !== "function" || original.__ictimeWrapped) { return; } const wrapped = function (...args) { const result = original.apply(this, args); setTimeout(dispatchSnapshot, 0); return result; }; wrapped.__ictimeWrapped = true; window[name] = wrapped; } function start() { wrapFunction("showSimulationResult"); wrapFunction("showAllSimulationResults"); wrapFunction("onTabChange"); setTimeout(dispatchSnapshot, 0); setInterval(dispatchSnapshot, 2000); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", start, { once: true }); } else { start(); } })(); `; (document.documentElement || document.head || document.body).appendChild(script); script.remove(); } function getSimulatorPlayerTabs() { return Array.from(document.querySelectorAll("#playerTab .nav-link")) .map((tab, index) => ({ tab, playerId: String(index + 1), name: (tab.textContent || "").trim(), active: tab.classList.contains("active"), enabled: !!document.getElementById(`player${index + 1}`)?.checked, })) .filter((entry) => entry.name); } function getCurrentSimulatorCharacterName() { const activeTab = document.querySelector("#playerTab .nav-link.active"); const activeText = (activeTab?.textContent || "").trim(); if (activeText) { return activeText; } const fallback = Array.from(document.querySelectorAll("#playerTab .nav-link")) .map((node) => (node.textContent || "").trim()) .find(Boolean); return fallback || ""; } function readSimulatorRenderedResult() { const resultRoot = findSimulatorResultRoot(); if (!resultRoot) { return null; } return { averageMinutes: parseNonNegativeDecimal(findLabeledValue(resultRoot, "平均完成时间") || 0), durationHours: findSimulatorDurationHours(), consumables: parseSimulatorConsumables(resultRoot), nonRandomDrops: parseSimulatorNonRandomDrops(resultRoot), }; } function captureCurrentSimulatorSnapshot() { const dungeonSelect = findSimulatorSelectByOptions(["奇幻洞穴", "阴森马戏团", "秘法要塞", "海盗基地"]); if (!dungeonSelect) { return null; } const rendered = readSimulatorRenderedResult(); if (!rendered) { return null; } const selectedCharacterName = getCurrentSimulatorCharacterName(); const snapshot = { dungeonName: (dungeonSelect.selectedOptions[0]?.textContent || "").trim(), dungeonTier: parseSimulatorDungeonTierValue( dungeonSelect.selectedOptions[0]?.textContent, document.body?.innerText || "" ), selectedCharacterName, characters: [{ id: selectedCharacterName || "player1", name: selectedCharacterName || "player1", averageMinutes: rendered.averageMinutes, durationHours: rendered.durationHours, consumables: rendered.consumables, nonRandomDrops: rendered.nonRandomDrops, }], capturedAt: Date.now(), }; return snapshot; } async function captureAllSimulatorCharactersSnapshot() { const dungeonSelect = findSimulatorSelectByOptions(["奇幻洞穴", "阴森马戏团", "秘法要塞", "海盗基地"]); if (!dungeonSelect) { return null; } const tabs = getSimulatorPlayerTabs(); if (!tabs.length) { return captureCurrentSimulatorSnapshot(); } const originalActive = tabs.find((entry) => entry.active) || tabs[0]; const characters = []; for (const entry of tabs) { const currentActiveName = getCurrentSimulatorCharacterName(); if (currentActiveName !== entry.name) { entry.tab.click(); await sleep(180); } const rendered = readSimulatorRenderedResult(); if (!rendered) { continue; } characters.push({ id: `player${entry.playerId}`, name: entry.name, averageMinutes: rendered.averageMinutes, durationHours: rendered.durationHours, consumables: rendered.consumables, nonRandomDrops: rendered.nonRandomDrops, }); } if (originalActive && getCurrentSimulatorCharacterName() !== originalActive.name) { originalActive.tab.click(); await sleep(180); } if (!characters.length) { return null; } return { dungeonName: (dungeonSelect.selectedOptions[0]?.textContent || "").trim(), dungeonTier: parseSimulatorDungeonTierValue( dungeonSelect.selectedOptions[0]?.textContent, document.body?.innerText || "" ), selectedCharacterName: originalActive?.name || getCurrentSimulatorCharacterName(), characters, capturedAt: Date.now(), }; } async function captureSimulatorSnapshot() { const snapshot = await captureAllSimulatorCharactersSnapshot(); if (!snapshot) { return null; } await sharedSetValue(SIMULATOR_IMPORT_STORAGE_KEY, snapshot); return snapshot; } async function startSimulatorBridge() { let isCapturing = false; let lastHandledRequestAt = 0; let lastPublishedSignature = ""; let publishQueued = false; let lastBridgeSnapshot = null; let requestBaselineInitialized = false; const handleBridgeSnapshot = async (event) => { const snapshot = event?.detail || null; if (!snapshot?.characters?.length) { return; } lastBridgeSnapshot = snapshot; const signature = JSON.stringify({ dungeonName: snapshot.dungeonName, dungeonTier: snapshot.dungeonTier, selectedCharacterName: snapshot.selectedCharacterName, characters: snapshot.characters.map((entry) => ({ name: entry.name, averageMinutes: entry.averageMinutes, durationHours: entry.durationHours, consumables: entry.consumables, nonRandomDrops: entry.nonRandomDrops, })), }); if (signature === lastPublishedSignature) { return; } lastPublishedSignature = signature; await sharedSetValue(SIMULATOR_IMPORT_STORAGE_KEY, snapshot); }; const publishVisibleSnapshot = async () => { if (isCapturing) { return; } if (lastBridgeSnapshot?.characters?.length) { await handleBridgeSnapshot({ detail: lastBridgeSnapshot }); return; } const snapshot = captureCurrentSimulatorSnapshot(); if (!snapshot?.characters?.length) { return; } const signature = JSON.stringify({ dungeonName: snapshot.dungeonName, dungeonTier: snapshot.dungeonTier, selectedCharacterName: snapshot.selectedCharacterName, averageMinutes: snapshot.characters[0]?.averageMinutes || 0, durationHours: snapshot.characters[0]?.durationHours || 0, consumables: snapshot.characters[0]?.consumables || [], nonRandomDrops: snapshot.characters[0]?.nonRandomDrops || [], }); if (signature === lastPublishedSignature) { return; } lastPublishedSignature = signature; await sharedSetValue(SIMULATOR_IMPORT_STORAGE_KEY, snapshot); }; const queuePublish = () => { if (publishQueued) { return; } publishQueued = true; setTimeout(async () => { publishQueued = false; try { await publishVisibleSnapshot(); } catch (error) { console.error("[ICTime] Failed to publish visible simulator snapshot.", error); } }, 150); }; const tick = async () => { if (isCapturing) { return; } const request = await sharedGetValue(SIMULATOR_IMPORT_REQUEST_KEY, null); const requestedAt = Number(request?.requestedAt || 0); if (!requestBaselineInitialized) { lastHandledRequestAt = requestedAt; requestBaselineInitialized = true; return; } if (!requestedAt || requestedAt <= lastHandledRequestAt) { return; } isCapturing = true; try { await captureSimulatorSnapshot(); lastHandledRequestAt = requestedAt; } catch (error) { console.error("[ICTime] Failed to capture simulator snapshot.", error); } finally { isCapturing = false; } }; installSimulatorPageBridge(); window.addEventListener(SIMULATOR_SNAPSHOT_EVENT, handleBridgeSnapshot); setInterval(tick, 500); setInterval(() => { queuePublish(); }, 2000); document.addEventListener("change", queuePublish, true); document.addEventListener("click", queuePublish, true); const observer = new MutationObserver(() => { queuePublish(); }); observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true }); queuePublish(); } if (location.hostname === "shykai.github.io" && location.pathname.startsWith("/MWICombatSimulatorTest/dist/")) { startSimulatorBridge(); return; } window.__ICTIME_VERSION__ = "1.0.1"; const previousController = window.__ICTIME_CONTROLLER__; if (previousController && typeof previousController.shutdown === "function") { previousController.shutdown(); } const instanceId = `ictime-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const SUPPORTED_ACTION_TYPES = new Set([ "/action_types/milking", "/action_types/foraging", "/action_types/woodcutting", "/action_types/cheesesmithing", "/action_types/crafting", "/action_types/tailoring", "/action_types/cooking", "/action_types/brewing", "/action_types/alchemy", "/action_types/enhancing", ]); const ACTION_TO_TOOL_STAT = { "/action_types/alchemy": "alchemySpeed", "/action_types/brewing": "brewingSpeed", "/action_types/cheesesmithing": "cheesesmithingSpeed", "/action_types/cooking": "cookingSpeed", "/action_types/crafting": "craftingSpeed", "/action_types/enhancing": "enhancingSpeed", "/action_types/foraging": "foragingSpeed", "/action_types/milking": "milkingSpeed", "/action_types/tailoring": "tailoringSpeed", "/action_types/woodcutting": "woodcuttingSpeed", }; const ACTION_TO_HOUSE = { "/action_types/alchemy": "/house_rooms/laboratory", "/action_types/brewing": "/house_rooms/brewery", "/action_types/cheesesmithing": "/house_rooms/forge", "/action_types/cooking": "/house_rooms/kitchen", "/action_types/crafting": "/house_rooms/workshop", "/action_types/enhancing": "/house_rooms/observatory", "/action_types/foraging": "/house_rooms/garden", "/action_types/milking": "/house_rooms/dairy_barn", "/action_types/tailoring": "/house_rooms/sewing_parlor", "/action_types/woodcutting": "/house_rooms/log_shed", }; const ENHANCEMENT_BONUS = { 0: 0, 1: 2, 2: 4.2, 3: 6.6, 4: 9.2, 5: 12, 6: 15, 7: 18.2, 8: 21.6, 9: 25.2, 10: 29, 11: 33.4, 12: 38.4, 13: 44, 14: 50.2, 15: 57, 16: 64.4, 17: 72.4, 18: 81, 19: 90.2, 20: 100, }; const PROCESSABLE_ITEM_MAP = new Map([ ["/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"], ["/items/rough_hide", "/items/rough_leather"], ["/items/reptile_hide", "/items/reptile_leather"], ["/items/gobo_hide", "/items/gobo_leather"], ["/items/beast_hide", "/items/beast_leather"], ["/items/umbral_hide", "/items/umbral_leather"], ]); const ESSENCE_DECOMPOSE_RULES = { "/items/alchemy_essence": { type: "fixed_source", sourceItemHrid: "/items/catalyst_of_decomposition" }, "/items/milking_essence": { type: "fixed_source", sourceItemHrid: "/items/holy_milk" }, "/items/foraging_essence": { type: "fixed_source", sourceItemHrid: "/items/star_fruit" }, "/items/woodcutting_essence": { type: "fixed_source", sourceItemHrid: "/items/arcane_log" }, "/items/cheesesmithing_essence": { type: "fixed_source", sourceItemHrid: "/items/holy_cheese" }, "/items/crafting_essence": { type: "fixed_source", sourceItemHrid: "/items/arcane_lumber" }, "/items/tailoring_essence": { type: "fixed_source", sourceItemHrid: "/items/umbral_hide" }, "/items/cooking_essence": { type: "fixed_source", sourceItemHrid: "/items/star_fruit_yogurt" }, "/items/brewing_essence": { type: "fixed_source", sourceItemHrid: "/items/emp_tea_leaf" }, }; const TIME_CALCULATOR_DEFAULT_ESSENCE_SOURCE_ITEM_HRIDS = { "/items/brewing_essence": "/items/emp_tea_leaf", "/items/tailoring_essence": "/items/umbral_hide", }; const FIXED_DECOMPOSE_SOURCE_RULES = { "/items/cheese": "/items/cheese_sword", "/items/lumber": "/items/wooden_bow", }; const FIXED_ENHANCING_ESSENCE_RULES = { "/items/enhancing_essence": { sourceItemHrid: "/items/cheese_boots", enhancementLevel: 14, catalystItemHrid: "", }, }; const FIXED_TRANSMUTE_SOURCE_RULES = { "/items/prime_catalyst": { sourceItemHrid: "/items/catalyst_of_coinification", actionHrid: "/actions/alchemy/transmute", }, }; const FIXED_ATTACHED_RARE_TOOLTIP_SOURCE_RULES = { "/items/butter_of_proficiency": { sourceItemHrid: "/items/holy_sword", fallbackSourceItemHrids: ["/items/holy_bulwark"], catalystItemHrid: "/items/catalyst_of_transmutation", catalystSuccessBonus: 0.075, }, "/items/thread_of_expertise": { sourceItemHrid: "/items/umbral_tunic", fallbackSourceItemHrids: ["/items/radiant_robe_top"], catalystItemHrid: "/items/catalyst_of_transmutation", catalystSuccessBonus: 0.075, }, "/items/branch_of_insight": { sourceItemHrid: "/items/arcane_crossbow", fallbackSourceItemHrids: ["/items/arcane_bow"], catalystItemHrid: "/items/catalyst_of_transmutation", catalystSuccessBonus: 0.075, }, }; const TRANSMUTE_CATALYST_SUCCESS_BONUSES = { "/items/catalyst_of_transmutation": 0.075, "/items/prime_catalyst": 0.125, }; const ATTACHED_RARE_TARGET_ITEM_HRIDS = [ "/items/butter_of_proficiency", "/items/thread_of_expertise", "/items/branch_of_insight", ]; const CONSUMABLE_VALUE_ATTACHED_RARE_ITEM_HRIDS = [ "/items/butter_of_proficiency", "/items/thread_of_expertise", ]; const ATTACHED_RARE_TARGET_ITEM_HRID_SET = new Set(ATTACHED_RARE_TARGET_ITEM_HRIDS); const ATTACHED_RARE_LABEL_ZH = { "/items/butter_of_proficiency": "油", "/items/thread_of_expertise": "线", "/items/branch_of_insight": "树枝", }; const ATTACHED_RARE_LABEL_EN = { "/items/butter_of_proficiency": "oil", "/items/thread_of_expertise": "thread", "/items/branch_of_insight": "branch", }; const ESSENCE_SOURCE_NAME_ZH = { "/items/alchemy_tea": "炼金茶", "/items/apple": "苹果", "/items/apple_gummy": "苹果软糖", "/items/apple_yogurt": "苹果酸奶", "/items/arabica_coffee_bean": "低级咖啡豆", "/items/arcane_log": "神秘原木", "/items/arcane_lumber": "神秘木板", "/items/artisan_tea": "工匠茶", "/items/attack_coffee": "攻击咖啡", "/items/azure_cheese": "蔚蓝奶酪", "/items/azure_milk": "蔚蓝牛奶", "/items/bamboo_branch": "竹子", "/items/bamboo_fabric": "竹子布料", "/items/basic_brewing_charm": "基础冲泡护符", "/items/basic_cheesesmithing_charm": "基础奶酪锻造护符", "/items/basic_cooking_charm": "基础烹饪护符", "/items/basic_crafting_charm": "基础制作护符", "/items/basic_foraging_charm": "基础采摘护符", "/items/basic_milking_charm": "基础挤奶护符", "/items/basic_tailoring_charm": "基础缝纫护符", "/items/basic_woodcutting_charm": "基础伐木护符", "/items/beast_hide": "野兽皮", "/items/beast_leather": "野兽皮革", "/items/birch_log": "白桦原木", "/items/birch_lumber": "白桦木板", "/items/black_tea_leaf": "黑茶叶", "/items/blackberry": "黑莓", "/items/blackberry_cake": "黑莓蛋糕", "/items/blackberry_donut": "黑莓甜甜圈", "/items/blessed_tea": "福气茶", "/items/blueberry": "蓝莓", "/items/blueberry_cake": "蓝莓蛋糕", "/items/blueberry_donut": "蓝莓甜甜圈", "/items/brewers_bottoms": "饮品师下装", "/items/brewers_top": "饮品师上衣", "/items/brewing_tea": "冲泡茶", "/items/burble_cheese": "深紫奶酪", "/items/burble_milk": "深紫牛奶", "/items/burble_tea_leaf": "紫茶叶", "/items/catalytic_tea": "催化茶", "/items/cedar_log": "雪松原木", "/items/cedar_lumber": "雪松木板", "/items/celestial_brush": "星空刷子", "/items/celestial_chisel": "星空凿子", "/items/celestial_hammer": "星空锤子", "/items/celestial_hatchet": "星空斧头", "/items/celestial_needle": "星空针", "/items/celestial_pot": "星空壶", "/items/celestial_shears": "星空剪刀", "/items/celestial_spatula": "星空锅铲", "/items/channeling_coffee": "吟唱咖啡", "/items/cheese": "奶酪", "/items/chimerical_chest": "奇幻宝箱", "/items/chimerical_chest_key": "奇幻宝箱钥匙", "/items/cheesemakers_bottoms": "奶酪师下装", "/items/cheesemakers_top": "奶酪师上衣", "/items/cheesesmithing_tea": "奶酪锻造茶", "/items/chefs_bottoms": "厨师下装", "/items/chefs_top": "厨师上衣", "/items/cocoon": "蚕茧", "/items/cooking_tea": "烹饪茶", "/items/cotton": "棉花", "/items/cotton_fabric": "棉花布料", "/items/crafters_bottoms": "工匠下装", "/items/crafters_top": "工匠上衣", "/items/crafting_tea": "制作茶", "/items/crimson_cheese": "绛红奶酪", "/items/crimson_milk": "绛红牛奶", "/items/critical_coffee": "暴击咖啡", "/items/cupcake": "纸杯蛋糕", "/items/dairyhands_bottoms": "挤奶工下装", "/items/dairyhands_top": "挤奶工上衣", "/items/defense_coffee": "防御咖啡", "/items/donut": "甜甜圈", "/items/dragon_fruit": "火龙果", "/items/dragon_fruit_gummy": "火龙果软糖", "/items/dragon_fruit_yogurt": "火龙果酸奶", "/items/efficiency_tea": "效率茶", "/items/egg": "鸡蛋", "/items/emp_tea_leaf": "虚空茶叶", "/items/enhancing_tea": "强化茶", "/items/enchanted_chest": "秘法宝箱", "/items/enchanted_chest_key": "秘法宝箱钥匙", "/items/blue_key_fragment": "蓝钥匙碎片", "/items/white_key_fragment": "白钥匙碎片", "/items/green_key_fragment": "绿钥匙碎片", "/items/orange_key_fragment": "橙钥匙碎片", "/items/brown_key_fragment": "棕钥匙碎片", "/items/purple_key_fragment": "紫钥匙碎片", "/items/burning_key_fragment": "燃烧钥匙碎片", "/items/dark_key_fragment": "暗钥匙碎片", "/items/stone_key_fragment": "石钥匙碎片", "/items/excelsa_coffee_bean": "特级咖啡豆", "/items/fieriosa_coffee_bean": "火山咖啡豆", "/items/flax": "亚麻", "/items/foragers_bottoms": "采摘者下装", "/items/foragers_top": "采摘者上衣", "/items/foraging_tea": "采摘茶", "/items/gathering_tea": "采集茶", "/items/ginkgo_log": "银杏原木", "/items/ginkgo_lumber": "银杏木板", "/items/gobo_hide": "哥布林皮", "/items/gobo_leather": "哥布林皮革", "/items/gourmet_tea": "美食茶", "/items/green_tea_leaf": "绿茶叶", "/items/gummy": "软糖", "/items/holy_cheese": "神圣奶酪", "/items/holy_milk": "神圣牛奶", "/items/intelligence_coffee": "智力咖啡", "/items/liberica_coffee_bean": "高级咖啡豆", "/items/linen_fabric": "亚麻布料", "/items/log": "原木", "/items/lucky_coffee": "幸运咖啡", "/items/lumber": "木板", "/items/lumberjacks_bottoms": "伐木工下装", "/items/lumberjacks_top": "伐木工上衣", "/items/magic_coffee": "魔法咖啡", "/items/marsberry": "火星莓", "/items/marsberry_cake": "火星莓蛋糕", "/items/marsberry_donut": "火星莓甜甜圈", "/items/melee_coffee": "近战咖啡", "/items/milk": "牛奶", "/items/milking_tea": "挤奶茶", "/items/mooberry": "哞莓", "/items/mooberry_cake": "哞莓蛋糕", "/items/mooberry_donut": "哞莓甜甜圈", "/items/moolong_tea_leaf": "哞龙茶叶", "/items/orange": "橙子", "/items/orange_gummy": "橙子软糖", "/items/orange_yogurt": "橙子酸奶", "/items/peach": "桃子", "/items/peach_gummy": "桃子软糖", "/items/peach_yogurt": "桃子酸奶", "/items/plum": "李子", "/items/plum_gummy": "李子软糖", "/items/plum_yogurt": "李子酸奶", "/items/pirate_chest": "海盗宝箱", "/items/pirate_chest_key": "海盗宝箱钥匙", "/items/processing_tea": "加工茶", "/items/purpleheart_log": "紫心原木", "/items/purpleheart_lumber": "紫心木板", "/items/radiant_fabric": "光辉布料", "/items/radiant_fiber": "光辉纤维", "/items/rainbow_cheese": "彩虹奶酪", "/items/rainbow_milk": "彩虹牛奶", "/items/ranged_coffee": "远程咖啡", "/items/red_tea_leaf": "红茶叶", "/items/redwood_log": "红杉原木", "/items/redwood_lumber": "红杉木板", "/items/reptile_hide": "爬行动物皮", "/items/reptile_leather": "爬行动物皮革", "/items/robusta_coffee_bean": "中级咖啡豆", "/items/rough_hide": "粗糙兽皮", "/items/rough_leather": "粗糙皮革", "/items/silk_fabric": "丝绸", "/items/sinister_chest": "阴森宝箱", "/items/sinister_chest_key": "阴森宝箱钥匙", "/items/spaceberry": "太空莓", "/items/spaceberry_cake": "太空莓蛋糕", "/items/spaceberry_donut": "太空莓甜甜圈", "/items/spacia_coffee_bean": "太空咖啡豆", "/items/stamina_coffee": "耐力咖啡", "/items/star_fruit": "杨桃", "/items/star_fruit_gummy": "杨桃软糖", "/items/star_fruit_yogurt": "杨桃酸奶", "/items/strawberry": "草莓", "/items/strawberry_cake": "草莓蛋糕", "/items/strawberry_donut": "草莓甜甜圈", "/items/sugar": "糖", "/items/super_alchemy_tea": "超级炼金茶", "/items/super_attack_coffee": "超级攻击咖啡", "/items/super_brewing_tea": "超级冲泡茶", "/items/super_cheesesmithing_tea": "超级奶酪锻造茶", "/items/super_cooking_tea": "超级烹饪茶", "/items/super_crafting_tea": "超级制作茶", "/items/super_defense_coffee": "超级防御咖啡", "/items/super_enhancing_tea": "超级强化茶", "/items/super_foraging_tea": "超级采摘茶", "/items/super_intelligence_coffee": "超级智力咖啡", "/items/super_magic_coffee": "超级魔法咖啡", "/items/super_melee_coffee": "超级近战咖啡", "/items/super_milking_tea": "超级挤奶茶", "/items/super_ranged_coffee": "超级远程咖啡", "/items/super_stamina_coffee": "超级耐力咖啡", "/items/super_tailoring_tea": "超级缝纫茶", "/items/super_woodcutting_tea": "超级伐木茶", "/items/swiftness_coffee": "迅捷咖啡", "/items/tailoring_tea": "缝纫茶", "/items/tailors_bottoms": "裁缝下装", "/items/tailors_top": "裁缝上衣", "/items/ultra_alchemy_tea": "究极炼金茶", "/items/ultra_attack_coffee": "究极攻击咖啡", "/items/ultra_brewing_tea": "究极冲泡茶", "/items/ultra_cheesesmithing_tea": "究极奶酪锻造茶", "/items/ultra_cooking_tea": "究极烹饪茶", "/items/ultra_crafting_tea": "究极制作茶", "/items/ultra_defense_coffee": "究极防御咖啡", "/items/ultra_enhancing_tea": "究极强化茶", "/items/ultra_foraging_tea": "究极采摘茶", "/items/ultra_intelligence_coffee": "究极智力咖啡", "/items/ultra_magic_coffee": "究极魔法咖啡", "/items/ultra_melee_coffee": "究极近战咖啡", "/items/ultra_milking_tea": "究极挤奶茶", "/items/ultra_ranged_coffee": "究极远程咖啡", "/items/ultra_stamina_coffee": "究极耐力咖啡", "/items/ultra_tailoring_tea": "究极缝纫茶", "/items/ultra_woodcutting_tea": "究极伐木茶", "/items/umbral_hide": "暗影皮", "/items/umbral_leather": "暗影皮革", "/items/verdant_cheese": "翠绿奶酪", "/items/verdant_milk": "翠绿牛奶", "/items/wheat": "小麦", "/items/wisdom_coffee": "经验咖啡", "/items/wisdom_tea": "经验茶", "/items/woodcutting_tea": "伐木茶", "/items/yogurt": "酸奶", }; const state = { actionDetailMap: null, itemDetailMap: null, characterSkills: null, characterSkillMap: null, characterItems: null, characterItemMap: null, characterItemByLocationMap: null, characterHouseRoomMap: null, actionTypeDrinkSlotsMap: null, characterLoadoutDict: null, characterSetting: null, currentCharacterName: "", communityActionTypeBuffsDict: null, houseActionTypeBuffsDict: null, achievementActionTypeBuffsDict: null, personalActionTypeBuffsDict: null, consumableActionTypeBuffsDict: null, equipmentActionTypeBuffsDict: null, mooPassActionTypeBuffsDict: null, enhancementLevelTotalBonusMultiplierTable: null, shopItemDetailMap: null, itemNameToHrid: new Map(), itemTimeCache: new Map(), essencePlanCache: new Map(), fixedDecomposePlanCache: new Map(), fixedEnhancedEssencePlanCache: new Map(), fixedTransmutePlanCache: new Map(), fixedAttachedRareTooltipPlanCache: new Map(), attachedRareYieldCache: new Map(), itemTargetRelationCache: new Map(), skillingScrollTimeSavingsCache: new Map(), outputActionCache: null, lastRuntimeHydrationAt: 0, cachedInitClientData: null, cachedInitClientDataRaw: "", maxEnhancementByItem: null, localizedItemNameMap: new Map(), translationLoadStarted: false, isRefreshingTooltips: false, tooltipObserver: null, timeCalculatorUiObserver: null, tooltipRefreshTimer: 0, isShutDown: false, lastTooltipRender: null, enhancingRefreshQueued: false, alchemyInferenceRefreshQueued: false, alchemyInferenceObserver: null, alchemyObservedPanel: null, alchemyInferenceDelayTimers: [], eventAbortController: null, timeCalculatorRefreshQueued: false, timeCalculatorRefreshPending: false, timeCalculatorTabButton: null, timeCalculatorTabPanel: null, timeCalculatorContainer: null, timeCalculatorEntries: [], timeCalculatorLoadedCharacterId: null, timeCalculatorCompactMode: false, timeCalculatorSettingsOpen: false, timeCalculatorEssenceSourceItemHrids: { ...TIME_CALCULATOR_DEFAULT_ESSENCE_SOURCE_ITEM_HRIDS }, timeCalculatorDrafts: { addItemQuery: "", consumableQueryByEntryId: {}, }, lastHoveredItemHrid: "", lastHoveredItemAt: 0, enhancingPanelRef: null, itemTooltipDataCache: new Map(), cyclicSolveDepth: 0, activeItemSolveSet: new Set(), itemFailureReasonCache: new Map(), }; const ENHANCING_ACTION_TYPE = "/action_types/enhancing"; const ENHANCING_ACTION_HRID = "/actions/enhancing/enhance"; const SKILLING_SCROLL_DEFAULT_DURATION_SECONDS = 1800; const SKILLING_SCROLL_VALUE_CONFIGS = { // Reuse the seal effect mapping already maintained in the labyrinth reference plugin. "/items/seal_of_gathering": { mode: "rate", baseItemHrid: "/items/holy_milk", baseActionTypeHrid: "/action_types/milking", buff: { uniqueHrid: "/buff_uniques/ictime_seal_of_gathering", typeHrid: "/buff_types/gathering", flatBoost: 0.18, ratioBoost: 0, ratioBoostLevelBonus: 0, flatBoostLevelBonus: 0, }, }, "/items/seal_of_efficiency": { mode: "rate", baseItemHrid: "/items/holy_milk", baseActionTypeHrid: "/action_types/milking", buff: { uniqueHrid: "/buff_uniques/ictime_seal_of_efficiency", typeHrid: "/buff_types/efficiency", flatBoost: 0.14, ratioBoost: 0, ratioBoostLevelBonus: 0, flatBoostLevelBonus: 0, }, }, "/items/seal_of_action_speed": { mode: "rate", baseItemHrid: "/items/holy_milk", baseActionTypeHrid: "/action_types/milking", buff: { uniqueHrid: "/buff_uniques/ictime_seal_of_action_speed", typeHrid: "/buff_types/action_speed", flatBoost: 0.15, ratioBoost: 0, ratioBoostLevelBonus: 0, flatBoostLevelBonus: 0, }, }, "/items/seal_of_gourmet": { mode: "rate", baseItemHrid: "/items/dragon_fruit_yogurt", baseActionTypeHrid: "/action_types/cooking", buff: { uniqueHrid: "/buff_uniques/ictime_seal_of_gourmet", typeHrid: "/buff_types/gourmet", flatBoost: 0.1, ratioBoost: 0, ratioBoostLevelBonus: 0, flatBoostLevelBonus: 0, }, }, "/items/seal_of_processing": { mode: "processing", baseItemHrid: "/items/holy_cheese", sourceItemHrid: "/items/holy_milk", sourceActionHrid: "/actions/milking/holy_cow", baseActionTypeHrid: "/action_types/milking", buff: { uniqueHrid: "/buff_uniques/ictime_seal_of_processing", typeHrid: "/buff_types/processing", flatBoost: 0.2, ratioBoost: 0, ratioBoostLevelBonus: 0, flatBoostLevelBonus: 0, }, }, "/items/seal_of_rare_find": { mode: "rare_find", baseItemHrid: "/items/butter_of_proficiency", sourceItemHrid: "/items/holy_milk", sourceActionHrid: "/actions/milking/holy_cow", baseActionTypeHrid: "/action_types/milking", buff: { uniqueHrid: "/buff_uniques/ictime_seal_of_rare_find", typeHrid: "/buff_types/rare_find", flatBoost: 0.6, ratioBoost: 0, ratioBoostLevelBonus: 0, flatBoostLevelBonus: 0, }, }, }; const ENHANCING_SUCCESS_RATES = [ 0.5, 0.45, 0.45, 0.4, 0.4, 0.4, 0.35, 0.35, 0.35, 0.35, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, ]; const DUNGEON_CHEST_ITEM_HRIDS = [ "/items/enchanted_chest", "/items/chimerical_chest", "/items/sinister_chest", "/items/pirate_chest", ]; const KEY_FRAGMENT_ITEM_HRIDS = [ "/items/blue_key_fragment", "/items/white_key_fragment", "/items/green_key_fragment", "/items/orange_key_fragment", "/items/brown_key_fragment", "/items/purple_key_fragment", "/items/burning_key_fragment", "/items/dark_key_fragment", "/items/stone_key_fragment", ]; const MWITOOLS_ZH_ITEM_NAME_OVERRIDES = { "/items/chimerical_chest": "奇幻宝箱", "/items/sinister_chest": "阴森宝箱", "/items/enchanted_chest": "秘法宝箱", "/items/pirate_chest": "海盗宝箱", "/items/blue_key_fragment": "蓝色钥匙碎片", "/items/green_key_fragment": "绿色钥匙碎片", "/items/purple_key_fragment": "紫色钥匙碎片", "/items/white_key_fragment": "白色钥匙碎片", "/items/orange_key_fragment": "橙色钥匙碎片", "/items/brown_key_fragment": "棕色钥匙碎片", "/items/stone_key_fragment": "石头钥匙碎片", "/items/dark_key_fragment": "黑暗钥匙碎片", "/items/burning_key_fragment": "燃烧钥匙碎片", }; const TIME_CALCULATOR_ITEM_NAME_OVERRIDES_ZH = { "/items/chimerical_chest": "\u5947\u5e7b\u5b9d\u7bb1", "/items/sinister_chest": "\u9634\u68ee\u5b9d\u7bb1", "/items/enchanted_chest": "\u79d8\u6cd5\u5b9d\u7bb1", "/items/pirate_chest": "\u6d77\u76d7\u5b9d\u7bb1", "/items/chimerical_refinement_chest": "\u5947\u5e7b\u7cbe\u70bc\u7bb1\u5b50", "/items/sinister_refinement_chest": "\u9634\u68ee\u7cbe\u70bc\u7bb1\u5b50", "/items/enchanted_refinement_chest": "\u79d8\u6cd5\u7cbe\u70bc\u7bb1\u5b50", "/items/pirate_refinement_chest": "\u6d77\u76d7\u7cbe\u70bc\u7bb1\u5b50", "/items/blue_key_fragment": "\u84dd\u8272\u94a5\u5319\u788e\u7247", "/items/white_key_fragment": "\u767d\u8272\u94a5\u5319\u788e\u7247", "/items/green_key_fragment": "\u7eff\u8272\u94a5\u5319\u788e\u7247", "/items/orange_key_fragment": "\u6a59\u8272\u94a5\u5319\u788e\u7247", "/items/brown_key_fragment": "\u68d5\u8272\u94a5\u5319\u788e\u7247", "/items/purple_key_fragment": "\u7d2b\u8272\u94a5\u5319\u788e\u7247", "/items/burning_key_fragment": "\u71c3\u70e7\u94a5\u5319\u788e\u7247", "/items/dark_key_fragment": "\u9ed1\u6697\u94a5\u5319\u788e\u7247", "/items/stone_key_fragment": "\u77f3\u5934\u94a5\u5319\u788e\u7247", }; const SIMULATOR_ITEM_NAME_ALIASES = Object.fromEntries( Object.entries(MWITOOLS_ZH_ITEM_NAME_OVERRIDES).map(([hrid, name]) => [name, hrid]) ); const REFINEMENT_SHARD_EXPECTED_COUNT_PER_CHEST = 1.875; const DUNGEON_CHEST_CONFIG = { "/items/chimerical_chest": { entryKeyItemHrid: "/items/chimerical_entry_key", keyItemHrid: "/items/chimerical_chest_key", tokenItemHrid: "/items/chimerical_token", refinementChestItemHrid: "/items/chimerical_refinement_chest", refinementShardItemHrid: "/items/chimerical_refinement_shard", refinementShardCountPerChest: REFINEMENT_SHARD_EXPECTED_COUNT_PER_CHEST, drops: [ { itemHrid: "/items/chimerical_essence", dropRate: 1, minCount: 400, maxCount: 800 }, { itemHrid: "/items/chimerical_essence", dropRate: 0.05, minCount: 2000, maxCount: 4000 }, { itemHrid: "/items/chimerical_token", dropRate: 1, minCount: 250, maxCount: 500 }, { itemHrid: "/items/chimerical_token", dropRate: 0.05, minCount: 1500, maxCount: 3000 }, { itemHrid: "/items/griffin_leather", dropRate: 0.1, minCount: 1, maxCount: 1 }, { itemHrid: "/items/manticore_sting", dropRate: 0.06, minCount: 1, maxCount: 1 }, { itemHrid: "/items/jackalope_antler", dropRate: 0.05, minCount: 1, maxCount: 1 }, { itemHrid: "/items/dodocamel_plume", dropRate: 0.02, minCount: 1, maxCount: 1 }, { itemHrid: "/items/griffin_talon", dropRate: 0.02, minCount: 1, maxCount: 1 }, ], }, "/items/sinister_chest": { entryKeyItemHrid: "/items/sinister_entry_key", keyItemHrid: "/items/sinister_chest_key", tokenItemHrid: "/items/sinister_token", refinementChestItemHrid: "/items/sinister_refinement_chest", refinementShardItemHrid: "/items/sinister_refinement_shard", refinementShardCountPerChest: REFINEMENT_SHARD_EXPECTED_COUNT_PER_CHEST, drops: [ { itemHrid: "/items/sinister_essence", dropRate: 1, minCount: 400, maxCount: 800 }, { itemHrid: "/items/sinister_essence", dropRate: 0.05, minCount: 2000, maxCount: 4000 }, { itemHrid: "/items/sinister_token", dropRate: 1, minCount: 250, maxCount: 500 }, { itemHrid: "/items/sinister_token", dropRate: 0.05, minCount: 1500, maxCount: 3000 }, { itemHrid: "/items/acrobats_ribbon", dropRate: 0.04, minCount: 1, maxCount: 1 }, { itemHrid: "/items/magicians_cloth", dropRate: 0.04, minCount: 1, maxCount: 1 }, { itemHrid: "/items/chaotic_chain", dropRate: 0.02, minCount: 1, maxCount: 1 }, { itemHrid: "/items/cursed_ball", dropRate: 0.02, minCount: 1, maxCount: 1 }, ], }, "/items/enchanted_chest": { entryKeyItemHrid: "/items/enchanted_entry_key", keyItemHrid: "/items/enchanted_chest_key", tokenItemHrid: "/items/enchanted_token", refinementChestItemHrid: "/items/enchanted_refinement_chest", refinementShardItemHrid: "/items/enchanted_refinement_shard", refinementShardCountPerChest: REFINEMENT_SHARD_EXPECTED_COUNT_PER_CHEST, drops: [ { itemHrid: "/items/enchanted_essence", dropRate: 1, minCount: 400, maxCount: 800 }, { itemHrid: "/items/enchanted_essence", dropRate: 0.05, minCount: 2000, maxCount: 4000 }, { itemHrid: "/items/enchanted_token", dropRate: 1, minCount: 250, maxCount: 500 }, { itemHrid: "/items/enchanted_token", dropRate: 0.05, minCount: 1500, maxCount: 3000 }, { itemHrid: "/items/knights_ingot", dropRate: 0.04, minCount: 1, maxCount: 1 }, { itemHrid: "/items/bishops_scroll", dropRate: 0.04, minCount: 1, maxCount: 1 }, { itemHrid: "/items/royal_cloth", dropRate: 0.04, minCount: 1, maxCount: 1 }, { itemHrid: "/items/regal_jewel", dropRate: 0.02, minCount: 1, maxCount: 1 }, { itemHrid: "/items/sundering_jewel", dropRate: 0.02, minCount: 1, maxCount: 1 }, ], }, "/items/pirate_chest": { entryKeyItemHrid: "/items/pirate_entry_key", keyItemHrid: "/items/pirate_chest_key", tokenItemHrid: "/items/pirate_token", refinementChestItemHrid: "/items/pirate_refinement_chest", refinementShardItemHrid: "/items/pirate_refinement_shard", refinementShardCountPerChest: REFINEMENT_SHARD_EXPECTED_COUNT_PER_CHEST, drops: [ { itemHrid: "/items/pirate_essence", dropRate: 1, minCount: 400, maxCount: 800 }, { itemHrid: "/items/pirate_essence", dropRate: 0.05, minCount: 2000, maxCount: 4000 }, { itemHrid: "/items/pirate_token", dropRate: 1, minCount: 250, maxCount: 500 }, { itemHrid: "/items/pirate_token", dropRate: 0.05, minCount: 1500, maxCount: 3000 }, { itemHrid: "/items/marksman_brooch", dropRate: 0.03, minCount: 1, maxCount: 1 }, { itemHrid: "/items/corsair_crest", dropRate: 0.03, minCount: 1, maxCount: 1 }, { itemHrid: "/items/damaged_anchor", dropRate: 0.03, minCount: 1, maxCount: 1 }, { itemHrid: "/items/maelstrom_plating", dropRate: 0.03, minCount: 1, maxCount: 1 }, { itemHrid: "/items/kraken_leather", dropRate: 0.03, minCount: 1, maxCount: 1 }, { itemHrid: "/items/kraken_fang", dropRate: 0.03, minCount: 1, maxCount: 1 }, ], }, }; const DUNGEON_TOKEN_SHOP_COSTS = { "/items/chimerical_token": { "/items/chimerical_essence": 1, "/items/griffin_leather": 600, "/items/manticore_sting": 1000, "/items/jackalope_antler": 1200, "/items/dodocamel_plume": 3000, "/items/griffin_talon": 3000, }, "/items/sinister_token": { "/items/sinister_essence": 1, "/items/acrobats_ribbon": 2000, "/items/magicians_cloth": 2000, "/items/chaotic_chain": 3000, "/items/cursed_ball": 3000, }, "/items/enchanted_token": { "/items/enchanted_essence": 1, "/items/royal_cloth": 2000, "/items/knights_ingot": 2000, "/items/bishops_scroll": 2000, "/items/regal_jewel": 3000, "/items/sundering_jewel": 3000, }, "/items/pirate_token": { "/items/pirate_essence": 1, "/items/marksman_brooch": 2000, "/items/corsair_crest": 2000, "/items/damaged_anchor": 2000, "/items/maelstrom_plating": 2000, "/items/kraken_leather": 2000, "/items/kraken_fang": 3000, }, }; const REFINEMENT_TIER_EXPECTED_COUNTS = { 0: 0, 1: 0.33, 2: 1, }; const REFINEMENT_CHEST_ITEM_HRIDS = Object.values(DUNGEON_CHEST_CONFIG) .map((config) => config.refinementChestItemHrid) .filter(Boolean); const REFINEMENT_SHARD_ITEM_HRIDS = Object.values(DUNGEON_CHEST_CONFIG) .map((config) => config.refinementShardItemHrid) .filter(Boolean); const REFINEMENT_CHEST_TO_BASE_CHEST_HRID = Object.fromEntries( Object.entries(DUNGEON_CHEST_CONFIG) .filter(([, config]) => config?.refinementChestItemHrid) .map(([chestItemHrid, config]) => [config.refinementChestItemHrid, chestItemHrid]) ); const REFINEMENT_SHARD_TO_BASE_CHEST_HRID = Object.fromEntries( Object.entries(DUNGEON_CHEST_CONFIG) .filter(([, config]) => config?.refinementShardItemHrid) .map(([chestItemHrid, config]) => [config.refinementShardItemHrid, chestItemHrid]) ); const TIME_CALCULATOR_ITEM_HRIDS = [ ...DUNGEON_CHEST_ITEM_HRIDS, ...REFINEMENT_CHEST_ITEM_HRIDS, ...KEY_FRAGMENT_ITEM_HRIDS, ]; const DUNGEON_MATERIAL_ITEM_HRIDS = new Set( [ ...Object.values(DUNGEON_TOKEN_SHOP_COSTS).flatMap((shopMap) => Object.keys(shopMap)), ...REFINEMENT_SHARD_ITEM_HRIDS, ] ); const DUNGEON_RELATED_ITEM_HRIDS = new Set([ ...Object.values(DUNGEON_CHEST_CONFIG).flatMap((config) => [ config.entryKeyItemHrid, config.keyItemHrid, config.tokenItemHrid, config.refinementChestItemHrid, config.refinementShardItemHrid, ...(config.drops || []).map((drop) => drop.itemHrid), ]), "/items/chimerical_quiver", "/items/sinister_cape", "/items/enchanted_cloak", ].filter(Boolean)); const isZh = String(localStorage.getItem("i18nextLng") || "").toLowerCase().startsWith("zh"); function clearCaches() { state.itemTimeCache.clear(); state.itemTooltipDataCache.clear(); state.essencePlanCache.clear(); state.fixedDecomposePlanCache.clear(); state.fixedEnhancedEssencePlanCache.clear(); state.fixedTransmutePlanCache.clear(); state.fixedAttachedRareTooltipPlanCache.clear(); state.attachedRareYieldCache.clear(); state.itemTargetRelationCache.clear(); state.skillingScrollTimeSavingsCache.clear(); state.itemFailureReasonCache.clear(); state.activeItemSolveSet.clear(); state.cyclicSolveDepth = 0; state.outputActionCache = null; state.maxEnhancementByItem = null; } function clearStructuralCaches() { state.outputActionCache = null; } function decodeEscapedJsonString(value) { if (typeof value !== "string") { return ""; } try { return JSON.parse(`"${value.replace(/"/g, '\\"')}"`); } catch (_error) { return value; } } const LZ_BASE64_KEY_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; const LZ_BASE64_REVERSE_DICT = Object.create(null); for (let i = 0; i < LZ_BASE64_KEY_STRING.length; i += 1) { LZ_BASE64_REVERSE_DICT[LZ_BASE64_KEY_STRING.charAt(i)] = i; } function lzDecompress(length, resetValue, getNextValue) { const dictionary = []; let enlargeIn = 4; let dictSize = 4; let numBits = 3; let entry = ""; const result = []; const data = { val: getNextValue(0), position: resetValue, index: 1, }; for (let i = 0; i < 3; i += 1) { dictionary[i] = i; } let bits = 0; let maxPower = Math.pow(2, 2); let power = 1; while (power !== maxPower) { const resb = data.val & data.position; data.position >>= 1; if (data.position === 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb > 0 ? 1 : 0) * power; power <<= 1; } let c = bits; if (c === 0) { bits = 0; maxPower = Math.pow(2, 8); power = 1; while (power !== maxPower) { const resb = data.val & data.position; data.position >>= 1; if (data.position === 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb > 0 ? 1 : 0) * power; power <<= 1; } c = String.fromCharCode(bits); } else if (c === 1) { bits = 0; maxPower = Math.pow(2, 16); power = 1; while (power !== maxPower) { const resb = data.val & data.position; data.position >>= 1; if (data.position === 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb > 0 ? 1 : 0) * power; power <<= 1; } c = String.fromCharCode(bits); } else if (c === 2) { return ""; } else { c = ""; } dictionary[3] = c; let w = c; result.push(c); while (true) { if (data.index > length) { return ""; } bits = 0; maxPower = Math.pow(2, numBits); power = 1; while (power !== maxPower) { const resb = data.val & data.position; data.position >>= 1; if (data.position === 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb > 0 ? 1 : 0) * power; power <<= 1; } c = bits; if (c === 0) { bits = 0; maxPower = Math.pow(2, 8); power = 1; while (power !== maxPower) { const resb = data.val & data.position; data.position >>= 1; if (data.position === 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb > 0 ? 1 : 0) * power; power <<= 1; } dictionary[dictSize++] = String.fromCharCode(bits); c = dictSize - 1; enlargeIn -= 1; } else if (c === 1) { bits = 0; maxPower = Math.pow(2, 16); power = 1; while (power !== maxPower) { const resb = data.val & data.position; data.position >>= 1; if (data.position === 0) { data.position = resetValue; data.val = getNextValue(data.index++); } bits |= (resb > 0 ? 1 : 0) * power; power <<= 1; } dictionary[dictSize++] = String.fromCharCode(bits); c = dictSize - 1; enlargeIn -= 1; } else if (c === 2) { return result.join(""); } if (enlargeIn === 0) { enlargeIn = Math.pow(2, numBits); numBits += 1; } if (dictionary[c]) { entry = dictionary[c]; } else if (c === dictSize) { entry = w + w.charAt(0); } else { return null; } result.push(entry); dictionary[dictSize++] = w + entry.charAt(0); enlargeIn -= 1; w = entry; if (enlargeIn === 0) { enlargeIn = Math.pow(2, numBits); numBits += 1; } } } function getLzStringHelper() { const runtimeLz = typeof LZString !== "undefined" ? LZString : window.LZString; if (runtimeLz && typeof runtimeLz.decompressFromUTF16 === "function") { return runtimeLz; } return { decompressFromUTF16(compressed) { if (compressed == null) { return ""; } if (compressed === "") { return null; } return lzDecompress(compressed.length, 16384, (index) => compressed.charCodeAt(index) - 32); }, decompressFromBase64(compressed) { if (compressed == null) { return ""; } const normalized = String(compressed || "").replace(/[^A-Za-z0-9+/=]/g, ""); if (!normalized) { return null; } return lzDecompress(normalized.length, 32, (index) => LZ_BASE64_REVERSE_DICT[normalized.charAt(index)] || 0); }, }; } function parseLocalizedItemNamesFromScript(scriptText) { if (typeof scriptText !== "string" || !scriptText.includes('"/items/')) { return 0; } let added = 0; const regex = /"((?:\\\/|\/)items\/[^"]+)":\s*"((?:\\.|[^"\\])*)"/g; let match; while ((match = regex.exec(scriptText))) { const rawKey = match[1].replace(/\\\//g, "/"); const decodedValue = decodeEscapedJsonString(match[2]); if (!rawKey.startsWith("/items/") || !decodedValue) { continue; } if (/^[\x00-\x7F]+$/.test(decodedValue)) { continue; } state.localizedItemNameMap.set(rawKey, decodedValue); added += 1; } return added; } async function loadLocalizedItemNames() { if (!isZh || state.translationLoadStarted || state.localizedItemNameMap.size > 0) { return; } state.translationLoadStarted = true; try { const scriptUrls = Array.from(document.querySelectorAll("script[src]")) .map((node) => node.src) .filter((src) => src && src.startsWith(location.origin) && src.endsWith(".js")); let added = 0; for (const src of scriptUrls) { if (state.localizedItemNameMap.size > 500) { break; } try { const response = await fetch(src, { credentials: "same-origin", cache: "force-cache" }); if (!response.ok) { continue; } const text = await response.text(); added += parseLocalizedItemNamesFromScript(text); } catch (_error) { continue; } } if (added > 0) { refreshOpenTooltips(); } } finally { state.translationLoadStarted = false; } } function getLocalizedItemName(itemHrid, fallbackName = "") { if (!itemHrid) { return fallbackName || ""; } if (isZh && MWITOOLS_ZH_ITEM_NAME_OVERRIDES[itemHrid]) { return MWITOOLS_ZH_ITEM_NAME_OVERRIDES[itemHrid]; } if (isZh && ESSENCE_SOURCE_NAME_ZH[itemHrid]) { return ESSENCE_SOURCE_NAME_ZH[itemHrid]; } if (isZh && itemHrid === "/items/catalyst_of_coinification") { return "点金催化剂"; } if (isZh && itemHrid === "/items/catalyst_of_decomposition") { return "分解催化剂"; } if (isZh && itemHrid === "/items/catalyst_of_transmutation") { return "转化催化剂"; } if (isZh && itemHrid === "/items/prime_catalyst") { return "至高催化剂"; } const localized = state.localizedItemNameMap.get(itemHrid); if (localized) { return localized; } const detailName = state.itemDetailMap?.[itemHrid]?.name || ""; if (detailName && (!isZh || /[^\x00-\x7F]/.test(detailName))) { return detailName; } if (isZh) { loadLocalizedItemNames(); } return fallbackName || detailName || itemHrid; } function isMissingDerivedRuntimeState() { return !state.characterItemByLocationMap || !state.characterSkillMap || !state.communityActionTypeBuffsDict || !state.houseActionTypeBuffsDict || !state.achievementActionTypeBuffsDict || !state.personalActionTypeBuffsDict || !state.consumableActionTypeBuffsDict || !state.equipmentActionTypeBuffsDict; } function getContainerValues(container) { if (!container) { return []; } if (container instanceof Map) { return Array.from(container.values()); } if (Array.isArray(container)) { return container.slice(); } if (typeof container === "object") { return Object.values(container); } return []; } function getContainerValue(container, key) { if (!container || !key) { return null; } if (container instanceof Map) { return container.get(key) || null; } if (typeof container === "object") { return container[key] || null; } return null; } function isLikelyGameState(candidate) { return Boolean( candidate && typeof candidate === "object" && candidate.character && (candidate.actionDetailMaps || candidate.itemDetailDict || candidate.characterItemMap) && (Object.prototype.hasOwnProperty.call(candidate, "gameConn") || Object.prototype.hasOwnProperty.call(candidate, "combatUnit") || Object.prototype.hasOwnProperty.call(candidate, "characterActions")) ); } function findGameStateFromFiber(rootFiber) { if (!rootFiber || typeof rootFiber !== "object") { return null; } const queue = [rootFiber]; const visited = new Set(); let steps = 0; while (queue.length > 0 && steps < 20000) { const fiber = queue.shift(); if (!fiber || typeof fiber !== "object" || visited.has(fiber)) { continue; } visited.add(fiber); steps += 1; const candidate = fiber.stateNode?.state; if (isLikelyGameState(candidate)) { return candidate; } if (fiber.child) { queue.push(fiber.child); } if (fiber.sibling) { queue.push(fiber.sibling); } } return null; } function getGameState() { const gamePage = document.querySelector('[class^="GamePage"]'); if (gamePage) { const reactKey = Object.keys(gamePage).find((key) => key.startsWith("__reactFiber$")); if (reactKey) { const fiberNode = gamePage[reactKey]; const directState = fiberNode?.return?.stateNode?.state || null; if (isLikelyGameState(directState)) { return directState; } } } const rootElement = document.getElementById("root"); let rootContainer = rootElement?._reactRootContainer || null; if (!rootContainer) { const fallbackRoot = Array.from(document.querySelectorAll("div")).find((el) => Object.prototype.hasOwnProperty.call(el, "_reactRootContainer") ); rootContainer = fallbackRoot?._reactRootContainer || null; } return findGameStateFromFiber(rootContainer?.current || null); } function normalizeActionDetailMap(obj) { if (!obj || typeof obj !== "object") { return null; } if (obj.actionDetailMap && typeof obj.actionDetailMap === "object") { return obj.actionDetailMap; } if (!obj.actionDetailMaps || typeof obj.actionDetailMaps !== "object") { return null; } const flattened = {}; for (const actionMap of Object.values(obj.actionDetailMaps)) { if (actionMap && typeof actionMap === "object") { Object.assign(flattened, actionMap); } } return Object.keys(flattened).length > 0 ? flattened : null; } function normalizeItemDetailMap(obj) { if (!obj || typeof obj !== "object") { return null; } if (obj.itemDetailMap && typeof obj.itemDetailMap === "object") { return obj.itemDetailMap; } if (obj.itemDetailDict && typeof obj.itemDetailDict === "object") { return obj.itemDetailDict; } return null; } function normalizeShopItemDetailMap(obj) { if (!obj || typeof obj !== "object") { return null; } if (obj.shopItemDetailMap && typeof obj.shopItemDetailMap === "object") { return obj.shopItemDetailMap; } return null; } function updateClientData(obj) { if (!obj || typeof obj !== "object") { return; } const actionDetailMap = normalizeActionDetailMap(obj); const itemDetailMap = normalizeItemDetailMap(obj); const shopItemDetailMap = normalizeShopItemDetailMap(obj); if (actionDetailMap) { state.actionDetailMap = actionDetailMap; } if (itemDetailMap) { state.itemDetailMap = itemDetailMap; state.itemNameToHrid.clear(); for (const [itemHrid, item] of Object.entries(itemDetailMap)) { if (item?.name) { state.itemNameToHrid.set(item.name, itemHrid); } } } if (shopItemDetailMap) { state.shopItemDetailMap = shopItemDetailMap; } if (obj.enhancementLevelTotalBonusMultiplierTable) { state.enhancementLevelTotalBonusMultiplierTable = obj.enhancementLevelTotalBonusMultiplierTable; } if (isZh) { loadLocalizedItemNames(); } } function updateCharacterData(obj, options = {}) { const { refreshTooltips = true } = options; if (!obj || typeof obj !== "object") { return; } let changed = false; if (obj.characterSkills) { state.characterSkills = obj.characterSkills; changed = true; } if (obj.characterSkillMap) { state.characterSkillMap = obj.characterSkillMap; state.characterSkills = getContainerValues(obj.characterSkillMap); changed = true; } if (obj.characterItems) { state.characterItems = obj.characterItems; changed = true; } if (obj.characterItemMap) { state.characterItemMap = obj.characterItemMap; state.characterItems = getContainerValues(obj.characterItemMap); changed = true; } if (obj.characterItemByLocationMap) { state.characterItemByLocationMap = obj.characterItemByLocationMap; changed = true; } if (obj.characterHouseRoomMap) { state.characterHouseRoomMap = obj.characterHouseRoomMap; changed = true; } if (obj.characterHouseRoomDict) { state.characterHouseRoomMap = obj.characterHouseRoomDict; changed = true; } if (obj.actionTypeDrinkSlotsMap) { state.actionTypeDrinkSlotsMap = obj.actionTypeDrinkSlotsMap; changed = true; } if (obj.actionTypeDrinkSlotsDict) { state.actionTypeDrinkSlotsMap = obj.actionTypeDrinkSlotsDict; changed = true; } if (obj.characterLoadoutDict) { state.characterLoadoutDict = obj.characterLoadoutDict; changed = true; } if (obj.characterSetting) { state.characterSetting = obj.characterSetting; changed = true; } if (typeof obj.currentCharacterName === "string") { state.currentCharacterName = obj.currentCharacterName.trim(); changed = true; } if (obj.communityActionTypeBuffsDict) { state.communityActionTypeBuffsDict = obj.communityActionTypeBuffsDict; changed = true; } if (obj.houseActionTypeBuffsDict) { state.houseActionTypeBuffsDict = obj.houseActionTypeBuffsDict; changed = true; } if (obj.achievementActionTypeBuffsDict) { state.achievementActionTypeBuffsDict = obj.achievementActionTypeBuffsDict; changed = true; } if (obj.personalActionTypeBuffsDict) { state.personalActionTypeBuffsDict = obj.personalActionTypeBuffsDict; changed = true; } if (obj.consumableActionTypeBuffsDict) { state.consumableActionTypeBuffsDict = obj.consumableActionTypeBuffsDict; changed = true; } if (obj.equipmentActionTypeBuffsDict) { state.equipmentActionTypeBuffsDict = obj.equipmentActionTypeBuffsDict; changed = true; } if (obj.mooPassActionTypeBuffsDict) { state.mooPassActionTypeBuffsDict = obj.mooPassActionTypeBuffsDict; changed = true; } if (changed) { if (isMissingDerivedRuntimeState()) { hydrateFromReactState({ refreshTooltips }); } if (state.timeCalculatorContainer?.isConnected && state.timeCalculatorLoadedCharacterId !== null && state.timeCalculatorLoadedCharacterId !== getCurrentCharacterId()) { rerenderTimeCalculatorPanel(); } } } function hydrateFromReactState(options = {}) { const { refreshTooltips = true } = options; try { const appState = getGameState(); if (!appState) { return false; } updateClientData(appState); updateCharacterData({ characterSkillMap: appState.characterSkillMap, characterItemMap: appState.characterItemMap, characterItemByLocationMap: appState.characterItemByLocationMap, characterHouseRoomDict: appState.characterHouseRoomDict, actionTypeDrinkSlotsDict: appState.actionTypeDrinkSlotsDict, characterLoadoutDict: appState.characterLoadoutDict, characterSetting: appState.characterSetting, currentCharacterName: appState.character?.name || appState.characterDTO?.name || appState.selectedCharacter?.name || appState.characterSetting?.name || appState.characterSetting?.characterName || "", communityActionTypeBuffsDict: appState.communityActionTypeBuffsDict, houseActionTypeBuffsDict: appState.houseActionTypeBuffsDict, achievementActionTypeBuffsDict: appState.achievementActionTypeBuffsDict, personalActionTypeBuffsDict: appState.personalActionTypeBuffsDict, consumableActionTypeBuffsDict: appState.consumableActionTypeBuffsDict, equipmentActionTypeBuffsDict: appState.equipmentActionTypeBuffsDict, mooPassActionTypeBuffsDict: appState.mooPassActionTypeBuffsDict, }, { refreshTooltips }); return true; } catch (error) { console.error("[ICTime] Failed to hydrate runtime state from React.", error); return false; } } function ensureRuntimeStateFresh(force = false, options = {}) { const now = Date.now(); if (!force && now - state.lastRuntimeHydrationAt < 1000) { return false; } state.lastRuntimeHydrationAt = now; return hydrateFromReactState(options); } function loadCachedClientData() { const raw = localStorage.getItem("initClientData"); if (!raw) { return false; } if (state.cachedInitClientData && state.cachedInitClientDataRaw === raw) { updateClientData(state.cachedInitClientData); return true; } const lz = getLzStringHelper(); const parsers = [ () => JSON.parse(raw), () => { if (!lz || typeof lz.decompressFromUTF16 !== "function") { return null; } const decompressed = lz.decompressFromUTF16(raw); return decompressed ? JSON.parse(decompressed) : null; }, () => { if (!lz || typeof lz.decompressFromBase64 !== "function") { return null; } const decompressed = lz.decompressFromBase64(raw); return decompressed ? JSON.parse(decompressed) : null; }, ]; for (const parser of parsers) { try { const parsed = parser(); if (parsed && typeof parsed === "object") { state.cachedInitClientData = parsed; state.cachedInitClientDataRaw = raw; updateClientData(parsed); return true; } } catch (_error) { // Try next parser. } } console.error("[ICTime] Failed to parse initClientData with all parsers."); return false; } function hookWebSocket() { const descriptor = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data"); if (!descriptor?.get) { return; } const originalGet = descriptor.get; descriptor.get = function hookedData() { const socket = this.currentTarget; if (!(socket instanceof WebSocket)) { return originalGet.call(this); } const url = socket.url || ""; if (!/api(-test)?\.milkywayidle(cn)?\.com\/ws/.test(url)) { return originalGet.call(this); } const message = originalGet.call(this); Object.defineProperty(this, "data", { value: message }); handleSocketMessage(message); return message; }; Object.defineProperty(MessageEvent.prototype, "data", descriptor); } function handleSocketMessage(message) { try { const obj = JSON.parse(message); if (!obj || typeof obj !== "object") { return; } if (obj.type === "init_client_data") { updateClientData(obj); return; } if (obj.type === "init_character_data") { updateCharacterData(obj); return; } updateCharacterData(obj); } catch { return; } } function getItemHridFromTooltip(tooltip) { const anchor = tooltip.querySelector('a[href*="#"]'); if (anchor) { const href = anchor.getAttribute("href") || ""; const hashIndex = href.indexOf("#"); if (hashIndex >= 0) { return `/items/${href.slice(hashIndex + 1)}`; } } const nameSpans = tooltip.querySelectorAll("div.ItemTooltipText_name__2JAHA span"); for (const span of nameSpans) { const text = (span.textContent || "").trim(); if (state.itemNameToHrid.has(text)) { return state.itemNameToHrid.get(text); } } const hoveredHrid = getItemHridFromHoveredSource(tooltip); if (hoveredHrid) { return hoveredHrid; } return null; } function getItemEnhancementLevelFromTooltip(tooltip) { if (!tooltip) { return 0; } const nameContainer = tooltip.querySelector("div.ItemTooltipText_name__2JAHA") || tooltip; const text = (nameContainer.textContent || "").replace(/\s+/g, " ").trim(); const match = text.match(/\+(\d+)\b/); return match ? Math.max(0, Number(match[1] || 0)) : 0; } function extractItemHridFromElement(element) { if (!element) { return null; } const isSvgUseElement = typeof SVGUseElement !== "undefined" && element instanceof SVGUseElement; if (isSvgUseElement) { const href = element.getAttribute("href") || element.getAttribute("xlink:href") || ""; const hashIndex = href.indexOf("#"); if (hashIndex >= 0 && href.includes("items_sprite")) { return `/items/${href.slice(hashIndex + 1).trim()}`; } } const uses = element.querySelectorAll("use"); for (const use of uses) { const href = use.getAttribute("href") || use.getAttribute("xlink:href") || ""; const hashIndex = href.indexOf("#"); if (hashIndex < 0) { continue; } const iconId = href.slice(hashIndex + 1).trim(); if (!iconId) { continue; } if (href.includes("items_sprite")) { return `/items/${iconId}`; } } return null; } function runUiGuarded(label, fn) { try { return fn(); } catch (error) { console.error(`[ICTime] ${label} failed.`, error); return null; } } function getItemHridFromHoveredSource(tooltip) { if (state.lastHoveredItemHrid && Date.now() - state.lastHoveredItemAt < 2000) { return state.lastHoveredItemHrid; } return null; } function trackHoveredItem(target) { if (!(target instanceof Element)) { return false; } let node = target; let depth = 0; while (node && depth < 6) { const itemHrid = extractItemHridFromElement(node); if (itemHrid) { state.lastHoveredItemHrid = itemHrid; state.lastHoveredItemAt = Date.now(); return true; } node = node.parentElement; depth += 1; } return false; } function queueTooltipRefresh(delay = 180) { if (state.isShutDown) { return; } if (state.tooltipRefreshTimer) { clearTimeout(state.tooltipRefreshTimer); } state.tooltipRefreshTimer = setTimeout(() => { state.tooltipRefreshTimer = 0; refreshOpenTooltips(); }, delay); } function getTooltipContentContainer(tooltip) { return ( tooltip.querySelector(".ItemTooltipText_itemTooltipText__zFq3A") || tooltip.querySelector('[class*="ItemTooltipText_itemTooltipText"]') || tooltip.querySelector('[class*="ItemTooltipText_text"]') ); } function buildOutputActionCache() { const cache = new Map(); if (!state.actionDetailMap) { return cache; } const gatheringCandidates = new Map(); for (const action of Object.values(state.actionDetailMap)) { if (!action || !SUPPORTED_ACTION_TYPES.has(action.type)) { continue; } const isProduction = Array.isArray(action.inputItems) && action.inputItems.length > 0; if (!isProduction) { continue; } for (const output of action.outputItems || []) { if (output?.itemHrid && !cache.has(output.itemHrid)) { cache.set(output.itemHrid, action.hrid); } } } for (const action of Object.values(state.actionDetailMap)) { if (!action || !SUPPORTED_ACTION_TYPES.has(action.type)) { continue; } const isGathering = !action.inputItems || action.inputItems.length === 0; if (!isGathering) { continue; } for (const drop of action.dropTable || []) { if (drop?.itemHrid) { const current = gatheringCandidates.get(drop.itemHrid); if (isBetterGatheringSource(action, drop.itemHrid, current)) { gatheringCandidates.set(drop.itemHrid, action.hrid); } } const processed = PROCESSABLE_ITEM_MAP.get(drop?.itemHrid); if (processed) { const currentProcessed = gatheringCandidates.get(processed); if (isBetterGatheringSource(action, processed, currentProcessed)) { gatheringCandidates.set(processed, action.hrid); } } } } for (const [itemHrid, actionHrid] of gatheringCandidates.entries()) { if (!cache.has(itemHrid)) { cache.set(itemHrid, actionHrid); } } return cache; } function getBaseGatheringOutputCount(action, targetItemHrid) { let count = 0; for (const drop of action?.dropTable || []) { const average = (drop.dropRate || 0) * (((drop.minCount || 0) + (drop.maxCount || 0)) / 2); if (drop.itemHrid === targetItemHrid) { count += average; } if (PROCESSABLE_ITEM_MAP.get(drop.itemHrid) === targetItemHrid) { count += average / 2; } } return count; } function isDedicatedGatheringAction(action, targetItemHrid) { const drops = action?.dropTable || []; if (drops.length !== 1) { return false; } const onlyDrop = drops[0]; return onlyDrop?.itemHrid === targetItemHrid && Number(onlyDrop.dropRate || 0) >= 1; } function isBetterGatheringSource(candidateAction, targetItemHrid, currentActionHrid) { if (!currentActionHrid) { return true; } const currentAction = state.actionDetailMap?.[currentActionHrid]; if (!currentAction) { return true; } const candidateDedicated = isDedicatedGatheringAction(candidateAction, targetItemHrid); const currentDedicated = isDedicatedGatheringAction(currentAction, targetItemHrid); if (candidateDedicated !== currentDedicated) { return candidateDedicated; } const candidateBaseCount = getBaseGatheringOutputCount(candidateAction, targetItemHrid); const currentBaseCount = getBaseGatheringOutputCount(currentAction, targetItemHrid); if (candidateBaseCount !== currentBaseCount) { return candidateBaseCount > currentBaseCount; } const candidateBaseSeconds = Number(candidateAction.baseTimeCost || 0); const currentBaseSeconds = Number(currentAction.baseTimeCost || 0); if (candidateBaseSeconds !== currentBaseSeconds) { return candidateBaseSeconds < currentBaseSeconds; } return false; } function findActionForItem(itemHrid) { if (!state.outputActionCache) { state.outputActionCache = buildOutputActionCache(); } const actionHrid = state.outputActionCache.get(itemHrid); return actionHrid ? state.actionDetailMap?.[actionHrid] || null : null; } function getEnhancementBonusMultiplier(level) { if (state.enhancementLevelTotalBonusMultiplierTable && state.enhancementLevelTotalBonusMultiplierTable[level] != null) { return state.enhancementLevelTotalBonusMultiplierTable[level]; } return (ENHANCEMENT_BONUS[level] || 0) / 100; } function getBuffAmount(buff) { if (!buff) { return 0; } return ( Number(buff.flatBoost || 0) + Number(buff.flatBoostLevelBonus || 0) + Number(buff.ratioBoost || 0) + Number(buff.ratioBoostLevelBonus || 0) ); } function clamp01(value) { if (!Number.isFinite(value)) { return 0; } return Math.max(0, Math.min(1, value)); } function getActionTypeBuffs(sourceKey, actionTypeHrid) { const dict = state[sourceKey]; const buffs = dict?.[actionTypeHrid]; return Array.isArray(buffs) ? buffs : []; } function sumBuffsByType(buffs, typeHrid) { let total = 0; for (const buff of buffs) { if (buff?.typeHrid !== typeHrid) { continue; } total += getBuffAmount(buff); } return total; } function getToolSlotForActionType(actionTypeHrid) { const skillId = actionTypeHrid?.split("/").pop() || ""; return skillId ? `/item_locations/${skillId}_tool` : ""; } function parseWearableReference(rawValue) { if (!rawValue) { return null; } const parts = String(rawValue).split("::"); if (parts.length < 4) { return null; } return { itemHrid: parts[2] || "", enhancementLevel: Number(parts[3] || 0) || 0, }; } function buildMaxEnhancementByItem() { if (state.maxEnhancementByItem) { return state.maxEnhancementByItem; } const maxByItem = new Map(); for (const item of getContainerValues(state.characterItemMap)) { if (!item?.itemHrid) { continue; } if (!Number.isFinite(Number(item.count)) || Number(item.count) <= 0) { continue; } const enhancement = Math.max(0, Number(item.enhancementLevel || 0)); const current = maxByItem.get(item.itemHrid); if (!Number.isFinite(current) || enhancement > current) { maxByItem.set(item.itemHrid, enhancement); } } state.maxEnhancementByItem = maxByItem; return maxByItem; } function listLoadouts() { return Object.values(state.characterLoadoutDict || {}).filter(Boolean); } function resolveSkillingLoadout(actionTypeHrid) { const loadouts = listLoadouts() .filter((loadout) => loadout?.isDefault) .sort((a, b) => Number(a?.id || 0) - Number(b?.id || 0)); const direct = loadouts.find((loadout) => loadout.actionTypeHrid === actionTypeHrid); if (direct) { return { source: "action", loadout: direct, }; } const fallback = loadouts.find((loadout) => !loadout.actionTypeHrid); if (fallback) { return { source: "global", loadout: fallback, }; } return { source: "current", loadout: null, }; } function resolveWearableEnhancement(entry, loadout) { if (!entry) { return 0; } if (loadout?.useExactEnhancement) { return Math.max(0, Number(entry.enhancementLevel || 0)); } const highest = buildMaxEnhancementByItem().get(entry.itemHrid); if (Number.isFinite(highest)) { return Math.max(0, highest); } return Math.max(0, Number(entry.enhancementLevel || 0)); } function getEquippedItems(actionTypeHrid = "") { const loadoutInfo = actionTypeHrid ? resolveSkillingLoadout(actionTypeHrid) : { loadout: null }; const loadout = loadoutInfo.loadout; if (loadout?.wearableMap) { const items = []; for (const [slotKey, rawRef] of Object.entries(loadout.wearableMap || {})) { const entry = parseWearableReference(rawRef); if (!entry?.itemHrid) { continue; } items.push({ itemHrid: entry.itemHrid, enhancementLevel: resolveWearableEnhancement(entry, loadout), itemLocationHrid: slotKey, count: 1, }); } return items; } const byLocation = state.characterItemByLocationMap; if (byLocation instanceof Map) { return Array.from(byLocation.values()).filter((item) => item && item.itemLocationHrid !== "/item_locations/inventory"); } if (byLocation && typeof byLocation === "object") { return Object.values(byLocation).filter((item) => item && item.itemLocationHrid !== "/item_locations/inventory"); } return (state.characterItems || []).filter((item) => item && item.itemLocationHrid !== "/item_locations/inventory"); } function getSkillLevel(skillHrid) { const fromMap = getContainerValue(state.characterSkillMap, skillHrid); if (fromMap?.level != null) { return Number(fromMap.level) || 0; } const fromArray = (state.characterSkills || []).find((entry) => entry.skillHrid === skillHrid); return Number(fromArray?.level || 0); } function buildEquipmentNoncombatTotals(actionTypeHrid) { const totals = {}; const toolSlot = getToolSlotForActionType(actionTypeHrid); for (const item of getEquippedItems(actionTypeHrid)) { const location = item.itemLocationHrid || ""; if (location.endsWith("_tool") && location !== toolSlot) { continue; } const equipmentDetail = state.itemDetailMap?.[item.itemHrid]?.equipmentDetail; if (!equipmentDetail) { continue; } const enhancementMultiplier = getEnhancementBonusMultiplier(item.enhancementLevel || 0); const baseStats = equipmentDetail.noncombatStats || {}; const enhancementStats = equipmentDetail.noncombatEnhancementBonuses || {}; for (const [key, value] of Object.entries(baseStats)) { if (Number.isFinite(Number(value))) { totals[key] = (totals[key] || 0) + Number(value); } } for (const [key, value] of Object.entries(enhancementStats)) { if (Number.isFinite(Number(value))) { totals[key] = (totals[key] || 0) + Number(value) * enhancementMultiplier; } } } return totals; } function getDrinkConcentration(actionTypeHrid) { const pouch = getEquippedItems(actionTypeHrid).find((item) => item.itemHrid === "/items/guzzling_pouch"); if (!pouch || !state.itemDetailMap?.["/items/guzzling_pouch"]?.equipmentDetail) { return 1; } const detail = state.itemDetailMap["/items/guzzling_pouch"].equipmentDetail; const base = detail.noncombatStats?.drinkConcentration || 0; const bonus = detail.noncombatEnhancementBonuses?.drinkConcentration || 0; return 1 + base + bonus * getEnhancementBonusMultiplier(pouch.enhancementLevel || 0); } function getTeaBuffs(actionTypeHrid) { const skillId = actionTypeHrid.replace("/action_types/", ""); const loadoutInfo = resolveSkillingLoadout(actionTypeHrid); const concentration = getDrinkConcentration(actionTypeHrid); const buffs = { efficiencyFraction: 0, quantityFraction: 0, lessResourceFraction: 0, processingFraction: 0, rareFindFraction: 0, successRateFraction: 0, alchemySuccessFraction: 0, skillLevelBonus: 0, actionLevelPenalty: 0, activeTeas: [], concentrationMultiplier: concentration, durationSeconds: 300 / concentration, loadoutInfo, }; const loadoutTeaList = Array.isArray(loadoutInfo.loadout?.drinkItemHrids) ? loadoutInfo.loadout.drinkItemHrids.filter(Boolean).map((itemHrid) => ({ itemHrid })) : []; const currentTeaList = state.actionTypeDrinkSlotsMap?.[actionTypeHrid] || []; const teaList = loadoutTeaList.length > 0 ? loadoutTeaList : currentTeaList; for (const tea of teaList) { if (!tea?.itemHrid) { continue; } buffs.activeTeas.push(tea.itemHrid); const teaDetail = state.itemDetailMap?.[tea.itemHrid]; for (const buff of teaDetail?.consumableDetail?.buffs || []) { if (buff.typeHrid === "/buff_types/artisan") { buffs.lessResourceFraction += buff.flatBoost; } else if (buff.typeHrid === "/buff_types/gathering" || buff.typeHrid === "/buff_types/gourmet") { buffs.quantityFraction += buff.flatBoost; } else if (buff.typeHrid === "/buff_types/processing") { buffs.processingFraction += buff.flatBoost; } else if (buff.typeHrid === "/buff_types/rare_find") { buffs.rareFindFraction += getBuffAmount(buff); } else if (buff.typeHrid === "/buff_types/efficiency") { buffs.efficiencyFraction += buff.flatBoost; } else if (buff.typeHrid === "/buff_types/success_rate") { buffs.successRateFraction += getBuffAmount(buff); } else if (buff.typeHrid === "/buff_types/alchemy_success") { buffs.alchemySuccessFraction += getBuffAmount(buff); } else if (buff.typeHrid === `/buff_types/${skillId}_level`) { buffs.skillLevelBonus += buff.flatBoost; } else if (buff.typeHrid === "/buff_types/action_level") { buffs.actionLevelPenalty += buff.flatBoost; } } } buffs.quantityFraction *= concentration; buffs.lessResourceFraction *= concentration; buffs.processingFraction *= concentration; buffs.rareFindFraction *= concentration; buffs.efficiencyFraction *= concentration; buffs.successRateFraction *= concentration; buffs.alchemySuccessFraction *= concentration; buffs.skillLevelBonus *= concentration; buffs.actionLevelPenalty *= concentration; return buffs; } function getEquippedItem(itemHrid) { let best = null; for (const item of getEquippedItems()) { if (item.itemHrid !== itemHrid) { continue; } if (!best || (item.enhancementLevel || 0) > (best.enhancementLevel || 0)) { best = item; } } return best; } function getGlobalActionBuffs(actionTypeHrid) { return [ ...getActionTypeBuffs("communityActionTypeBuffsDict", actionTypeHrid), ...getActionTypeBuffs("houseActionTypeBuffsDict", actionTypeHrid), ...getActionTypeBuffs("achievementActionTypeBuffsDict", actionTypeHrid), ...getActionTypeBuffs("mooPassActionTypeBuffsDict", actionTypeHrid), ]; } function getActionSummary(action) { const skillId = action.type.replace("/action_types/", ""); const totals = buildEquipmentNoncombatTotals(action.type); const globalBuffs = getGlobalActionBuffs(action.type); const teaBuffs = getTeaBuffs(action.type); const actionSpeedFraction = Number(totals[`${skillId}Speed`] || 0) + Number(totals.skillingSpeed || 0) + sumBuffsByType(globalBuffs, "/buff_types/action_speed"); const equipmentEfficiencyFraction = Number(totals[`${skillId}Efficiency`] || 0) + Number(totals.skillingEfficiency || 0); const equipmentRareFindFraction = Number(totals[`${skillId}RareFind`] || 0) + Number(totals.skillingRareFind || 0); const buffEfficiencyFraction = sumBuffsByType(globalBuffs, "/buff_types/efficiency") + teaBuffs.efficiencyFraction; const buffRareFindFraction = sumBuffsByType(globalBuffs, "/buff_types/rare_find") + teaBuffs.rareFindFraction; const processingFraction = sumBuffsByType(globalBuffs, "/buff_types/processing") + teaBuffs.processingFraction; const baseLevel = Math.max(getSkillLevel(action.levelRequirement?.skillHrid), Number(action.levelRequirement?.level || 0)); const levelBonus = sumBuffsByType(globalBuffs, `/buff_types/${skillId}_level`) + teaBuffs.skillLevelBonus - sumBuffsByType(globalBuffs, "/buff_types/action_level") - teaBuffs.actionLevelPenalty; const effectiveLevel = baseLevel + levelBonus; const levelEfficiencyFraction = Math.max(effectiveLevel - Number(action.levelRequirement?.level || 0), 0) / 100; const efficiencyFraction = equipmentEfficiencyFraction + buffEfficiencyFraction + levelEfficiencyFraction; const rareFindFraction = equipmentRareFindFraction + buffRareFindFraction; const buffQuantityFraction = sumBuffsByType(globalBuffs, "/buff_types/gathering") + sumBuffsByType(globalBuffs, "/buff_types/gourmet"); const gatheringQuantityFraction = (SUPPORTED_ACTION_TYPES.has(action.type) && (!action.inputItems || action.inputItems.length === 0)) ? Number(totals.gatheringQuantity || 0) + buffQuantityFraction + teaBuffs.quantityFraction : buffQuantityFraction + teaBuffs.quantityFraction; const baseSeconds = (action.baseTimeCost || 0) / 1000000000; const speedSeconds = Math.max(baseSeconds / (1 + actionSpeedFraction), 3); const successRateFraction = sumBuffsByType(globalBuffs, "/buff_types/success_rate") + teaBuffs.successRateFraction; const alchemySuccessFraction = sumBuffsByType(globalBuffs, "/buff_types/alchemy_success") + teaBuffs.alchemySuccessFraction; return { seconds: speedSeconds, baseSeconds, actionSpeedFraction, equipmentEfficiencyFraction, buffEfficiencyFraction, efficiencyFraction, equipmentRareFindFraction, buffRareFindFraction, rareFindFraction, processingFraction, gatheringQuantityFraction, effectiveLevel, successRateFraction, alchemySuccessFraction, teaBuffs, }; } function getAlchemyDecomposeEfficiencyFraction(sourceItemHrid, actionSummary) { const sourceItem = state.itemDetailMap?.[sourceItemHrid]; if (!sourceItem || !actionSummary) { return 0; } const itemLevel = Math.max(1, Number(sourceItem.itemLevel || 0)); const levelEfficiencyFraction = Math.max(Number(actionSummary.effectiveLevel || 0) - itemLevel, 0) / 100; return Math.max( 0, Number(actionSummary.equipmentEfficiencyFraction || 0) + Number(actionSummary.buffEfficiencyFraction || 0) + levelEfficiencyFraction ); } function getAlchemyDecomposeSuccessChance(sourceItemHrid, actionSummary) { const sourceItem = state.itemDetailMap?.[sourceItemHrid]; if (!sourceItem || !actionSummary) { return 0; } const itemLevel = Math.max(1, Number(sourceItem.itemLevel || 0)); const effectiveLevel = Math.max(0, Number(actionSummary.effectiveLevel || getSkillLevel("/skills/alchemy"))); const levelMultiplier = effectiveLevel >= itemLevel ? 1 : Math.max(0, 1 - 0.5 * (1 - effectiveLevel / itemLevel)); const baseChance = 0.6 * levelMultiplier; const multipliedChance = baseChance * (1 + Number(actionSummary.alchemySuccessFraction || 0)); return clamp01(multipliedChance + Number(actionSummary.successRateFraction || 0)); } function getAlchemyTransmuteSuccessChance(sourceItemHrid, actionSummary) { const sourceItem = state.itemDetailMap?.[sourceItemHrid]; if (!sourceItem || !actionSummary) { return 0; } const itemLevel = Math.max(1, Number(sourceItem.itemLevel || 0)); const effectiveLevel = Math.max(0, Number(actionSummary.effectiveLevel || getSkillLevel("/skills/alchemy"))); const levelMultiplier = effectiveLevel >= itemLevel ? 1 : Math.max(0, 1 - 0.5 * (1 - effectiveLevel / itemLevel)); const baseChance = Number(sourceItem.alchemyDetail?.transmuteSuccessRate || 0) * levelMultiplier; const multipliedChance = baseChance * (1 + Number(actionSummary.alchemySuccessFraction || 0)); return clamp01(multipliedChance + Number(actionSummary.successRateFraction || 0)); } function getAlchemyTransmuteCatalystSuccessBonus(catalystItemHrid, fallback = 0) { if (!catalystItemHrid) { return 0; } const configured = TRANSMUTE_CATALYST_SUCCESS_BONUSES[catalystItemHrid]; if (Number.isFinite(configured)) { return Math.max(0, Number(configured || 0)); } return Math.max(0, Number(fallback || 0)); } function getAlchemyDecomposeEnhancingEssenceOutput(itemLevel, enhancementLevel) { const safeLevel = Math.max(0, Number(itemLevel || 0)); const safeEnhancementLevel = Math.max(0, Number(enhancementLevel || 0)); if (safeEnhancementLevel <= 0) { return 0; } return Math.round(2 * (0.5 + 0.1 * Math.pow(1.05, safeLevel)) * Math.pow(2, safeEnhancementLevel)); } function getEnhancedEquipmentEssenceInfo(itemHrid, enhancementLevel, recommendation, catalystItemHrid = "") { const safeEnhancementLevel = Math.max(0, Number(enhancementLevel || 0)); if (safeEnhancementLevel <= 0) { return null; } const itemDetail = state.itemDetailMap?.[itemHrid]; const decomposeAction = state.actionDetailMap?.["/actions/alchemy/decompose"]; if (!itemDetail || !decomposeAction || !recommendation) { return null; } const actionSummary = getActionSummary(decomposeAction); const catalystMultiplier = catalystItemHrid === "/items/catalyst_of_decomposition" ? 1.15 : catalystItemHrid === "/items/prime_catalyst" ? 1.25 : 1; const successChance = clamp01(getAlchemyDecomposeSuccessChance(itemHrid, actionSummary) * catalystMultiplier); const efficiencyFraction = getAlchemyDecomposeEfficiencyFraction(itemHrid, actionSummary); const efficiencyMultiplier = 1 + efficiencyFraction; const essenceOutputCount = getAlchemyDecomposeEnhancingEssenceOutput(itemDetail.itemLevel, safeEnhancementLevel); const expectedEssenceCount = essenceOutputCount * successChance * efficiencyMultiplier; if (!Number.isFinite(expectedEssenceCount) || expectedEssenceCount <= 0) { return null; } const teaPerAction = actionSummary.seconds / Math.max(actionSummary.teaBuffs.durationSeconds || 300, 1); let teaSecondsTotal = 0; for (const teaItemHrid of actionSummary.teaBuffs.activeTeas) { const teaSeconds = calculateItemSeconds(teaItemHrid, new Set([itemHrid])); if (teaSeconds == null || !Number.isFinite(teaSeconds) || teaSeconds <= 0) { continue; } teaSecondsTotal += teaPerAction * teaSeconds; } let sideOutputSeconds = 0; for (const output of itemDetail.alchemyDetail?.decomposeItems || []) { if (!output?.itemHrid || output.itemHrid === "/items/enhancing_essence") { continue; } const outputDetail = state.itemDetailMap?.[output.itemHrid]; if (outputDetail?.categoryHrid === "/item_categories/equipment") { continue; } const outputSeconds = calculateItemSeconds(output.itemHrid, new Set([itemHrid])); if (outputSeconds == null || !Number.isFinite(outputSeconds) || outputSeconds <= 0) { continue; } sideOutputSeconds += Number(output.count || 0) * outputSeconds * successChance * efficiencyMultiplier; } let catalystSecondsTotal = 0; if (catalystItemHrid) { const catalystSeconds = calculateItemSeconds(catalystItemHrid, new Set([itemHrid])); if (catalystSeconds != null && Number.isFinite(catalystSeconds) && catalystSeconds > 0) { catalystSecondsTotal = catalystSeconds * successChance; } } const sourceSeconds = Math.max(0, Number(recommendation.totalSeconds || 0)) * efficiencyMultiplier; const netSeconds = Math.max(0, actionSummary.seconds + teaSecondsTotal + catalystSecondsTotal + sourceSeconds - sideOutputSeconds); return { secondsPerEssence: netSeconds / expectedEssenceCount, expectedEssenceCount, essenceOutputCount, successChance, efficiencyFraction, teaSecondsTotal, catalystSecondsTotal, sourceSeconds, sideOutputSeconds, netSeconds, actionSeconds: actionSummary.seconds, catalystItemHrid, }; } function getCountExpectationAtScaledValue(scaledCount, processingChance) { const safeScaledCount = Math.max(0, Number(scaledCount || 0)); if (!safeScaledCount) { return { totalExpectedCount: 0, baseItemExpectedCount: 0, processedItemExpectedCount: 0, }; } const lowerCount = Math.floor(safeScaledCount); const upperCount = Math.ceil(safeScaledCount); const upperProbability = safeScaledCount - lowerCount; const lowerProbability = 1 - upperProbability; const processedExpectedCount = processingChance * (lowerProbability * Math.floor(lowerCount / 2) + upperProbability * Math.floor(upperCount / 2)); const baseExpectedCount = (1 - processingChance) * safeScaledCount + processingChance * (lowerProbability * (lowerCount % 2) + upperProbability * (upperCount % 2)); return { totalExpectedCount: safeScaledCount, baseItemExpectedCount: baseExpectedCount, processedItemExpectedCount: processedExpectedCount, }; } function getDropExpectedCounts(drop, quantityMultiplier, processingFraction) { const dropRate = clamp01(Number(drop?.dropRate || 0)); const minCount = Math.max(0, Number(drop?.minCount || 0)); const maxCount = Math.max(minCount, Number(drop?.maxCount || 0)); const processingChance = clamp01(Number(processingFraction || 0)); if (!dropRate || !maxCount || !quantityMultiplier) { return { totalExpectedCount: 0, baseItemExpectedCount: 0, processedItemExpectedCount: 0, }; } const scaledMinCount = minCount * quantityMultiplier; const scaledMaxCount = maxCount * quantityMultiplier; if (scaledMaxCount <= scaledMinCount) { const pointExpectation = getCountExpectationAtScaledValue(scaledMinCount, processingChance); return { totalExpectedCount: pointExpectation.totalExpectedCount * dropRate, baseItemExpectedCount: pointExpectation.baseItemExpectedCount * dropRate, processedItemExpectedCount: pointExpectation.processedItemExpectedCount * dropRate, }; } let totalExpectedCount = 0; let baseItemExpectedCount = 0; let processedItemExpectedCount = 0; const intervalWidth = scaledMaxCount - scaledMinCount; const startSegment = Math.floor(scaledMinCount); const endSegment = Math.ceil(scaledMaxCount); for (let segment = startSegment; segment < endSegment; segment += 1) { const segmentStart = Math.max(scaledMinCount, segment); const segmentEnd = Math.min(scaledMaxCount, segment + 1); const segmentWidth = segmentEnd - segmentStart; if (segmentWidth <= 0) { continue; } // Within a unit interval, expected outputs are linear in x, so the midpoint average is exact. const midpoint = segmentStart + segmentWidth / 2; const segmentExpectation = getCountExpectationAtScaledValue(midpoint, processingChance); const weight = (segmentWidth / intervalWidth) * dropRate; totalExpectedCount += segmentExpectation.totalExpectedCount * weight; baseItemExpectedCount += segmentExpectation.baseItemExpectedCount * weight; processedItemExpectedCount += segmentExpectation.processedItemExpectedCount * weight; } return { totalExpectedCount, baseItemExpectedCount, processedItemExpectedCount, }; } function getDirectDisplayOutputCountPerAction(action, targetItemHrid, summary) { const isProduction = Array.isArray(action.inputItems) && action.inputItems.length > 0; if (isProduction) { const directOutput = (action.outputItems || []).find((output) => output.itemHrid === targetItemHrid); if (!directOutput) { return 0; } return (directOutput.count || 0) * (1 + summary.gatheringQuantityFraction); } let count = 0; for (const drop of action.dropTable || []) { const expectedCounts = getDropExpectedCounts( drop, 1 + summary.gatheringQuantityFraction, summary.processingFraction ); if (drop.itemHrid === targetItemHrid) { count += PROCESSABLE_ITEM_MAP.has(drop.itemHrid) ? expectedCounts.baseItemExpectedCount : expectedCounts.totalExpectedCount; } if (PROCESSABLE_ITEM_MAP.get(drop.itemHrid) === targetItemHrid) { count += expectedCounts.processedItemExpectedCount; } } return count; } function getAdditionalProcessedOutputFromInputs(action, targetItemHrid, summary) { const isProduction = Array.isArray(action?.inputItems) && action.inputItems.length > 0; if (!isProduction || !targetItemHrid) { return 0; } let additionalCount = 0; for (const input of getDisplayInputs(action, summary)) { const sourceAction = findActionForItem(input.itemHrid); if (!sourceAction || sourceAction.hrid === action.hrid) { continue; } const sourceSummary = getActionSummary(sourceAction); const sourceBaseOutput = getDirectDisplayOutputCountPerAction(sourceAction, input.itemHrid, sourceSummary); if (!Number.isFinite(sourceBaseOutput) || sourceBaseOutput <= 0) { continue; } const sourceProcessedOutput = getDirectDisplayOutputCountPerAction(sourceAction, targetItemHrid, sourceSummary); if (!Number.isFinite(sourceProcessedOutput) || sourceProcessedOutput <= 0) { continue; } additionalCount += input.count * (sourceProcessedOutput / sourceBaseOutput); } return additionalCount; } function getDisplayOutputCountPerAction(action, targetItemHrid, summary) { const directCount = getDirectDisplayOutputCountPerAction(action, targetItemHrid, summary); const additionalProcessedCount = getAdditionalProcessedOutputFromInputs(action, targetItemHrid, summary); return directCount + additionalProcessedCount; } function getEffectiveOutputCountPerAction(action, targetItemHrid, summary) { return getDisplayOutputCountPerAction(action, targetItemHrid, summary) * (1 + summary.efficiencyFraction); } function getProcessingProductDetail(action, targetItemHrid, summary) { if (!action || !targetItemHrid || !PROCESSABLE_ITEM_MAP.has(targetItemHrid)) { return null; } const processedItemHrid = PROCESSABLE_ITEM_MAP.get(targetItemHrid); let expectedCount = 0; for (const drop of action.dropTable || []) { if (drop.itemHrid !== targetItemHrid) { continue; } const expectedCounts = getDropExpectedCounts( drop, 1 + summary.gatheringQuantityFraction, summary.processingFraction ); expectedCount += expectedCounts.processedItemExpectedCount; } if (!Number.isFinite(expectedCount) || expectedCount <= 0) { return null; } return { itemHrid: processedItemHrid, itemName: getLocalizedItemName(processedItemHrid), expectedCount, }; } function getAdjustedInputs(action, summary) { const teaBuffs = summary.teaBuffs; const isProduction = Array.isArray(action.inputItems) && action.inputItems.length > 0; const efficiencyMultiplier = isProduction ? 1 + summary.efficiencyFraction : 1; const inputs = []; for (const input of action.inputItems || []) { inputs.push({ itemHrid: input.itemHrid, count: (input.count || 0) * (1 - teaBuffs.lessResourceFraction) * efficiencyMultiplier, }); } if (action.upgradeItemHrid) { inputs.push({ itemHrid: action.upgradeItemHrid, count: efficiencyMultiplier, }); } return inputs; } function getDisplayInputs(action, summary) { const teaBuffs = summary.teaBuffs; const inputs = []; for (const input of action.inputItems || []) { inputs.push({ itemHrid: input.itemHrid, count: (input.count || 0) * (1 - teaBuffs.lessResourceFraction), }); } if (action.upgradeItemHrid) { inputs.push({ itemHrid: action.upgradeItemHrid, count: 1, }); } return inputs; } function itemDependsOnCurrentRecipe(itemHrid, targetItemHrid) { if (!itemHrid || !targetItemHrid) { return false; } const pending = [itemHrid]; const visited = new Set(); while (pending.length > 0) { const currentItemHrid = pending.pop(); if (!currentItemHrid) { continue; } if (currentItemHrid === targetItemHrid) { return true; } if (visited.has(currentItemHrid)) { continue; } visited.add(currentItemHrid); if (visited.size > 256) { return false; } const fixedDecomposeSourceItemHrid = FIXED_DECOMPOSE_SOURCE_RULES[currentItemHrid]; if (fixedDecomposeSourceItemHrid) { if (!visited.has(fixedDecomposeSourceItemHrid)) { pending.push(fixedDecomposeSourceItemHrid); } const decomposeActionTypeHrid = state.actionDetailMap?.["/actions/alchemy/decompose"]?.type || "/action_types/alchemy"; const decomposeTeaBuffs = getTeaBuffs(decomposeActionTypeHrid); for (const teaItemHrid of decomposeTeaBuffs.activeTeas || []) { if (teaItemHrid && teaItemHrid !== currentItemHrid && !visited.has(teaItemHrid)) { pending.push(teaItemHrid); } } } const essenceRule = ESSENCE_DECOMPOSE_RULES[currentItemHrid]; if (essenceRule) { for (const [sourceItemHrid, itemDetail] of Object.entries(state.itemDetailMap || {})) { const match = (itemDetail?.alchemyDetail?.decomposeItems || []).find((entry) => entry.itemHrid === currentItemHrid); if (!match || !isAllowedEssenceDecomposeSource(currentItemHrid, sourceItemHrid)) { continue; } if (!visited.has(sourceItemHrid)) { pending.push(sourceItemHrid); } } const decomposeActionTypeHrid = state.actionDetailMap?.["/actions/alchemy/decompose"]?.type || "/action_types/alchemy"; const decomposeTeaBuffs = getTeaBuffs(decomposeActionTypeHrid); for (const teaItemHrid of decomposeTeaBuffs.activeTeas || []) { if (teaItemHrid && teaItemHrid !== currentItemHrid && !visited.has(teaItemHrid)) { pending.push(teaItemHrid); } } } const fixedEnhancedEssenceRule = FIXED_ENHANCING_ESSENCE_RULES[currentItemHrid]; if (fixedEnhancedEssenceRule?.sourceItemHrid && !visited.has(fixedEnhancedEssenceRule.sourceItemHrid)) { pending.push(fixedEnhancedEssenceRule.sourceItemHrid); } const fixedTransmuteRule = FIXED_TRANSMUTE_SOURCE_RULES[currentItemHrid]; if (fixedTransmuteRule?.sourceItemHrid) { if (!visited.has(fixedTransmuteRule.sourceItemHrid)) { pending.push(fixedTransmuteRule.sourceItemHrid); } const transmuteActionTypeHrid = state.actionDetailMap?.[fixedTransmuteRule.actionHrid || "/actions/alchemy/transmute"]?.type || "/action_types/alchemy"; const transmuteTeaBuffs = getTeaBuffs(transmuteActionTypeHrid); for (const teaItemHrid of transmuteTeaBuffs.activeTeas || []) { if (teaItemHrid && teaItemHrid !== currentItemHrid && !visited.has(teaItemHrid)) { pending.push(teaItemHrid); } } } const action = findActionForItem(currentItemHrid); if (!action) { continue; } for (const input of action.inputItems || []) { if (input?.itemHrid && !visited.has(input.itemHrid)) { pending.push(input.itemHrid); } } if (action.upgradeItemHrid && !visited.has(action.upgradeItemHrid)) { pending.push(action.upgradeItemHrid); } const teaBuffs = getTeaBuffs(action.type); for (const teaItemHrid of teaBuffs.activeTeas || []) { if (teaItemHrid && teaItemHrid !== currentItemHrid && !visited.has(teaItemHrid)) { pending.push(teaItemHrid); } } } return false; } function getItemTargetRelationCacheKey(itemHrid, targetItemHrid) { return `${itemHrid}=>${targetItemHrid}`; } function getFixedSourceDecomposeRelationToTarget(itemHrid, targetItemHrid, sourceItemHrid, stack = new Set()) { if (!itemHrid || !targetItemHrid || !sourceItemHrid) { return null; } const decomposeAction = state.actionDetailMap?.["/actions/alchemy/decompose"]; const sourceItemDetail = state.itemDetailMap?.[sourceItemHrid]; const match = (sourceItemDetail?.alchemyDetail?.decomposeItems || []).find((entry) => entry.itemHrid === itemHrid); if (!decomposeAction || !sourceItemDetail || !match) { return null; } const actionSummary = getActionSummary(decomposeAction); const bulkMultiplier = Math.max(1, Number(sourceItemDetail?.alchemyDetail?.bulkMultiplier || 1)); const outputCount = Number(match.count || 0) * bulkMultiplier; if (!Number.isFinite(outputCount) || outputCount <= 0) { return null; } const successChance = getAlchemyDecomposeSuccessChance(sourceItemHrid, actionSummary); const efficiencyFraction = getAlchemyDecomposeEfficiencyFraction(sourceItemHrid, actionSummary); const efficiencyMultiplier = 1 + efficiencyFraction; const expectedOutputCount = outputCount * successChance * efficiencyMultiplier; if (!Number.isFinite(expectedOutputCount) || expectedOutputCount <= 0) { return null; } const sourceStack = new Set(stack); if (itemDependsOnCurrentRecipe(sourceItemHrid, itemHrid)) { sourceStack.add(itemHrid); } const sourceRelation = getItemSecondsLinearRelationToTarget(sourceItemHrid, targetItemHrid, sourceStack); if (!sourceRelation || !Number.isFinite(sourceRelation.baseSeconds) || sourceRelation.baseSeconds < 0 || !Number.isFinite(sourceRelation.targetSecondsCoefficient) || sourceRelation.targetSecondsCoefficient < 0) { return null; } const teaPerAction = Number(actionSummary.seconds || 0) / Math.max(actionSummary.teaBuffs.durationSeconds || 300, 1); let teaSecondsTotal = 0; for (const teaItemHrid of actionSummary.teaBuffs.activeTeas || []) { if (itemDependsOnCurrentRecipe(teaItemHrid, itemHrid) || itemDependsOnCurrentRecipe(teaItemHrid, targetItemHrid)) { continue; } const teaStack = new Set(stack); teaStack.add(itemHrid); const teaSeconds = calculateItemSeconds(teaItemHrid, teaStack); if (teaSeconds == null || !Number.isFinite(teaSeconds) || teaSeconds < 0) { if (isRecursiveDependencyFailureReason(getDependencyFailureReason(teaItemHrid))) { continue; } return null; } teaSecondsTotal += teaPerAction * teaSeconds; } return { baseSeconds: ( Number(actionSummary.seconds || 0) + teaSecondsTotal + Number(sourceRelation.baseSeconds || 0) * bulkMultiplier * efficiencyMultiplier ) / expectedOutputCount, targetSecondsCoefficient: ( Number(sourceRelation.targetSecondsCoefficient || 0) * bulkMultiplier * efficiencyMultiplier ) / expectedOutputCount, }; } function getItemSecondsLinearRelationToTarget(itemHrid, targetItemHrid, stack = new Set()) { if (!itemHrid || !targetItemHrid) { return null; } if (itemHrid === targetItemHrid) { return { baseSeconds: 0, targetSecondsCoefficient: 1, }; } const cacheKey = getItemTargetRelationCacheKey(itemHrid, targetItemHrid); if (state.itemTargetRelationCache.has(cacheKey)) { return state.itemTargetRelationCache.get(cacheKey); } if (!itemDependsOnCurrentRecipe(itemHrid, targetItemHrid)) { const directSeconds = calculateItemSeconds(itemHrid, stack); if (directSeconds == null || !Number.isFinite(directSeconds) || directSeconds < 0) { state.itemTargetRelationCache.set(cacheKey, null); return null; } const directRelation = { baseSeconds: directSeconds, targetSecondsCoefficient: 0, }; state.itemTargetRelationCache.set(cacheKey, directRelation); return directRelation; } if (stack.has(itemHrid)) { state.itemTargetRelationCache.set(cacheKey, null); return null; } if (getGeneralShopPurchaseInfo(itemHrid)) { const shopRelation = { baseSeconds: 0, targetSecondsCoefficient: 0, }; state.itemTargetRelationCache.set(cacheKey, shopRelation); return shopRelation; } const dungeonMaterialPlan = getDungeonMaterialPlan(itemHrid); if (dungeonMaterialPlan) { const dungeonRelation = { baseSeconds: dungeonMaterialPlan.secondsPerItem, targetSecondsCoefficient: 0, }; state.itemTargetRelationCache.set(cacheKey, dungeonRelation); return dungeonRelation; } if (FIXED_DECOMPOSE_SOURCE_RULES[itemHrid]) { const fixedDecomposeRelation = getFixedSourceDecomposeRelationToTarget( itemHrid, targetItemHrid, FIXED_DECOMPOSE_SOURCE_RULES[itemHrid], stack ); state.itemTargetRelationCache.set(cacheKey, fixedDecomposeRelation); return fixedDecomposeRelation; } const essenceRule = ESSENCE_DECOMPOSE_RULES[itemHrid]; if (essenceRule?.type === "fixed_source") { const fixedEssenceRelation = getFixedSourceDecomposeRelationToTarget( itemHrid, targetItemHrid, getConfiguredEssenceDecomposeSourceItemHrid(itemHrid), stack ); state.itemTargetRelationCache.set(cacheKey, fixedEssenceRelation); return fixedEssenceRelation; } if (isTimeCalculatorSupportedItem(itemHrid) || FIXED_TRANSMUTE_SOURCE_RULES[itemHrid] || FIXED_ENHANCING_ESSENCE_RULES[itemHrid] || FIXED_ATTACHED_RARE_TOOLTIP_SOURCE_RULES[itemHrid] || ESSENCE_DECOMPOSE_RULES[itemHrid]) { state.itemTargetRelationCache.set(cacheKey, null); return null; } const action = findActionForItem(itemHrid); if (!action) { const emptyRelation = { baseSeconds: 0, targetSecondsCoefficient: 0, }; state.itemTargetRelationCache.set(cacheKey, emptyRelation); return emptyRelation; } const summary = getActionSummary(action); const outputCount = getEffectiveOutputCountPerAction(action, itemHrid, summary); if (!outputCount || !Number.isFinite(outputCount)) { state.itemTargetRelationCache.set(cacheKey, null); return null; } stack.add(itemHrid); try { let baseSecondsPerAction = Number(summary.seconds || 0); let targetSecondsCoefficientPerAction = 0; for (const input of getAdjustedInputs(action, summary)) { let inputRelation = null; if (input.itemHrid === targetItemHrid) { inputRelation = { baseSeconds: 0, targetSecondsCoefficient: 1, }; } else if (itemDependsOnCurrentRecipe(input.itemHrid, targetItemHrid)) { inputRelation = getItemSecondsLinearRelationToTarget(input.itemHrid, targetItemHrid, stack); } else { const inputSeconds = calculateItemSeconds(input.itemHrid, stack); if (inputSeconds != null && Number.isFinite(inputSeconds) && inputSeconds >= 0) { inputRelation = { baseSeconds: inputSeconds, targetSecondsCoefficient: 0, }; } } if (!inputRelation) { state.itemTargetRelationCache.set(cacheKey, null); return null; } baseSecondsPerAction += Number(input.count || 0) * Number(inputRelation.baseSeconds || 0); targetSecondsCoefficientPerAction += Number(input.count || 0) * Number(inputRelation.targetSecondsCoefficient || 0); } const teaPerAction = Number(summary.seconds || 0) / Math.max(summary.teaBuffs.durationSeconds || 300, 1); let selfTeaCoefficient = 0; for (const teaItemHrid of summary.teaBuffs.activeTeas || []) { if (teaItemHrid === itemHrid) { selfTeaCoefficient += teaPerAction; continue; } if (itemDependsOnCurrentRecipe(teaItemHrid, itemHrid) || itemDependsOnCurrentRecipe(teaItemHrid, targetItemHrid)) { continue; } const teaSeconds = calculateItemSeconds(teaItemHrid, stack); if (teaSeconds == null || !Number.isFinite(teaSeconds) || teaSeconds < 0) { if (isRecursiveDependencyFailureReason(getDependencyFailureReason(teaItemHrid))) { continue; } state.itemTargetRelationCache.set(cacheKey, null); return null; } baseSecondsPerAction += teaPerAction * teaSeconds; } const denominator = outputCount - selfTeaCoefficient; if (!Number.isFinite(denominator) || denominator <= 0) { state.itemTargetRelationCache.set(cacheKey, null); return null; } const relation = { baseSeconds: baseSecondsPerAction / denominator, targetSecondsCoefficient: targetSecondsCoefficientPerAction / denominator, }; state.itemTargetRelationCache.set(cacheKey, relation); return relation; } finally { stack.delete(itemHrid); } } function calculateItemSeconds(itemHrid, stack = new Set()) { if (!itemHrid) { return null; } if (state.itemTimeCache.has(itemHrid)) { return state.itemTimeCache.get(itemHrid); } if (stack.size === 0 && isMissingDerivedRuntimeState()) { ensureRuntimeStateFresh(); } if (stack.size > 64) { state.itemFailureReasonCache.set(itemHrid, isZh ? "递归层级过深,已截断" : "Truncated: recursion depth exceeded"); return null; } if (stack.has(itemHrid)) { if (state.itemTimeCache.has(itemHrid)) { return state.itemTimeCache.get(itemHrid); } state.itemFailureReasonCache.set(itemHrid, isZh ? "递归依赖环,已截断" : "Truncated: recursive dependency cycle"); return null; } if (state.activeItemSolveSet.has(itemHrid)) { state.itemFailureReasonCache.set(itemHrid, isZh ? "递归依赖环,已截断" : "Truncated: recursive dependency cycle"); return null; } state.activeItemSolveSet.add(itemHrid); state.cyclicSolveDepth += 1; try { const timeCalculatorEntry = getConfiguredTimeCalculatorEntry(itemHrid); if (timeCalculatorEntry) { const summary = getTimeCalculatorEntrySummary(timeCalculatorEntry); if (summary.failureReason) { state.itemFailureReasonCache.set(itemHrid, summary.failureReason); return null; } const result = Number.isFinite(summary.secondsPerChest) && summary.secondsPerChest > 0 ? summary.secondsPerChest : 0; state.itemFailureReasonCache.delete(itemHrid); state.itemTimeCache.set(itemHrid, result); return result; } if (isTimeCalculatorSupportedItem(itemHrid)) { const reason = getMissingConfiguredTimeReason(itemHrid); state.itemFailureReasonCache.set(itemHrid, reason); return null; } if (getGeneralShopPurchaseInfo(itemHrid)) { state.itemFailureReasonCache.delete(itemHrid); state.itemTimeCache.set(itemHrid, 0); return 0; } const dungeonMaterialPlan = getDungeonMaterialPlan(itemHrid); if (dungeonMaterialPlan) { state.itemFailureReasonCache.delete(itemHrid); state.itemTimeCache.set(itemHrid, dungeonMaterialPlan.secondsPerItem); return dungeonMaterialPlan.secondsPerItem; } if (DUNGEON_MATERIAL_ITEM_HRIDS.has(itemHrid) && state.itemFailureReasonCache.has(itemHrid)) { return null; } const fixedDecomposePlan = getFixedDecomposePlan(itemHrid, stack); if (fixedDecomposePlan) { state.itemTimeCache.set(itemHrid, fixedDecomposePlan.totalSeconds); return fixedDecomposePlan.totalSeconds; } if (FIXED_DECOMPOSE_SOURCE_RULES[itemHrid] && state.itemFailureReasonCache.has(itemHrid)) { return null; } const fixedTransmutePlan = getFixedTransmutePlan(itemHrid, stack); if (fixedTransmutePlan) { state.itemTimeCache.set(itemHrid, fixedTransmutePlan.totalSeconds); return fixedTransmutePlan.totalSeconds; } if (FIXED_TRANSMUTE_SOURCE_RULES[itemHrid] && state.itemFailureReasonCache.has(itemHrid)) { return null; } const fixedEnhancedEssencePlan = getFixedEnhancedEssencePlan(itemHrid); if (fixedEnhancedEssencePlan) { state.itemTimeCache.set(itemHrid, fixedEnhancedEssencePlan.totalSeconds); return fixedEnhancedEssencePlan.totalSeconds; } if (FIXED_ENHANCING_ESSENCE_RULES[itemHrid] && state.itemFailureReasonCache.has(itemHrid)) { return null; } const fixedAttachedRareTooltipPlan = getFixedAttachedRareTooltipPlan(itemHrid, stack); if (fixedAttachedRareTooltipPlan) { state.itemTimeCache.set(itemHrid, fixedAttachedRareTooltipPlan.totalSeconds); return fixedAttachedRareTooltipPlan.totalSeconds; } if (FIXED_ATTACHED_RARE_TOOLTIP_SOURCE_RULES[itemHrid] && state.itemFailureReasonCache.has(itemHrid)) { return null; } const essencePlan = getEssenceDecomposePlan(itemHrid, stack); if (essencePlan) { state.itemTimeCache.set(itemHrid, essencePlan.totalSeconds); return essencePlan.totalSeconds; } if (ESSENCE_DECOMPOSE_RULES[itemHrid] && state.itemFailureReasonCache.has(itemHrid)) { return null; } const action = findActionForItem(itemHrid); if (!action) { if (DUNGEON_RELATED_ITEM_HRIDS.has(itemHrid) && state.itemFailureReasonCache.has(itemHrid)) { return null; } if (DUNGEON_RELATED_ITEM_HRIDS.has(itemHrid)) { state.itemFailureReasonCache.set( itemHrid, isZh ? "地牢成品暂不参与时间计算" : "Dungeon equipment is not timed yet" ); return null; } state.itemFailureReasonCache.delete(itemHrid); state.itemTimeCache.set(itemHrid, 0); return 0; } const summary = getActionSummary(action); stack.add(itemHrid); const actionSeconds = summary.seconds; const outputCount = getEffectiveOutputCountPerAction(action, itemHrid, summary); if (!outputCount || !Number.isFinite(outputCount)) { stack.delete(itemHrid); state.itemFailureReasonCache.set(itemHrid, isZh ? "产出无效,已截断" : "Truncated: invalid output"); return null; } let totalSecondsPerAction = actionSeconds; for (const input of getAdjustedInputs(action, summary)) { const inputSeconds = calculateItemSeconds(input.itemHrid, stack); if (inputSeconds == null) { stack.delete(itemHrid); state.itemFailureReasonCache.set(itemHrid, getDependencyFailureReason(input.itemHrid)); return null; } totalSecondsPerAction += input.count * inputSeconds; } const teaPerAction = actionSeconds / Math.max(summary.teaBuffs.durationSeconds || 300, 1); let selfTeaCoefficient = 0; for (const teaItemHrid of summary.teaBuffs.activeTeas) { if (teaItemHrid === itemHrid) { selfTeaCoefficient += teaPerAction; continue; } if (itemDependsOnCurrentRecipe(teaItemHrid, itemHrid)) { continue; } const teaSeconds = calculateItemSeconds(teaItemHrid, stack); if (teaSeconds == null) { if (isRecursiveDependencyFailureReason(getDependencyFailureReason(teaItemHrid))) { continue; } stack.delete(itemHrid); state.itemFailureReasonCache.set(itemHrid, getDependencyFailureReason(teaItemHrid)); return null; } totalSecondsPerAction += teaPerAction * teaSeconds; } const denominator = outputCount - selfTeaCoefficient; const result = denominator > 0 ? totalSecondsPerAction / denominator : null; stack.delete(itemHrid); if (result != null) { state.itemFailureReasonCache.delete(itemHrid); state.itemTimeCache.set(itemHrid, result); } else { state.itemFailureReasonCache.set(itemHrid, isZh ? "分母无效,已截断" : "Truncated: invalid denominator"); } return result; } finally { state.activeItemSolveSet.delete(itemHrid); state.cyclicSolveDepth = Math.max(0, state.cyclicSolveDepth - 1); if (state.cyclicSolveDepth === 0) { state.activeItemSolveSet.clear(); } } } function formatSignedPercent(value, digits = 1) { const prefix = value > 0 ? "+" : ""; return `${prefix}${value.toFixed(digits)}%`; } function formatPercent(value, digits = 1) { return `${Number(value).toFixed(digits)}%`; } function formatNumber(value) { return Number(value).toFixed(2); } function formatPreciseNumber(value) { const numeric = Number(value || 0); if (!Number.isFinite(numeric)) { return "0"; } const abs = Math.abs(numeric); let text = "0"; if (abs === 0) { text = "0"; } else if (abs >= 100) { text = numeric.toFixed(2); } else if (abs >= 1) { text = numeric.toFixed(3); } else if (abs >= 0.01) { text = numeric.toFixed(4); } else if (abs >= 0.0001) { text = numeric.toFixed(6); } else { text = numeric.toExponential(3); } return text .replace(/(\.\d*?[1-9])0+$/u, "$1") .replace(/\.0+$/u, ""); } function formatAttachedRareNumber(value) { const numeric = Number(value || 0); if (!Number.isFinite(numeric) || numeric === 0) { return "0"; } if (Math.abs(numeric) < 0.001) { return numeric .toFixed(9) .replace(/(\.\d*?[1-9])0+$/u, "$1") .replace(/\.0+$/u, ""); } return formatPreciseNumber(numeric); } function parseNonNegativeDecimal(value) { const raw = String(value ?? "").trim(); let normalized = raw; if (raw.includes(",") && raw.includes(".")) { normalized = raw.replace(/,/g, ""); } else if (raw.includes(",")) { normalized = raw.replace(/,/g, "."); } const parsed = Number(normalized || 0); return Number.isFinite(parsed) ? Math.max(0, parsed) : 0; } function getExpectedDropCount(drop) { if (!drop) { return 0; } const minCount = Number(drop.minCount || 0); const maxCount = Number(drop.maxCount || 0); const dropRate = Number(drop.dropRate || 0); return Math.max(0, ((minCount + maxCount) / 2) * dropRate); } function getAttachedRareYieldCacheKey(itemHrid, targetRareHrid) { return `${itemHrid}::${targetRareHrid}`; } function getAttachedRareLabel(targetRareHrid) { return isZh ? (ATTACHED_RARE_LABEL_ZH[targetRareHrid] || getLocalizedItemName(targetRareHrid, targetRareHrid)) : (ATTACHED_RARE_LABEL_EN[targetRareHrid] || getLocalizedItemName(targetRareHrid, targetRareHrid)); } function getDirectRareOutputCountPerAction(action, targetRareHrid, summary) { if (!action || !targetRareHrid || !ATTACHED_RARE_TARGET_ITEM_HRID_SET.has(targetRareHrid)) { return 0; } const efficiencyMultiplier = 1 + Math.max(0, Number(summary?.efficiencyFraction || 0)); const rareFindMultiplier = Math.max(0, 1 + Number(summary?.rareFindFraction || 0)); let total = 0; for (const drop of action.rareDropTable || []) { if (drop?.itemHrid !== targetRareHrid) { continue; } total += getExpectedDropCount(drop); } return total * efficiencyMultiplier * rareFindMultiplier; } function getAttachedRareYieldPerItem(itemHrid, targetRareHrid, stack = new Set()) { if (!itemHrid || !targetRareHrid || !ATTACHED_RARE_TARGET_ITEM_HRID_SET.has(targetRareHrid)) { return 0; } const cacheKey = getAttachedRareYieldCacheKey(itemHrid, targetRareHrid); if (state.attachedRareYieldCache.has(cacheKey)) { return state.attachedRareYieldCache.get(cacheKey); } if (stack.has(cacheKey)) { return 0; } const action = findActionForItem(itemHrid); if (!action) { state.attachedRareYieldCache.set(cacheKey, 0); return 0; } const summary = getActionSummary(action); const outputCount = getEffectiveOutputCountPerAction(action, itemHrid, summary); if (!Number.isFinite(outputCount) || outputCount <= 0) { state.attachedRareYieldCache.set(cacheKey, 0); return 0; } stack.add(cacheKey); let propagatedInputRarePerAction = 0; for (const input of getAdjustedInputs(action, summary)) { const attachedRare = getAttachedRareYieldPerItem(input.itemHrid, targetRareHrid, stack); if (!Number.isFinite(attachedRare) || attachedRare <= 0) { continue; } propagatedInputRarePerAction += Number(input.count || 0) * attachedRare; } stack.delete(cacheKey); const directRarePerAction = getDirectRareOutputCountPerAction(action, targetRareHrid, summary); const result = (directRarePerAction + propagatedInputRarePerAction) / outputCount; const safeResult = Number.isFinite(result) && result > 0 ? result : 0; state.attachedRareYieldCache.set(cacheKey, safeResult); return safeResult; } function getAttachedRareTooltipLines(itemHrid) { const lines = []; for (const targetRareHrid of ATTACHED_RARE_TARGET_ITEM_HRIDS) { const amount = getAttachedRareYieldPerItem(itemHrid, targetRareHrid); if (!Number.isFinite(amount) || amount <= 0) { continue; } const countPerRare = 1 / amount; if (!Number.isFinite(countPerRare) || countPerRare <= 0) { continue; } lines.push( isZh ? `生产${formatAttachedRareNumber(countPerRare)}个此物品附带1个${getAttachedRareLabel(targetRareHrid)}` : `Produce ${formatAttachedRareNumber(countPerRare)} of this item for 1 extra ${getAttachedRareLabel(targetRareHrid)}` ); } return lines; } function getConsumableAttachedRareTimeSavings(itemHrid) { if (!itemHrid) { return { totalSeconds: 0, breakdown: [], }; } let totalSeconds = 0; const breakdown = []; for (const targetRareHrid of CONSUMABLE_VALUE_ATTACHED_RARE_ITEM_HRIDS) { const attachedCount = Number(getAttachedRareYieldPerItem(itemHrid, targetRareHrid) || 0); if (!Number.isFinite(attachedCount) || attachedCount <= 0) { continue; } const targetSeconds = calculateItemSeconds(targetRareHrid, new Set([itemHrid])); if (!Number.isFinite(targetSeconds) || targetSeconds == null || targetSeconds <= 0) { continue; } const savedSeconds = attachedCount * targetSeconds; if (!Number.isFinite(savedSeconds) || savedSeconds <= 0) { continue; } totalSeconds += savedSeconds; breakdown.push({ itemHrid: targetRareHrid, attachedCount, targetSeconds, savedSeconds, }); } return { totalSeconds, breakdown, }; } function getMissingConfiguredTimeReason(itemHrid) { const itemName = getLocalizedItemName(itemHrid, state.itemDetailMap?.[itemHrid]?.name || itemHrid); return isZh ? `缺少${itemName}数据,已截断` : `Truncated: missing ${itemName} data`; } function isRecursiveDependencyFailureReason(reason) { return typeof reason === "string" && ( reason.includes("递归依赖环") || reason.includes("recursive dependency cycle") ); } function getGeneralShopPurchaseInfo(itemHrid) { if (!itemHrid || itemHrid.includes("_charm")) { return null; } for (const detail of Object.values(state.shopItemDetailMap || {})) { if (!detail || detail.itemHrid !== itemHrid) { continue; } const costs = Array.isArray(detail.costs) ? detail.costs : []; if ( detail.category === "/shop_categories/general" && costs.length === 1 && costs[0]?.itemHrid === "/items/coin" ) { return detail; } } return null; } function getDependencyFailureReason(itemHrid) { return state.itemFailureReasonCache.get(itemHrid) || getMissingConfiguredTimeReason(itemHrid); } function parseUiNumber(value) { const raw = String(value ?? "").trim().replace(/\s+/g, ""); if (!raw) { return 0; } let normalized = raw; if (normalized.includes(",") && normalized.includes(".")) { normalized = normalized.replace(/,/g, ""); } else if (normalized.includes(",")) { normalized = normalized.replace(/,/g, "."); } const match = normalized.match(/-?\d+(?:\.\d+)?/); const parsed = Number(match ? match[0] : normalized); return Number.isFinite(parsed) ? parsed : 0; } function parseUiPercent(value) { return clamp01(parseUiNumber(value) / 100); } function parseUiDurationSeconds(value) { const raw = String(value ?? "").trim().toLowerCase(); if (!raw) { return 0; } let total = 0; const units = [ { pattern: /(-?\d+(?:[.,]\d+)?)d/g, scale: 86400 }, { pattern: /(-?\d+(?:[.,]\d+)?)h/g, scale: 3600 }, { pattern: /(-?\d+(?:[.,]\d+)?)min/g, scale: 60 }, { pattern: /(-?\d+(?:[.,]\d+)?)m(?![a-z])/g, scale: 60 }, { pattern: /(-?\d+(?:[.,]\d+)?)s/g, scale: 1 }, ]; for (const unit of units) { let match; while ((match = unit.pattern.exec(raw))) { total += parseUiNumber(match[1]) * unit.scale; } } if (total > 0) { return total; } return Math.max(0, parseUiNumber(raw)); } function isResolvableItemSeconds(itemHrid, seconds) { if (!itemHrid) { return false; } if (itemHrid === "/items/coin") { return true; } if (seconds == null || !Number.isFinite(seconds) || seconds < 0) { return false; } if (seconds > 0) { return true; } return Boolean( getGeneralShopPurchaseInfo(itemHrid) || getConfiguredTimeCalculatorEntry(itemHrid) || getDungeonMaterialPlan(itemHrid) || FIXED_DECOMPOSE_SOURCE_RULES[itemHrid] || FIXED_TRANSMUTE_SOURCE_RULES[itemHrid] || FIXED_ENHANCING_ESSENCE_RULES[itemHrid] || ESSENCE_DECOMPOSE_RULES[itemHrid] || findActionForItem(itemHrid) ); } function isAlchemyInferenceResolvableItemSeconds(itemHrid, seconds) { if (ATTACHED_RARE_TARGET_ITEM_HRID_SET.has(itemHrid)) { return false; } return isResolvableItemSeconds(itemHrid, seconds); } function getDungeonMaterialPlan(itemHrid) { if (!DUNGEON_MATERIAL_ITEM_HRIDS.has(itemHrid)) { return null; } const refinementBaseChestItemHrid = REFINEMENT_SHARD_TO_BASE_CHEST_HRID[itemHrid] || ""; if (refinementBaseChestItemHrid) { const config = DUNGEON_CHEST_CONFIG[refinementBaseChestItemHrid]; const entry = config?.refinementChestItemHrid ? getConfiguredTimeCalculatorEntry(config.refinementChestItemHrid) : null; if (!entry) { /* state.itemFailureReasonCache.set(itemHrid, isZh ? "鏈厤缃簿鐐煎疂绠辨椂闂达紝宸叉埅鏂? : "Truncated: missing refinement chest time"); */ state.itemFailureReasonCache.set(itemHrid, "Truncated: missing refinement chest time"); return null; } const summary = getTimeCalculatorEntrySummary(entry); if (summary.failureReason) { state.itemFailureReasonCache.set(itemHrid, summary.failureReason); return null; } if (!Number.isFinite(summary.secondsPerChest) || summary.secondsPerChest <= 0) { /* state.itemFailureReasonCache.set(itemHrid, isZh ? "绮剧偧瀹濈鏃堕棿鏃犳晥锛屽凡鎴柇" : "Truncated: invalid refinement chest time"); */ state.itemFailureReasonCache.set(itemHrid, "Truncated: invalid refinement chest time"); return null; } const keyItemHrid = getRefinementChestOpenKeyHrid(config.refinementChestItemHrid); const keySeconds = keyItemHrid ? calculateItemSeconds(keyItemHrid) : 0; if (keyItemHrid && (!Number.isFinite(keySeconds) || keySeconds <= 0)) { state.itemFailureReasonCache.set(itemHrid, getDependencyFailureReason(keyItemHrid)); return null; } const directExpected = Math.max(0.0001, Number(config.refinementShardCountPerChest || 1)); const totalSecondsPerOpen = summary.secondsPerChest + (Number.isFinite(keySeconds) ? keySeconds : 0); const secondsPerItem = totalSecondsPerOpen / directExpected; if (!Number.isFinite(secondsPerItem) || secondsPerItem <= 0) { /* state.itemFailureReasonCache.set(itemHrid, isZh ? "绮剧偧纰庣墖鍒嗘瘝鏃犳晥锛屽凡鎴柇" : "Truncated: invalid refinement denominator"); */ state.itemFailureReasonCache.set(itemHrid, "Truncated: invalid refinement denominator"); return null; } const chestName = getLocalizedItemName( config.refinementChestItemHrid, state.itemDetailMap?.[config.refinementChestItemHrid]?.name || config.refinementChestItemHrid ); const keyName = keyItemHrid ? getLocalizedItemName(keyItemHrid, state.itemDetailMap?.[keyItemHrid]?.name || keyItemHrid) : ""; return { itemHrid, chestItemHrid: config.refinementChestItemHrid, chestName, keyItemHrid, keyName, tokenItemHrid: "", chestSeconds: summary.secondsPerChest, keySeconds: Number.isFinite(keySeconds) ? keySeconds : 0, directExpected, tokenExpected: 0, tokenCost: 0, shopExpected: 0, totalExpected: directExpected, totalSecondsPerOpen, secondsPerItem, }; } let bestPlan = null; let failureReason = ""; for (const [chestItemHrid, config] of Object.entries(DUNGEON_CHEST_CONFIG)) { const entry = getConfiguredTimeCalculatorEntry(chestItemHrid); if (!entry) { failureReason = isZh ? "未配置对应宝箱时间,已截断" : "Truncated: missing chest time"; continue; } const summary = getTimeCalculatorEntrySummary(entry); if (summary.failureReason) { failureReason = summary.failureReason; continue; } if (!Number.isFinite(summary.secondsPerChest) || summary.secondsPerChest <= 0) { failureReason = isZh ? "宝箱时间无效,已截断" : "Truncated: invalid chest time"; continue; } const keySeconds = calculateItemSeconds(config.keyItemHrid); if (!Number.isFinite(keySeconds) || keySeconds <= 0) { failureReason = getDependencyFailureReason(config.keyItemHrid); continue; } const directExpected = (config.drops || []) .filter((drop) => drop.itemHrid === itemHrid) .reduce((total, drop) => total + getExpectedDropCount(drop), 0); const tokenExpected = (config.drops || []) .filter((drop) => drop.itemHrid === config.tokenItemHrid) .reduce((total, drop) => total + getExpectedDropCount(drop), 0); const tokenCost = Number(DUNGEON_TOKEN_SHOP_COSTS?.[config.tokenItemHrid]?.[itemHrid] || 0); const shopExpected = tokenCost > 0 ? tokenExpected / tokenCost : 0; const totalExpected = directExpected + shopExpected; if (!Number.isFinite(totalExpected) || totalExpected <= 0) { continue; } const totalSecondsPerOpen = summary.secondsPerChest + keySeconds; const secondsPerItem = totalSecondsPerOpen / totalExpected; if (!Number.isFinite(secondsPerItem) || secondsPerItem <= 0) { failureReason = isZh ? "地牢材料分母无效,已截断" : "Truncated: invalid dungeon denominator"; continue; } const chestName = getLocalizedItemName(chestItemHrid, state.itemDetailMap?.[chestItemHrid]?.name || chestItemHrid); const keyName = getLocalizedItemName(config.keyItemHrid, state.itemDetailMap?.[config.keyItemHrid]?.name || config.keyItemHrid); const plan = { itemHrid, chestItemHrid, chestName, keyItemHrid: config.keyItemHrid, keyName, tokenItemHrid: config.tokenItemHrid, chestSeconds: summary.secondsPerChest, keySeconds, directExpected, tokenExpected, tokenCost, shopExpected, totalExpected, totalSecondsPerOpen, secondsPerItem, }; if (!bestPlan || plan.secondsPerItem < bestPlan.secondsPerItem) { bestPlan = plan; } } if (!bestPlan && failureReason) { state.itemFailureReasonCache.set(itemHrid, failureReason); } return bestPlan; } function formatAutoDuration(seconds) { const safeSeconds = Math.max(0, Number(seconds || 0)); if (safeSeconds >= 24 * 3600) { return `${formatNumber(safeSeconds / 86400)} d`; } if (safeSeconds >= 600 * 60) { return `${formatNumber(safeSeconds / 3600)} h`; } if (safeSeconds >= 600) { return `${formatNumber(safeSeconds / 60)} min`; } return `${formatNumber(safeSeconds)} s`; } function getPerActionCostBreakdown(itemHrid, action, summary) { const stack = new Set([itemHrid]); let inputSecondsTotal = 0; for (const input of getAdjustedInputs(action, summary)) { const inputSeconds = calculateItemSeconds(input.itemHrid, stack); if (inputSeconds == null) { continue; } inputSecondsTotal += input.count * inputSeconds; } const teaPerAction = action.baseTimeCost ? summary.seconds / Math.max(summary.teaBuffs.durationSeconds || 300, 1) : 0; let teaSecondsTotal = 0; for (const teaItemHrid of summary.teaBuffs.activeTeas) { if (teaItemHrid === itemHrid) { continue; } const teaSeconds = calculateItemSeconds(teaItemHrid, stack); if (teaSeconds == null) { continue; } teaSecondsTotal += teaPerAction * teaSeconds; } return { inputSecondsTotal, teaSecondsTotal, }; } function getLoadoutDisplayText(action) { const loadoutInfo = resolveSkillingLoadout(action?.type); if (!loadoutInfo.loadout) { return isZh ? "配装: 当前装备" : "Loadout: Current"; } const mode = loadoutInfo.loadout.useExactEnhancement ? (isZh ? "精确强化" : "Exact") : (isZh ? "最高强化" : "Highest"); return isZh ? `配装: ${loadoutInfo.loadout.name || loadoutInfo.loadout.id} (${mode})` : `Loadout: ${loadoutInfo.loadout.name || loadoutInfo.loadout.id} (${mode})`; } function isAllowedEssenceDecomposeSource(essenceHrid, sourceItemHrid) { const rule = ESSENCE_DECOMPOSE_RULES[essenceHrid]; if (!rule || !sourceItemHrid) { return false; } const itemDetail = state.itemDetailMap?.[sourceItemHrid]; const action = findActionForItem(sourceItemHrid); if (rule.type === "fixed_source") { return sourceItemHrid === getConfiguredEssenceDecomposeSourceItemHrid(essenceHrid); } if (rule.type === "raw_skill") { return Boolean(action && action.type === rule.actionTypeHrid && (!action.inputItems || action.inputItems.length === 0)); } if (rule.type === "resource_skill") { return Boolean(action && action.type === rule.actionTypeHrid && itemDetail?.categoryHrid === "/item_categories/resource"); } if (rule.type === "food_skill") { return Boolean(action && action.type === rule.actionTypeHrid && itemDetail?.categoryHrid === "/item_categories/food"); } if (rule.type === "brewing_base") { return Boolean( sourceItemHrid.endsWith("_tea_leaf") || sourceItemHrid.endsWith("_coffee_bean") || (action && action.type === "/action_types/brewing") ); } return false; } function getFixedDecomposePlan(itemHrid, stack = new Set()) { const sourceItemHrid = FIXED_DECOMPOSE_SOURCE_RULES[itemHrid]; if (!sourceItemHrid) { return null; } if (state.fixedDecomposePlanCache.has(itemHrid)) { return state.fixedDecomposePlanCache.get(itemHrid); } const decomposeAction = state.actionDetailMap?.["/actions/alchemy/decompose"]; const sourceItemDetail = state.itemDetailMap?.[sourceItemHrid]; const match = (sourceItemDetail?.alchemyDetail?.decomposeItems || []).find((entry) => entry.itemHrid === itemHrid); if (!decomposeAction || !sourceItemDetail || !match) { const failureReason = !match ? (isZh ? "分解产出无效,已截断" : "Truncated: invalid decompose output") : getDependencyFailureReason(sourceItemHrid); state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedDecomposePlanCache.set(itemHrid, null); return null; } const actionSummary = getActionSummary(decomposeAction); const bulkMultiplier = Math.max(1, Number(sourceItemDetail?.alchemyDetail?.bulkMultiplier || 1)); const outputCount = Number(match.count || 0) * bulkMultiplier; if (!outputCount) { const failureReason = isZh ? "分解产出无效,已截断" : "Truncated: invalid decompose output"; state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedDecomposePlanCache.set(itemHrid, null); return null; } const successChance = getAlchemyDecomposeSuccessChance(sourceItemHrid, actionSummary); const efficiencyFraction = getAlchemyDecomposeEfficiencyFraction(sourceItemHrid, actionSummary); const efficiencyMultiplier = 1 + efficiencyFraction; const expectedOutputCount = outputCount * successChance * efficiencyMultiplier; if (!expectedOutputCount) { const failureReason = isZh ? "分解期望无效,已截断" : "Truncated: invalid decompose expectation"; state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedDecomposePlanCache.set(itemHrid, null); return null; } const sourceStack = new Set(stack); sourceStack.add(itemHrid); const sourceItemSeconds = calculateItemSeconds(sourceItemHrid, sourceStack); if (sourceItemSeconds == null || !Number.isFinite(sourceItemSeconds) || sourceItemSeconds < 0) { state.itemFailureReasonCache.set(itemHrid, getDependencyFailureReason(sourceItemHrid)); state.fixedDecomposePlanCache.set(itemHrid, null); return null; } const sourceBaseSeconds = sourceItemSeconds * bulkMultiplier; const sourceSeconds = sourceBaseSeconds * efficiencyMultiplier; const teaPerAction = actionSummary.seconds / Math.max(actionSummary.teaBuffs.durationSeconds || 300, 1); let teaSecondsTotal = 0; for (const teaItemHrid of actionSummary.teaBuffs.activeTeas) { if (itemDependsOnCurrentRecipe(teaItemHrid, itemHrid)) { continue; } const teaStack = new Set(stack); teaStack.add(itemHrid); const teaSeconds = calculateItemSeconds(teaItemHrid, teaStack); if (teaSeconds == null) { continue; } teaSecondsTotal += teaPerAction * teaSeconds; } const totalSeconds = (actionSummary.seconds + teaSecondsTotal + sourceSeconds) / expectedOutputCount; if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) { const failureReason = isZh ? "分解总耗时无效,已截断" : "Truncated: invalid decompose total"; state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedDecomposePlanCache.set(itemHrid, null); return null; } const plan = { itemHrid: sourceItemHrid, itemName: getLocalizedItemName(sourceItemHrid, sourceItemDetail?.name || sourceItemHrid), outputCount, bulkMultiplier, efficiencyFraction, successChance, expectedOutputCount, sourceItemSeconds, sourceBaseSeconds, sourceSeconds, teaSecondsTotal, actionSeconds: actionSummary.seconds, totalSeconds, action: decomposeAction, }; state.itemFailureReasonCache.delete(itemHrid); state.fixedDecomposePlanCache.set(itemHrid, plan); state.itemTooltipDataCache.delete(itemHrid); return plan; } function getFixedEnhancedEssencePlan(itemHrid) { const rule = FIXED_ENHANCING_ESSENCE_RULES[itemHrid]; if (!rule) { return null; } if (state.fixedEnhancedEssencePlanCache.has(itemHrid)) { return state.fixedEnhancedEssencePlanCache.get(itemHrid); } const sourceItemDetail = state.itemDetailMap?.[rule.sourceItemHrid]; if (!sourceItemDetail) { const failureReason = getDependencyFailureReason(rule.sourceItemHrid); state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedEnhancedEssencePlanCache.set(itemHrid, null); return null; } const recommendation = getEnhancingRecommendationForItem(rule.sourceItemHrid, rule.enhancementLevel); if (!recommendation) { const failureReason = getDependencyFailureReason(rule.sourceItemHrid) || (isZh ? "强化来源无法计算" : "Enhancing source unavailable"); state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedEnhancedEssencePlanCache.set(itemHrid, null); return null; } const essenceInfo = getEnhancedEquipmentEssenceInfo( rule.sourceItemHrid, rule.enhancementLevel, recommendation, rule.catalystItemHrid || "" ); if (!Number.isFinite(essenceInfo?.secondsPerEssence) || essenceInfo.secondsPerEssence <= 0) { const failureReason = isZh ? "强化精华期望无效,已截断" : "Truncated: invalid enhancing essence expectation"; state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedEnhancedEssencePlanCache.set(itemHrid, null); return null; } const plan = { itemHrid: rule.sourceItemHrid, itemName: getLocalizedItemName(rule.sourceItemHrid, sourceItemDetail?.name || rule.sourceItemHrid), enhancementLevel: rule.enhancementLevel, recommendation, essenceInfo, catalystItemHrid: rule.catalystItemHrid || "", catalystItemName: rule.catalystItemHrid ? getLocalizedItemName(rule.catalystItemHrid, state.itemDetailMap?.[rule.catalystItemHrid]?.name || rule.catalystItemHrid) : "", action: state.actionDetailMap?.["/actions/alchemy/decompose"] || null, totalSeconds: essenceInfo.secondsPerEssence, }; state.itemFailureReasonCache.delete(itemHrid); state.fixedEnhancedEssencePlanCache.set(itemHrid, plan); state.itemTooltipDataCache.delete(itemHrid); return plan; } function getFixedTransmutePlan(itemHrid, stack = new Set()) { const rule = FIXED_TRANSMUTE_SOURCE_RULES[itemHrid]; if (!rule) { return null; } if (state.fixedTransmutePlanCache.has(itemHrid)) { return state.fixedTransmutePlanCache.get(itemHrid); } const transmuteAction = state.actionDetailMap?.[rule.actionHrid || "/actions/alchemy/transmute"]; const sourceItemHrid = rule.sourceItemHrid; const sourceItemDetail = state.itemDetailMap?.[sourceItemHrid]; const transmuteDrop = (sourceItemDetail?.alchemyDetail?.transmuteDropTable || []).find((drop) => drop?.itemHrid === itemHrid); if (!transmuteAction || !sourceItemDetail || !transmuteDrop) { const failureReason = !transmuteDrop ? (isZh ? "转化产出无效,已截断" : "Truncated: invalid transmute output") : getDependencyFailureReason(sourceItemHrid); state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedTransmutePlanCache.set(itemHrid, null); return null; } const actionSummary = getActionSummary(transmuteAction); const successChance = getAlchemyTransmuteSuccessChance(sourceItemHrid, actionSummary); const efficiencyFraction = getAlchemyDecomposeEfficiencyFraction(sourceItemHrid, actionSummary); const efficiencyMultiplier = Math.max(1 + efficiencyFraction, 1); const averageTargetCount = ((Number(transmuteDrop.minCount || 0) + Number(transmuteDrop.maxCount || 0)) / 2) * Number(transmuteDrop.dropRate || 0); const expectedOutputCount = successChance * averageTargetCount * efficiencyMultiplier; if (!Number.isFinite(expectedOutputCount) || expectedOutputCount <= 0) { const failureReason = isZh ? "转化期望无效,已截断" : "Truncated: invalid transmute expectation"; state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedTransmutePlanCache.set(itemHrid, null); return null; } const sourceStack = new Set(stack); sourceStack.add(itemHrid); const sourceItemSeconds = calculateItemSeconds(sourceItemHrid, sourceStack); if (sourceItemSeconds == null || !Number.isFinite(sourceItemSeconds) || sourceItemSeconds < 0) { state.itemFailureReasonCache.set(itemHrid, getDependencyFailureReason(sourceItemHrid)); state.fixedTransmutePlanCache.set(itemHrid, null); return null; } const teaPerAction = Number(actionSummary.seconds || 0) / Math.max(actionSummary.teaBuffs.durationSeconds || 300, 1); let teaSecondsTotal = 0; for (const teaItemHrid of actionSummary.teaBuffs.activeTeas) { if (itemDependsOnCurrentRecipe(teaItemHrid, itemHrid)) { continue; } const teaStack = new Set(stack); teaStack.add(itemHrid); const teaSeconds = calculateItemSeconds(teaItemHrid, teaStack); if (teaSeconds == null || !Number.isFinite(teaSeconds) || teaSeconds < 0) { continue; } teaSecondsTotal += teaPerAction * teaSeconds; } const sourceSeconds = sourceItemSeconds * efficiencyMultiplier; let sideOutputSeconds = 0; const sideOutputs = []; for (const drop of sourceItemDetail.alchemyDetail?.transmuteDropTable || []) { if (!drop?.itemHrid || drop.itemHrid === itemHrid) { continue; } const averageCount = (Number(drop.minCount || 0) + Number(drop.maxCount || 0)) / 2; const expectedCount = successChance * Number(drop.dropRate || 0) * averageCount * efficiencyMultiplier; if (!Number.isFinite(expectedCount) || expectedCount <= 0) { continue; } const outputStack = new Set(stack); outputStack.add(itemHrid); const outputSeconds = calculateItemSeconds(drop.itemHrid, outputStack); if (outputSeconds == null || !Number.isFinite(outputSeconds) || outputSeconds <= 0) { continue; } const expectedSeconds = expectedCount * outputSeconds; sideOutputSeconds += expectedSeconds; sideOutputs.push({ itemHrid: drop.itemHrid, itemName: getLocalizedItemName(drop.itemHrid, state.itemDetailMap?.[drop.itemHrid]?.name || drop.itemHrid), expectedCount, outputSeconds, expectedSeconds, }); } const netSeconds = Math.max(0, Number(actionSummary.seconds || 0) + teaSecondsTotal + sourceSeconds - sideOutputSeconds); const totalSeconds = netSeconds / expectedOutputCount; if (!Number.isFinite(totalSeconds) || totalSeconds < 0) { const failureReason = isZh ? "转化总耗时无效,已截断" : "Truncated: invalid transmute total"; state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedTransmutePlanCache.set(itemHrid, null); return null; } const plan = { itemHrid: sourceItemHrid, itemName: getLocalizedItemName(sourceItemHrid, sourceItemDetail?.name || sourceItemHrid), successChance, sourceItemSeconds, sourceSeconds, teaSecondsTotal, sideOutputSeconds, netSeconds, actionSeconds: Number(actionSummary.seconds || 0), rawActionSeconds: actionSummary.seconds, efficiencyFraction, efficiencyMultiplier, expectedOutputCount, averageTargetCount, transmuteDropRate: Number(transmuteDrop.dropRate || 0), totalSeconds, sideOutputs, action: transmuteAction, }; state.itemFailureReasonCache.delete(itemHrid); state.fixedTransmutePlanCache.set(itemHrid, plan); state.itemTooltipDataCache.delete(itemHrid); return plan; } function getFixedAttachedRareTooltipPlan(itemHrid, stack = new Set()) { const rule = FIXED_ATTACHED_RARE_TOOLTIP_SOURCE_RULES[itemHrid]; if (!rule) { return null; } if (state.fixedAttachedRareTooltipPlanCache.has(itemHrid)) { return state.fixedAttachedRareTooltipPlanCache.get(itemHrid); } const transmuteAction = state.actionDetailMap?.["/actions/alchemy/transmute"]; const sourceItemCandidates = [ rule.sourceItemHrid, ...((Array.isArray(rule.fallbackSourceItemHrids) ? rule.fallbackSourceItemHrids : []).filter(Boolean)), ].filter(Boolean); let sourceItemHrid = rule.sourceItemHrid; let sourceItemDetail = null; let targetDrop = null; for (const candidateItemHrid of sourceItemCandidates) { const candidateItemDetail = state.itemDetailMap?.[candidateItemHrid]; const candidateTargetDrop = (candidateItemDetail?.alchemyDetail?.transmuteDropTable || []) .find((drop) => drop?.itemHrid === itemHrid); if (!candidateItemDetail || !candidateTargetDrop) { continue; } sourceItemHrid = candidateItemHrid; sourceItemDetail = candidateItemDetail; targetDrop = candidateTargetDrop; break; } if (!transmuteAction || !sourceItemDetail || !targetDrop) { const failureReason = !targetDrop ? (isZh ? "转化产出无效,已截断" : "Truncated: invalid transmute output") : getDependencyFailureReason(sourceItemHrid); state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedAttachedRareTooltipPlanCache.set(itemHrid, null); return null; } const actionSummary = getActionSummary(transmuteAction); const baseSuccessChance = getAlchemyTransmuteSuccessChance(sourceItemHrid, actionSummary); const catalystSuccessBonus = getAlchemyTransmuteCatalystSuccessBonus( rule.catalystItemHrid || "", rule.catalystSuccessBonus || 0 ); const successChance = clamp01(baseSuccessChance + catalystSuccessBonus); const efficiencyFraction = getAlchemyDecomposeEfficiencyFraction(sourceItemHrid, actionSummary); const efficiencyMultiplier = Math.max(1 + efficiencyFraction, 1); const averageTargetCount = ((Number(targetDrop.minCount || 0) + Number(targetDrop.maxCount || 0)) / 2) * Number(targetDrop.dropRate || 0); const directTargetExpectedCount = successChance * averageTargetCount * efficiencyMultiplier; if (!Number.isFinite(directTargetExpectedCount) || directTargetExpectedCount <= 0) { const failureReason = isZh ? "转化期望无效,已截断" : "Truncated: invalid transmute expectation"; state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedAttachedRareTooltipPlanCache.set(itemHrid, null); return null; } const sourceStack = new Set(stack); sourceStack.add(itemHrid); const sourceItemRelation = getItemSecondsLinearRelationToTarget(sourceItemHrid, itemHrid, sourceStack); if (!sourceItemRelation || !Number.isFinite(sourceItemRelation.baseSeconds) || sourceItemRelation.baseSeconds < 0 || !Number.isFinite(sourceItemRelation.targetSecondsCoefficient) || sourceItemRelation.targetSecondsCoefficient < 0) { state.itemFailureReasonCache.set(itemHrid, getDependencyFailureReason(sourceItemHrid)); state.fixedAttachedRareTooltipPlanCache.set(itemHrid, null); return null; } const inputBaseSecondsTotal = Number(sourceItemRelation.baseSeconds || 0) * efficiencyMultiplier; const inputTargetSecondsCoefficient = Number(sourceItemRelation.targetSecondsCoefficient || 0) * efficiencyMultiplier; const teaPerAction = Number(actionSummary.seconds || 0) / Math.max(actionSummary.teaBuffs.durationSeconds || 300, 1); let teaSecondsTotal = 0; for (const teaItemHrid of actionSummary.teaBuffs.activeTeas || []) { if (itemDependsOnCurrentRecipe(teaItemHrid, itemHrid)) { continue; } const teaStack = new Set(stack); teaStack.add(itemHrid); const teaSeconds = calculateItemSeconds(teaItemHrid, teaStack); if (teaSeconds == null || !Number.isFinite(teaSeconds) || teaSeconds < 0) { continue; } teaSecondsTotal += teaPerAction * teaSeconds; } let catalystBaseSecondsTotal = 0; let catalystTargetSecondsCoefficient = 0; let catalystSecondsTotal = 0; if (rule.catalystItemHrid) { const catalystStack = new Set(stack); catalystStack.add(itemHrid); const catalystRelation = getItemSecondsLinearRelationToTarget(rule.catalystItemHrid, itemHrid, catalystStack); if (!catalystRelation || !Number.isFinite(catalystRelation.baseSeconds) || catalystRelation.baseSeconds < 0 || !Number.isFinite(catalystRelation.targetSecondsCoefficient) || catalystRelation.targetSecondsCoefficient < 0) { state.itemFailureReasonCache.set(itemHrid, getDependencyFailureReason(rule.catalystItemHrid)); state.fixedAttachedRareTooltipPlanCache.set(itemHrid, null); return null; } const catalystConsumptionPerAction = successChance * efficiencyMultiplier; catalystBaseSecondsTotal = Math.max(0, Number(catalystRelation.baseSeconds || 0)) * catalystConsumptionPerAction; catalystTargetSecondsCoefficient = Math.max(0, Number(catalystRelation.targetSecondsCoefficient || 0)) * catalystConsumptionPerAction; } const inputAttachedTargetExpectedCount = efficiencyMultiplier * Math.max(0, Number(getAttachedRareYieldPerItem(sourceItemHrid, itemHrid) || 0)); let knownOutputSeconds = 0; let knownOutputAttachedTargetExpectedCount = 0; const sideOutputs = []; for (const drop of sourceItemDetail.alchemyDetail?.transmuteDropTable || []) { if (!drop?.itemHrid || drop.itemHrid === itemHrid) { continue; } const averageCount = (Number(drop.minCount || 0) + Number(drop.maxCount || 0)) / 2; const expectedCount = successChance * Number(drop.dropRate || 0) * averageCount * efficiencyMultiplier; if (!Number.isFinite(expectedCount) || expectedCount <= 0) { continue; } const outputStack = new Set(stack); outputStack.add(itemHrid); const outputSeconds = calculateItemSeconds(drop.itemHrid, outputStack); const attachedRare = Math.max(0, Number(getAttachedRareYieldPerItem(drop.itemHrid, itemHrid) || 0)); const attachedExpectedCount = expectedCount * attachedRare; const expectedSeconds = Number.isFinite(outputSeconds) && outputSeconds >= 0 ? expectedCount * Math.max(0, Number(outputSeconds || 0)) : 0; knownOutputSeconds += expectedSeconds; knownOutputAttachedTargetExpectedCount += attachedExpectedCount; sideOutputs.push({ itemHrid: drop.itemHrid, itemName: getLocalizedItemName(drop.itemHrid, state.itemDetailMap?.[drop.itemHrid]?.name || drop.itemHrid), expectedCount, outputSeconds: Number.isFinite(outputSeconds) ? Math.max(0, Number(outputSeconds || 0)) : 0, expectedSeconds, attachedExpectedCount, }); } const effectiveTargetExpectedCount = directTargetExpectedCount + inputAttachedTargetExpectedCount - knownOutputAttachedTargetExpectedCount - inputTargetSecondsCoefficient - catalystTargetSecondsCoefficient; if (!Number.isFinite(effectiveTargetExpectedCount) || effectiveTargetExpectedCount <= 0) { const failureReason = isZh ? "转化总期望无效,已截断" : "Truncated: invalid transmute denominator"; state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedAttachedRareTooltipPlanCache.set(itemHrid, null); return null; } const totalSeconds = (inputBaseSecondsTotal + Number(actionSummary.seconds || 0) + teaSecondsTotal + catalystSecondsTotal - knownOutputSeconds) / effectiveTargetExpectedCount; if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) { const failureReason = isZh ? "转化总耗时无效,已截断" : "Truncated: invalid transmute total"; state.itemFailureReasonCache.set(itemHrid, failureReason); state.fixedAttachedRareTooltipPlanCache.set(itemHrid, null); return null; } const sourceItemSeconds = Number(sourceItemRelation.baseSeconds || 0) + Number(sourceItemRelation.targetSecondsCoefficient || 0) * totalSeconds; const inputSecondsTotal = inputBaseSecondsTotal + inputTargetSecondsCoefficient * totalSeconds; catalystSecondsTotal = catalystBaseSecondsTotal + catalystTargetSecondsCoefficient * totalSeconds; const plan = { targetItemHrid: itemHrid, targetItemName: getLocalizedItemName(itemHrid, state.itemDetailMap?.[itemHrid]?.name || itemHrid), itemHrid: sourceItemHrid, itemName: getLocalizedItemName(sourceItemHrid, sourceItemDetail?.name || sourceItemHrid), catalystItemHrid: rule.catalystItemHrid || "", catalystItemName: rule.catalystItemHrid ? getLocalizedItemName(rule.catalystItemHrid, state.itemDetailMap?.[rule.catalystItemHrid]?.name || rule.catalystItemHrid) : "", successChance, baseSuccessChance, catalystSuccessBonus, efficiencyFraction, efficiencyMultiplier, directTargetExpectedCount, inputAttachedTargetExpectedCount, knownOutputAttachedTargetExpectedCount, effectiveTargetExpectedCount, sourceItemSeconds, sourceItemBaseSeconds: Number(sourceItemRelation.baseSeconds || 0), sourceItemTargetSecondsCoefficient: Number(sourceItemRelation.targetSecondsCoefficient || 0), inputBaseSecondsTotal, inputTargetSecondsCoefficient, inputSecondsTotal, catalystBaseSecondsTotal, catalystTargetSecondsCoefficient, catalystSecondsTotal, knownOutputSeconds, teaSecondsTotal, actionSeconds: Number(actionSummary.seconds || 0), totalSeconds, targetDropRate: Number(targetDrop.dropRate || 0), targetAverageCount: (Number(targetDrop.minCount || 0) + Number(targetDrop.maxCount || 0)) / 2, sideOutputs, action: transmuteAction, }; state.itemFailureReasonCache.delete(itemHrid); state.fixedAttachedRareTooltipPlanCache.set(itemHrid, plan); state.itemTooltipDataCache.delete(itemHrid); return plan; } function getEssenceDecomposePlan(itemHrid, stack = new Set()) { const rule = ESSENCE_DECOMPOSE_RULES[itemHrid]; if (!rule) { return null; } if (state.essencePlanCache.has(itemHrid)) { return state.essencePlanCache.get(itemHrid); } const decomposeAction = state.actionDetailMap?.["/actions/alchemy/decompose"]; if (!decomposeAction) { return null; } const actionSummary = getActionSummary(decomposeAction); let best = null; let failureReason = ""; for (const [sourceItemHrid, itemDetail] of Object.entries(state.itemDetailMap || {})) { const match = (itemDetail?.alchemyDetail?.decomposeItems || []).find((entry) => entry.itemHrid === itemHrid); if (!match || !isAllowedEssenceDecomposeSource(itemHrid, sourceItemHrid)) { continue; } const bulkMultiplier = Math.max(1, Number(itemDetail?.alchemyDetail?.bulkMultiplier || 1)); const outputCount = Number(match.count || 0) * bulkMultiplier; if (!outputCount) { failureReason = isZh ? "分解产出无效,已截断" : "Truncated: invalid decompose output"; continue; } const successChance = getAlchemyDecomposeSuccessChance(sourceItemHrid, actionSummary); const efficiencyFraction = getAlchemyDecomposeEfficiencyFraction(sourceItemHrid, actionSummary); const efficiencyMultiplier = 1 + efficiencyFraction; const expectedOutputCount = outputCount * successChance * efficiencyMultiplier; if (!expectedOutputCount) { failureReason = isZh ? "分解期望无效,已截断" : "Truncated: invalid decompose expectation"; continue; } const sourceStack = new Set(stack); sourceStack.add(itemHrid); const sourceItemSeconds = calculateItemSeconds(sourceItemHrid, sourceStack); if (sourceItemSeconds == null || !Number.isFinite(sourceItemSeconds) || sourceItemSeconds < 0) { failureReason = getDependencyFailureReason(sourceItemHrid); continue; } const sourceBaseSeconds = sourceItemSeconds * bulkMultiplier; const sourceSeconds = sourceBaseSeconds * efficiencyMultiplier; const teaPerAction = actionSummary.seconds / Math.max(actionSummary.teaBuffs.durationSeconds || 300, 1); let teaSecondsTotal = 0; for (const teaItemHrid of actionSummary.teaBuffs.activeTeas) { if (itemDependsOnCurrentRecipe(teaItemHrid, itemHrid)) { continue; } const teaStack = new Set(stack); teaStack.add(itemHrid); const teaSeconds = calculateItemSeconds(teaItemHrid, teaStack); if (teaSeconds == null) { continue; } teaSecondsTotal += teaPerAction * teaSeconds; } const totalSeconds = (actionSummary.seconds + teaSecondsTotal + sourceSeconds) / expectedOutputCount; if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) { failureReason = isZh ? "分解总耗时无效,已截断" : "Truncated: invalid decompose total"; continue; } const sourceName = getLocalizedItemName(sourceItemHrid, itemDetail?.name || sourceItemHrid); if (!best || totalSeconds < best.totalSeconds) { best = { itemHrid: sourceItemHrid, itemName: sourceName, outputCount, bulkMultiplier, efficiencyFraction, successChance, expectedOutputCount, sourceItemSeconds, sourceBaseSeconds, sourceSeconds, teaSecondsTotal, actionSeconds: actionSummary.seconds, totalSeconds, action: decomposeAction, }; } } if (best) { state.itemFailureReasonCache.delete(itemHrid); } else if (rule.type === "fixed_source") { const configuredSourceItemHrid = getConfiguredEssenceDecomposeSourceItemHrid(itemHrid); state.itemFailureReasonCache.set(itemHrid, failureReason || getDependencyFailureReason(configuredSourceItemHrid)); } else if (failureReason) { state.itemFailureReasonCache.set(itemHrid, failureReason); } state.essencePlanCache.set(itemHrid, best); state.itemTooltipDataCache.delete(itemHrid); return best; } function getItemCalculationDetail(itemHrid) { if (isMissingDerivedRuntimeState()) { ensureRuntimeStateFresh(); } const timeCalculatorEntry = getConfiguredTimeCalculatorEntry(itemHrid); if (timeCalculatorEntry) { const summary = getTimeCalculatorEntrySummary(timeCalculatorEntry); if (summary.itemType === "fragment") { return [ isZh ? `24小时碎片${formatNumber(summary.quantityPer24h)}` : `24h qty ${formatNumber(summary.quantityPer24h)}`, isZh ? `食物${formatAutoDuration(summary.foodSeconds)}` : `food ${formatAutoDuration(summary.foodSeconds)}`, isZh ? `饮料${formatAutoDuration(summary.drinkSeconds)}` : `drink ${formatAutoDuration(summary.drinkSeconds)}`, ].join(" | "); } return [ isZh ? `\u5355\u6b21\u5730\u7262${formatAutoDuration(summary.runMinutes * 60)}` : `run ${formatAutoDuration(summary.runMinutes * 60)}`, ...(summary.dungeonEntryKeySeconds > 0 ? [isZh ? `\u5730\u7262\u94a5\u5319${formatAutoDuration(summary.dungeonEntryKeySeconds)}` : `entry key ${formatAutoDuration(summary.dungeonEntryKeySeconds)}`] : []), isZh ? `${summary.itemType === "refinement_chest" ? "\u7cbe\u70bc\u7bb1\u5b50\u671f\u671b" : "\u5b9d\u7bb1\u671f\u671b"}${formatNumber(summary.expectedChestCount)}` : `exp ${formatNumber(summary.expectedChestCount)}`, isZh ? `\u98df\u7269${formatAutoDuration(summary.foodSeconds)}` : `food ${formatAutoDuration(summary.foodSeconds)}`, isZh ? `\u996e\u6599${formatAutoDuration(summary.drinkSeconds)}` : `drink ${formatAutoDuration(summary.drinkSeconds)}`, ].join(" | "); return [ isZh ? `单次地牢${formatAutoDuration(summary.runMinutes * 60)}` : `run ${formatAutoDuration(summary.runMinutes * 60)}`, ...(summary.dungeonEntryKeySeconds > 0 ? [isZh ? `地牢钥匙${formatAutoDuration(summary.dungeonEntryKeySeconds)}` : `entry key ${formatAutoDuration(summary.dungeonEntryKeySeconds)}`] : []), isZh ? `宝箱期望${formatNumber(summary.expectedChestCount)}` : `exp ${formatNumber(summary.expectedChestCount)}`, isZh ? `食物${formatAutoDuration(summary.foodSeconds)}` : `food ${formatAutoDuration(summary.foodSeconds)}`, isZh ? `饮料${formatAutoDuration(summary.drinkSeconds)}` : `drink ${formatAutoDuration(summary.drinkSeconds)}`, ].join(" | "); } const dungeonMaterialPlan = getDungeonMaterialPlan(itemHrid); if (dungeonMaterialPlan) { return [ isZh ? `宝箱${formatAutoDuration(dungeonMaterialPlan.chestSeconds)}` : `chest ${formatAutoDuration(dungeonMaterialPlan.chestSeconds)}`, isZh ? `钥匙${formatAutoDuration(dungeonMaterialPlan.keySeconds)}` : `key ${formatAutoDuration(dungeonMaterialPlan.keySeconds)}`, isZh ? `直掉${formatNumber(dungeonMaterialPlan.directExpected)}` : `drop ${formatNumber(dungeonMaterialPlan.directExpected)}`, isZh ? `代币换算${formatNumber(dungeonMaterialPlan.shopExpected)}` : `shop ${formatNumber(dungeonMaterialPlan.shopExpected)}`, isZh ? `总期望${formatNumber(dungeonMaterialPlan.totalExpected)}` : `exp ${formatNumber(dungeonMaterialPlan.totalExpected)}`, ].join(" | "); } if (getGeneralShopPurchaseInfo(itemHrid)) { return isZh ? "商店购买" : "Shop purchase"; } const fixedAttachedRareTooltipPlan = getFixedAttachedRareTooltipPlan(itemHrid); if (fixedAttachedRareTooltipPlan) { const parts = [ isZh ? `单次转化${formatAutoDuration(fixedAttachedRareTooltipPlan.actionSeconds)}` : `transmute ${formatAutoDuration(fixedAttachedRareTooltipPlan.actionSeconds)}`, isZh ? `效率${formatSignedPercent(fixedAttachedRareTooltipPlan.efficiencyFraction * 100, 2)}` : `eff ${formatSignedPercent(fixedAttachedRareTooltipPlan.efficiencyFraction * 100, 2)}`, isZh ? `成功${formatPercent(fixedAttachedRareTooltipPlan.successChance * 100, 2)}` : `succ ${formatPercent(fixedAttachedRareTooltipPlan.successChance * 100, 2)}`, isZh ? `总期望${formatAttachedRareNumber(fixedAttachedRareTooltipPlan.effectiveTargetExpectedCount)}` : `exp ${formatAttachedRareNumber(fixedAttachedRareTooltipPlan.effectiveTargetExpectedCount)}`, ]; if (Number.isFinite(fixedAttachedRareTooltipPlan.inputAttachedTargetExpectedCount) && fixedAttachedRareTooltipPlan.inputAttachedTargetExpectedCount > 0) { parts.push( isZh ? `输入附带${formatAttachedRareNumber(fixedAttachedRareTooltipPlan.inputAttachedTargetExpectedCount)}` : `input extra ${formatAttachedRareNumber(fixedAttachedRareTooltipPlan.inputAttachedTargetExpectedCount)}` ); } if (Number.isFinite(fixedAttachedRareTooltipPlan.knownOutputAttachedTargetExpectedCount) && fixedAttachedRareTooltipPlan.knownOutputAttachedTargetExpectedCount > 0) { parts.push( isZh ? `产出附带抵扣${formatAttachedRareNumber(fixedAttachedRareTooltipPlan.knownOutputAttachedTargetExpectedCount)}` : `output extra ${formatAttachedRareNumber(fixedAttachedRareTooltipPlan.knownOutputAttachedTargetExpectedCount)}` ); } if (Number.isFinite(fixedAttachedRareTooltipPlan.teaSecondsTotal) && fixedAttachedRareTooltipPlan.teaSecondsTotal > 0) { parts.push(isZh ? `单次茶${formatAutoDuration(fixedAttachedRareTooltipPlan.teaSecondsTotal)}` : `tea ${formatAutoDuration(fixedAttachedRareTooltipPlan.teaSecondsTotal)}`); } if (Number.isFinite(fixedAttachedRareTooltipPlan.catalystSecondsTotal) && fixedAttachedRareTooltipPlan.catalystSecondsTotal > 0) { parts.push(isZh ? `单次催化剂${formatAutoDuration(fixedAttachedRareTooltipPlan.catalystSecondsTotal)}` : `cat ${formatAutoDuration(fixedAttachedRareTooltipPlan.catalystSecondsTotal)}`); } if (Number.isFinite(fixedAttachedRareTooltipPlan.sourceItemSeconds) && fixedAttachedRareTooltipPlan.sourceItemSeconds > 0) { parts.push(isZh ? `原料Time${formatAutoDuration(fixedAttachedRareTooltipPlan.sourceItemSeconds)}` : `src ${formatAutoDuration(fixedAttachedRareTooltipPlan.sourceItemSeconds)}`); } if (Number.isFinite(fixedAttachedRareTooltipPlan.knownOutputSeconds) && fixedAttachedRareTooltipPlan.knownOutputSeconds > 0) { parts.push( isZh ? `副产物抵扣${formatAutoDuration(fixedAttachedRareTooltipPlan.knownOutputSeconds)}` : `side ${formatAutoDuration(fixedAttachedRareTooltipPlan.knownOutputSeconds)}` ); } return parts.join(" | "); } const fixedDecomposePlan = getFixedDecomposePlan(itemHrid); if (fixedDecomposePlan) { const parts = [ isZh ? `单次分解${formatAutoDuration(fixedDecomposePlan.actionSeconds)}` : `decomp ${formatAutoDuration(fixedDecomposePlan.actionSeconds)}`, isZh ? `效率${formatSignedPercent(fixedDecomposePlan.efficiencyFraction * 100, 2)}` : `eff ${formatSignedPercent(fixedDecomposePlan.efficiencyFraction * 100, 2)}`, isZh ? `成功${formatPercent(fixedDecomposePlan.successChance * 100, 2)}` : `succ ${formatPercent(fixedDecomposePlan.successChance * 100, 2)}`, ]; if (Number.isFinite(fixedDecomposePlan.teaSecondsTotal) && fixedDecomposePlan.teaSecondsTotal > 0) { parts.push(isZh ? `单次茶${formatAutoDuration(fixedDecomposePlan.teaSecondsTotal)}` : `tea ${formatAutoDuration(fixedDecomposePlan.teaSecondsTotal)}`); } if (Number.isFinite(fixedDecomposePlan.sourceItemSeconds) && fixedDecomposePlan.sourceItemSeconds > 0) { parts.push( isZh ? `原料Time${formatAutoDuration(fixedDecomposePlan.sourceItemSeconds)}` : `src ${formatAutoDuration(fixedDecomposePlan.sourceItemSeconds)}` ); } if (Number.isFinite(fixedDecomposePlan.sourceSeconds) && fixedDecomposePlan.sourceSeconds > 0) { parts.push( isZh ? `本次原料${formatAutoDuration(fixedDecomposePlan.sourceSeconds)}` : `mat ${formatAutoDuration(fixedDecomposePlan.sourceSeconds)}` ); } return parts.join(" | "); } const fixedTransmutePlan = getFixedTransmutePlan(itemHrid); if (fixedTransmutePlan) { const parts = [ isZh ? `单次转化${formatAutoDuration(fixedTransmutePlan.actionSeconds)}` : `transmute ${formatAutoDuration(fixedTransmutePlan.actionSeconds)}`, isZh ? `效率${formatSignedPercent(fixedTransmutePlan.efficiencyFraction * 100, 2)}` : `eff ${formatSignedPercent(fixedTransmutePlan.efficiencyFraction * 100, 2)}`, isZh ? `成功${formatPercent(fixedTransmutePlan.successChance * 100, 2)}` : `succ ${formatPercent(fixedTransmutePlan.successChance * 100, 2)}`, isZh ? `期望产出${formatNumber(fixedTransmutePlan.expectedOutputCount)}` : `exp ${formatNumber(fixedTransmutePlan.expectedOutputCount)}`, ]; if (Number.isFinite(fixedTransmutePlan.teaSecondsTotal) && fixedTransmutePlan.teaSecondsTotal > 0) { parts.push(isZh ? `单次茶${formatAutoDuration(fixedTransmutePlan.teaSecondsTotal)}` : `tea ${formatAutoDuration(fixedTransmutePlan.teaSecondsTotal)}`); } if (Number.isFinite(fixedTransmutePlan.sourceItemSeconds) && fixedTransmutePlan.sourceItemSeconds > 0) { parts.push(isZh ? `原料Time${formatAutoDuration(fixedTransmutePlan.sourceItemSeconds)}` : `src ${formatAutoDuration(fixedTransmutePlan.sourceItemSeconds)}`); } if (Number.isFinite(fixedTransmutePlan.sideOutputSeconds) && fixedTransmutePlan.sideOutputSeconds > 0) { parts.push(isZh ? `副产物抵扣${formatAutoDuration(fixedTransmutePlan.sideOutputSeconds)}` : `side ${formatAutoDuration(fixedTransmutePlan.sideOutputSeconds)}`); } return parts.join(" | "); } const fixedEnhancedEssencePlan = getFixedEnhancedEssencePlan(itemHrid); if (fixedEnhancedEssencePlan) { const parts = [ isZh ? `单次分解${formatAutoDuration(fixedEnhancedEssencePlan.essenceInfo.actionSeconds)}` : `decomp ${formatAutoDuration(fixedEnhancedEssencePlan.essenceInfo.actionSeconds)}`, isZh ? `效率${formatSignedPercent(fixedEnhancedEssencePlan.essenceInfo.efficiencyFraction * 100, 2)}` : `eff ${formatSignedPercent(fixedEnhancedEssencePlan.essenceInfo.efficiencyFraction * 100, 2)}`, isZh ? `成功${formatPercent(fixedEnhancedEssencePlan.essenceInfo.successChance * 100, 2)}` : `succ ${formatPercent(fixedEnhancedEssencePlan.essenceInfo.successChance * 100, 2)}`, isZh ? `+${fixedEnhancedEssencePlan.enhancementLevel}总时间${formatAutoDuration(fixedEnhancedEssencePlan.recommendation.totalSeconds || 0)}` : `+${fixedEnhancedEssencePlan.enhancementLevel} total ${formatAutoDuration(fixedEnhancedEssencePlan.recommendation.totalSeconds || 0)}`, ]; if (Number.isFinite(fixedEnhancedEssencePlan.essenceInfo.expectedEssenceCount) && fixedEnhancedEssencePlan.essenceInfo.expectedEssenceCount > 0) { parts.push( isZh ? `期望精华${formatNumber(fixedEnhancedEssencePlan.essenceInfo.expectedEssenceCount)}` : `exp ${formatNumber(fixedEnhancedEssencePlan.essenceInfo.expectedEssenceCount)}` ); } if (Number.isFinite(fixedEnhancedEssencePlan.essenceInfo.teaSecondsTotal) && fixedEnhancedEssencePlan.essenceInfo.teaSecondsTotal > 0) { parts.push(isZh ? `单次茶${formatAutoDuration(fixedEnhancedEssencePlan.essenceInfo.teaSecondsTotal)}` : `tea ${formatAutoDuration(fixedEnhancedEssencePlan.essenceInfo.teaSecondsTotal)}`); } if (Number.isFinite(fixedEnhancedEssencePlan.essenceInfo.catalystSecondsTotal) && fixedEnhancedEssencePlan.essenceInfo.catalystSecondsTotal > 0) { parts.push(isZh ? `单次催化剂${formatAutoDuration(fixedEnhancedEssencePlan.essenceInfo.catalystSecondsTotal)}` : `cat ${formatAutoDuration(fixedEnhancedEssencePlan.essenceInfo.catalystSecondsTotal)}`); } return parts.join(" | "); } const essencePlan = getEssenceDecomposePlan(itemHrid); if (essencePlan) { const parts = [ isZh ? `单次分解${formatAutoDuration(essencePlan.actionSeconds)}` : `decomp ${formatAutoDuration(essencePlan.actionSeconds)}`, isZh ? `效率${formatSignedPercent(essencePlan.efficiencyFraction * 100, 2)}` : `eff ${formatSignedPercent(essencePlan.efficiencyFraction * 100, 2)}`, isZh ? `成功${formatPercent(essencePlan.successChance * 100, 2)}` : `succ ${formatPercent(essencePlan.successChance * 100, 2)}`, ]; if (Number.isFinite(essencePlan.teaSecondsTotal) && essencePlan.teaSecondsTotal > 0) { parts.push(isZh ? `单次茶${formatAutoDuration(essencePlan.teaSecondsTotal)}` : `tea ${formatAutoDuration(essencePlan.teaSecondsTotal)}`); } if (Number.isFinite(essencePlan.sourceItemSeconds) && essencePlan.sourceItemSeconds > 0) { parts.push( isZh ? `原料Time${formatAutoDuration(essencePlan.sourceItemSeconds)}` : `src ${formatAutoDuration(essencePlan.sourceItemSeconds)}` ); } if (Number.isFinite(essencePlan.sourceSeconds) && essencePlan.sourceSeconds > 0) { parts.push( isZh ? `本次原料${formatAutoDuration(essencePlan.sourceSeconds)}` : `mat ${formatAutoDuration(essencePlan.sourceSeconds)}` ); } return parts.join(" | "); } const action = findActionForItem(itemHrid); if (!action) { return null; } const actionInfo = getActionSummary(action); const outputCount = getDisplayOutputCountPerAction(action, itemHrid, actionInfo); const breakdown = getPerActionCostBreakdown(itemHrid, action, actionInfo); const displayInputs = getDisplayInputs(action, actionInfo); const parts = [ isZh ? `单次耗时${formatAutoDuration(actionInfo.seconds)}` : `act ${formatAutoDuration(actionInfo.seconds)}`, isZh ? `效率${formatSignedPercent(actionInfo.efficiencyFraction * 100, 2)}` : `eff ${formatSignedPercent(actionInfo.efficiencyFraction * 100, 2)}`, ]; if (Number.isFinite(outputCount) && outputCount > 0) { parts.push(isZh ? `产出${formatNumber(outputCount)}` : `out ${formatNumber(outputCount)}`); } const processingProductDetail = getProcessingProductDetail(action, itemHrid, actionInfo); if (processingProductDetail) { parts.push( isZh ? `加工${processingProductDetail.itemName}${formatNumber(processingProductDetail.expectedCount)}` : `proc ${processingProductDetail.itemName} ${formatNumber(processingProductDetail.expectedCount)}` ); } if (Number.isFinite(breakdown.teaSecondsTotal) && breakdown.teaSecondsTotal > 0) { parts.push(isZh ? `单次茶${formatAutoDuration(breakdown.teaSecondsTotal)}` : `tea ${formatAutoDuration(breakdown.teaSecondsTotal)}`); } if (displayInputs.length === 1) { const sourceSeconds = calculateItemSeconds(displayInputs[0].itemHrid); if (Number.isFinite(sourceSeconds) && sourceSeconds > 0) { parts.push(isZh ? `原料Time${formatAutoDuration(sourceSeconds)}` : `src ${formatAutoDuration(sourceSeconds)}`); } } if (Number.isFinite(breakdown.inputSecondsTotal) && breakdown.inputSecondsTotal > 0) { parts.push(isZh ? `本次原料${formatAutoDuration(breakdown.inputSecondsTotal)}` : `mat ${formatAutoDuration(breakdown.inputSecondsTotal)}`); } return parts.join(" | "); } function getItemLoadoutDetail(itemHrid) { if (isMissingDerivedRuntimeState()) { ensureRuntimeStateFresh(); } if (getConfiguredTimeCalculatorEntry(itemHrid)) { return isZh ? "来源: 时间计算面板" : "Source: Time calculator"; } if (getGeneralShopPurchaseInfo(itemHrid)) { return isZh ? "来源: 商店购买" : "Source: Shop purchase"; } const fixedAttachedRareTooltipPlan = getFixedAttachedRareTooltipPlan(itemHrid); if (fixedAttachedRareTooltipPlan) { const catalystText = fixedAttachedRareTooltipPlan.catalystItemName ? `${isZh ? `催化剂: ${fixedAttachedRareTooltipPlan.catalystItemName}` : `Catalyst: ${fixedAttachedRareTooltipPlan.catalystItemName}`} | ` : ""; return `${isZh ? `转化: ${fixedAttachedRareTooltipPlan.itemName}` : `Transmute: ${fixedAttachedRareTooltipPlan.itemName}`} | ${catalystText}${getLoadoutDisplayText(fixedAttachedRareTooltipPlan.action)}`; } const fixedDecomposePlan = getFixedDecomposePlan(itemHrid); if (fixedDecomposePlan) { return `${isZh ? `分解: ${fixedDecomposePlan.itemName}` : `Decompose: ${fixedDecomposePlan.itemName}`} | ${getLoadoutDisplayText(fixedDecomposePlan.action)}`; } const fixedTransmutePlan = getFixedTransmutePlan(itemHrid); if (fixedTransmutePlan) { return `${isZh ? `转化: ${fixedTransmutePlan.itemName}` : `Transmute: ${fixedTransmutePlan.itemName}`} | ${getLoadoutDisplayText(fixedTransmutePlan.action)}`; } const fixedEnhancedEssencePlan = getFixedEnhancedEssencePlan(itemHrid); if (fixedEnhancedEssencePlan) { const sourceLabel = `${fixedEnhancedEssencePlan.itemName} +${fixedEnhancedEssencePlan.enhancementLevel}`; return `${isZh ? `分解: ${sourceLabel}` : `Decompose: ${sourceLabel}`} | ${getLoadoutDisplayText(fixedEnhancedEssencePlan.action)}`; } const dungeonMaterialPlan = getDungeonMaterialPlan(itemHrid); if (dungeonMaterialPlan) { return isZh ? `来源: ${dungeonMaterialPlan.chestName} + ${dungeonMaterialPlan.keyName}` : `Source: ${dungeonMaterialPlan.chestName} + ${dungeonMaterialPlan.keyName}`; } const essencePlan = getEssenceDecomposePlan(itemHrid); if (essencePlan) { return `${isZh ? `分解: ${essencePlan.itemName}` : `Decompose: ${essencePlan.itemName}`} | ${getLoadoutDisplayText(essencePlan.action)}`; } const action = findActionForItem(itemHrid); if (!action) { return null; } return getLoadoutDisplayText(action); } function getLoadoutById(loadoutId) { if (!Number.isFinite(Number(loadoutId))) { return null; } return state.characterLoadoutDict?.[Number(loadoutId)] || null; } function getEquippedItemsForLoadout(actionTypeHrid, explicitLoadout) { const loadout = explicitLoadout || resolveSkillingLoadout(actionTypeHrid).loadout; if (loadout?.wearableMap) { const items = []; for (const [slotKey, rawRef] of Object.entries(loadout.wearableMap || {})) { const entry = parseWearableReference(rawRef); if (!entry?.itemHrid) { continue; } items.push({ itemHrid: entry.itemHrid, enhancementLevel: resolveWearableEnhancement(entry, loadout), itemLocationHrid: slotKey, count: 1, }); } return items; } return getEquippedItems(actionTypeHrid); } function buildEquipmentNoncombatTotalsForLoadout(actionTypeHrid, explicitLoadout) { const totals = {}; const toolSlot = getToolSlotForActionType(actionTypeHrid); for (const item of getEquippedItemsForLoadout(actionTypeHrid, explicitLoadout)) { const location = item.itemLocationHrid || ""; if (location.endsWith("_tool") && location !== toolSlot) { continue; } const equipmentDetail = state.itemDetailMap?.[item.itemHrid]?.equipmentDetail; if (!equipmentDetail) { continue; } const enhancementMultiplier = getEnhancementBonusMultiplier(item.enhancementLevel || 0); const baseStats = equipmentDetail.noncombatStats || {}; const enhancementStats = equipmentDetail.noncombatEnhancementBonuses || {}; for (const [key, value] of Object.entries(baseStats)) { if (Number.isFinite(Number(value))) { totals[key] = (totals[key] || 0) + Number(value); } } for (const [key, value] of Object.entries(enhancementStats)) { if (Number.isFinite(Number(value))) { totals[key] = (totals[key] || 0) + Number(value) * enhancementMultiplier; } } } return totals; } function getDrinkConcentrationForLoadout(actionTypeHrid, explicitLoadout) { const pouch = getEquippedItemsForLoadout(actionTypeHrid, explicitLoadout).find((item) => item.itemHrid === "/items/guzzling_pouch"); if (!pouch || !state.itemDetailMap?.["/items/guzzling_pouch"]?.equipmentDetail) { return 1; } const detail = state.itemDetailMap["/items/guzzling_pouch"].equipmentDetail; const base = detail.noncombatStats?.drinkConcentration || 0; const bonus = detail.noncombatEnhancementBonuses?.drinkConcentration || 0; return 1 + base + bonus * getEnhancementBonusMultiplier(pouch.enhancementLevel || 0); } function getTeaBuffsForLoadout(actionTypeHrid, explicitLoadout) { const skillId = actionTypeHrid.replace("/action_types/", ""); const concentration = getDrinkConcentrationForLoadout(actionTypeHrid, explicitLoadout); const buffs = { blessedFraction: 0, efficiencyFraction: 0, quantityFraction: 0, lessResourceFraction: 0, processingFraction: 0, successRateFraction: 0, alchemySuccessFraction: 0, skillLevelBonus: 0, actionLevelPenalty: 0, wisdomFraction: 0, activeTeas: [], concentrationMultiplier: concentration, durationSeconds: 300 / concentration, }; const loadoutTeaList = Array.isArray(explicitLoadout?.drinkItemHrids) ? explicitLoadout.drinkItemHrids.filter(Boolean).map((itemHrid) => ({ itemHrid })) : []; const currentTeaList = state.actionTypeDrinkSlotsMap?.[actionTypeHrid] || []; const teaList = loadoutTeaList.length > 0 ? loadoutTeaList : currentTeaList; for (const tea of teaList) { if (!tea?.itemHrid) { continue; } buffs.activeTeas.push(tea.itemHrid); const teaDetail = state.itemDetailMap?.[tea.itemHrid]; for (const buff of teaDetail?.consumableDetail?.buffs || []) { if (buff.typeHrid === "/buff_types/artisan") { buffs.lessResourceFraction += buff.flatBoost; } else if (buff.typeHrid === "/buff_types/gathering" || buff.typeHrid === "/buff_types/gourmet") { buffs.quantityFraction += buff.flatBoost; } else if (buff.typeHrid === "/buff_types/processing") { buffs.processingFraction += buff.flatBoost; } else if (buff.typeHrid === "/buff_types/efficiency") { buffs.efficiencyFraction += buff.flatBoost; } else if (buff.typeHrid === "/buff_types/success_rate") { buffs.successRateFraction += getBuffAmount(buff); } else if (buff.typeHrid === "/buff_types/alchemy_success") { buffs.alchemySuccessFraction += getBuffAmount(buff); } else if (buff.typeHrid === "/buff_types/blessed") { buffs.blessedFraction += getBuffAmount(buff); } else if (buff.typeHrid === "/buff_types/wisdom") { buffs.wisdomFraction += getBuffAmount(buff); } else if (buff.typeHrid === `/buff_types/${skillId}_level`) { buffs.skillLevelBonus += buff.flatBoost; } else if (buff.typeHrid === "/buff_types/action_level") { buffs.actionLevelPenalty += buff.flatBoost; } } } buffs.blessedFraction *= concentration; buffs.efficiencyFraction *= concentration; buffs.quantityFraction *= concentration; buffs.lessResourceFraction *= concentration; buffs.processingFraction *= concentration; buffs.successRateFraction *= concentration; buffs.alchemySuccessFraction *= concentration; buffs.skillLevelBonus *= concentration; buffs.actionLevelPenalty *= concentration; buffs.wisdomFraction *= concentration; return buffs; } function getHouseRoomLevel(roomHrid) { const room = getContainerValue(state.characterHouseRoomMap, roomHrid); return Math.max(0, Number(room?.level || 0)); } function getEnhancingPanel() { if (state.enhancingPanelRef?.isConnected) { return state.enhancingPanelRef; } const candidates = Array.from(document.querySelectorAll("div")).filter((element) => { const text = element.innerText || ""; return text.includes("推荐等级") && text.includes("目标等级") && text.includes("保护") && text.includes("成功率"); }); if (!candidates.length) { state.enhancingPanelRef = null; return null; } candidates.sort((left, right) => (left.innerText || "").length - (right.innerText || "").length); state.enhancingPanelRef = candidates[0] || null; return state.enhancingPanelRef; } function shouldRefreshEnhancingFromTarget(target) { if (!(target instanceof Element)) { return false; } const panel = getEnhancingPanel(); if (!panel || !panel.isConnected) { return false; } if (panel.contains(target)) { return true; } const clickable = target.closest("button, [role='button'], input, select, textarea, label"); const text = ((clickable && clickable.textContent) || target.textContent || "").trim(); return text === "强化" || text === "当前行动"; } function getItemsSpriteBaseHref() { const itemUse = Array.from(document.querySelectorAll("use")).find((node) => { const value = node.getAttribute("href") || node.getAttribute("xlink:href") || ""; return value.includes("items_sprite") && value.includes("#"); }); if (!itemUse) { return `${location.origin}/static/media/items_sprite.svg`; } const value = itemUse.getAttribute("href") || itemUse.getAttribute("xlink:href") || ""; return value.split("#")[0]; } function getIconHrefByItemHrid(itemHrid) { return `${getItemsSpriteBaseHref()}#${(itemHrid || "").split("/").pop() || ""}`; } function createIconSvg(iconHref, sizePx = 18) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", `${sizePx}px`); svg.setAttribute("height", `${sizePx}px`); svg.style.display = "block"; const use = document.createElementNS("http://www.w3.org/2000/svg", "use"); use.setAttributeNS("http://www.w3.org/1999/xlink", "href", iconHref); svg.appendChild(use); return svg; } function getVisibleEnhancingProtectionNames(panel) { const names = new Map(); if (!panel) { return names; } for (const icon of panel.querySelectorAll('svg[role="img"][aria-label]')) { const label = (icon.getAttribute("aria-label") || "").trim(); if (!label || label === "Guide" || label === "Skill" || label === "Unlimited") { continue; } const use = icon.querySelector("use"); const href = use?.getAttribute("href") || use?.getAttribute("xlink:href") || ""; const hashIndex = href.indexOf("#"); if (hashIndex < 0 || !href.includes("items_sprite")) { continue; } names.set(`/items/${href.slice(hashIndex + 1)}`, label); } return names; } function findDescendantWithText(root, text) { if (!root) { return null; } const candidates = root.querySelectorAll("div, span"); for (const candidate of candidates) { if ((candidate.textContent || "").trim().startsWith(text)) { return candidate; } } return null; } function getEnhancingNotesContainer(panel) { if (!panel) { return null; } const candidates = Array.from(panel.querySelectorAll("div")).filter((element) => { const text = (element.textContent || "").trim().replace(/\s+/g, " "); return text.startsWith("推荐等级") && text.length < 40; }); const noteText = candidates.sort((left, right) => { return (left.textContent || "").length - (right.textContent || "").length; })[0] || null; return noteText?.parentElement || null; } function getEnhancingPanelSelection() { const panel = getEnhancingPanel(); if (!panel) { return null; } const itemUse = Array.from(panel.querySelectorAll("use")).find((node) => { const value = node.getAttribute("href") || node.getAttribute("xlink:href") || ""; return value.includes("items_sprite"); }); const href = itemUse ? (itemUse.getAttribute("href") || itemUse.getAttribute("xlink:href") || "") : ""; const hashIndex = href.indexOf("#"); const itemHrid = hashIndex >= 0 ? `/items/${href.slice(hashIndex + 1)}` : ""; if (!itemHrid || !state.itemDetailMap?.[itemHrid]) { return null; } const targetInput = panel.querySelector('input[role="spinbutton"], input[type="number"]'); const targetLevel = Math.max(1, Math.min(20, Number(targetInput?.value || 1) || 1)); const loadoutContainer = findDescendantWithText(panel, "配装")?.parentElement || null; const loadoutInput = loadoutContainer ? Array.from(loadoutContainer.querySelectorAll("input")).find((input) => /^\d+$/.test(String(input.value || ""))) : null; const loadout = getLoadoutById(Number(loadoutInput?.value || 0)) || resolveSkillingLoadout(ENHANCING_ACTION_TYPE).loadout; return { panel, itemHrid, targetLevel, loadout, notesContainer: getEnhancingNotesContainer(panel), }; } function solveLinearSystem(matrix, constants) { const size = constants.length; const rows = matrix.map((row, index) => row.slice().concat([constants[index]])); for (let col = 0; col < size; col += 1) { let pivot = col; for (let row = col + 1; row < size; row += 1) { if (Math.abs(rows[row][col]) > Math.abs(rows[pivot][col])) { pivot = row; } } if (Math.abs(rows[pivot][col]) < 1e-12) { return null; } if (pivot !== col) { const temp = rows[col]; rows[col] = rows[pivot]; rows[pivot] = temp; } const pivotValue = rows[col][col]; for (let currentCol = col; currentCol <= size; currentCol += 1) { rows[col][currentCol] /= pivotValue; } for (let row = 0; row < size; row += 1) { if (row === col) { continue; } const factor = rows[row][col]; if (!factor) { continue; } for (let currentCol = col; currentCol <= size; currentCol += 1) { rows[row][currentCol] -= factor * rows[col][currentCol]; } } } return rows.map((row) => row[size]); } function getEnhancingProtectionOptions(itemHrid) { const itemDetail = state.itemDetailMap?.[itemHrid]; if (!itemDetail) { return []; } const seen = new Set(); const options = []; for (const candidateHrid of [itemHrid].concat(itemDetail.protectionItemHrids || [])) { if (!candidateHrid || candidateHrid === "/items/mirror_of_protection" || candidateHrid.includes("_refined") || seen.has(candidateHrid)) { continue; } seen.add(candidateHrid); options.push(candidateHrid); } return options; } function getEnhancingAttemptMetrics(itemHrid, explicitLoadout) { const itemDetail = state.itemDetailMap?.[itemHrid]; const actionDetail = state.actionDetailMap?.[ENHANCING_ACTION_HRID]; if (!itemDetail || !actionDetail) { return null; } const totals = buildEquipmentNoncombatTotalsForLoadout(ENHANCING_ACTION_TYPE, explicitLoadout); const teaBuffs = getTeaBuffsForLoadout(ENHANCING_ACTION_TYPE, explicitLoadout); const globalBuffs = getGlobalActionBuffs(ENHANCING_ACTION_TYPE); const observatoryLevel = getHouseRoomLevel("/house_rooms/observatory"); const enhancingLevel = getSkillLevel("/skills/enhancing"); const effectiveLevel = enhancingLevel + teaBuffs.skillLevelBonus; const itemLevel = Math.max(1, Number(itemDetail.itemLevel || 1)); const enhancingSuccessBonus = Number(totals.enhancingSuccess || 0) * 100 + sumBuffsByType(globalBuffs, "/buff_types/enhancing_success") * 100; const actionSpeedBonus = Number(totals.enhancingSpeed || 0) * 100 + Number(totals.skillingSpeed || 0) * 100 + sumBuffsByType(globalBuffs, "/buff_types/action_speed") * 100 + teaBuffs.actionLevelPenalty * 0; let totalBonus = 1; if (effectiveLevel >= itemLevel) { totalBonus = 1 + (0.05 * (effectiveLevel + observatoryLevel - itemLevel) + enhancingSuccessBonus) / 100; } else { totalBonus = (1 - (0.5 * (1 - effectiveLevel / itemLevel))) + ((0.05 * observatoryLevel) + enhancingSuccessBonus) / 100; } const speedBonus = (enhancingLevel > itemLevel ? (effectiveLevel + observatoryLevel - itemLevel) : observatoryLevel) + actionSpeedBonus; const attemptSeconds = Math.max((actionDetail.baseTimeCost / 1000000000) / (1 + speedBonus / 100), 3); return { attemptSeconds, totalBonus, itemLevel, effectiveLevel, observatoryLevel, teaBuffs, }; } function getEnhancingMaterialSeconds(itemHrid) { if (itemHrid === "/items/coin") { return 0; } const seconds = calculateItemSeconds(itemHrid); return Number.isFinite(seconds) && seconds > 0 ? seconds : 0; } function getEnhancingTeaSeconds(teaBuffs, attemptSeconds) { const teaPerAction = Number(attemptSeconds || 0) / Math.max(teaBuffs?.durationSeconds || 300, 1); let total = 0; for (const teaItemHrid of teaBuffs?.activeTeas || []) { const teaSeconds = calculateItemSeconds(teaItemHrid); if (teaSeconds == null || !Number.isFinite(teaSeconds) || teaSeconds <= 0) { continue; } total += teaPerAction * teaSeconds; } return total; } function solveEnhancingAttempts(stopAt, protectAt, successMultiplier, blessedExtraChance) { const size = stopAt + 1; const matrix = Array.from({ length: size }, () => Array(size).fill(0)); const attemptsConstants = Array(size).fill(0); const protectsConstants = Array(size).fill(0); matrix[stopAt][stopAt] = 1; for (let level = 0; level < stopAt; level += 1) { const baseSuccessChance = clamp01((ENHANCING_SUCCESS_RATES[level] || ENHANCING_SUCCESS_RATES[ENHANCING_SUCCESS_RATES.length - 1]) * successMultiplier); const doubleSuccessChance = clamp01(baseSuccessChance * blessedExtraChance); const normalSuccessChance = Math.max(0, baseSuccessChance - doubleSuccessChance); const failureChance = Math.max(0, 1 - baseSuccessChance); const failureUsesProtection = level >= protectAt; const failureDestination = failureUsesProtection ? Math.max(0, level - 1) : 0; const normalSuccessDestination = Math.min(stopAt, level + 1); const doubleSuccessDestination = Math.min(stopAt, level + 2); matrix[level][level] = 1; matrix[level][normalSuccessDestination] -= normalSuccessChance; matrix[level][doubleSuccessDestination] -= doubleSuccessChance; matrix[level][failureDestination] -= failureChance; attemptsConstants[level] = 1; protectsConstants[level] = failureChance * (failureUsesProtection ? 1 : 0); } const attempts = solveLinearSystem(matrix, attemptsConstants); const protects = solveLinearSystem(matrix, protectsConstants); if (!attempts || !protects) { return null; } return { attempts: attempts[0], protects: protects[0], }; } function getEnhancingRecommendationForItem(itemHrid, targetLevel, explicitLoadout = null) { if (isMissingDerivedRuntimeState()) { ensureRuntimeStateFresh(); } const itemDetail = state.itemDetailMap?.[itemHrid]; if (!itemDetail) { return null; } if (targetLevel < 2) { return { itemHrid, targetLevel, loadout: explicitLoadout || resolveSkillingLoadout(ENHANCING_ACTION_TYPE).loadout, recommendProtectAt: 0, recommendMaterialHrid: "", recommendMaterialName: isZh ? "无需" : "None", totalSeconds: calculateItemSeconds(itemHrid) || 0, attempts: 0, protects: 0, enhancementMaterialCounts: [], perAttemptSeconds: 0, attemptSeconds: 0, }; } const loadout = explicitLoadout || resolveSkillingLoadout(ENHANCING_ACTION_TYPE).loadout; const metrics = getEnhancingAttemptMetrics(itemHrid, loadout); if (!metrics) { return null; } const activeProtectionOptions = getEnhancingProtectionOptions(itemHrid); const protectionCandidates = activeProtectionOptions .map((candidateHrid) => ({ itemHrid: candidateHrid, itemName: getLocalizedItemName(candidateHrid, state.itemDetailMap?.[candidateHrid]?.name || candidateHrid), seconds: getEnhancingMaterialSeconds(candidateHrid), })) .sort((left, right) => left.seconds - right.seconds); const bestProtectionMaterial = protectionCandidates[0] || null; const hasValidProtectionTime = Boolean( bestProtectionMaterial && Number.isFinite(bestProtectionMaterial.seconds) && bestProtectionMaterial.seconds > 0 ); const baseItemSeconds = Math.max(0, Number(calculateItemSeconds(itemHrid) || 0)); const enhancementMaterialSeconds = (itemDetail.enhancementCosts || []).reduce((total, cost) => { if (!cost?.itemHrid || cost.itemHrid === "/items/coin") { return total; } return total + Number(cost.count || 0) * getEnhancingMaterialSeconds(cost.itemHrid); }, 0); const enhancementMaterialCounts = (itemDetail.enhancementCosts || []) .filter((cost) => cost?.itemHrid && cost.itemHrid !== "/items/coin") .map((cost) => ({ itemHrid: cost.itemHrid, itemName: getLocalizedItemName(cost.itemHrid, state.itemDetailMap?.[cost.itemHrid]?.name || cost.itemHrid), countPerAttempt: Number(cost.count || 0), })); const teaSeconds = getEnhancingTeaSeconds(metrics.teaBuffs, metrics.attemptSeconds); const perAttemptSeconds = metrics.attemptSeconds + enhancementMaterialSeconds + teaSeconds; const blessedExtraChance = Math.max(0, Number(metrics.teaBuffs.blessedFraction || 0)); let bestPlan = null; if (!hasValidProtectionTime) { const fallbackProtectAt = targetLevel >= 7 ? 7 : (targetLevel >= 2 ? targetLevel : 0); const fallbackSolved = fallbackProtectAt > 0 ? solveEnhancingAttempts(targetLevel, fallbackProtectAt, metrics.totalBonus, blessedExtraChance) : null; const fallbackMaterial = protectionCandidates[0] || { itemHrid: activeProtectionOptions[0] || "", itemName: "", seconds: 0, }; return { itemHrid, targetLevel, loadout, recommendProtectAt: fallbackProtectAt, recommendMaterialHrid: fallbackMaterial.itemHrid || "", recommendMaterialName: fallbackMaterial.itemName || (isZh ? "无法计算" : "Unavailable"), totalSeconds: baseItemSeconds + perAttemptSeconds * Number(fallbackSolved?.attempts || 0) + Number(fallbackMaterial.seconds || 0) * Number(fallbackSolved?.protects || 0), attempts: Number(fallbackSolved?.attempts || 0), protects: Number(fallbackSolved?.protects || 0), enhancementMaterialCounts, perAttemptSeconds, attemptSeconds: metrics.attemptSeconds, baseItemSeconds, }; } const protectionCandidatesToTry = [targetLevel + 1]; for (let protectAt = 2; protectAt <= targetLevel; protectAt += 1) { protectionCandidatesToTry.push(protectAt); } for (const protectAt of protectionCandidatesToTry) { const solved = solveEnhancingAttempts(targetLevel, protectAt, metrics.totalBonus, blessedExtraChance); if (!solved) { continue; } const totalSeconds = baseItemSeconds + perAttemptSeconds * solved.attempts + (bestProtectionMaterial ? bestProtectionMaterial.seconds * solved.protects : 0); if (!bestPlan || totalSeconds < bestPlan.totalSeconds) { bestPlan = { itemHrid, targetLevel, loadout, recommendProtectAt: protectAt > targetLevel ? 0 : protectAt, recommendMaterialHrid: bestProtectionMaterial?.itemHrid || "", recommendMaterialName: bestProtectionMaterial?.itemName || (isZh ? "无" : "None"), totalSeconds, attempts: solved.attempts, protects: solved.protects, enhancementMaterialCounts, perAttemptSeconds, attemptSeconds: metrics.attemptSeconds, baseItemSeconds, }; } } return bestPlan; } function getEnhancingRecommendation() { const selection = getEnhancingPanelSelection(); if (!selection) { return null; } const recommendation = getEnhancingRecommendationForItem(selection.itemHrid, selection.targetLevel, selection.loadout); return recommendation ? { ...selection, ...recommendation } : null; } function renderEnhancingRecommendation() { document.querySelectorAll(".ictime-enhancing-recommend").forEach((node) => node.remove()); } function queueEnhancingRefresh() { if (state.enhancingRefreshQueued || state.isShutDown) { return; } state.enhancingRefreshQueued = true; requestAnimationFrame(() => { state.enhancingRefreshQueued = false; renderEnhancingRecommendation(); }); } function getAlchemyTransmutePanel() { const panel = document.querySelector('[class*="SkillActionDetail_alchemyComponent"]'); if (!(panel instanceof HTMLElement) || !panel.isConnected) { return null; } const selectedTab = Array.from(document.querySelectorAll('[class*="AlchemyPanel_tabsComponentContainer"] button, [class*="AlchemyPanel_tabsComponentContainer"] [role="tab"]')) .find((button) => button.classList.contains("Mui-selected") || button.getAttribute("aria-selected") === "true"); const selectedText = (selectedTab?.textContent || "").trim().toLowerCase(); if (selectedText && !["转化", "transmute", "当前行动", "current action"].includes(selectedText)) { return null; } return panel; } function shouldRefreshAlchemyInferenceFromTarget(target) { if (!(target instanceof Element)) { return false; } const panel = getAlchemyTransmutePanel(); if (panel?.contains(target)) { return true; } const clickable = target.closest("button, [role='button'], input, select, textarea, label"); const text = ((clickable && clickable.textContent) || target.textContent || "").trim(); return ["转化", "当前行动", "炼金", "Transmute", "Current Action", "Alchemy"].includes(text); } function isAlchemyInferenceOwnedNode(node) { if (node instanceof Element) { return node.matches(".ictime-alchemy-inference, .ictime-alchemy-inference-row") || Boolean(node.closest(".ictime-alchemy-inference, .ictime-alchemy-inference-row")); } return node instanceof Text ? Boolean(node.parentElement?.closest(".ictime-alchemy-inference, .ictime-alchemy-inference-row")) : false; } function ensureAlchemyInferenceObserver() { const panel = getAlchemyTransmutePanel(); if (state.alchemyObservedPanel === panel && state.alchemyInferenceObserver) { return; } state.alchemyInferenceObserver?.disconnect(); state.alchemyInferenceObserver = null; state.alchemyObservedPanel = null; if (!panel) { return; } const observer = new MutationObserver((mutations) => { if (state.isShutDown) { return; } let shouldRefresh = false; for (const mutation of mutations) { if (mutation.type === "characterData") { if (!isAlchemyInferenceOwnedNode(mutation.target)) { shouldRefresh = true; break; } continue; } if (mutation.type !== "childList") { continue; } const changedNodes = [...mutation.addedNodes, ...mutation.removedNodes]; if (!changedNodes.length) { continue; } if (changedNodes.every((node) => isAlchemyInferenceOwnedNode(node))) { continue; } shouldRefresh = true; break; } if (shouldRefresh) { queueAlchemyInferenceRefresh(); } }); observer.observe(panel, { childList: true, subtree: true, characterData: true }); state.alchemyInferenceObserver = observer; state.alchemyObservedPanel = panel; } function getAlchemyNotesContainer(panel) { if (!panel) { return null; } return panel.querySelector('[class*="SkillActionDetail_notes"]') || panel; } function extractItemHridFromElement(element) { if (!(element instanceof Element)) { return ""; } const anchor = element.querySelector('a[href*="#"]'); if (anchor) { const href = anchor.getAttribute("href") || ""; const hashIndex = href.indexOf("#"); if (hashIndex >= 0) { return `/items/${href.slice(hashIndex + 1)}`; } } const use = element.querySelector("use"); if (use) { const href = use.getAttribute("href") || use.getAttribute("xlink:href") || ""; const hashIndex = href.indexOf("#"); if (hashIndex >= 0) { return `/items/${href.slice(hashIndex + 1)}`; } } const labelled = element.querySelector("[aria-label]") || element.closest("[aria-label]"); const label = labelled?.getAttribute("aria-label") || ""; return findItemHridByDisplayName(label); } function parseAlchemyDropRows(panel, selector, section = "output") { return Array.from(panel.querySelectorAll(selector)).map((row) => { const children = Array.from(row.children || []); const countText = children[0]?.textContent || ""; const nameText = (children[1]?.textContent || row.querySelector('[class*="Item_name"]')?.textContent || "").trim(); const rateText = children[2]?.textContent || ""; const itemHrid = extractItemHridFromElement(row) || findItemHridByDisplayName(nameText); const count = Math.max(0, parseUiNumber(countText)); const rate = rateText ? parseUiPercent(rateText) : 1; return { row, itemHrid, itemName: nameText || getLocalizedItemName(itemHrid, itemHrid), count, rate, expectedCount: count * rate, section, }; }).filter((entry) => entry.itemHrid && Number.isFinite(entry.expectedCount) && entry.expectedCount > 0); } function getAlchemyTransmutePanelSelection() { const panel = getAlchemyTransmutePanel(); if (!panel) { return null; } const requirementsRoot = panel.querySelector('[class*="SkillActionDetail_itemRequirements"]'); const inputItems = []; for (const container of requirementsRoot?.querySelectorAll('[class*="Item_itemContainer"]') || []) { let countNode = container.previousElementSibling; while (countNode && !String(countNode.className || "").includes("SkillActionDetail_inputCount")) { countNode = countNode.previousElementSibling; } const count = Math.max(0, parseUiNumber(countNode?.textContent || 0)); const rawName = (container.querySelector('[class*="Item_name"]')?.textContent || "").trim(); const baseName = rawName.split("+")[0].trim(); const itemHrid = extractItemHridFromElement(container) || findItemHridByDisplayName(baseName); if (!itemHrid) { continue; } inputItems.push({ itemHrid, itemName: baseName || getLocalizedItemName(itemHrid, itemHrid), count, }); } if (!inputItems.length) { return null; } const sourceItemHrid = inputItems.find((item) => item.itemHrid !== "/items/coin")?.itemHrid || inputItems[0].itemHrid; const sourceItemDetail = state.itemDetailMap?.[sourceItemHrid]; if (!(sourceItemDetail?.alchemyDetail?.transmuteDropTable || []).length) { return null; } const successNode = panel.querySelector('[class*="SkillActionDetail_successRate"] [class*="SkillActionDetail_value"]'); const timeNode = panel.querySelector('[class*="SkillActionDetail_timeCost"] [class*="SkillActionDetail_value"]'); const successChance = parseUiPercent(successNode?.textContent || 0); const actionSeconds = parseUiDurationSeconds(timeNode?.textContent || 0); if (!Number.isFinite(successChance) || successChance <= 0 || !Number.isFinite(actionSeconds) || actionSeconds <= 0) { return null; } const transmuteAction = state.actionDetailMap?.["/actions/alchemy/transmute"]; const actionSummary = transmuteAction ? getActionSummary(transmuteAction) : null; const efficiencyFraction = getAlchemyDecomposeEfficiencyFraction(sourceItemHrid, actionSummary); const efficiencyMultiplier = Math.max(1 + efficiencyFraction, 1); const catalystElement = panel.querySelector( '[class*="SkillActionDetail_catalystItemInput"] [class*="Item_item"] [aria-label], ' + '[class*="SkillActionDetail_catalystItemInputContainer"] [class*="Item_item"] [aria-label]' ); const catalystItemHrid = catalystElement ? (extractItemHridFromElement(catalystElement) || findItemHridByDisplayName(catalystElement.getAttribute("aria-label") || "")) : ""; const outputItems = parseAlchemyDropRows(panel, '[class*="SkillActionDetail_alchemyOutput"] [class*="SkillActionDetail_drop__"]', "output"); const essenceDrops = parseAlchemyDropRows(panel, '[class*="SkillActionDetail_essenceDrops"] [class*="SkillActionDetail_drop__"]', "essence"); const rareDrops = parseAlchemyDropRows(panel, '[class*="SkillActionDetail_rareDrops"] [class*="SkillActionDetail_drop__"]', "rare"); return { panel, notesContainer: getAlchemyNotesContainer(panel), sourceItemHrid, sourceItemName: getLocalizedItemName(sourceItemHrid, sourceItemDetail?.name || sourceItemHrid), inputItems, catalystItemHrid, successChance, actionSeconds, rawActionSeconds: actionSeconds, efficiencyFraction, efficiencyMultiplier, outputs: outputItems.concat(essenceDrops, rareDrops), }; } function getCurrentAlchemyTransmuteInference() { const selection = getAlchemyTransmutePanelSelection(); if (!selection) { return null; } const efficiencyMultiplier = Math.max(1, Number(selection.efficiencyMultiplier || (1 + Number(selection.efficiencyFraction || 0)) || 1)); let inputBaseSecondsTotal = 0; for (const input of selection.inputItems) { if (input.itemHrid === "/items/coin") { continue; } const seconds = calculateItemSeconds(input.itemHrid); if (!isAlchemyInferenceResolvableItemSeconds(input.itemHrid, seconds)) { return null; } inputBaseSecondsTotal += Number(input.count || 0) * Math.max(0, Number(seconds || 0)); } const inputSecondsTotal = inputBaseSecondsTotal * efficiencyMultiplier; let catalystBaseSecondsTotal = 0; let catalystSecondsTotal = 0; if (selection.catalystItemHrid) { const catalystSeconds = calculateItemSeconds(selection.catalystItemHrid); if (!isAlchemyInferenceResolvableItemSeconds(selection.catalystItemHrid, catalystSeconds)) { return null; } catalystBaseSecondsTotal = Math.max(0, Number(catalystSeconds || 0)); catalystSecondsTotal = catalystBaseSecondsTotal * selection.successChance * efficiencyMultiplier; } const teaBuffs = getTeaBuffs("/action_types/alchemy"); const actionSeconds = Number(selection.actionSeconds || 0); const teaPerAction = actionSeconds / Math.max(teaBuffs.durationSeconds || 300, 1); let teaSecondsTotal = 0; for (const teaItemHrid of teaBuffs.activeTeas || []) { const teaSeconds = calculateItemSeconds(teaItemHrid); if (teaSeconds == null || !Number.isFinite(teaSeconds) || teaSeconds < 0) { continue; } teaSecondsTotal += teaPerAction * teaSeconds; } const consideredOutputs = selection.outputs.filter((output) => output.section === "output"); if (!consideredOutputs.length) { return null; } let knownOutputBaseSeconds = 0; let knownOutputSeconds = 0; const knownOutputs = []; const unknownOutputs = []; for (const output of consideredOutputs) { const baseExpectedCount = output.expectedCount; const weightedExpectedCount = baseExpectedCount * selection.successChance * efficiencyMultiplier; if (!Number.isFinite(weightedExpectedCount) || weightedExpectedCount <= 0) { continue; } const seconds = calculateItemSeconds(output.itemHrid); if (!isAlchemyInferenceResolvableItemSeconds(output.itemHrid, seconds)) { unknownOutputs.push({ ...output, baseExpectedCount, weightedExpectedCount, }); continue; } const outputSeconds = Math.max(0, Number(seconds || 0)); knownOutputBaseSeconds += baseExpectedCount * outputSeconds; knownOutputSeconds += weightedExpectedCount * outputSeconds; knownOutputs.push({ ...output, baseExpectedCount, weightedExpectedCount, }); } if (unknownOutputs.length !== 1) { return null; } const unknownOutput = unknownOutputs[0]; if (!Number.isFinite(unknownOutput.weightedExpectedCount) || unknownOutput.weightedExpectedCount <= 0) { return null; } const isAttachedRareTarget = ATTACHED_RARE_TARGET_ITEM_HRID_SET.has(unknownOutput.itemHrid); const directTargetExpectedCount = unknownOutput.weightedExpectedCount; let inputAttachedTargetExpectedCount = 0; let knownOutputAttachedTargetExpectedCount = 0; if (isAttachedRareTarget) { for (const input of selection.inputItems) { if (!input?.itemHrid || input.itemHrid === "/items/coin") { continue; } const attachedRare = getAttachedRareYieldPerItem(input.itemHrid, unknownOutput.itemHrid); if (!Number.isFinite(attachedRare) || attachedRare <= 0) { continue; } inputAttachedTargetExpectedCount += Number(input.count || 0) * efficiencyMultiplier * attachedRare; } for (const output of knownOutputs) { const attachedRare = getAttachedRareYieldPerItem(output.itemHrid, unknownOutput.itemHrid); if (!Number.isFinite(attachedRare) || attachedRare <= 0) { continue; } knownOutputAttachedTargetExpectedCount += output.weightedExpectedCount * attachedRare; } } const effectiveTargetExpectedCount = directTargetExpectedCount + inputAttachedTargetExpectedCount - knownOutputAttachedTargetExpectedCount; if (!Number.isFinite(effectiveTargetExpectedCount) || effectiveTargetExpectedCount <= 0) { return null; } const inferredSeconds = (inputSecondsTotal + catalystSecondsTotal + actionSeconds + teaSecondsTotal - knownOutputSeconds) / effectiveTargetExpectedCount; if (!Number.isFinite(inferredSeconds) || inferredSeconds <= 0) { return null; } return { ...selection, targetItemHrid: unknownOutput.itemHrid, targetItemName: unknownOutput.itemName || getLocalizedItemName(unknownOutput.itemHrid, unknownOutput.itemHrid), targetConditionalCount: unknownOutput.baseExpectedCount, targetExpectedCount: effectiveTargetExpectedCount, directTargetExpectedCount, inputAttachedTargetExpectedCount, knownOutputAttachedTargetExpectedCount, effectiveTargetExpectedCount, isAttachedRareTarget, targetRow: unknownOutput.row, inferredSeconds, inputBaseSecondsTotal, inputSecondsTotal, catalystBaseSecondsTotal, catalystSecondsTotal, rawActionSeconds: Number(selection.rawActionSeconds || selection.actionSeconds || 0), actionSeconds, efficiencyFraction: Number(selection.efficiencyFraction || 0), efficiencyMultiplier, teaSecondsTotal, knownOutputBaseSeconds, knownOutputSeconds, }; } function renderAlchemyTransmuteInference() { document.querySelectorAll(".ictime-alchemy-inference").forEach((node) => node.remove()); document.querySelectorAll(".ictime-alchemy-inference-row").forEach((node) => node.remove()); const inference = getCurrentAlchemyTransmuteInference(); if (!inference) { return; } if (inference.targetRow instanceof HTMLElement) { const inline = document.createElement("span"); inline.className = "ictime-alchemy-inference-row"; inline.dataset.ictimeOwner = instanceId; inline.style.marginLeft = "6px"; inline.style.color = "#7dd3fc"; inline.style.fontSize = "0.85em"; inline.textContent = isZh ? `ICTime推导 ${formatAutoDuration(inference.inferredSeconds)}` : `ICTime ${formatAutoDuration(inference.inferredSeconds)}`; inference.targetRow.appendChild(inline); } if (isTimeCalculatorCompactModeEnabled()) { return; } const host = inference.notesContainer || inference.panel; if (!(host instanceof HTMLElement)) { return; } const efficiencyMultiplier = Math.max(1, Number(inference.efficiencyMultiplier || (1 + Number(inference.efficiencyFraction || 0)) || 1)); const efficiencyText = formatPreciseNumber(efficiencyMultiplier); const successRateTextCurrent = `${formatPreciseNumber(inference.successChance * 100)}%`; const actionTermTextCurrent = isZh ? `行动(${formatAutoDuration(inference.actionSeconds)})` : `action(${formatAutoDuration(inference.actionSeconds)})`; const inputTermTextCurrent = efficiencyMultiplier > 1 ? (isZh ? `输入(${formatAutoDuration(inference.inputBaseSecondsTotal)} * ${efficiencyText} = ${formatAutoDuration(inference.inputSecondsTotal)})` : `input(${formatAutoDuration(inference.inputBaseSecondsTotal)} * ${efficiencyText} = ${formatAutoDuration(inference.inputSecondsTotal)})`) : (isZh ? `输入(${formatAutoDuration(inference.inputSecondsTotal)})` : `input(${formatAutoDuration(inference.inputSecondsTotal)})`); const catalystTermTextCurrent = Number(inference.catalystBaseSecondsTotal || 0) > 0 ? (efficiencyMultiplier > 1 || Number(inference.successChance || 0) < 1 ? (isZh ? `催化剂(${formatAutoDuration(inference.catalystBaseSecondsTotal)} * ${successRateTextCurrent} * ${efficiencyText} = ${formatAutoDuration(inference.catalystSecondsTotal)})` : `catalyst(${formatAutoDuration(inference.catalystBaseSecondsTotal)} * ${successRateTextCurrent} * ${efficiencyText} = ${formatAutoDuration(inference.catalystSecondsTotal)})`) : (isZh ? `催化剂(${formatAutoDuration(inference.catalystSecondsTotal)})` : `catalyst(${formatAutoDuration(inference.catalystSecondsTotal)})`)) : (isZh ? "催化剂(0 s)" : "catalyst(0 s)"); const knownOutputTermTextCurrent = (efficiencyMultiplier > 1 || Number(inference.successChance || 0) < 1) && Number(inference.knownOutputBaseSeconds || 0) > 0 ? (isZh ? `其余产出(${formatAutoDuration(inference.knownOutputBaseSeconds)} * ${successRateTextCurrent} * ${efficiencyText} = ${formatAutoDuration(inference.knownOutputSeconds)})` : `other outputs(${formatAutoDuration(inference.knownOutputBaseSeconds)} * ${successRateTextCurrent} * ${efficiencyText} = ${formatAutoDuration(inference.knownOutputSeconds)})`) : (isZh ? `其余产出(${formatAutoDuration(inference.knownOutputSeconds)})` : `other outputs(${formatAutoDuration(inference.knownOutputSeconds)})`); const numeratorTextCurrent = isZh ? `${inputTermTextCurrent} + ${actionTermTextCurrent} + 茶(${formatAutoDuration(inference.teaSecondsTotal)}) + ${catalystTermTextCurrent} - ${knownOutputTermTextCurrent}` : `${inputTermTextCurrent} + ${actionTermTextCurrent} + tea(${formatAutoDuration(inference.teaSecondsTotal)}) + ${catalystTermTextCurrent} - ${knownOutputTermTextCurrent}`; const directDenominatorTextCurrent = efficiencyMultiplier > 1 || Number(inference.successChance || 0) < 1 ? `${formatPreciseNumber(inference.targetConditionalCount)} * ${successRateTextCurrent} * ${efficiencyText} = ${formatPreciseNumber(inference.directTargetExpectedCount || inference.targetExpectedCount)}` : formatPreciseNumber(inference.directTargetExpectedCount || inference.targetExpectedCount); const denominatorTextCurrent = formatPreciseNumber(inference.effectiveTargetExpectedCount || inference.targetExpectedCount); const targetRateTextCurrent = `${formatPreciseNumber(inference.targetConditionalCount * 100)}%`; const expectedRateTextCurrent = `${formatPreciseNumber((inference.directTargetExpectedCount || inference.targetExpectedCount) * 100)}%`; const formulaBlockCurrent = document.createElement("div"); formulaBlockCurrent.className = "ictime-alchemy-inference"; formulaBlockCurrent.dataset.ictimeOwner = instanceId; formulaBlockCurrent.style.marginTop = "8px"; formulaBlockCurrent.style.color = "#7dd3fc"; formulaBlockCurrent.style.fontSize = "0.85rem"; formulaBlockCurrent.style.lineHeight = "1.35"; const formulaTitleCurrent = document.createElement("div"); formulaTitleCurrent.textContent = isZh ? `ICTime推导公式: (${numeratorTextCurrent}) / ${denominatorTextCurrent} = ${formatAutoDuration(inference.inferredSeconds)}` : `ICTime formula: (${numeratorTextCurrent}) / ${denominatorTextCurrent} = ${formatAutoDuration(inference.inferredSeconds)}`; const formulaDetailCurrent = document.createElement("div"); formulaDetailCurrent.style.opacity = "0.85"; if (inference.isAttachedRareTarget) { const attachedLabel = getAttachedRareLabel(inference.targetItemHrid); formulaDetailCurrent.textContent = isZh ? `目标期望产出: 直接转化(${directDenominatorTextCurrent}) + 输入附带${attachedLabel}(${formatPreciseNumber(inference.inputAttachedTargetExpectedCount)}) - 其余产出附带${attachedLabel}(${formatPreciseNumber(inference.knownOutputAttachedTargetExpectedCount)}) = ${formatPreciseNumber(inference.effectiveTargetExpectedCount)}` : `expected target output: direct(${directDenominatorTextCurrent}) + input extra ${attachedLabel}(${formatPreciseNumber(inference.inputAttachedTargetExpectedCount)}) - other outputs extra ${attachedLabel}(${formatPreciseNumber(inference.knownOutputAttachedTargetExpectedCount)}) = ${formatPreciseNumber(inference.effectiveTargetExpectedCount)}`; } else { formulaDetailCurrent.textContent = isZh ? `目标期望产出: ${targetRateTextCurrent} * ${successRateTextCurrent}${efficiencyMultiplier > 1 ? ` * ${efficiencyText}` : ""} = ${expectedRateTextCurrent}` : `expected target output: ${targetRateTextCurrent} * ${successRateTextCurrent}${efficiencyMultiplier > 1 ? ` * ${efficiencyText}` : ""} = ${expectedRateTextCurrent}`; } formulaBlockCurrent.appendChild(formulaTitleCurrent); formulaBlockCurrent.appendChild(formulaDetailCurrent); host.appendChild(formulaBlockCurrent); return; const actionTermText = Number(inference.efficiencyFraction || 0) > 0 ? (isZh ? `行动(${formatAutoDuration(inference.rawActionSeconds || inference.effectiveActionSeconds)} / ${formatPreciseNumber(1 + Number(inference.efficiencyFraction || 0))} = ${formatAutoDuration(inference.effectiveActionSeconds || inference.actionSeconds)})` : `action(${formatAutoDuration(inference.rawActionSeconds || inference.effectiveActionSeconds)} / ${formatPreciseNumber(1 + Number(inference.efficiencyFraction || 0))} = ${formatAutoDuration(inference.effectiveActionSeconds || inference.actionSeconds)})`) : (isZh ? `行动(${formatAutoDuration(inference.effectiveActionSeconds || inference.actionSeconds)})` : `action(${formatAutoDuration(inference.effectiveActionSeconds || inference.actionSeconds)})`); const numeratorText = isZh ? `输入(${formatAutoDuration(inference.inputSecondsTotal)}) + ${actionTermText} + 茶(${formatAutoDuration(inference.teaSecondsTotal)}) + 催化剂(${formatAutoDuration(inference.catalystSecondsTotal)}) - 其余产出(${formatAutoDuration(inference.knownOutputSeconds)})` : `input(${formatAutoDuration(inference.inputSecondsTotal)}) + ${actionTermText} + tea(${formatAutoDuration(inference.teaSecondsTotal)}) + catalyst(${formatAutoDuration(inference.catalystSecondsTotal)}) - other outputs(${formatAutoDuration(inference.knownOutputSeconds)})`; const denominatorText = formatPreciseNumber(inference.targetExpectedCount); const targetRateText = `${formatPreciseNumber(inference.targetConditionalCount * 100)}%`; const successRateText = `${formatPreciseNumber(inference.successChance * 100)}%`; const expectedRateText = `${formatPreciseNumber(inference.targetExpectedCount * 100)}%`; const formulaBlock = document.createElement("div"); formulaBlock.className = "ictime-alchemy-inference"; formulaBlock.dataset.ictimeOwner = instanceId; formulaBlock.style.marginTop = "8px"; formulaBlock.style.color = "#7dd3fc"; formulaBlock.style.fontSize = "0.85rem"; formulaBlock.style.lineHeight = "1.35"; const formulaTitle = document.createElement("div"); formulaTitle.textContent = isZh ? `ICTime推导公式: (${numeratorText}) / ${denominatorText} = ${formatAutoDuration(inference.inferredSeconds)}` : `ICTime formula: (${numeratorText}) / ${denominatorText} = ${formatAutoDuration(inference.inferredSeconds)}`; const formulaDetail = document.createElement("div"); formulaDetail.style.opacity = "0.85"; formulaDetail.textContent = isZh ? `目标期望产出: ${targetRateText} × ${successRateText} = ${expectedRateText}` : `expected target output: ${targetRateText} * ${successRateText} = ${expectedRateText}`; formulaBlock.appendChild(formulaTitle); formulaBlock.appendChild(formulaDetail); host.appendChild(formulaBlock); return; const block = document.createElement("div"); block.className = "ictime-alchemy-inference"; block.dataset.ictimeOwner = instanceId; block.style.marginTop = "8px"; block.style.color = "#7dd3fc"; block.style.fontSize = "0.85rem"; block.style.lineHeight = "1.35"; const title = document.createElement("div"); title.textContent = isZh ? `ICTime推导: ${inference.targetItemName} ≈ ${formatAutoDuration(inference.inferredSeconds)}` : `ICTime derive: ${inference.targetItemName} ≈ ${formatAutoDuration(inference.inferredSeconds)}`; const detail = document.createElement("div"); detail.style.opacity = "0.85"; detail.textContent = isZh ? `输入${formatAutoDuration(inference.inputSecondsTotal)} + 行动${formatAutoDuration(inference.actionSeconds)} + 茶${formatAutoDuration(inference.teaSecondsTotal)} - 其余产出${formatAutoDuration(inference.knownOutputSeconds)}` : `input ${formatAutoDuration(inference.inputSecondsTotal)} + action ${formatAutoDuration(inference.actionSeconds)} + tea ${formatAutoDuration(inference.teaSecondsTotal)} - other outputs ${formatAutoDuration(inference.knownOutputSeconds)}`; block.appendChild(title); block.appendChild(detail); host.appendChild(block); } function queueAlchemyInferenceRefresh() { if (state.alchemyInferenceRefreshQueued || state.isShutDown) { return; } state.alchemyInferenceRefreshQueued = true; requestAnimationFrame(() => { state.alchemyInferenceRefreshQueued = false; ensureAlchemyInferenceObserver(); renderAlchemyTransmuteInference(); }); } function scheduleAlchemyInferenceRefreshBurst() { for (const timerId of state.alchemyInferenceDelayTimers) { clearTimeout(timerId); } state.alchemyInferenceDelayTimers = [120, 400, 900].map((delayMs) => setTimeout(() => { queueAlchemyInferenceRefresh(); }, delayMs)); } function getCurrentCharacterId() { const fromUrl = Number(new URLSearchParams(location.search).get("characterId") || 0); return Number.isFinite(fromUrl) && fromUrl > 0 ? fromUrl : 0; } function getTimeCalculatorStorageKey(characterId = getCurrentCharacterId()) { return `ICTime_TimeCalculator_${characterId || "default"}`; } function normalizeDungeonTier(value) { const numeric = Math.floor(Number(value || 0)); if (numeric >= 2) { return 2; } if (numeric >= 1) { return 1; } return 0; } function getRefinementExpectedCountByTier(tier) { return Number(REFINEMENT_TIER_EXPECTED_COUNTS[normalizeDungeonTier(tier)] || 0); } function getBaseDungeonChestHrid(itemHrid) { if (DUNGEON_CHEST_CONFIG[itemHrid]) { return itemHrid; } return REFINEMENT_CHEST_TO_BASE_CHEST_HRID[itemHrid] || REFINEMENT_SHARD_TO_BASE_CHEST_HRID[itemHrid] || ""; } function getDungeonChestConfigByAnyItem(itemHrid) { const baseChestItemHrid = getBaseDungeonChestHrid(itemHrid); return baseChestItemHrid ? DUNGEON_CHEST_CONFIG[baseChestItemHrid] || null : null; } function getRefinementChestOpenKeyHrid(itemHrid) { const config = getDungeonChestConfigByAnyItem(itemHrid); if (!config?.refinementChestItemHrid) { return ""; } const runtimeOpenKeyHrid = state.itemDetailMap?.[config.refinementChestItemHrid]?.openKeyItemHrid; return runtimeOpenKeyHrid || config.keyItemHrid || ""; } function getCombatChestQuantityMultiplier() { return Math.max(0.0001, 1 + getCombatChestQuantityFraction()); } function isTimeCalculatorSupportedItem(itemHrid) { return TIME_CALCULATOR_ITEM_HRIDS.includes(itemHrid); } function getTimeCalculatorEntryType(itemHrid) { if (DUNGEON_CHEST_ITEM_HRIDS.includes(itemHrid)) { return "chest"; } if (REFINEMENT_CHEST_ITEM_HRIDS.includes(itemHrid)) { return "refinement_chest"; } if (KEY_FRAGMENT_ITEM_HRIDS.includes(itemHrid)) { return "fragment"; } return ""; } function getTimeCalculatorItemDisplayName(itemHrid) { if (!itemHrid) { return ""; } if (isZh && TIME_CALCULATOR_ITEM_NAME_OVERRIDES_ZH[itemHrid]) { return TIME_CALCULATOR_ITEM_NAME_OVERRIDES_ZH[itemHrid]; } return getLocalizedItemName(itemHrid, state.itemDetailMap?.[itemHrid]?.name || itemHrid); } function getConfiguredEssenceDecomposeSourceItemHrid(essenceHrid) { loadTimeCalculatorData(); const essenceRule = ESSENCE_DECOMPOSE_RULES[essenceHrid]; const fixedRuleSourceItemHrid = essenceRule?.type === "fixed_source" ? (essenceRule.sourceItemHrid || "") : ""; const defaultSourceItemHrid = TIME_CALCULATOR_DEFAULT_ESSENCE_SOURCE_ITEM_HRIDS[essenceHrid] || fixedRuleSourceItemHrid || ""; const configuredSourceItemHrid = state.timeCalculatorEssenceSourceItemHrids?.[essenceHrid] || defaultSourceItemHrid; if (!state.itemDetailMap) { return configuredSourceItemHrid || defaultSourceItemHrid; } if (isValidTimeCalculatorEssenceSourceItemHrid(essenceHrid, configuredSourceItemHrid)) { return configuredSourceItemHrid; } if (isValidTimeCalculatorEssenceSourceItemHrid(essenceHrid, defaultSourceItemHrid)) { return defaultSourceItemHrid; } const firstOption = getTimeCalculatorEssenceSourceOptions(essenceHrid)[0]; return firstOption?.itemHrid || configuredSourceItemHrid || defaultSourceItemHrid; } function isValidTimeCalculatorEssenceSourceItemHrid(essenceHrid, sourceItemHrid) { if (!essenceHrid || !sourceItemHrid) { return false; } const itemDetail = state.itemDetailMap?.[sourceItemHrid]; const decomposeItems = itemDetail?.alchemyDetail?.decomposeItems || []; if (!decomposeItems.some((entry) => entry?.itemHrid === essenceHrid)) { return false; } if (essenceHrid === "/items/brewing_essence") { return sourceItemHrid.endsWith("_tea_leaf"); } if (essenceHrid === "/items/tailoring_essence") { return sourceItemHrid.endsWith("_hide"); } return true; } function getTimeCalculatorEssenceSourceOptions(essenceHrid) { const options = []; const seen = new Set(); for (const [itemHrid, itemDetail] of Object.entries(state.itemDetailMap || {})) { if (!isValidTimeCalculatorEssenceSourceItemHrid(essenceHrid, itemHrid)) { continue; } if (seen.has(itemHrid)) { continue; } seen.add(itemHrid); options.push({ itemHrid, itemName: getLocalizedItemName(itemHrid, itemDetail?.name || itemHrid), }); } options.sort((left, right) => left.itemName.localeCompare(right.itemName, isZh ? "zh-CN" : "en")); return options; } function setTimeCalculatorEssenceSourceItemHrid(essenceHrid, sourceItemHrid) { loadTimeCalculatorData(); const nextSourceItemHrid = getConfiguredEssenceDecomposeSourceItemHrid(essenceHrid) === sourceItemHrid ? sourceItemHrid : (isValidTimeCalculatorEssenceSourceItemHrid(essenceHrid, sourceItemHrid) ? sourceItemHrid : getConfiguredEssenceDecomposeSourceItemHrid(essenceHrid)); const currentSourceItemHrid = getConfiguredEssenceDecomposeSourceItemHrid(essenceHrid); if (currentSourceItemHrid === nextSourceItemHrid) { return; } state.timeCalculatorEssenceSourceItemHrids = { ...TIME_CALCULATOR_DEFAULT_ESSENCE_SOURCE_ITEM_HRIDS, ...(state.timeCalculatorEssenceSourceItemHrids || {}), [essenceHrid]: nextSourceItemHrid, }; saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); refreshOpenTooltips(); renderAlchemyTransmuteInference(); renderEnhancingRecommendation(); } function loadTimeCalculatorData() { const characterId = getCurrentCharacterId(); if (state.timeCalculatorLoadedCharacterId === characterId) { return; } state.timeCalculatorLoadedCharacterId = characterId; state.timeCalculatorEntries = []; state.timeCalculatorCompactMode = false; state.timeCalculatorEssenceSourceItemHrids = { ...TIME_CALCULATOR_DEFAULT_ESSENCE_SOURCE_ITEM_HRIDS }; const raw = localStorage.getItem(getTimeCalculatorStorageKey(characterId)); if (!raw) { return; } try { const parsed = JSON.parse(raw); state.timeCalculatorCompactMode = Boolean(parsed?.compactMode); const parsedEssenceSources = parsed?.essenceSources && typeof parsed.essenceSources === "object" ? parsed.essenceSources : {}; state.timeCalculatorEssenceSourceItemHrids = { ...TIME_CALCULATOR_DEFAULT_ESSENCE_SOURCE_ITEM_HRIDS, ...parsedEssenceSources, }; const entries = Array.isArray(parsed?.entries) ? parsed.entries : []; state.timeCalculatorEntries = entries .filter((entry) => entry && isTimeCalculatorSupportedItem(entry.itemHrid || entry.chestItemHrid)) .map((entry, index) => ({ id: String(entry.id || `chest-${Date.now()}-${index}`), itemHrid: entry.itemHrid || entry.chestItemHrid, collapsed: Boolean(entry.collapsed), dungeonTier: normalizeDungeonTier(entry.dungeonTier), runMinutes: parseNonNegativeDecimal(entry.runMinutes), quantityPer24h: parseNonNegativeDecimal(entry.quantityPer24h), foods: Array.isArray(entry.foods) ? entry.foods.map((item, itemIndex) => ({ id: String(item.id || `food-${index}-${itemIndex}`), itemHrid: item.itemHrid, perHour: parseNonNegativeDecimal(item.perHour), })).filter((item) => item.itemHrid) : [], drinks: Array.isArray(entry.drinks) ? entry.drinks.map((item, itemIndex) => ({ id: String(item.id || `drink-${index}-${itemIndex}`), itemHrid: item.itemHrid, perHour: parseNonNegativeDecimal(item.perHour), })).filter((item) => item.itemHrid) : [], })); } catch (error) { console.error("[ICTime] Failed to load time calculator data.", error); } } function saveTimeCalculatorData() { const characterId = getCurrentCharacterId(); state.timeCalculatorLoadedCharacterId = characterId; localStorage.setItem(getTimeCalculatorStorageKey(characterId), JSON.stringify({ compactMode: Boolean(state.timeCalculatorCompactMode), essenceSources: { ...TIME_CALCULATOR_DEFAULT_ESSENCE_SOURCE_ITEM_HRIDS, ...(state.timeCalculatorEssenceSourceItemHrids || {}), }, entries: state.timeCalculatorEntries.map((entry) => ({ id: entry.id, itemHrid: entry.itemHrid, collapsed: Boolean(entry.collapsed), dungeonTier: normalizeDungeonTier(entry.dungeonTier), runMinutes: parseNonNegativeDecimal(entry.runMinutes), quantityPer24h: parseNonNegativeDecimal(entry.quantityPer24h), foods: (entry.foods || []).map((item) => ({ id: item.id, itemHrid: item.itemHrid, perHour: parseNonNegativeDecimal(item.perHour), })), drinks: (entry.drinks || []).map((item) => ({ id: item.id, itemHrid: item.itemHrid, perHour: parseNonNegativeDecimal(item.perHour), })), })), })); clearCaches(); } function isTimeCalculatorCompactModeEnabled() { loadTimeCalculatorData(); return Boolean(state.timeCalculatorCompactMode); } function isTimeCalculatorSettingsOpen() { return Boolean(state.timeCalculatorSettingsOpen); } function setTimeCalculatorSettingsOpen(open) { const nextValue = Boolean(open); if (state.timeCalculatorSettingsOpen === nextValue) { return; } state.timeCalculatorSettingsOpen = nextValue; rerenderTimeCalculatorPanel(); } function setTimeCalculatorCompactMode(enabled) { loadTimeCalculatorData(); const nextValue = Boolean(enabled); if (state.timeCalculatorCompactMode === nextValue) { return; } state.timeCalculatorCompactMode = nextValue; saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); refreshOpenTooltips(); renderAlchemyTransmuteInference(); renderEnhancingRecommendation(); } function rerenderTimeCalculatorPanel() { if (state.timeCalculatorContainer?.isConnected) { renderTimeCalculatorPanel(); return; } queueTimeCalculatorRefresh(); } function shouldDeferTimeCalculatorRefresh() { if (state.timeCalculatorSettingsOpen) { return true; } const container = state.timeCalculatorContainer; const activeElement = document.activeElement; if (!container?.isConnected || !(activeElement instanceof HTMLElement) || !container.contains(activeElement)) { return false; } return activeElement.isContentEditable || ["INPUT", "SELECT", "TEXTAREA"].includes(activeElement.tagName); } function flushPendingTimeCalculatorRefresh() { if (!state.timeCalculatorRefreshPending || state.timeCalculatorRefreshQueued || state.isShutDown) { return; } if (shouldDeferTimeCalculatorRefresh()) { return; } state.timeCalculatorRefreshPending = false; queueTimeCalculatorRefresh(); } function moveTimeCalculatorEntry(entryId, offset) { const index = state.timeCalculatorEntries.findIndex((entry) => entry.id === entryId); if (index < 0) { return; } const nextIndex = Math.max(0, Math.min(state.timeCalculatorEntries.length - 1, index + offset)); if (nextIndex === index) { return; } const [entry] = state.timeCalculatorEntries.splice(index, 1); state.timeCalculatorEntries.splice(nextIndex, 0, entry); saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); } function syncTimeCalculatorEntriesFromCardOrder(container) { if (!(container instanceof HTMLElement)) { return; } const orderedIds = Array.from(container.querySelectorAll(".ictime-timecalc-entry-card")) .map((node) => node.dataset.entryId || "") .filter(Boolean); if (!orderedIds.length) { return; } const entryMap = new Map(state.timeCalculatorEntries.map((entry) => [entry.id, entry])); const reorderedEntries = []; const seen = new Set(); for (const entryId of orderedIds) { const entry = entryMap.get(entryId); if (!entry || seen.has(entryId)) { continue; } reorderedEntries.push(entry); seen.add(entryId); } for (const entry of state.timeCalculatorEntries) { if (entry?.id && !seen.has(entry.id)) { reorderedEntries.push(entry); } } state.timeCalculatorEntries = reorderedEntries; saveTimeCalculatorData(); } function animateTimeCalculatorCardReorder(container, draggingCard, targetCard = null) { if (!(container instanceof HTMLElement) || !(draggingCard instanceof HTMLElement)) { return; } const cards = Array.from(container.querySelectorAll(".ictime-timecalc-entry-card")); const firstRects = new Map(); cards.forEach((card) => { firstRects.set(card, card.getBoundingClientRect()); }); if (targetCard instanceof HTMLElement && targetCard !== draggingCard) { container.insertBefore(draggingCard, targetCard); } else if (container.lastElementChild !== draggingCard) { container.appendChild(draggingCard); } cards.forEach((card) => { const first = firstRects.get(card); const last = card.getBoundingClientRect(); if (!first) { return; } const dx = first.left - last.left; const dy = first.top - last.top; if (!dx && !dy) { return; } card.style.transform = `translate(${dx}px, ${dy}px)`; card.style.transition = "transform 0s"; card.style.willChange = "transform"; requestAnimationFrame(() => { card.style.transform = ""; card.style.transition = "transform 150ms cubic-bezier(.2,.8,.2,1)"; }); }); } function enableTimeCalculatorPointerSort(container) { if (!(container instanceof HTMLElement) || container.dataset.ictimePointerSort === "true") { return; } container.dataset.ictimePointerSort = "true"; let draggingCard = null; let captureHandle = null; let pointerY = 0; let rafPending = false; const processMove = () => { rafPending = false; if (!(draggingCard instanceof HTMLElement)) { return; } const cards = Array.from(container.querySelectorAll(".ictime-timecalc-entry-card")); const draggingIndex = cards.indexOf(draggingCard); if (draggingIndex < 0) { return; } for (const card of cards) { if (card === draggingCard) { continue; } const box = card.getBoundingClientRect(); const middle = box.top + (box.height / 2); if (pointerY < middle) { if (cards[draggingIndex] !== card) { animateTimeCalculatorCardReorder(container, draggingCard, card); } return; } } animateTimeCalculatorCardReorder(container, draggingCard, null); }; const onMove = (event) => { if (!(draggingCard instanceof HTMLElement)) { return; } pointerY = event.clientY; if (!rafPending) { rafPending = true; requestAnimationFrame(processMove); } }; const finishDrag = (pointerId = null) => { if (!(draggingCard instanceof HTMLElement)) { return; } draggingCard.style.opacity = ""; draggingCard.style.zIndex = ""; if (captureHandle instanceof HTMLElement) { captureHandle.style.cursor = "grab"; if (pointerId != null && typeof captureHandle.releasePointerCapture === "function") { try { captureHandle.releasePointerCapture(pointerId); } catch (_error) { // Ignore stale pointer capture cleanup. } } } document.body.style.userSelect = ""; syncTimeCalculatorEntriesFromCardOrder(container); draggingCard = null; captureHandle = null; document.removeEventListener("pointermove", onMove); document.removeEventListener("pointerup", onUp); document.removeEventListener("pointercancel", onCancel); }; const onUp = (event) => { finishDrag(event.pointerId); }; const onCancel = () => { finishDrag(null); }; container.addEventListener("pointerdown", (event) => { const handle = event.target instanceof Element ? event.target.closest(".ictime-timecalc-drag-handle") : null; if (!(handle instanceof HTMLElement)) { return; } const card = handle.closest(".ictime-timecalc-entry-card"); if (!(card instanceof HTMLElement)) { return; } event.preventDefault(); draggingCard = card; captureHandle = handle; pointerY = event.clientY; draggingCard.style.opacity = "0.55"; draggingCard.style.zIndex = "1"; document.body.style.userSelect = "none"; handle.style.cursor = "grabbing"; if (typeof handle.setPointerCapture === "function") { try { handle.setPointerCapture(event.pointerId); } catch (_error) { // Ignore pointer capture failures on detached nodes. } } document.addEventListener("pointermove", onMove); document.addEventListener("pointerup", onUp); document.addEventListener("pointercancel", onCancel); }); } function getTimeCalculatorItemOptions() { return TIME_CALCULATOR_ITEM_HRIDS .filter((itemHrid) => state.itemDetailMap?.[itemHrid]) .map((itemHrid) => ({ itemHrid, itemName: getTimeCalculatorItemDisplayName(itemHrid), })); } function getTimeCalculatorConsumableOptions(kind = "") { const results = []; for (const item of Object.values(state.itemDetailMap || {})) { if (!item?.hrid || !item.consumableDetail) { continue; } const consumable = item.consumableDetail; const isFood = Number(consumable.hitpointRestore || 0) > 0 || Number(consumable.manapointRestore || 0) > 0; const isDrink = Array.isArray(consumable.buffs) && consumable.buffs.length > 0; if ((kind === "food" && !isFood) || (kind === "drink" && !isDrink) || (!isFood && !isDrink)) { continue; } results.push({ itemHrid: item.hrid, itemName: getLocalizedItemName(item.hrid, item.name || item.hrid), kind: isFood ? "food" : "drink", }); } results.sort((left, right) => left.itemName.localeCompare(right.itemName, isZh ? "zh-CN" : "en")); return results; } function getCombatChestQuantityFraction() { const combatBuffs = getActionTypeBuffs("communityActionTypeBuffsDict", "/action_types/combat"); return Math.max(0, sumBuffsByType(combatBuffs, "/buff_types/combat_drop_quantity")); } function parseSimulatorDungeonTierValue(...sources) { for (const source of sources) { if (Number.isFinite(Number(source))) { return normalizeDungeonTier(source); } } for (const source of sources) { const text = String(source || ""); const match = text.match(/T\s*([012])/i); if (match) { return normalizeDungeonTier(match[1]); } } return 0; } function getCurrentCharacterName() { if (state.currentCharacterName) { return state.currentCharacterName; } const appState = getGameState?.() || null; const candidates = [ appState?.character?.name, appState?.characterDTO?.name, appState?.selectedCharacter?.name, appState?.characterName, appState?.characterSetting?.name, appState?.characterSetting?.characterName, ].filter((value) => typeof value === "string" && value.trim()); if (candidates.length > 0) { return candidates[0].trim(); } const activeCharacterLabel = Array.from(document.querySelectorAll("button, div, span")) .map((node) => (node.textContent || "").trim()) .find((text) => text && text.length <= 24 && /活跃角色|当前角色|切换角色/.test(text) === false && /Lv\\.|等级|推荐|时间计算/.test(text) === false); return activeCharacterLabel || ""; } function mapDungeonNameToChestHrid(dungeonName) { const text = String(dungeonName || ""); if (text.includes("秘法要塞")) { return "/items/enchanted_chest"; } if (text.includes("奇幻洞穴")) { return "/items/chimerical_chest"; } if (text.includes("阴森马戏团")) { return "/items/sinister_chest"; } if (text.includes("海盗基地")) { return "/items/pirate_chest"; } return ""; } function findItemHridByDisplayName(itemName) { const target = String(itemName || "").trim(); if (!target) { return ""; } if (SIMULATOR_ITEM_NAME_ALIASES[target]) { return SIMULATOR_ITEM_NAME_ALIASES[target]; } for (const [hrid, localized] of state.localizedItemNameMap.entries()) { if (localized === target) { return hrid; } } for (const [hrid, localized] of Object.entries(MWITOOLS_ZH_ITEM_NAME_OVERRIDES)) { if (localized === target) { return hrid; } } for (const [hrid, item] of Object.entries(state.itemDetailMap || {})) { if ((item?.name || "").trim() === target) { return hrid; } if (getLocalizedItemName(hrid, item?.name || hrid) === target) { return hrid; } } const normalized = target .replace(/钥匙碎片/g, "") .replace(/颜色/g, "") .replace(/黑暗/g, "暗") .replace(/石头/g, "石") .replace(/蓝色/g, "蓝") .replace(/绿色/g, "绿") .replace(/紫色/g, "紫") .replace(/白色/g, "白") .replace(/橙色/g, "橙") .replace(/棕色/g, "棕") .replace(/\s+/g, ""); const fragmentFallbacks = { "蓝": "/items/blue_key_fragment", "绿": "/items/green_key_fragment", "紫": "/items/purple_key_fragment", "白": "/items/white_key_fragment", "橙": "/items/orange_key_fragment", "棕": "/items/brown_key_fragment", "石": "/items/stone_key_fragment", "暗": "/items/dark_key_fragment", "燃烧": "/items/burning_key_fragment", }; if (fragmentFallbacks[normalized]) { return fragmentFallbacks[normalized]; } return ""; } function buildSimulatorConsumables(consumables) { const foods = []; const drinks = []; for (const consumable of consumables || []) { const itemHrid = findItemHridByDisplayName(consumable.name); if (!itemHrid) { continue; } const option = getTimeCalculatorConsumableOptions().find((candidate) => candidate.itemHrid === itemHrid); if (!option) { continue; } const target = option.kind === "food" ? foods : drinks; target.push({ id: `${option.kind}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, itemHrid, perHour: parseNonNegativeDecimal(consumable.perHour), }); } return { foods, drinks }; } function normalizeCharacterName(value) { return String(value || "") .trim() .replace(/[\s\u3000]+/g, "") .replace(/[()()\[\]【】\-_.]/g, "") .toLowerCase(); } function upsertTimeCalculatorEntry(payload) { const existing = state.timeCalculatorEntries.find((entry) => entry.itemHrid === payload.itemHrid); if (existing) { existing.collapsed = typeof payload.collapsed === "boolean" ? payload.collapsed : Boolean(existing.collapsed); existing.dungeonTier = payload.itemHrid && REFINEMENT_CHEST_ITEM_HRIDS.includes(payload.itemHrid) ? normalizeDungeonTier(payload.dungeonTier || existing.dungeonTier || 1) : 0; existing.runMinutes = parseNonNegativeDecimal(payload.runMinutes); existing.quantityPer24h = parseNonNegativeDecimal(payload.quantityPer24h); existing.foods = Array.isArray(payload.foods) ? payload.foods : []; existing.drinks = Array.isArray(payload.drinks) ? payload.drinks : []; return existing; } state.timeCalculatorEntries.push({ id: payload.id || `entry-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, itemHrid: payload.itemHrid, collapsed: typeof payload.collapsed === "boolean" ? payload.collapsed : false, dungeonTier: payload.itemHrid && REFINEMENT_CHEST_ITEM_HRIDS.includes(payload.itemHrid) ? normalizeDungeonTier(payload.dungeonTier || 1) : 0, runMinutes: parseNonNegativeDecimal(payload.runMinutes), quantityPer24h: parseNonNegativeDecimal(payload.quantityPer24h), foods: Array.isArray(payload.foods) ? payload.foods : [], drinks: Array.isArray(payload.drinks) ? payload.drinks : [], }); return state.timeCalculatorEntries[state.timeCalculatorEntries.length - 1]; } function getConfiguredTimeCalculatorEntry(itemHrid) { loadTimeCalculatorData(); return (state.timeCalculatorEntries || []).find((entry) => entry?.itemHrid === itemHrid) || null; } function getDungeonEntryKeyHridByChest(itemHrid) { return getDungeonChestConfigByAnyItem(itemHrid)?.entryKeyItemHrid || ""; } async function importFromSimulatorSnapshot() { ensureRuntimeStateFresh(true, { refreshTooltips: false }); const previousSnapshot = await sharedGetValue(SIMULATOR_IMPORT_STORAGE_KEY, null); const requestAt = Date.now(); await sharedSetValue(SIMULATOR_IMPORT_REQUEST_KEY, { requestedAt: requestAt }); let snapshot = null; const startedAt = Date.now(); while (Date.now() - startedAt < 12000) { const candidate = await sharedGetValue(SIMULATOR_IMPORT_STORAGE_KEY, null); if (candidate?.characters?.length) { snapshot = candidate; } if (candidate?.capturedAt && candidate.capturedAt >= requestAt - 500) { snapshot = candidate; break; } await new Promise((resolve) => setTimeout(resolve, 250)); } if ((!snapshot?.characters?.length) && previousSnapshot?.characters?.length) { snapshot = previousSnapshot; } state.lastSimulatorImportSnapshot = snapshot; if (!snapshot?.characters?.length) { return false; } const mappedChestItemHrid = mapDungeonNameToChestHrid(snapshot.dungeonName); const currentCharacterName = getCurrentCharacterName(); const normalizedCurrentCharacterName = normalizeCharacterName(currentCharacterName); const matched = snapshot.characters.find((entry) => normalizeCharacterName(entry.name) === normalizedCurrentCharacterName); if (!matched) { state.lastSimulatorImportResult = { failed: true, reason: "character_mismatch", currentCharacterName, normalizedCurrentCharacterName, snapshotSelectedCharacterName: snapshot.selectedCharacterName || "", snapshotCharacterNames: snapshot.characters.map((entry) => entry.name || ""), }; return false; } const { foods, drinks } = buildSimulatorConsumables(matched.consumables); let importedCount = 0; const chestItemHrid = mappedChestItemHrid || ""; const chestConfig = chestItemHrid ? DUNGEON_CHEST_CONFIG[chestItemHrid] : null; const dungeonTier = parseSimulatorDungeonTierValue(snapshot?.dungeonTier, snapshot?.dungeonName); if (chestItemHrid && parseNonNegativeDecimal(matched.averageMinutes) > 0) { if (dungeonTier <= 0) { upsertTimeCalculatorEntry({ id: `chest-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, itemHrid: chestItemHrid, runMinutes: parseNonNegativeDecimal(matched.averageMinutes), quantityPer24h: 0, foods, drinks, }); importedCount += 1; } else if (chestConfig?.refinementChestItemHrid) { upsertTimeCalculatorEntry({ id: `refinement-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, itemHrid: chestConfig.refinementChestItemHrid, dungeonTier, runMinutes: parseNonNegativeDecimal(matched.averageMinutes), quantityPer24h: 0, foods, drinks, }); importedCount += 1; } } const durationHours = Math.max(0.0001, parseNonNegativeDecimal(matched.durationHours || 24)); state.lastSimulatorImportResult = { dungeonName: snapshot.dungeonName || "", dungeonTier, chestItemHrid, matchedCharacterName: matched.name || "", durationHours, drops: (matched.nonRandomDrops || []).map((drop) => ({ name: drop.name, itemHrid: findItemHridByDisplayName(drop.name), count: parseNonNegativeDecimal(drop.count), })), }; for (const drop of matched.nonRandomDrops || []) { const fragmentHrid = findItemHridByDisplayName(drop.name); if (!KEY_FRAGMENT_ITEM_HRIDS.includes(fragmentHrid)) { continue; } upsertTimeCalculatorEntry({ id: `fragment-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, itemHrid: fragmentHrid, runMinutes: 0, quantityPer24h: parseNonNegativeDecimal(drop.count) * (24 / durationHours), foods: foods.map((item) => ({ ...item, id: `food-${Date.now()}-${Math.random().toString(36).slice(2, 6)}` })), drinks: drinks.map((item) => ({ ...item, id: `drink-${Date.now()}-${Math.random().toString(36).slice(2, 6)}` })), }); importedCount += 1; } if (!importedCount) { return false; } saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); return true; } function getTimeCalculatorEntrySummary(entry) { const itemType = getTimeCalculatorEntryType(entry?.itemHrid); const runMinutes = parseNonNegativeDecimal(entry?.runMinutes); const runSeconds = runMinutes * 60; const quantityPer24h = parseNonNegativeDecimal(entry?.quantityPer24h); const dungeonEntryKeyHrid = itemType === "chest" || itemType === "refinement_chest" ? getDungeonEntryKeyHridByChest(entry?.itemHrid) : ""; const rawDungeonEntryKeySeconds = dungeonEntryKeyHrid ? calculateItemSeconds(dungeonEntryKeyHrid) : 0; const dungeonEntryKeyFailureReason = dungeonEntryKeyHrid && (!Number.isFinite(rawDungeonEntryKeySeconds) || rawDungeonEntryKeySeconds <= 0) ? getDependencyFailureReason(dungeonEntryKeyHrid) : ""; const dungeonEntryKeySeconds = Number.isFinite(rawDungeonEntryKeySeconds) && rawDungeonEntryKeySeconds > 0 ? rawDungeonEntryKeySeconds : 0; const foodSeconds = (entry?.foods || []).reduce((total, item) => { const itemSeconds = calculateItemSeconds(item.itemHrid); if (!Number.isFinite(itemSeconds) || itemSeconds <= 0) { return total; } const hours = itemType === "fragment" ? 24 : (runMinutes / 60); return total + itemSeconds * parseNonNegativeDecimal(item.perHour) * hours; }, 0); const drinkSeconds = (entry?.drinks || []).reduce((total, item) => { const itemSeconds = calculateItemSeconds(item.itemHrid); if (!Number.isFinite(itemSeconds) || itemSeconds <= 0) { return total; } const hours = itemType === "fragment" ? 24 : (runMinutes / 60); return total + itemSeconds * parseNonNegativeDecimal(item.perHour) * hours; }, 0); if (itemType === "fragment") { const expectedChestCount = Math.max(0.0001, quantityPer24h); const baseSeconds = 24 * 60 * 60; const totalSeconds = baseSeconds + foodSeconds + drinkSeconds; return { itemType, runMinutes, runSeconds, quantityPer24h, failureReason: dungeonEntryKeyFailureReason, dungeonEntryKeyHrid, dungeonEntryKeySeconds: Number.isFinite(dungeonEntryKeySeconds) ? dungeonEntryKeySeconds : 0, dungeonEntryKeyContributionSeconds: 0, foodSeconds, foodContributionSeconds: totalSeconds > 0 ? (foodSeconds / expectedChestCount) : 0, drinkSeconds, drinkContributionSeconds: totalSeconds > 0 ? (drinkSeconds / expectedChestCount) : 0, totalSeconds, expectedChestCount, secondsPerChest: totalSeconds / expectedChestCount, dungeonTier: 0, }; } const chestQuantityMultiplier = getCombatChestQuantityMultiplier(); const adjustedDungeonEntryKeySeconds = dungeonEntryKeySeconds * chestQuantityMultiplier; const sharedRunSeconds = runSeconds + foodSeconds + drinkSeconds; const adjustedTotalSeconds = sharedRunSeconds + adjustedDungeonEntryKeySeconds; const baseSeconds = runSeconds + foodSeconds + drinkSeconds + dungeonEntryKeySeconds; if (itemType === "refinement_chest") { const dungeonTier = normalizeDungeonTier(entry?.dungeonTier || 1); const refinementBaseCount = getRefinementExpectedCountByTier(dungeonTier); if (!refinementBaseCount) { return { itemType, runMinutes, runSeconds, quantityPer24h, /* failureReason: isZh ? "绮剧偧瀹濈闅炬害鏃犳晥锛屽凡鎴柇" : "Truncated: invalid refinement tier", */ failureReason: "Truncated: invalid refinement tier", dungeonEntryKeyHrid, dungeonEntryKeySeconds: adjustedDungeonEntryKeySeconds, dungeonEntryKeyContributionSeconds: 0, foodSeconds, foodContributionSeconds: 0, drinkSeconds, drinkContributionSeconds: 0, totalSeconds: adjustedTotalSeconds, expectedChestCount: 0, refinementExpectedCount: 0, normalExpectedCount: 0, secondsPerChest: 0, dungeonTier, }; } const baseChestItemHrid = getBaseDungeonChestHrid(entry?.itemHrid); const baseChestEntry = baseChestItemHrid ? getConfiguredTimeCalculatorEntry(baseChestItemHrid) : null; if (!baseChestEntry) { return { itemType, runMinutes, runSeconds, quantityPer24h, /* failureReason: isZh ? "鏈厤缃甌0瀹濈鏃堕棿锛屽凡鎴柇" : "Truncated: missing T0 chest time", */ failureReason: "Truncated: missing T0 chest time", dungeonEntryKeyHrid, dungeonEntryKeySeconds: adjustedDungeonEntryKeySeconds, dungeonEntryKeyContributionSeconds: 0, foodSeconds, foodContributionSeconds: 0, drinkSeconds, drinkContributionSeconds: 0, totalSeconds: adjustedTotalSeconds, expectedChestCount: 0, refinementExpectedCount: 0, normalExpectedCount: 0, secondsPerChest: 0, dungeonTier, }; } const baseChestSummary = getTimeCalculatorEntrySummary(baseChestEntry); if (baseChestSummary.failureReason) { return { itemType, runMinutes, runSeconds, quantityPer24h, failureReason: baseChestSummary.failureReason, dungeonEntryKeyHrid, dungeonEntryKeySeconds: adjustedDungeonEntryKeySeconds, dungeonEntryKeyContributionSeconds: 0, foodSeconds, foodContributionSeconds: 0, drinkSeconds, drinkContributionSeconds: 0, totalSeconds: adjustedTotalSeconds, expectedChestCount: 0, refinementExpectedCount: 0, normalExpectedCount: 0, secondsPerChest: 0, dungeonTier, }; } const normalExpectedCount = chestQuantityMultiplier; const refinementExpectedCount = Math.max(0.0001, refinementBaseCount * chestQuantityMultiplier); const normalChestBaselineSeconds = baseChestSummary.secondsPerChest * normalExpectedCount; const remainingSeconds = adjustedTotalSeconds - normalChestBaselineSeconds; if (!Number.isFinite(remainingSeconds) || remainingSeconds <= 0) { return { itemType, runMinutes, runSeconds, quantityPer24h, /* failureReason: isZh ? "绮剧偧瀹濈鍒嗗瓙鏃犳晥锛屽凡鎴柇" : "Truncated: invalid refinement numerator", */ failureReason: "Truncated: invalid refinement numerator", dungeonEntryKeyHrid, dungeonEntryKeySeconds: adjustedDungeonEntryKeySeconds, dungeonEntryKeyContributionSeconds: 0, foodSeconds, foodContributionSeconds: 0, drinkSeconds, drinkContributionSeconds: 0, totalSeconds: adjustedTotalSeconds, expectedChestCount: refinementExpectedCount, refinementExpectedCount, normalExpectedCount, normalChestBaselineSeconds, secondsPerChest: 0, dungeonTier, }; } return { itemType, runMinutes, runSeconds, quantityPer24h, failureReason: dungeonEntryKeyFailureReason, dungeonEntryKeyHrid, dungeonEntryKeySeconds: Number.isFinite(adjustedDungeonEntryKeySeconds) ? adjustedDungeonEntryKeySeconds : 0, dungeonEntryKeyContributionSeconds: adjustedDungeonEntryKeySeconds, foodSeconds, foodContributionSeconds: foodSeconds, drinkSeconds, drinkContributionSeconds: drinkSeconds, totalSeconds: adjustedTotalSeconds, expectedChestCount: refinementExpectedCount, refinementExpectedCount, refinementBaseCount, normalExpectedCount, normalChestBaselineSeconds, secondsPerChest: remainingSeconds / refinementExpectedCount, dungeonTier, }; } const expectedChestCount = chestQuantityMultiplier; return { itemType, runMinutes, runSeconds, quantityPer24h, failureReason: dungeonEntryKeyFailureReason, dungeonEntryKeyHrid, dungeonEntryKeySeconds: Number.isFinite(adjustedDungeonEntryKeySeconds) ? adjustedDungeonEntryKeySeconds : 0, dungeonEntryKeyContributionSeconds: adjustedDungeonEntryKeySeconds, foodSeconds, foodContributionSeconds: foodSeconds, drinkSeconds, drinkContributionSeconds: drinkSeconds, totalSeconds: adjustedTotalSeconds, expectedChestCount, secondsPerChest: adjustedTotalSeconds / expectedChestCount, dungeonTier: 0, }; } function getTimeCalculatorPanelRoots() { const tabsContainer = document.querySelector('[class^="CharacterManagement_tabsComponentContainer"] [class*="TabsComponent_tabsContainer"]'); const tabPanelsContainer = document.querySelector('[class^="CharacterManagement_tabsComponentContainer"] [class*="TabsComponent_tabPanelsContainer"]'); return { tabsContainer, tabPanelsContainer, }; } function syncTimeCalculatorPanelHiddenState(tabPanelsContainer = getTimeCalculatorPanelRoots().tabPanelsContainer) { if (!(tabPanelsContainer instanceof HTMLElement)) { return; } for (const panelNode of tabPanelsContainer.querySelectorAll('[class*="TabPanel_tabPanel"]')) { if (!(panelNode instanceof HTMLElement)) { continue; } panelNode.hidden = panelNode.classList.contains("TabPanel_hidden__26UM3"); } } function createTimeCalculatorSearchControl(options, placeholderText, addButtonText, onAdd, config = {}) { const { getDraftValue = () => "", setDraftValue = () => {}, } = config; const wrapper = document.createElement("div"); wrapper.style.display = "flex"; wrapper.style.alignItems = "center"; wrapper.style.gap = "6px"; wrapper.style.position = "relative"; const input = document.createElement("input"); input.type = "text"; input.placeholder = placeholderText; input.style.background = "#dde2f8"; input.style.color = "#000000"; input.style.border = "none"; input.style.borderRadius = "4px"; input.style.padding = "4px"; input.style.margin = "2px"; input.style.minWidth = "180px"; input.style.flex = "1"; input.autocomplete = "off"; input.value = String(getDraftValue() || ""); const addButton = document.createElement("button"); addButton.textContent = addButtonText; addButton.style.background = "#4caf50"; addButton.style.color = "#ffffff"; addButton.style.border = "none"; addButton.style.borderRadius = "4px"; addButton.style.padding = "4px 8px"; addButton.style.cursor = "pointer"; const searchResults = document.createElement("div"); searchResults.style.background = "#2c2e45"; searchResults.style.border = "none"; searchResults.style.borderRadius = "4px"; searchResults.style.padding = "4px"; searchResults.style.margin = "2px"; searchResults.style.width = "240px"; searchResults.style.maxHeight = "260px"; searchResults.style.overflowY = "auto"; searchResults.style.zIndex = "1000"; searchResults.style.display = "none"; searchResults.style.position = "absolute"; searchResults.style.left = "4px"; searchResults.style.top = "36px"; const optionMap = new Map(options.map((option) => [option.itemName, option])); const hideResults = () => { searchResults.style.display = "none"; }; const populateResults = (filteredOptions) => { searchResults.innerHTML = ""; filteredOptions.forEach((option, index) => { const resultItem = document.createElement("div"); resultItem.style.borderBottom = "1px solid #98a7e9"; resultItem.style.borderRadius = "4px"; resultItem.style.padding = "4px"; resultItem.style.alignItems = "center"; resultItem.style.display = "flex"; resultItem.style.cursor = "pointer"; if (index === 0) { resultItem.style.background = "#4a4c6a"; } const itemIcon = document.createElement("div"); itemIcon.appendChild(createIconSvg(getIconHrefByItemHrid(option.itemHrid), 18)); const itemName = document.createElement("span"); itemName.textContent = option.itemName; itemName.style.marginLeft = "2px"; resultItem.appendChild(itemIcon); resultItem.appendChild(itemName); resultItem.addEventListener("mouseenter", () => { resultItem.style.background = "#4a4c6a"; }); resultItem.addEventListener("mouseleave", () => { resultItem.style.background = "transparent"; }); resultItem.addEventListener("click", () => { input.value = option.itemName; setDraftValue(option.itemName); hideResults(); }); searchResults.appendChild(resultItem); }); searchResults.style.display = filteredOptions.length ? "block" : "none"; }; const triggerAdd = () => { const option = optionMap.get(input.value.trim()) || null; if (!option?.itemHrid) { return; } onAdd(option.itemHrid); input.value = ""; setDraftValue(""); hideResults(); }; addButton.addEventListener("click", triggerAdd); input.addEventListener("focus", () => { setTimeout(() => { input.select(); }, 0); const searchTerm = input.value.toLowerCase().trim(); if (searchTerm.length >= 1) { const filtered = options.filter((option) => option.itemName.toLowerCase().includes(searchTerm)); populateResults(filtered); } }); input.addEventListener("input", () => { setDraftValue(input.value); const searchTerm = input.value.toLowerCase().trim(); if (searchTerm.length < 1) { hideResults(); return; } const filtered = options.filter((option) => option.itemName.toLowerCase().includes(searchTerm)); populateResults(filtered); }); input.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); triggerAdd(); } else if (event.key === "Escape") { hideResults(); } }); document.addEventListener("click", (event) => { if (!wrapper.contains(event.target)) { hideResults(); } }); wrapper.appendChild(input); wrapper.appendChild(addButton); wrapper.appendChild(searchResults); return wrapper; } function createTimeCalculatorItemBadge(itemHrid, itemName) { const badge = document.createElement("div"); badge.style.minWidth = "40px"; badge.style.alignItems = "center"; badge.style.display = "flex"; const iconContainer = document.createElement("div"); iconContainer.style.marginLeft = "2px"; const svg = createIconSvg(getIconHrefByItemHrid(itemHrid), 18); iconContainer.appendChild(svg); const name = document.createElement("span"); name.textContent = itemName; name.style.padding = "4px 1px"; name.style.marginLeft = "2px"; name.style.whiteSpace = "nowrap"; name.style.overflow = "hidden"; badge.appendChild(iconContainer); badge.appendChild(name); return badge; } function makeTimeCalculatorItemRow(entry, kind, item) { const row = document.createElement("div"); row.style.display = "flex"; row.style.alignItems = "center"; row.style.gap = "6px"; row.style.marginTop = "4px"; const itemName = getLocalizedItemName(item.itemHrid, state.itemDetailMap?.[item.itemHrid]?.name || item.itemHrid); const name = createTimeCalculatorItemBadge(item.itemHrid, itemName); name.style.flex = "1"; row.appendChild(name); const rateInput = document.createElement("input"); rateInput.type = "number"; rateInput.min = "0"; rateInput.step = "0.01"; rateInput.value = String(Number(item.perHour || 0)); rateInput.style.background = "#dde2f8"; rateInput.style.color = "#000000"; rateInput.style.border = "none"; rateInput.style.borderRadius = "4px"; rateInput.style.padding = "4px"; rateInput.style.width = "88px"; rateInput.title = isZh ? "每小时消耗数量" : "Per-hour consumption"; const commitRateInput = () => { item.perHour = Math.max(0, Number(rateInput.value || 0)); saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); }; rateInput.addEventListener("change", commitRateInput); rateInput.addEventListener("blur", commitRateInput); rateInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); rateInput.blur(); } }); row.appendChild(rateInput); const removeButton = document.createElement("button"); removeButton.textContent = isZh ? "删除" : "Remove"; removeButton.style.background = "#b33939"; removeButton.style.color = "#ffffff"; removeButton.style.border = "none"; removeButton.style.borderRadius = "4px"; removeButton.style.padding = "4px 8px"; removeButton.style.cursor = "pointer"; removeButton.textContent = isZh ? "\u5220\u9664" : "Remove"; removeButton.addEventListener("click", () => { entry[kind] = (entry[kind] || []).filter((candidate) => candidate.id !== item.id); saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); }); row.appendChild(removeButton); return row; } function createTimeCalculatorEntryCard(entry) { const summary = getTimeCalculatorEntrySummary(entry); const card = document.createElement("div"); card.className = "ictime-timecalc-entry-card"; card.dataset.entryId = entry.id; card.style.background = "#2c2e45"; card.style.borderRadius = "6px"; card.style.padding = "8px"; card.style.margin = "6px 0"; card.style.transition = "transform 150ms cubic-bezier(.2,.8,.2,1)"; const header = document.createElement("div"); header.style.display = "flex"; header.style.alignItems = "center"; header.style.justifyContent = "space-between"; header.style.textAlign = "left"; card.appendChild(header); const title = createTimeCalculatorItemBadge( entry.itemHrid, getTimeCalculatorItemDisplayName(entry.itemHrid) ); title.style.flex = "1"; title.style.fontWeight = "bold"; header.appendChild(title); const headerButtons = document.createElement("div"); headerButtons.style.display = "flex"; headerButtons.style.alignItems = "center"; headerButtons.style.gap = "6px"; header.appendChild(headerButtons); const dragHandle = document.createElement("span"); dragHandle.className = "ictime-timecalc-drag-handle"; dragHandle.textContent = isZh ? "拖动" : "Drag"; dragHandle.style.cursor = "grab"; dragHandle.style.padding = "0 6px"; dragHandle.style.opacity = "0.68"; dragHandle.style.fontSize = "0.78rem"; dragHandle.style.lineHeight = "1.4"; dragHandle.style.borderRadius = "4px"; dragHandle.style.background = "#4a4c6a"; dragHandle.style.color = "#ffffff"; dragHandle.style.touchAction = "none"; dragHandle.textContent = isZh ? "\u62d6\u52a8" : "Drag"; headerButtons.appendChild(dragHandle); const toggleButton = document.createElement("button"); toggleButton.textContent = entry.collapsed ? (isZh ? "展开" : "Expand") : (isZh ? "折叠" : "Collapse"); toggleButton.style.background = "#4a4c6a"; toggleButton.style.color = "#ffffff"; toggleButton.style.border = "none"; toggleButton.style.borderRadius = "4px"; toggleButton.style.padding = "4px 8px"; toggleButton.style.cursor = "pointer"; toggleButton.textContent = entry.collapsed ? (isZh ? "\u5c55\u5f00" : "Expand") : (isZh ? "\u6298\u53e0" : "Collapse"); toggleButton.addEventListener("click", () => { entry.collapsed = !entry.collapsed; saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); }); headerButtons.appendChild(toggleButton); const removeButton = document.createElement("button"); removeButton.textContent = isZh ? "删除" : "Remove"; removeButton.style.background = "#b33939"; removeButton.style.color = "#ffffff"; removeButton.style.border = "none"; removeButton.style.borderRadius = "4px"; removeButton.style.padding = "4px 8px"; removeButton.style.cursor = "pointer"; removeButton.textContent = isZh ? "\u5220\u9664" : "Remove"; removeButton.addEventListener("click", () => { state.timeCalculatorEntries = state.timeCalculatorEntries.filter((candidate) => candidate.id !== entry.id); saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); }); headerButtons.appendChild(removeButton); const collapsedSummary = document.createElement("div"); collapsedSummary.style.marginTop = "8px"; collapsedSummary.style.fontSize = "0.82rem"; collapsedSummary.style.lineHeight = "1.35"; collapsedSummary.style.textAlign = "left"; collapsedSummary.textContent = summary.itemType === "fragment" ? `${isZh ? "获得一个耗时" : "Time per fragment"}:${formatAutoDuration(summary.secondsPerChest)}` : `${isZh ? "获得一个耗时" : "Time per chest"}:${formatAutoDuration(summary.secondsPerChest)}`; card.appendChild(collapsedSummary); /* collapsedSummary.textContent = summary.itemType === "fragment" ? `${isZh ? "鑾峰緱涓€涓€楁椂" : "Time per fragment"}锛?{formatAutoDuration(summary.secondsPerChest)}` : summary.itemType === "refinement_chest" ? `${isZh ? "鑾峰緱涓€涓簿鐐煎疂绠辨椂闂? : "Time per refinement chest"}锛?{formatAutoDuration(summary.secondsPerChest)}` : `${isZh ? "鑾峰緱涓€涓€楁椂" : "Time per chest"}锛?{formatAutoDuration(summary.secondsPerChest)}`; */ collapsedSummary.textContent = summary.itemType === "fragment" ? `Time per fragment: ${formatAutoDuration(summary.secondsPerChest)}` : summary.itemType === "refinement_chest" ? `Time per refinement chest: ${formatAutoDuration(summary.secondsPerChest)}` : `Time per chest: ${formatAutoDuration(summary.secondsPerChest)}`; collapsedSummary.textContent = summary.itemType === "fragment" ? `${isZh ? "\u83b7\u5f97\u4e00\u4e2a\u788e\u7247\u8017\u65f6" : "Time per fragment"}: ${formatAutoDuration(summary.secondsPerChest)}` : summary.itemType === "refinement_chest" ? `${isZh ? "\u83b7\u5f97\u4e00\u4e2a\u7cbe\u70bc\u7bb1\u5b50\u8017\u65f6" : "Time per refinement chest"}: ${formatAutoDuration(summary.secondsPerChest)}` : `${isZh ? "\u83b7\u5f97\u4e00\u4e2a\u5b9d\u7bb1\u8017\u65f6" : "Time per chest"}: ${formatAutoDuration(summary.secondsPerChest)}`; if (entry.collapsed) { return card; } const timeRow = document.createElement("div"); timeRow.style.display = "flex"; timeRow.style.alignItems = "center"; timeRow.style.gap = "6px"; timeRow.style.marginTop = "8px"; timeRow.style.textAlign = "left"; card.appendChild(timeRow); const timeLabel = document.createElement("div"); timeLabel.textContent = summary.itemType === "fragment" ? (isZh ? "24小时碎片数量" : "24h fragment qty") : (isZh ? "单次地牢时间(分钟)" : "Dungeon time (min)"); timeLabel.textContent = summary.itemType === "fragment" ? (isZh ? "24\u5c0f\u65f6\u788e\u7247\u6570\u91cf" : "24h fragment qty") : (isZh ? "\u5355\u6b21\u5730\u7262\u65f6\u95f4(\u5206\u949f)" : "Dungeon time (min)"); timeLabel.style.flex = "1"; timeRow.appendChild(timeLabel); const timeInput = document.createElement("input"); timeInput.type = "number"; timeInput.min = "0"; timeInput.step = "any"; timeInput.inputMode = "decimal"; timeInput.value = String(parseNonNegativeDecimal(summary.itemType === "fragment" ? entry.quantityPer24h || 0 : entry.runMinutes || 0)); timeInput.style.background = "#dde2f8"; timeInput.style.color = "#000000"; timeInput.style.border = "none"; timeInput.style.borderRadius = "4px"; timeInput.style.padding = "4px"; timeInput.style.width = "96px"; const commitTimeInput = () => { if (summary.itemType === "fragment") { entry.quantityPer24h = parseNonNegativeDecimal(timeInput.value); } else { entry.runMinutes = parseNonNegativeDecimal(timeInput.value); } timeInput.value = String(summary.itemType === "fragment" ? entry.quantityPer24h : entry.runMinutes); saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); }; timeInput.addEventListener("change", commitTimeInput); timeInput.addEventListener("blur", commitTimeInput); timeInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); timeInput.blur(); } }); timeRow.appendChild(timeInput); if (summary.itemType === "refinement_chest") { const tierRow = document.createElement("div"); tierRow.style.display = "flex"; tierRow.style.alignItems = "center"; tierRow.style.gap = "6px"; tierRow.style.marginTop = "8px"; tierRow.style.textAlign = "left"; card.appendChild(tierRow); const tierLabel = document.createElement("div"); tierLabel.textContent = isZh ? "\u5730\u7262T\u7b49\u7ea7" : "Dungeon tier"; tierLabel.style.flex = "1"; tierRow.appendChild(tierLabel); const tierSelect = document.createElement("select"); tierSelect.style.background = "#dde2f8"; tierSelect.style.color = "#000000"; tierSelect.style.border = "none"; tierSelect.style.borderRadius = "4px"; tierSelect.style.padding = "4px"; [ { value: "1", label: "T1" }, { value: "2", label: "T2" }, ].forEach((optionConfig) => { const option = document.createElement("option"); option.value = optionConfig.value; option.textContent = optionConfig.label; tierSelect.appendChild(option); }); tierSelect.value = String(normalizeDungeonTier(entry.dungeonTier || 1) || 1); tierSelect.addEventListener("change", () => { entry.dungeonTier = normalizeDungeonTier(tierSelect.value || 1); saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); }); tierRow.appendChild(tierSelect); } const summaryBlock = document.createElement("div"); summaryBlock.style.marginTop = "8px"; summaryBlock.style.fontSize = "0.78rem"; summaryBlock.style.lineHeight = "1.35"; summaryBlock.style.textAlign = "left"; summaryBlock.innerHTML = summary.itemType === "fragment" ? [ `${isZh ? "单碎片时间" : "Time/fragment"}:${formatAutoDuration(summary.secondsPerChest)}`, `${isZh ? "食物时间" : "Food time"}:${formatAutoDuration(summary.foodSeconds)}`, `${isZh ? "饮料时间" : "Drink time"}:${formatAutoDuration(summary.drinkSeconds)}`, ].join("<br>") : [ `${isZh ? "单次总时间" : "Total/run"}:${formatAutoDuration(summary.totalSeconds)}`, `${isZh ? "单箱时间" : "Time/chest"}:${formatAutoDuration(summary.secondsPerChest)}`, `${isZh ? "地牢钥匙时间" : "Entry key time"}:${formatAutoDuration(summary.dungeonEntryKeySeconds)}`, `${isZh ? "食物时间" : "Food time"}:${formatAutoDuration(summary.foodSeconds)}`, `${isZh ? "饮料时间" : "Drink time"}:${formatAutoDuration(summary.drinkSeconds)}`, ].join("<br>"); card.appendChild(summaryBlock); summaryBlock.innerHTML = summary.itemType === "fragment" ? [ `${isZh ? "\u5355\u788e\u7247\u65f6\u95f4" : "Time/fragment"}: ${formatAutoDuration(summary.secondsPerChest)}`, `${isZh ? "\u98df\u7269\u65f6\u95f4" : "Food time"}: ${formatAutoDuration(summary.foodSeconds)}`, `${isZh ? "\u996e\u6599\u65f6\u95f4" : "Drink time"}: ${formatAutoDuration(summary.drinkSeconds)}`, ].join("<br>") : [ `${isZh ? "\u5730\u7262\u94a5\u5319\u65f6\u95f4" : "Entry key time"}: ${formatAutoDuration(summary.dungeonEntryKeyContributionSeconds || 0)}`, `${isZh ? "\u98df\u7269\u65f6\u95f4" : "Food time"}: ${formatAutoDuration(summary.foodContributionSeconds || 0)}`, `${isZh ? "\u996e\u6599\u65f6\u95f4" : "Drink time"}: ${formatAutoDuration(summary.drinkContributionSeconds || 0)}`, ].join("<br>"); if (summary.itemType !== "fragment") { summaryBlock.innerHTML = [ `${isZh ? "鍦扮墷閽ュ寵鏃堕棿" : "Entry key time"}锛?{formatAutoDuration(summary.dungeonEntryKeyContributionSeconds || 0)}`, `${isZh ? "椋熺墿鏃堕棿" : "Food time"}锛?{formatAutoDuration(summary.foodContributionSeconds || 0)}`, `${isZh ? "楗枡鏃堕棿" : "Drink time"}锛?{formatAutoDuration(summary.drinkContributionSeconds || 0)}`, ].join("<br>"); } summaryBlock.innerHTML = summary.itemType === "fragment" ? [ `${isZh ? "\u5355\u788e\u7247\u65f6\u95f4" : "Time/fragment"}: ${formatAutoDuration(summary.secondsPerChest)}`, `${isZh ? "\u98df\u7269\u65f6\u95f4" : "Food time"}: ${formatAutoDuration(summary.foodSeconds)}`, `${isZh ? "\u996e\u6599\u65f6\u95f4" : "Drink time"}: ${formatAutoDuration(summary.drinkSeconds)}`, ].join("<br>") : [ `${isZh ? "\u5730\u7262\u94a5\u5319\u65f6\u95f4" : "Entry key time"}: ${formatAutoDuration(summary.dungeonEntryKeyContributionSeconds || 0)}`, `${isZh ? "\u98df\u7269\u65f6\u95f4" : "Food time"}: ${formatAutoDuration(summary.foodContributionSeconds || 0)}`, `${isZh ? "\u996e\u6599\u65f6\u95f4" : "Drink time"}: ${formatAutoDuration(summary.drinkContributionSeconds || 0)}`, ].join("<br>"); const consumableSection = document.createElement("div"); consumableSection.style.marginTop = "10px"; consumableSection.style.textAlign = "left"; card.appendChild(consumableSection); const consumableHeader = document.createElement("div"); consumableHeader.textContent = isZh ? "消耗品(每小时消耗)" : "Consumables (per hour)"; consumableHeader.style.fontWeight = "bold"; consumableHeader.textContent = isZh ? "\u6d88\u8017\u54c1\uff08\u6bcf\u5c0f\u65f6\u6d88\u8017\uff09" : "Consumables (per hour)"; consumableSection.appendChild(consumableHeader); const consumableControls = document.createElement("div"); consumableControls.style.marginTop = "4px"; consumableSection.appendChild(consumableControls); consumableControls.appendChild(createTimeCalculatorSearchControl( getTimeCalculatorConsumableOptions(), isZh ? "搜索食物或饮料..." : "Search food or drink...", isZh ? "加入消耗品" : "Add consumable", (itemHrid) => { const option = getTimeCalculatorConsumableOptions().find((candidate) => candidate.itemHrid === itemHrid); if (!option) { return; } const targetKey = option.kind === "food" ? "foods" : "drinks"; const existing = [...(entry.foods || []), ...(entry.drinks || [])].find((item) => item.itemHrid === itemHrid); if (!existing) { entry[targetKey].push({ id: `${targetKey.slice(0, -1)}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, itemHrid, perHour: 0, }); } saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); }, { getDraftValue: () => state.timeCalculatorDrafts?.consumableQueryByEntryId?.[entry.id] || "", setDraftValue: (value) => { if (!state.timeCalculatorDrafts) { state.timeCalculatorDrafts = { addItemQuery: "", consumableQueryByEntryId: {} }; } if (!state.timeCalculatorDrafts.consumableQueryByEntryId) { state.timeCalculatorDrafts.consumableQueryByEntryId = {}; } state.timeCalculatorDrafts.consumableQueryByEntryId[entry.id] = String(value || ""); }, } )); const consumableSearchInput = consumableControls.querySelector("input"); if (consumableSearchInput) { consumableSearchInput.placeholder = isZh ? "\u641c\u7d22\u98df\u7269\u6216\u996e\u6599..." : "Search food or drink..."; } const consumableSearchButton = consumableControls.querySelector("button"); if (consumableSearchButton) { consumableSearchButton.textContent = isZh ? "\u52a0\u5165\u6d88\u8017\u54c1" : "Add consumable"; } for (const item of entry.foods || []) { consumableSection.appendChild(makeTimeCalculatorItemRow(entry, "foods", item)); } for (const item of entry.drinks || []) { consumableSection.appendChild(makeTimeCalculatorItemRow(entry, "drinks", item)); } return card; } function renderTimeCalculatorPanel() { if (isMissingDerivedRuntimeState()) { ensureRuntimeStateFresh(); } if (!state.itemDetailMap) { loadCachedClientData(); } const container = state.timeCalculatorContainer; if (!container) { return; } loadTimeCalculatorData(); state.timeCalculatorRefreshPending = false; container.innerHTML = ""; const addSection = document.createElement("div"); addSection.style.background = "#2c2e45"; addSection.style.borderRadius = "6px"; addSection.style.padding = "8px"; addSection.style.marginBottom = "8px"; addSection.style.position = "relative"; container.appendChild(addSection); const headerRow = document.createElement("div"); headerRow.style.display = "flex"; headerRow.style.alignItems = "center"; headerRow.style.justifyContent = "space-between"; headerRow.style.gap = "8px"; addSection.appendChild(headerRow); const addTitle = document.createElement("div"); addTitle.textContent = isZh ? "添加物品" : "Add item"; addTitle.style.fontWeight = "bold"; addTitle.textContent = isZh ? "\u6dfb\u52a0\u7269\u54c1" : "Add item"; headerRow.appendChild(addTitle); const headerActions = document.createElement("div"); headerActions.style.display = "flex"; headerActions.style.alignItems = "center"; headerActions.style.gap = "6px"; headerRow.appendChild(headerActions); const refreshButton = document.createElement("button"); refreshButton.type = "button"; refreshButton.dataset.ictimeTimeCalcRefreshButton = "true"; refreshButton.textContent = "\u21bb"; refreshButton.title = isZh ? "\u5237\u65b0\u65f6\u95f4\u8ba1\u7b97" : "Refresh time calculator"; refreshButton.setAttribute("aria-label", isZh ? "\u5237\u65b0\u65f6\u95f4\u8ba1\u7b97" : "Refresh time calculator"); refreshButton.style.width = "22px"; refreshButton.style.height = "22px"; refreshButton.style.padding = "0"; refreshButton.style.border = "none"; refreshButton.style.borderRadius = "999px"; refreshButton.style.cursor = "pointer"; refreshButton.style.fontSize = "13px"; refreshButton.style.lineHeight = "1"; refreshButton.style.color = "#ffffff"; refreshButton.style.boxShadow = "0 0 1px rgba(0, 0, 0, 0.8)"; refreshButton.style.background = "#56628a"; refreshButton.addEventListener("click", () => { clearCaches(); rerenderTimeCalculatorPanel(); }); headerActions.appendChild(refreshButton); const settingsButton = document.createElement("button"); settingsButton.type = "button"; settingsButton.dataset.ictimeTimeCalcSettingsButton = "true"; settingsButton.textContent = "\u2699"; settingsButton.title = isZh ? "\u8bbe\u7f6e" : "Settings"; settingsButton.setAttribute("aria-label", isZh ? "\u8bbe\u7f6e" : "Settings"); settingsButton.style.width = "22px"; settingsButton.style.height = "22px"; settingsButton.style.padding = "0"; settingsButton.style.border = "none"; settingsButton.style.borderRadius = "999px"; settingsButton.style.cursor = "pointer"; settingsButton.style.fontSize = "13px"; settingsButton.style.lineHeight = "1"; settingsButton.style.color = "#ffffff"; settingsButton.style.boxShadow = "0 0 1px rgba(0, 0, 0, 0.8)"; settingsButton.style.background = isTimeCalculatorSettingsOpen() ? "#7682b6" : "#56628a"; settingsButton.addEventListener("click", () => { setTimeCalculatorSettingsOpen(!isTimeCalculatorSettingsOpen()); }); headerActions.appendChild(settingsButton); const settingsPanel = document.createElement("div"); settingsPanel.dataset.ictimeTimeCalcSettingsPanel = "true"; settingsPanel.style.position = "absolute"; settingsPanel.style.top = "36px"; settingsPanel.style.right = "8px"; settingsPanel.style.width = "min(420px, calc(100% - 16px))"; settingsPanel.style.maxWidth = "calc(100% - 16px)"; settingsPanel.style.padding = "10px"; settingsPanel.style.borderRadius = "8px"; settingsPanel.style.background = "#1c202f"; settingsPanel.style.border = "1.5px solid rgba(214, 222, 255, 0.24)"; settingsPanel.style.boxShadow = "0 0 5px 1px rgba(0, 0, 0, 0.65)"; settingsPanel.style.zIndex = "3"; settingsPanel.style.display = isTimeCalculatorSettingsOpen() ? "flex" : "none"; settingsPanel.style.flexDirection = "column"; settingsPanel.style.gap = "10px"; addSection.appendChild(settingsPanel); const settingsPanelHeader = document.createElement("div"); settingsPanelHeader.style.display = "flex"; settingsPanelHeader.style.alignItems = "center"; settingsPanelHeader.style.justifyContent = "space-between"; settingsPanelHeader.style.gap = "8px"; settingsPanel.appendChild(settingsPanelHeader); const settingsPanelTitle = document.createElement("div"); settingsPanelTitle.textContent = isZh ? "\u8bbe\u7f6e" : "Settings"; settingsPanelTitle.style.fontWeight = "bold"; settingsPanelTitle.style.fontSize = "0.95rem"; settingsPanelHeader.appendChild(settingsPanelTitle); const settingsCloseButton = document.createElement("button"); settingsCloseButton.type = "button"; settingsCloseButton.textContent = "\u00d7"; settingsCloseButton.title = isZh ? "\u5173\u95ed" : "Close"; settingsCloseButton.setAttribute("aria-label", isZh ? "\u5173\u95ed" : "Close"); settingsCloseButton.style.width = "20px"; settingsCloseButton.style.height = "20px"; settingsCloseButton.style.padding = "0"; settingsCloseButton.style.border = "none"; settingsCloseButton.style.borderRadius = "999px"; settingsCloseButton.style.cursor = "pointer"; settingsCloseButton.style.fontSize = "14px"; settingsCloseButton.style.lineHeight = "1"; settingsCloseButton.style.color = "#ffffff"; settingsCloseButton.style.background = "#bb5e5e"; settingsCloseButton.onclick = (event) => { event.preventDefault(); event.stopPropagation(); if (!state.timeCalculatorSettingsOpen) { return false; } state.timeCalculatorSettingsOpen = false; rerenderTimeCalculatorPanel(); return false; }; settingsPanelHeader.appendChild(settingsCloseButton); const settingsPanelContent = document.createElement("div"); settingsPanelContent.style.display = "flex"; settingsPanelContent.style.flexDirection = "column"; settingsPanelContent.style.gap = "10px"; settingsPanel.appendChild(settingsPanelContent); const compactModeRow = document.createElement("label"); compactModeRow.style.display = "flex"; compactModeRow.style.alignItems = "center"; compactModeRow.style.gap = "6px"; compactModeRow.style.cursor = "pointer"; compactModeRow.style.flexWrap = "wrap"; const compactModeInput = document.createElement("input"); compactModeInput.type = "checkbox"; compactModeInput.dataset.ictimeTimeCalcCompactMode = "true"; compactModeInput.checked = isTimeCalculatorCompactModeEnabled(); compactModeInput.addEventListener("change", () => { setTimeCalculatorCompactMode(compactModeInput.checked); }); const compactModeText = document.createElement("span"); compactModeText.textContent = isZh ? "简洁模式(隐藏悬浮窗细节 / 炼金公式 / 强化细节)" : "Compact mode (hide tooltip detail / alchemy formula / enhancing detail)"; compactModeText.style.fontSize = "0.82rem"; compactModeText.textContent = isZh ? "\u7b80\u6d01\u6a21\u5f0f\uff08\u9690\u85cf\u60ac\u6d6e\u7a97\u7ec6\u8282 / \u70bc\u91d1\u516c\u5f0f / \u5f3a\u5316\u7ec6\u8282\uff09" : "Compact mode (hide tooltip detail / alchemy formula / enhancing detail)"; compactModeRow.appendChild(compactModeInput); compactModeRow.appendChild(compactModeText); settingsPanelContent.appendChild(compactModeRow); const essenceSourceConfigs = [ { essenceHrid: "/items/brewing_essence", label: isZh ? "\u51b2\u6ce1\u7cbe\u534e\u5206\u89e3\u8336\u53f6" : "Brewing essence leaf", }, { essenceHrid: "/items/tailoring_essence", label: isZh ? "\u88c1\u7f1d\u7cbe\u534e\u5206\u89e3\u76ae" : "Tailoring essence hide", }, ]; for (const config of essenceSourceConfigs) { const sourceRow = document.createElement("label"); sourceRow.style.display = "flex"; sourceRow.style.alignItems = "center"; sourceRow.style.gap = "8px"; sourceRow.style.flexWrap = "wrap"; const sourceLabel = document.createElement("span"); sourceLabel.textContent = config.label; sourceLabel.style.fontSize = "0.82rem"; sourceLabel.style.minWidth = "120px"; sourceRow.appendChild(sourceLabel); const sourceSelect = document.createElement("select"); sourceSelect.dataset.ictimeTimeCalcEssenceSource = config.essenceHrid; sourceSelect.style.flex = "1"; sourceSelect.style.minWidth = "180px"; sourceSelect.style.padding = "4px 6px"; sourceSelect.style.borderRadius = "4px"; sourceSelect.style.border = "1px solid rgba(255, 255, 255, 0.18)"; sourceSelect.style.background = "#1e2032"; sourceSelect.style.color = "#ffffff"; const options = getTimeCalculatorEssenceSourceOptions(config.essenceHrid); const selectedSourceItemHrid = getConfiguredEssenceDecomposeSourceItemHrid(config.essenceHrid); const optionMap = new Map(options.map((option) => [option.itemHrid, option])); if (selectedSourceItemHrid && !optionMap.has(selectedSourceItemHrid)) { optionMap.set(selectedSourceItemHrid, { itemHrid: selectedSourceItemHrid, itemName: getLocalizedItemName( selectedSourceItemHrid, state.itemDetailMap?.[selectedSourceItemHrid]?.name || selectedSourceItemHrid ), }); } for (const option of Array.from(optionMap.values())) { const optionNode = document.createElement("option"); optionNode.value = option.itemHrid; optionNode.textContent = option.itemName; sourceSelect.appendChild(optionNode); } sourceSelect.value = selectedSourceItemHrid; sourceSelect.addEventListener("change", () => { setTimeCalculatorEssenceSourceItemHrid(config.essenceHrid, sourceSelect.value); }); sourceRow.appendChild(sourceSelect); settingsPanelContent.appendChild(sourceRow); } const importButton = document.createElement("button"); importButton.textContent = isZh ? "模拟器导入" : "Import Simulator"; importButton.style.background = "#1770b3"; importButton.style.color = "#ffffff"; importButton.style.border = "none"; importButton.style.borderRadius = "4px"; importButton.style.padding = "4px 10px"; importButton.style.cursor = "pointer"; importButton.style.marginTop = "6px"; importButton.textContent = isZh ? "\u6a21\u62df\u5668\u5bfc\u5165" : "Import Simulator"; importButton.addEventListener("click", async () => { const ok = await importFromSimulatorSnapshot(); if (!ok) { const debugCharacterName = state.lastSimulatorImportResult?.currentCharacterName || getCurrentCharacterName() || ""; const simulatorCharacterNames = (state.lastSimulatorImportResult?.snapshotCharacterNames || []).join(", "); alert(isZh ? `没有可导入的模拟器结果,或当前地下城/角色无法匹配。\n当前读取角色名:${debugCharacterName || "(空)"}\n模拟器角色列表:${simulatorCharacterNames || "(空)"}` : `No simulator result available or current dungeon/character could not be matched.\nCurrent character name: ${debugCharacterName || "(empty)"}\nSimulator characters: ${simulatorCharacterNames || "(empty)"}`); return; } alert(isZh ? "模拟器数据已导入完成。" : "Simulator data imported."); }); addSection.appendChild(importButton); const addControls = document.createElement("div"); addControls.style.marginTop = "6px"; addSection.appendChild(addControls); addControls.appendChild(createTimeCalculatorSearchControl( getTimeCalculatorItemOptions(), isZh ? "搜索宝箱或钥匙碎片..." : "Search chest or key fragment...", isZh ? "加入" : "Add", (itemHrid) => { if (state.timeCalculatorEntries.some((entry) => entry.itemHrid === itemHrid)) { return; } state.timeCalculatorEntries.push({ id: `chest-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, itemHrid, collapsed: false, dungeonTier: REFINEMENT_CHEST_ITEM_HRIDS.includes(itemHrid) ? 1 : 0, runMinutes: 0, quantityPer24h: 0, foods: [], drinks: [], }); saveTimeCalculatorData(); rerenderTimeCalculatorPanel(); }, { getDraftValue: () => state.timeCalculatorDrafts?.addItemQuery || "", setDraftValue: (value) => { if (!state.timeCalculatorDrafts) { state.timeCalculatorDrafts = { addItemQuery: "", consumableQueryByEntryId: {} }; } state.timeCalculatorDrafts.addItemQuery = String(value || ""); }, } )); const addSearchInput = addControls.querySelector("input"); if (addSearchInput) { addSearchInput.placeholder = isZh ? "\u641c\u7d22\u5b9d\u7bb1/\u7cbe\u70bc\u7bb1\u5b50/\u94a5\u5319\u788e\u7247..." : "Search chest or key fragment..."; } const addSearchButton = addControls.querySelector("button"); if (addSearchButton) { addSearchButton.textContent = isZh ? "\u52a0\u5165" : "Add"; } const entriesHost = document.createElement("div"); entriesHost.className = "ictime-timecalc-entries"; entriesHost.style.display = "flex"; entriesHost.style.flexDirection = "column"; container.appendChild(entriesHost); for (const entry of state.timeCalculatorEntries) { entriesHost.appendChild(createTimeCalculatorEntryCard(entry)); } enableTimeCalculatorPointerSort(entriesHost); } function ensureTimeCalculatorUI() { if (state.isShutDown || !state.itemDetailMap) { return; } const { tabsContainer, tabPanelsContainer } = getTimeCalculatorPanelRoots(); if (!tabsContainer || !tabPanelsContainer) { return; } syncTimeCalculatorPanelHiddenState(tabPanelsContainer); if (!state.timeCalculatorTabButton || !state.timeCalculatorTabButton.isConnected) { const oldTabButtons = tabsContainer.querySelectorAll("button"); if (oldTabButtons.length < 2) { return; } const tabButton = oldTabButtons[1].cloneNode(true); if (tabButton.children[0]) { tabButton.children[0].textContent = isZh ? "时间计算" : "Time Calc"; } else { tabButton.textContent = isZh ? "时间计算" : "Time Calc"; } if (tabButton.children[0]) { tabButton.children[0].textContent = isZh ? "\u65f6\u95f4\u8ba1\u7b97" : "Time Calc"; } else { tabButton.textContent = isZh ? "\u65f6\u95f4\u8ba1\u7b97" : "Time Calc"; } tabButton.dataset.ictimeTimeCalc = "button"; oldTabButtons[0].parentElement.appendChild(tabButton); state.timeCalculatorTabButton = tabButton; } if (!state.timeCalculatorTabPanel || !state.timeCalculatorTabPanel.isConnected) { const oldTabPanels = tabPanelsContainer.querySelectorAll('[class*="TabPanel_tabPanel"]'); if (oldTabPanels.length < 2) { return; } const tabPanel = oldTabPanels[1].cloneNode(false); tabPanel.dataset.ictimeTimeCalc = "panel"; oldTabPanels[0].parentElement.appendChild(tabPanel); state.timeCalculatorTabPanel = tabPanel; const panel = document.createElement("div"); panel.className = "ictime-timecalc-container"; panel.style.padding = "6px"; panel.style.color = "#ffffff"; panel.addEventListener("focusout", () => { setTimeout(() => { flushPendingTimeCalculatorRefresh(); }, 0); }, true); tabPanel.appendChild(panel); state.timeCalculatorContainer = panel; const sourceButtons = Array.from(tabsContainer.querySelectorAll("button")).filter((button) => button !== state.timeCalculatorTabButton); const sourcePanels = Array.from(tabPanelsContainer.querySelectorAll('[class*="TabPanel_tabPanel"]')).filter((panelNode) => panelNode !== state.timeCalculatorTabPanel); for (const button of sourceButtons) { button.addEventListener("click", () => { if (!state.timeCalculatorTabPanel || !state.timeCalculatorTabButton) { return; } state.timeCalculatorTabPanel.hidden = true; state.timeCalculatorTabPanel.classList.add("TabPanel_hidden__26UM3"); state.timeCalculatorTabButton.classList.remove("Mui-selected"); state.timeCalculatorTabButton.setAttribute("aria-selected", "false"); state.timeCalculatorTabButton.tabIndex = -1; requestAnimationFrame(() => syncTimeCalculatorPanelHiddenState(tabPanelsContainer)); }, true); } state.timeCalculatorTabButton.addEventListener("click", () => { sourceButtons.forEach((button) => { button.classList.remove("Mui-selected"); button.setAttribute("aria-selected", "false"); button.tabIndex = -1; }); sourcePanels.forEach((panelNode) => { panelNode.hidden = true; panelNode.classList.add("TabPanel_hidden__26UM3"); }); state.timeCalculatorTabButton.classList.add("Mui-selected"); state.timeCalculatorTabButton.setAttribute("aria-selected", "true"); state.timeCalculatorTabButton.tabIndex = 0; state.timeCalculatorTabPanel.classList.remove("TabPanel_hidden__26UM3"); state.timeCalculatorTabPanel.hidden = false; syncTimeCalculatorPanelHiddenState(tabPanelsContainer); }, true); } renderTimeCalculatorPanel(); } function queueTimeCalculatorRefresh() { if (state.isShutDown) { return; } if (shouldDeferTimeCalculatorRefresh()) { state.timeCalculatorRefreshPending = true; return; } if (state.timeCalculatorRefreshQueued) { return; } state.timeCalculatorRefreshQueued = true; requestAnimationFrame(() => { state.timeCalculatorRefreshQueued = false; if (shouldDeferTimeCalculatorRefresh()) { state.timeCalculatorRefreshPending = true; return; } state.timeCalculatorRefreshPending = false; ensureTimeCalculatorUI(); }); } function getConsumableValueDetail(itemHrid, itemSeconds) { const itemDetail = state.itemDetailMap?.[itemHrid]; const consumable = itemDetail?.consumableDetail; if (!consumable) { return null; } const hp = Math.max(0, Number(consumable.hitpointRestore || 0)); const mp = Math.max(0, Number(consumable.manapointRestore || 0)); if (!hp && !mp) { return null; } const divisorSeconds = Number(itemSeconds || 0); if (!Number.isFinite(divisorSeconds) || divisorSeconds <= 0) { return null; } const buildValueParts = (seconds) => { if (!Number.isFinite(seconds) || seconds <= 0) { return []; } const parts = []; if (hp > 0) { parts.push(isZh ? `回血性价比${formatNumber(hp / seconds)}` : `hp/value ${formatNumber(hp / seconds)}`); } if (mp > 0) { parts.push(isZh ? `回蓝性价比${formatNumber(mp / seconds)}` : `mp/value ${formatNumber(mp / seconds)}`); } return parts; }; const baseParts = buildValueParts(divisorSeconds); if (!baseParts.length) { return null; } const savings = getConsumableAttachedRareTimeSavings(itemHrid); let adjustedText = ""; const adjustedSeconds = divisorSeconds - Number(savings.totalSeconds || 0); if (Number.isFinite(adjustedSeconds) && adjustedSeconds > 0 && Number(savings.totalSeconds || 0) > 0) { const adjustedParts = buildValueParts(adjustedSeconds); if (adjustedParts.length) { adjustedText = isZh ? `扣附带油线时间后:${adjustedParts.join(" | ")}` : `After oil/thread deduction: ${adjustedParts.join(" | ")}`; } } return { baseText: baseParts.join(" | "), adjustedText, }; } function getConsumableValueDetailNormalized(itemHrid, itemSeconds) { const itemDetail = state.itemDetailMap?.[itemHrid]; const consumable = itemDetail?.consumableDetail; if (!consumable) { return null; } const hp = Math.max(0, Number(consumable.hitpointRestore || 0)); const mp = Math.max(0, Number(consumable.manapointRestore || 0)); if (!hp && !mp) { return null; } const divisorSeconds = Number(itemSeconds || 0); if (!Number.isFinite(divisorSeconds) || divisorSeconds <= 0) { return null; } const buildValueParts = (seconds) => { if (!Number.isFinite(seconds) || seconds <= 0) { return []; } const parts = []; if (hp > 0) { parts.push(isZh ? `\u56de\u8840\u6027\u4ef7\u6bd4${formatNumber(hp / seconds)}` : `hp/value ${formatNumber(hp / seconds)}`); } if (mp > 0) { parts.push(isZh ? `\u56de\u84dd\u6027\u4ef7\u6bd4${formatNumber(mp / seconds)}` : `mp/value ${formatNumber(mp / seconds)}`); } return parts; }; const baseParts = buildValueParts(divisorSeconds); if (!baseParts.length) { return null; } const savings = getConsumableAttachedRareTimeSavings(itemHrid); let adjustedText = ""; const adjustedSeconds = divisorSeconds - Number(savings.totalSeconds || 0); if (Number.isFinite(adjustedSeconds) && adjustedSeconds > 0 && Number(savings.totalSeconds || 0) > 0) { const adjustedParts = buildValueParts(adjustedSeconds); if (adjustedParts.length) { adjustedText = isZh ? `\u6263\u9644\u5e26\u6cb9\u7ebf\u65f6\u95f4\u540e\uff1a${adjustedParts.join(" | ")}` : `After oil/thread deduction: ${adjustedParts.join(" | ")}`; } } return { baseText: baseParts.join(" | "), adjustedText, }; } function normalizeScrollDurationSeconds(value) { const numericValue = Number(value || 0); if (!Number.isFinite(numericValue) || numericValue <= 0) { return 0; } if (numericValue >= 1e10) { return numericValue / 1e9; } if (numericValue >= 1e6) { return numericValue / 1000; } return numericValue; } function tryExtractDurationSecondsFromText(text) { const rawText = String(text || "").trim(); if (!rawText) { return 0; } let match = rawText.match(/(\d+(?:\.\d+)?)\s*(?:h|hr|hrs|hour|hours|\u5c0f\u65f6)/i); if (match) { return Number(match[1]) * 3600; } match = rawText.match(/(\d+(?:\.\d+)?)\s*(?:m|min|mins|minute|minutes|\u5206\u949f)/i); if (match) { return Number(match[1]) * 60; } return 0; } function isSkillingScrollItem(itemDetailOrHrid) { const itemDetail = itemDetailOrHrid && typeof itemDetailOrHrid === "object" ? itemDetailOrHrid : state.itemDetailMap?.[itemDetailOrHrid]; const hrid = String(itemDetail?.hrid || itemDetailOrHrid || ""); if (!hrid) { return false; } return hrid.includes("_scroll") || hrid.startsWith("/items/seal_of_") || itemDetail?.categoryHrid === "/item_categories/scroll" || Boolean(itemDetail?.scrollDetail?.personalBuffTypeHrid); } function getSkillingScrollValueConfig(itemDetailOrHrid) { const itemDetail = itemDetailOrHrid && typeof itemDetailOrHrid === "object" ? itemDetailOrHrid : state.itemDetailMap?.[itemDetailOrHrid]; const hrid = String(itemDetail?.hrid || itemDetailOrHrid || ""); if (!hrid) { return null; } const config = SKILLING_SCROLL_VALUE_CONFIGS[hrid]; return config ? { ...config, itemHrid: hrid } : null; } function getSkillingScrollDurationSeconds(itemDetail) { if (!itemDetail) { return 0; } if (!itemDetail?.consumableDetail) { return isSkillingScrollItem(itemDetail) ? SKILLING_SCROLL_DEFAULT_DURATION_SECONDS : 0; } const consumable = itemDetail.consumableDetail; const durationCandidates = [ consumable.duration, consumable.durationSeconds, consumable.effectDuration, consumable.effectDurationSeconds, consumable.buffDuration, consumable.buffDurationSeconds, consumable.activeDuration, consumable.activeDurationSeconds, ]; for (const buff of consumable.buffs || []) { durationCandidates.push( buff?.duration, buff?.durationSeconds, buff?.effectDuration, buff?.effectDurationSeconds, buff?.buffDuration, buff?.buffDurationSeconds ); } for (const candidate of durationCandidates) { const seconds = normalizeScrollDurationSeconds(candidate); if (seconds > 0) { return seconds; } } const textCandidates = [ itemDetail.name, itemDetail.itemName, itemDetail.description, itemDetail.itemDescription, itemDetail.consumableDetail?.description, ]; for (const candidate of textCandidates) { const seconds = tryExtractDurationSecondsFromText(candidate); if (seconds > 0) { return seconds; } } const cooldownSeconds = normalizeScrollDurationSeconds(consumable.cooldownDuration); if (cooldownSeconds > 0) { return cooldownSeconds; } return isSkillingScrollItem(itemDetail) ? SKILLING_SCROLL_DEFAULT_DURATION_SECONDS : 0; } function isSkillingScrollBuffRelevantToHolyMilk(buffTypeHrid) { if (!buffTypeHrid || typeof buffTypeHrid !== "string") { return false; } return buffTypeHrid === "/buff_types/gathering" || buffTypeHrid === "/buff_types/efficiency" || buffTypeHrid === "/buff_types/action_speed" || buffTypeHrid === "/buff_types/action_level" || buffTypeHrid === "/buff_types/milking_level"; } function getSkillingScrollBuffs(itemDetail) { if (!isSkillingScrollItem(itemDetail)) { return []; } const config = getSkillingScrollValueConfig(itemDetail); if (config?.buff?.typeHrid) { return [{ ...config.buff, flatBoost: Number(config.buff.flatBoost || 0), }]; } if (Array.isArray(itemDetail?.consumableDetail?.buffs) && itemDetail.consumableDetail.buffs.length) { return itemDetail.consumableDetail.buffs .filter((buff) => isSkillingScrollBuffRelevantToHolyMilk(buff?.typeHrid)) .map((buff) => ({ ...buff, flatBoost: Number(buff?.flatBoost || 0), })); } return []; } function withTemporaryActionBuffs(actionTypeHrid, extraBuffs, computeFn) { if (!Array.isArray(extraBuffs) || !extraBuffs.length || typeof computeFn !== "function") { return computeFn(); } if (!state.communityActionTypeBuffsDict) { state.communityActionTypeBuffsDict = {}; } const originalBuffs = Array.isArray(state.communityActionTypeBuffsDict[actionTypeHrid]) ? state.communityActionTypeBuffsDict[actionTypeHrid] : []; state.communityActionTypeBuffsDict[actionTypeHrid] = [ ...originalBuffs, ...extraBuffs.map((buff) => ({ ...buff })), ]; clearCaches(); try { return computeFn(); } finally { if (originalBuffs.length > 0) { state.communityActionTypeBuffsDict[actionTypeHrid] = originalBuffs; } else { delete state.communityActionTypeBuffsDict[actionTypeHrid]; } clearCaches(); } } function buildSkillingScrollSavingsResult(config, durationSeconds, baseRate, buffedRate) { const baseSecondsPerItem = calculateItemSeconds(config.baseItemHrid); if (!Number.isFinite(baseSecondsPerItem) || baseSecondsPerItem <= 0 || !Number.isFinite(durationSeconds) || durationSeconds <= 0) { return null; } const baseItemName = ATTACHED_RARE_TARGET_ITEM_HRID_SET.has(config.baseItemHrid) ? getAttachedRareLabel(config.baseItemHrid) : getLocalizedItemName( config.baseItemHrid, state.itemDetailMap?.[config.baseItemHrid]?.name || config.baseItemHrid ); const extraItemCount = Math.max(0, durationSeconds * (Math.max(0, Number(buffedRate || 0)) - Math.max(0, Number(baseRate || 0)))); const savedSeconds = Math.max(0, extraItemCount * baseSecondsPerItem); return { itemHrid: config.itemHrid, durationSeconds, baseItemHrid: config.baseItemHrid, baseItemName, baseSecondsPerItem, buffedSecondsPerItem: Number(buffedRate || 0) > 0 ? (1 / Number(buffedRate || 0)) : 0, extraItemCount, savedSeconds, }; } function getRateBasedSkillingScrollTimeSavings(config, durationSeconds, extraBuffs) { const baseSecondsPerItem = calculateItemSeconds(config.baseItemHrid); if (!Number.isFinite(baseSecondsPerItem) || baseSecondsPerItem <= 0) { return null; } const buffedSecondsPerItem = withTemporaryActionBuffs(config.baseActionTypeHrid, extraBuffs, () => calculateItemSeconds(config.baseItemHrid) ); if (!Number.isFinite(buffedSecondsPerItem) || buffedSecondsPerItem <= 0) { return null; } const result = buildSkillingScrollSavingsResult(config, durationSeconds, 1 / baseSecondsPerItem, 1 / buffedSecondsPerItem); if (!result) { return null; } result.buffedSecondsPerItem = buffedSecondsPerItem; return result; } function getProcessingScrollTimeSavings(config, durationSeconds, extraBuffs) { const sourceAction = state.actionDetailMap?.[config.sourceActionHrid]; if (!sourceAction) { return null; } const baseSecondsPerProcessedItem = calculateItemSeconds(config.baseItemHrid); const baseSecondsPerSourceItem = calculateItemSeconds(config.sourceItemHrid); if (!Number.isFinite(baseSecondsPerProcessedItem) || baseSecondsPerProcessedItem <= 0) { return null; } if (!Number.isFinite(baseSecondsPerSourceItem) || baseSecondsPerSourceItem <= 0) { return null; } const getRates = () => { const summary = getActionSummary(sourceAction); if (!Number.isFinite(summary?.seconds) || summary.seconds <= 0) { return null; } const processedCount = getEffectiveOutputCountPerAction(sourceAction, config.baseItemHrid, summary); const sourceCount = getEffectiveOutputCountPerAction(sourceAction, config.sourceItemHrid, summary); return { processedRate: Number.isFinite(processedCount) && processedCount > 0 ? processedCount / summary.seconds : 0, sourceRate: Number.isFinite(sourceCount) && sourceCount > 0 ? sourceCount / summary.seconds : 0, }; }; const baseRates = getRates(); const buffedRates = withTemporaryActionBuffs(config.baseActionTypeHrid, extraBuffs, getRates); if (!baseRates || !buffedRates) { return null; } const extraProcessedRate = Math.max(0, Number(buffedRates.processedRate || 0) - Number(baseRates.processedRate || 0)); const lostSourceRate = Math.max(0, Number(baseRates.sourceRate || 0) - Number(buffedRates.sourceRate || 0)); const savedSeconds = Math.max( 0, durationSeconds * ( extraProcessedRate * baseSecondsPerProcessedItem - lostSourceRate * baseSecondsPerSourceItem ) ); const baseItemName = getLocalizedItemName( config.baseItemHrid, state.itemDetailMap?.[config.baseItemHrid]?.name || config.baseItemHrid ); return { itemHrid: config.itemHrid, durationSeconds, baseItemHrid: config.baseItemHrid, baseItemName, baseSecondsPerItem: baseSecondsPerProcessedItem, buffedSecondsPerItem: 0, extraItemCount: savedSeconds / baseSecondsPerProcessedItem, savedSeconds, }; } function getRareFindScrollTimeSavings(config, durationSeconds, extraBuffs) { const sourceAction = state.actionDetailMap?.[config.sourceActionHrid]; if (!sourceAction) { return null; } const getRate = () => { const summary = getActionSummary(sourceAction); if (!Number.isFinite(summary?.seconds) || summary.seconds <= 0) { return 0; } const sourceItemsPerSecond = getEffectiveOutputCountPerAction(sourceAction, config.sourceItemHrid, summary) / summary.seconds; const attachedRarePerItem = getAttachedRareYieldPerItem(config.sourceItemHrid, config.baseItemHrid); return Math.max(0, Number(sourceItemsPerSecond || 0)) * Math.max(0, Number(attachedRarePerItem || 0)); }; const baseRate = getRate(); const buffedRate = withTemporaryActionBuffs(config.baseActionTypeHrid, extraBuffs, getRate); return buildSkillingScrollSavingsResult(config, durationSeconds, baseRate, buffedRate); } function getSkillingScrollTimeSavings(itemHrid) { if (!itemHrid) { return null; } if (state.skillingScrollTimeSavingsCache.has(itemHrid)) { return state.skillingScrollTimeSavingsCache.get(itemHrid); } const itemDetail = state.itemDetailMap?.[itemHrid]; const config = getSkillingScrollValueConfig(itemDetail || itemHrid); const extraBuffs = getSkillingScrollBuffs(itemDetail); if (!isSkillingScrollItem(itemDetail) || !config || !extraBuffs.length) { state.skillingScrollTimeSavingsCache.set(itemHrid, null); return null; } const durationSeconds = getSkillingScrollDurationSeconds(itemDetail); if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) { state.skillingScrollTimeSavingsCache.set(itemHrid, null); return null; } let result = null; if (config.mode === "processing") { result = getProcessingScrollTimeSavings(config, durationSeconds, extraBuffs); } else if (config.mode === "rare_find") { result = getRareFindScrollTimeSavings(config, durationSeconds, extraBuffs); } else { result = getRateBasedSkillingScrollTimeSavings(config, durationSeconds, extraBuffs); } state.skillingScrollTimeSavingsCache.set(itemHrid, result); return result; } function getSkillingScrollTooltipText(itemHrid) { const savings = getSkillingScrollTimeSavings(itemHrid); if (!savings) { return ""; } const prefix = isZh ? `\u4ee5${savings.baseItemName}\u4e3a\u57fa\u51c6` : `Based on ${savings.baseItemName}`; const durationText = formatAutoDuration(savings.durationSeconds); const savedText = formatAutoDuration(savings.savedSeconds); const extraItemText = formatNumber(savings.extraItemCount); const line1 = isZh ? `${prefix}\uff1a${durationText}\u5377\u8f74\u7701\u65f6${savedText}` : `${prefix}: save ${savedText} over ${durationText}`; const line2 = isZh ? `\u7b49\u6548\u591a\u4ea7${extraItemText}\u4e2a${savings.baseItemName}` : `Equivalent extra ${extraItemText} ${savings.baseItemName}`; return `${line1}\n${line2}`; } function getTooltipRenderData(itemHrid, enhancementLevel = 0) { if (!itemHrid) { return null; } const cacheKey = `${itemHrid}#${Math.max(0, Number(enhancementLevel || 0))}`; if (state.itemTooltipDataCache.has(cacheKey)) { return state.itemTooltipDataCache.get(cacheKey); } if (enhancementLevel > 0) { const recommendation = getEnhancingRecommendationForItem(itemHrid, enhancementLevel); if (!recommendation) { const failureReason = state.itemFailureReasonCache.get(itemHrid) || (isZh ? "强化信息无法计算" : "Enhancing data unavailable"); const data = { itemHrid, enhancementLevel, unavailable: true, failureReason, isEnhancedEquipment: true, }; state.itemTooltipDataCache.set(cacheKey, data); return data; } const protectText = recommendation.recommendProtectAt > 0 ? `${isZh ? "推荐保护等级" : "Recommended protect"}:${recommendation.recommendProtectAt}${isZh ? "级" : ""}` : `${isZh ? "推荐保护等级" : "Recommended protect"}:${isZh ? "无需" : "None"}`; const totalText = `${isZh ? "总时间消耗" : "Total time"}:${formatAutoDuration(recommendation.totalSeconds || 0)}`; const essenceInfo = getEnhancedEquipmentEssenceInfo(itemHrid, enhancementLevel, recommendation); const decompositionCatalystInfo = getEnhancedEquipmentEssenceInfo( itemHrid, enhancementLevel, recommendation, "/items/catalyst_of_decomposition" ); const primeCatalystInfo = getEnhancedEquipmentEssenceInfo( itemHrid, enhancementLevel, recommendation, "/items/prime_catalyst" ); const essenceText = Number.isFinite(essenceInfo?.secondsPerEssence) && essenceInfo.secondsPerEssence > 0 ? `${isZh ? "强化精华时间" : "Enhancing essence time"}:${formatAutoDuration(essenceInfo.secondsPerEssence)}` : ""; const extraParts = []; if (Number.isFinite(essenceInfo?.essenceOutputCount) && essenceInfo.essenceOutputCount > 0) { extraParts.push( `${isZh ? "分解精华数量" : "Essence count"}:${formatNumber(essenceInfo.essenceOutputCount)}` ); } if (Number.isFinite(decompositionCatalystInfo?.secondsPerEssence) && decompositionCatalystInfo.secondsPerEssence > 0) { extraParts.push( `${isZh ? "分解催化剂强化精华时间" : "Decomp catalyst essence time"}:${formatAutoDuration(decompositionCatalystInfo.secondsPerEssence)}` ); } if (Number.isFinite(primeCatalystInfo?.secondsPerEssence) && primeCatalystInfo.secondsPerEssence > 0) { extraParts.push( `${isZh ? "至高催化剂强化精华时间" : "Prime catalyst essence time"}:${formatAutoDuration(primeCatalystInfo.secondsPerEssence)}` ); } const data = { itemHrid, enhancementLevel, isEnhancedEquipment: true, seconds: recommendation.totalSeconds || 0, detailText: protectText, attachedRareText: "", loadoutText: totalText, consumableText: essenceText, consumableAdjustedText: "", scrollText: "", extraText: extraParts.join(" | "), isEssence: false, }; state.itemTooltipDataCache.set(cacheKey, data); return data; } const fixedAttachedRareTooltipPlan = getFixedAttachedRareTooltipPlan(itemHrid); if (fixedAttachedRareTooltipPlan) { const consumableDetail = getConsumableValueDetailNormalized(itemHrid, fixedAttachedRareTooltipPlan.totalSeconds); const data = { itemHrid, seconds: fixedAttachedRareTooltipPlan.totalSeconds, detailText: getItemCalculationDetail(itemHrid), attachedRareText: "", loadoutText: getItemLoadoutDetail(itemHrid), consumableText: consumableDetail?.baseText || "", consumableAdjustedText: consumableDetail?.adjustedText || "", scrollText: "", isEssence: false, }; state.itemTooltipDataCache.set(cacheKey, data); return data; } const skillingScrollText = getSkillingScrollTooltipText(itemHrid); const seconds = calculateItemSeconds(itemHrid); if (seconds == null || !Number.isFinite(seconds) || seconds <= 0) { let data = null; if (skillingScrollText) { data = { itemHrid, isSkillingScroll: true, detailText: "", attachedRareText: "", loadoutText: "", consumableText: "", consumableAdjustedText: "", scrollText: skillingScrollText, isEssence: false, }; } else { const failureReason = state.itemFailureReasonCache.get(itemHrid) || ""; data = failureReason ? { itemHrid, unavailable: true, failureReason, } : null; } state.itemTooltipDataCache.set(cacheKey, data); return data; } const consumableDetail = getConsumableValueDetailNormalized(itemHrid, seconds); const data = { itemHrid, seconds, detailText: getItemCalculationDetail(itemHrid), attachedRareText: getAttachedRareTooltipLines(itemHrid).join("\n"), loadoutText: getItemLoadoutDetail(itemHrid), consumableText: consumableDetail?.baseText || "", consumableAdjustedText: consumableDetail?.adjustedText || "", scrollText: skillingScrollText, isSkillingScroll: Boolean(skillingScrollText), isEssence: Boolean(getFixedEnhancedEssencePlan(itemHrid) || getEssenceDecomposePlan(itemHrid)), }; state.itemTooltipDataCache.set(cacheKey, data); return data; } function ensureTooltipLabel(contentContainer, className, fontSize) { let label = contentContainer.querySelector(`.${className}`); if (!label) { label = document.createElement("div"); label.className = className; label.dataset.ictimeOwner = instanceId; label.style.color = "#000000"; label.style.fontSize = fontSize; label.style.lineHeight = "1.2"; label.style.marginTop = "2px"; contentContainer.appendChild(label); } label.dataset.ictimeOwner = instanceId; return label; } function decorateTooltip(tooltip) { runUiGuarded("decorateTooltip", () => { if (state.isShutDown || !tooltip?.isConnected) { return; } const contentContainer = getTooltipContentContainer(tooltip); const anchor = tooltip.querySelector('a[href*="#"]'); const hasItemName = tooltip.querySelectorAll("div.ItemTooltipText_name__2JAHA span").length > 0; if (!contentContainer && !anchor && !hasItemName) { return; } if (isMissingDerivedRuntimeState()) { ensureRuntimeStateFresh(); } if (!state.actionDetailMap || !state.itemDetailMap) { return; } const itemHrid = getItemHridFromTooltip(tooltip); if (!itemHrid) { return; } const enhancementLevel = getItemEnhancementLevelFromTooltip(tooltip); tooltip.dataset.ictimeItemHrid = itemHrid; tooltip.dataset.ictimeVersion = window.__ICTIME_VERSION__ || ""; if (!contentContainer) { return; } const renderData = getTooltipRenderData(itemHrid, enhancementLevel); if (!renderData) { return; } if (renderData.unavailable) { const label = ensureTooltipLabel(contentContainer, "ictime-label", "0.75rem"); label.textContent = renderData.isEnhancedEquipment ? (isZh ? "ICTime: 强化信息不可用" : "ICTime: Enhancing unavailable") : (isZh ? "ICTime: 已截断" : "ICTime: Truncated"); const detailLabel = ensureTooltipLabel(contentContainer, "ictime-detail", "0.72rem"); detailLabel.textContent = renderData.failureReason; const attachedRareLabel = ensureTooltipLabel(contentContainer, "ictime-attached-rare", "0.72rem"); attachedRareLabel.textContent = ""; attachedRareLabel.style.display = "none"; const loadoutLabel = ensureTooltipLabel(contentContainer, "ictime-loadout", "0.72rem"); loadoutLabel.textContent = ""; loadoutLabel.style.display = "none"; const consumableLabel = ensureTooltipLabel(contentContainer, "ictime-consumable", "0.72rem"); consumableLabel.textContent = ""; consumableLabel.style.display = "none"; const consumableAdjustedLabel = ensureTooltipLabel(contentContainer, "ictime-consumable-adjusted", "0.72rem"); consumableAdjustedLabel.textContent = ""; consumableAdjustedLabel.style.display = "none"; const scrollLabel = ensureTooltipLabel(contentContainer, "ictime-scroll", "0.72rem"); scrollLabel.textContent = ""; scrollLabel.style.display = "none"; const extraLabel = ensureTooltipLabel(contentContainer, "ictime-extra", "0.72rem"); extraLabel.textContent = ""; extraLabel.style.display = "none"; return; } state.lastTooltipRender = { itemHrid, enhancementLevel, seconds: renderData.seconds, detailText: renderData.detailText, attachedRareText: renderData.attachedRareText, loadoutText: renderData.loadoutText, consumableText: renderData.consumableText, consumableAdjustedText: renderData.consumableAdjustedText, scrollText: renderData.scrollText, extraText: renderData.extraText, renderedAt: Date.now(), tooltipTextBefore: tooltip.innerText || "", }; const compactMode = isTimeCalculatorCompactModeEnabled(); const label = ensureTooltipLabel(contentContainer, "ictime-label", "0.75rem"); label.textContent = renderData.isEnhancedEquipment ? (isZh ? `ICTime: 强化+${enhancementLevel}` : `ICTime: Enhance +${enhancementLevel}`) : renderData.isEssence ? `Time: ${formatAutoDuration(renderData.seconds)} | Time500: ${formatAutoDuration(renderData.seconds * 500)}` : `Time: ${formatAutoDuration(renderData.seconds)}`; if (renderData.isSkillingScroll) { label.textContent = isZh ? "ICTime: \u5377\u8f74\u4ef7\u503c" : "ICTime: Scroll value"; } const detailLabel = ensureTooltipLabel(contentContainer, "ictime-detail", "0.72rem"); detailLabel.textContent = renderData.isEnhancedEquipment ? (renderData.detailText || "") : (renderData.detailText || (isZh ? "战斗/其他来源暂未纳入计算" : "Combat/other sources not included")); if (renderData.isSkillingScroll) { detailLabel.textContent = ""; } if (compactMode) { detailLabel.textContent = ""; } detailLabel.style.display = detailLabel.textContent ? "" : "none"; const attachedRareLabel = ensureTooltipLabel(contentContainer, "ictime-attached-rare", "0.72rem"); attachedRareLabel.style.whiteSpace = "pre-line"; attachedRareLabel.textContent = renderData.attachedRareText || ""; attachedRareLabel.style.display = renderData.attachedRareText ? "" : "none"; const loadoutLabel = ensureTooltipLabel(contentContainer, "ictime-loadout", "0.72rem"); loadoutLabel.style.display = renderData.loadoutText ? "" : "none"; loadoutLabel.textContent = renderData.loadoutText || ""; const consumableLabel = ensureTooltipLabel(contentContainer, "ictime-consumable", "0.72rem"); consumableLabel.textContent = renderData.consumableText || ""; consumableLabel.style.display = renderData.consumableText ? "" : "none"; if (compactMode && renderData.isEnhancedEquipment) { consumableLabel.textContent = ""; consumableLabel.style.display = "none"; } const consumableAdjustedLabel = ensureTooltipLabel(contentContainer, "ictime-consumable-adjusted", "0.72rem"); consumableAdjustedLabel.textContent = renderData.consumableAdjustedText || ""; consumableAdjustedLabel.style.display = renderData.consumableAdjustedText ? "" : "none"; if (compactMode && renderData.isEnhancedEquipment) { consumableAdjustedLabel.textContent = ""; consumableAdjustedLabel.style.display = "none"; } const scrollLabel = ensureTooltipLabel(contentContainer, "ictime-scroll", "0.72rem"); scrollLabel.style.whiteSpace = "pre-line"; scrollLabel.textContent = renderData.scrollText || ""; scrollLabel.style.display = renderData.scrollText ? "" : "none"; const extraLabel = ensureTooltipLabel(contentContainer, "ictime-extra", "0.72rem"); extraLabel.textContent = renderData.extraText || ""; extraLabel.style.display = renderData.extraText ? "" : "none"; if (compactMode) { extraLabel.textContent = ""; extraLabel.style.display = "none"; } }); } function refreshOpenTooltips() { if (state.isShutDown || state.isRefreshingTooltips) { return; } state.isRefreshingTooltips = true; try { document.querySelectorAll(".MuiTooltip-popper").forEach((tooltip) => { runUiGuarded("refreshOpenTooltips", () => decorateTooltip(tooltip)); }); } finally { state.isRefreshingTooltips = false; } } function observeTooltips() { if (state.tooltipObserver) { state.tooltipObserver.disconnect(); } const observer = new MutationObserver((mutations) => { if (state.isShutDown) { return; } for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if (!(addedNode instanceof HTMLElement)) { continue; } if (addedNode.classList.contains("MuiTooltip-popper")) { runUiGuarded("observeTooltips", () => decorateTooltip(addedNode)); } } } }); observer.observe(document.body, { childList: true, subtree: true }); state.tooltipObserver = observer; } function shouldObserveTimeCalculatorRootNode(node) { if (!(node instanceof HTMLElement)) { return false; } const selector = '[class^="CharacterManagement_tabsComponentContainer"], [class*="TabsComponent_tabsContainer"], [class*="TabsComponent_tabPanelsContainer"]'; if (node.matches?.(selector)) { return true; } return Boolean(node.querySelector?.(selector)); } function observeTimeCalculatorUI() { if (state.timeCalculatorUiObserver) { state.timeCalculatorUiObserver.disconnect(); } const observer = new MutationObserver((mutations) => { if (state.isShutDown || state.timeCalculatorTabButton?.isConnected) { return; } for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if (shouldObserveTimeCalculatorRootNode(addedNode)) { queueTimeCalculatorRefresh(); return; } } } }); observer.observe(document.body, { childList: true, subtree: true }); state.timeCalculatorUiObserver = observer; } function shutdownInstance() { state.isShutDown = true; state.tooltipObserver?.disconnect(); state.tooltipObserver = null; state.timeCalculatorUiObserver?.disconnect(); state.timeCalculatorUiObserver = null; if (state.tooltipRefreshTimer) { clearTimeout(state.tooltipRefreshTimer); state.tooltipRefreshTimer = 0; } state.alchemyInferenceObserver?.disconnect(); state.alchemyInferenceObserver = null; state.alchemyObservedPanel = null; for (const timerId of state.alchemyInferenceDelayTimers) { clearTimeout(timerId); } state.alchemyInferenceDelayTimers = []; state.eventAbortController?.abort(); state.eventAbortController = null; state.timeCalculatorTabButton?.remove(); state.timeCalculatorTabButton = null; state.timeCalculatorTabPanel?.remove(); state.timeCalculatorTabPanel = null; state.timeCalculatorContainer = null; document.querySelectorAll(".ictime-alchemy-inference, .ictime-alchemy-inference-row").forEach((node) => node.remove()); } function startWhenReady() { if (!document.body) { requestAnimationFrame(startWhenReady); return; } if (!state.actionDetailMap || !state.itemDetailMap) { loadCachedClientData(); } if (isMissingDerivedRuntimeState()) { hydrateFromReactState(); } if (!state.eventAbortController) { state.eventAbortController = new AbortController(); const enhancingEventHandler = (event) => { runUiGuarded("enhancingEventHandler", () => { if (shouldRefreshEnhancingFromTarget(event.target)) { queueEnhancingRefresh(); } if (shouldRefreshAlchemyInferenceFromTarget(event.target)) { queueAlchemyInferenceRefresh(); scheduleAlchemyInferenceRefreshBurst(); } }); }; document.addEventListener("mouseover", (event) => { runUiGuarded("trackHoveredItem", () => { if (trackHoveredItem(event.target)) { queueTooltipRefresh(); } }); }, { capture: true, passive: true, signal: state.eventAbortController.signal }); document.addEventListener("input", enhancingEventHandler, { capture: true, signal: state.eventAbortController.signal }); document.addEventListener("change", enhancingEventHandler, { capture: true, signal: state.eventAbortController.signal }); document.addEventListener("click", enhancingEventHandler, { capture: true, signal: state.eventAbortController.signal }); } observeTooltips(); observeTimeCalculatorUI(); refreshOpenTooltips(); queueEnhancingRefresh(); queueAlchemyInferenceRefresh(); queueTimeCalculatorRefresh(); } window.__ICTIME_DEBUG__ = { state, instanceId, findActionForItem, getActionSummary, getDisplayOutputCountPerAction, getEffectiveOutputCountPerAction, calculateItemSeconds, hydrateFromReactState, resolveSkillingLoadout, clearCaches, shutdownInstance, getEssenceDecomposePlan, getFixedTransmutePlan, getFixedAttachedRareTooltipPlan, getFixedEnhancedEssencePlan, getAttachedRareYieldPerItem, getItemSecondsLinearRelationToTarget, getSkillingScrollTimeSavings, getCurrentAlchemyTransmuteInference, renderAlchemyTransmuteInference, getEnhancingRecommendationForItem, getItemCalculationDetail, getItemLoadoutDetail, getEnhancingRecommendation, renderEnhancingRecommendation, renderTimeCalculatorPanel, getTimeCalculatorEntrySummary, }; window.__ICTIME_CONTROLLER__ = { instanceId, shutdown: shutdownInstance, }; hookWebSocket(); loadCachedClientData(); startWhenReady(); })();