// ==UserScript==
// @name ICTime
// @name:en ICTime
// @name:zh-CN ICTime 时间计算
// @namespace http://tampermonkey.net/
// @version 1.0.3
// @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/lz-string@1.5.0/libs/lz-string.min.js
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// @downloadURL https://update.greasyfork.icu/scripts/573779/ICTime.user.js
// @updateURL https://update.greasyfork.icu/scripts/573779/ICTime.meta.js
// ==/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.3";
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 normalizeDungeonPartyCount(value) {
const numeric = Math.floor(Number(value || 5));
if (numeric >= 5) {
return 5;
}
if (numeric >= 1) {
return numeric;
}
return 5;
}
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 getDungeonPartyChestQuantityMultiplier(partyCount) {
return Math.max(0.0001, 5 / normalizeDungeonPartyCount(partyCount));
}
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),
partyCount: normalizeDungeonPartyCount(entry.partyCount),
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(shouldClearDerivedCaches = true) {
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),
partyCount: normalizeDungeonPartyCount(entry.partyCount),
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),
})),
})),
}));
if (shouldClearDerivedCaches) {
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(false);
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(false);
}
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.partyCount = payload.itemHrid && (DUNGEON_CHEST_ITEM_HRIDS.includes(payload.itemHrid) || REFINEMENT_CHEST_ITEM_HRIDS.includes(payload.itemHrid))
? normalizeDungeonPartyCount(payload.partyCount || existing.partyCount || 5)
: 5;
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,
partyCount: payload.itemHrid && (DUNGEON_CHEST_ITEM_HRIDS.includes(payload.itemHrid) || REFINEMENT_CHEST_ITEM_HRIDS.includes(payload.itemHrid))
? normalizeDungeonPartyCount(payload.partyCount || 5)
: 5,
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 partyCount = itemType === "chest" || itemType === "refinement_chest"
? normalizeDungeonPartyCount(entry?.partyCount || 5)
: 5;
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,
partyCount,
partyMultiplier: 1,
chestQuantityMultiplier: 1,
};
}
const partyMultiplier = getDungeonPartyChestQuantityMultiplier(partyCount);
const chestQuantityMultiplier = getCombatChestQuantityMultiplier() * partyMultiplier;
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,
partyCount,
partyMultiplier,
chestQuantityMultiplier,
};
}
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,
partyCount,
partyMultiplier,
chestQuantityMultiplier,
};
}
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,
partyCount,
partyMultiplier,
chestQuantityMultiplier,
};
}
const baseChestEntryKeyHrid = getDungeonEntryKeyHridByChest(baseChestItemHrid);
const rawBaseChestEntryKeySeconds = baseChestEntryKeyHrid ? calculateItemSeconds(baseChestEntryKeyHrid) : 0;
const baseChestEntryKeyFailureReason =
baseChestEntryKeyHrid && (!Number.isFinite(rawBaseChestEntryKeySeconds) || rawBaseChestEntryKeySeconds <= 0)
? getDependencyFailureReason(baseChestEntryKeyHrid)
: "";
if (baseChestEntryKeyFailureReason) {
return {
itemType,
runMinutes,
runSeconds,
quantityPer24h,
failureReason: baseChestEntryKeyFailureReason,
dungeonEntryKeyHrid,
dungeonEntryKeySeconds: adjustedDungeonEntryKeySeconds,
dungeonEntryKeyContributionSeconds: 0,
foodSeconds,
foodContributionSeconds: 0,
drinkSeconds,
drinkContributionSeconds: 0,
totalSeconds: adjustedTotalSeconds,
expectedChestCount: 0,
refinementExpectedCount: 0,
normalExpectedCount: 0,
secondsPerChest: 0,
dungeonTier,
partyCount,
partyMultiplier,
chestQuantityMultiplier,
};
}
const normalExpectedCount = chestQuantityMultiplier;
const refinementExpectedCount = Math.max(0.0001, refinementBaseCount * chestQuantityMultiplier);
const baseChestRunSeconds = parseNonNegativeDecimal(baseChestEntry?.runMinutes) * 60;
const baseChestFoodSeconds = Math.max(0, Number(baseChestSummary.foodSeconds || 0));
const baseChestDrinkSeconds = Math.max(0, Number(baseChestSummary.drinkSeconds || 0));
const baseChestEntryKeySeconds = Number.isFinite(rawBaseChestEntryKeySeconds) && rawBaseChestEntryKeySeconds > 0
? rawBaseChestEntryKeySeconds
: 0;
const normalChestBaselineSeconds =
baseChestRunSeconds +
baseChestFoodSeconds +
baseChestDrinkSeconds +
baseChestEntryKeySeconds * chestQuantityMultiplier;
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,
baseChestRunSeconds,
baseChestFoodSeconds,
baseChestDrinkSeconds,
baseChestEntryKeySeconds,
normalChestBaselineSeconds,
secondsPerChest: 0,
dungeonTier,
partyCount,
partyMultiplier,
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: refinementExpectedCount,
refinementExpectedCount,
refinementBaseCount,
normalExpectedCount,
baseChestRunSeconds,
baseChestFoodSeconds,
baseChestDrinkSeconds,
baseChestEntryKeySeconds,
normalChestBaselineSeconds,
secondsPerChest: remainingSeconds / refinementExpectedCount,
dungeonTier,
partyCount,
partyMultiplier,
chestQuantityMultiplier,
};
}
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,
partyCount,
partyMultiplier,
chestQuantityMultiplier,
};
}
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(false);
if (card.isConnected) {
card.replaceWith(createTimeCalculatorEntryCard(entry));
return;
}
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 === "chest" || summary.itemType === "refinement_chest") {
const partyRow = document.createElement("div");
partyRow.style.display = "flex";
partyRow.style.alignItems = "center";
partyRow.style.gap = "6px";
partyRow.style.marginTop = "8px";
partyRow.style.textAlign = "left";
card.appendChild(partyRow);
const partyLabel = document.createElement("div");
partyLabel.textContent = isZh ? "\u4eba\u6570" : "Party size";
partyLabel.style.flex = "1";
partyRow.appendChild(partyLabel);
const partySelect = document.createElement("select");
partySelect.style.background = "#dde2f8";
partySelect.style.color = "#000000";
partySelect.style.border = "none";
partySelect.style.borderRadius = "4px";
partySelect.style.padding = "4px";
for (let count = 1; count <= 5; count += 1) {
const option = document.createElement("option");
option.value = String(count);
option.textContent = String(count);
partySelect.appendChild(option);
}
partySelect.value = String(normalizeDungeonPartyCount(entry.partyCount || 5));
partySelect.addEventListener("change", () => {
entry.partyCount = normalizeDungeonPartyCount(partySelect.value || 5);
saveTimeCalculatorData();
rerenderTimeCalculatorPanel();
});
partyRow.appendChild(partySelect);
}
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("
")
: [
`${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("
");
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("
")
: [
`${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("
");
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("
");
}
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("
")
: [
`${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("
");
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,
partyCount: 5,
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();
})();