ICTime

Show item time cost and related helper info in Milky Way Idle.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
})();