/* forge_stats.js — Aktions-Statistiken (dwarf/elf/pet/smelt/enchant/arena/downgrade). * Player-Lookup-Unterkategorie (#ai-forge-panel) + Untertab im Leaderboards-Tab. * Balken-Laenge = VERSUCHE; Erfolg farbig, Fail rot. Nutzt base()/_fp3() aus index.html. */ (function () { "use strict"; const $ = (id) => document.getElementById(id); const ACTION_LABEL = { dwarf: "Dwarf Forge", elf: "Elf", pet_enhance: "Pet Enhance", smelt: "Smelt", enchant: "Enchant", arena: "Arena", nightmare: "Nightmare Arena", downgrade: "Downgrade" }; const LOOKUP_ORDER = ["dwarf", "elf", "pet_enhance", "smelt", "enchant", "arena", "nightmare", "downgrade"]; // Aktion -> breakdown-dim fuer die globalen Totals (deckt sich mit item_totals/forge_api). const TOTAL_DIM = { arena: "drop", nightmare: "drop", enchant: "enchant", smelt: "ore", elf: "input", pet_enhance: "pet", downgrade: "level" }; const RARITIES = ["Common", "Rare", "Epic", "Legendary"]; const ROMAN = ["", "I", "II", "III", "IV", "V"]; const ENCH_COLOR = { Flame: "#ff6a3d", Thorns: "#7ee081", Looting: "#ffd23d", Freeze: "#5fc8ff", Fortune: "#ffb020" }; const ENCH_LV = { 1: "#8fd0ff", 2: "#4ea3ff", 3: "#2f6fe0" }; // Enchant L1→L3 (Blau-Rampe) const ORE_COLOR = { Coal: "#8894a6", Iron: "#c9d1dc", Diamond: "#5fe6ff", Emerald: "#42e695", Ruby: "#ff5a7a", Gold: "#ffcc33", Sapphire: "#5a8cff" }; const GREEN = "var(--green,#00e676)", RED = "#a51d2a", BLUE = "#4ea3ff"; // Fail = dunkles Rot // Erfolgs-Verb je Aktion (Header + Balken-Tooltips) const SUCCESS_VERB = { dwarf: "forged", elf: "combined", pet_enhance: "enhanced", smelt: "smelted", enchant: "enchanted", arena: "found", nightmare: "found", downgrade: "downgraded" }; // Elf: Epic-Input -> Legendary-Output (aus Katalog-Themen + Logdaten bestaetigt). const ELF_MAP = { "dragon's poison": "Dragon's Devil", "solbrand": "Divinity Edge", "soulkeeper's blade": "Last Horizon", "dragon's anger": "Dragon's Soul", "sunward bulwark": "Asgardian Aegis", "sealguard": "Final Bastion" }; const rarityColor = (r) => ({ Common: "#9fb0c3", Rare: "#4ea3ff", Epic: "#b060ff", Legendary: "#ffb020" }[r] || "var(--accent,#00c8ff)"); const esc = (s) => String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); const enc = (s) => encodeURIComponent(String(s == null ? "" : s)).replace(/'/g, "%27"); const fInt = (n) => (n == null ? "0" : Number(n).toLocaleString("en-US")); function fShort(n) { n = Number(n) || 0; const a = Math.abs(n); if (a >= 1e6) return (n / 1e6).toFixed(a >= 1e7 ? 1 : 2) + "M"; if (a >= 1e3) return (n / 1e3).toFixed(a >= 1e4 ? 0 : 1) + "k"; return String(n); } const fPct = (x) => (x == null ? "–" : (x * 100).toFixed(1) + "%"); const fPct0 = (x) => (x == null ? "–" : Math.round(x * 100) + "%"); const _fpow = (n) => (typeof _fp3 === "function" ? _fp3(n) : String(Math.round(n))); const fVal = (net) => (net >= 0 ? "+" : "") + _fpow(net); const luckClass = (p) => (p == null ? "" : p >= 0.6 ? "f-lucky" : p <= 0.4 ? "f-unlucky" : "f-neutral"); const rcls = (r) => "r-" + String(r || "").toLowerCase(); function ago(dateStr) { if (!dateStr) return ""; const d = Date.parse(dateStr + "T00:00:00Z"); if (isNaN(d)) return dateStr; const days = Math.floor((Date.now() - d) / 86400000); return days <= 0 ? "today" : days === 1 ? "1 day ago" : days + " days ago"; } async function api(path) { const r = await fetch(`${(typeof base === "function" ? base() : "")}/forge${path}`); if (!r.ok) { let e = {}; try { e = await r.json(); } catch (_) {} throw new Error((e && e.detail) || `HTTP ${r.status}`); } return r.json(); } // Split-Balken: Laenge ∝ Summe der Segmente; Segmente farbig; Zahl je Segment IM // Segment (wenn Platz), Gesamtsumme rechts. Kein Zahlenblock mehr vorne. function splitBar(labelHtml, segs, maxTotal, opts) { opts = opts || {}; segs = segs.filter((s) => s.v > 0); const total = segs.reduce((s, x) => s + x.v, 0); const fillPct = maxTotal > 0 ? Math.max(1.5, total / maxTotal * 100) : 0; // Zahl OPTIMISTISCH in JEDES Segment (auch bei nur einer Farbe). measureBar entscheidet // nach dem Layout: passt alles rein -> drin lassen; passt EINE nicht -> ALLE Zahlen (in // Reihenfolge, je Segmentfarbe) direkt HINTER das Balkenende. const inner = segs.map((s) => `${fInt(s.v)}`).join(""); const arrow = opts.clickable ? '▸' : ""; return `
${labelHtml}
${inner}
${arrow}
`; } // Nach dem Layout messen: passen ALLE Segment-Zahlen rein -> drin lassen. Passt EINE nicht // -> ALLE Zahlen (in Segment-Reihenfolge, je Segmentfarbe) direkt HINTER das Balkenende. // So bleibt die Reihenfolge klar und die Zahlen kleben am Balken (nicht am rechten Rand). function measureBar(bar) { if (bar.dataset.measured) return; const fill = bar.querySelector(".fbar-fill"); const track = bar.querySelector(".fbar-track"); if (!fill || !track || !track.clientWidth) return; // noch nicht sichtbar -> spaeter const segs = [...fill.querySelectorAll(".sbar-seg")]; if (!segs.length) { bar.dataset.measured = "1"; return; } let overflow = false; segs.forEach((seg) => { const n = seg.querySelector(".sbar-num"); if (n && n.offsetWidth > seg.clientWidth + 1) overflow = true; }); if (overflow) { const items = segs.map((seg) => { const n = seg.querySelector(".sbar-num"); return n ? `${n.textContent}` : ""; }).filter(Boolean); segs.forEach((seg) => { const n = seg.querySelector(".sbar-num"); if (n) n.remove(); }); const after = document.createElement("span"); after.className = "fbar-after"; after.innerHTML = items.join("·"); track.appendChild(after); // direkt hinter dem Fill (Flex) -> klebt am Balken } bar.dataset.measured = "1"; } function measureIn(node) { if (node.nodeType !== 1) return; if (node.matches && node.matches(".fbar")) measureBar(node); if (node.querySelectorAll) node.querySelectorAll(".fbar").forEach(measureBar); } let _barObs = null; function initBarMeasure() { if (_barObs || typeof MutationObserver !== "function") return; _barObs = new MutationObserver((muts) => { muts.forEach((m) => m.addedNodes.forEach(measureIn)); }); ["ai-forge-panel", "tab-leaderboard"].forEach((id) => { const r = $(id); if (r) _barObs.observe(r, { childList: true, subtree: true }); }); } const NAME_NOTE = `
📌 Everything here is by display name, not account — a rename starts a fresh history.
`; const sw = (c) => ``; const LEGEND_NOTE = `
Each bar's length = attempts. Inside, a bar is split into its outcomes: ${sw("var(--green,#00e676)")}any coloured part = a success, ${sw(RED)}dark red = a fail. For enchants the coloured parts are the enchant level reached (${sw(ENCH_LV[1])}L1 ${sw(ENCH_LV[2])}L2 ${sw(ENCH_LV[3])}L3); for dwarf/elf they're coloured by item rarity.
`; async function ensureRates() {} // Attempts sind jetzt exakt (Shield-Burns rueckabgebildet) // ════════════════ PLAYER-LOOKUP ════════════════ async function loadForgeStats(name) { const panel = $("ai-forge-panel"); if (!panel) return; try { window.smTrack && smTrack("lookup:forge"); } catch (e) {} panel.style.display = ""; const body = $("ai-forge-body"), luck = $("ai-forge-luck"); body.innerHTML = '
⟳ Loading…
'; luck.innerHTML = ""; try { await ensureRates(); const d = await api(`/player?name=${encodeURIComponent(name)}`); luck.innerHTML = luckBadgeHTML(d.overall); const ord = (x) => { const i = LOOKUP_ORDER.indexOf(x.action); return i < 0 ? 99 : i; }; const acts = (d.actions || []).slice().sort((a, b) => ord(a) - ord(b)); body.innerHTML = NAME_NOTE + LEGEND_NOTE + (acts.map((a) => actionCard(d.name, a)).join("") || '
No forge actions logged.
'); } catch (e) { luck.innerHTML = ""; body.innerHTML = /No forge stats/i.test(e.message) ? '
No forge activity logged for this name yet.
' : `
✗ ${esc(e.message)}
`; } } function forgeHideStats() { const p = $("ai-forge-panel"); if (p) p.style.display = "none"; } // minimaler Header: nur Zahlen (keine Balken/Prozente) function actionCard(name, a) { let counts; const verb = SUCCESS_VERB[a.action] || "ok"; if (!a.has_rate) counts = `${fInt(a.successes || a.attempts)} ${verb}`; else counts = `${fInt(a.successes)} ${verb} · ${fInt(a.fails)} failed`; let extra = ""; // Luck pro Aktion = Prozent ueber/unter dem Erwarteten. NUR dwarf/elf/pet (dort sind die // Odds sauber; dwarf/elf inkl. Fortune). smelt/enchant/downgrade: kein Luck (Odds nicht // rekonstruierbar). arena: nur Legendary-Count. if (a.luck_pct != null) { const s = a.luck_pct >= 0 ? "+" : ""; extra += `luck ${s}${a.luck_pct.toFixed(1)}%`; } // Value pro Aktion (dwarf/elf/pet): Coins ueber/unter dem erwarteten Wert (Input immer // verloren, Output nur bei Erfolg). fVal setzt das Vorzeichen selbst. if (a.value_luck_coins != null) { extra += `value ${fVal(a.value_luck_coins)}`; } if ((a.action === "arena" || a.action === "nightmare") && a.legendary != null) extra += `${fInt(a.legendary)} legendary`; return `
${esc(ACTION_LABEL[a.action] || a.action)} ${counts} ${extra}▸
`; } async function forgeToggleDetail(head, nameEnc, action) { const det = head.parentElement.querySelector(".forge-action-detail"); const caret = head.querySelector(".forge-caret"); if (det.style.display !== "none") { det.style.display = "none"; caret.textContent = "▸"; return; } try { window.smTrack && smTrack("stat:" + String(action).toLowerCase().replace(/[^a-z0-9_.\-]/g, "").slice(0, 39)); } catch (e) {} caret.textContent = "▾"; det.style.display = ""; det.innerHTML = '
⟳…
'; try { const d = await api(`/player-detail?name=${nameEnc}&action=${action}&limit=1500`); det.innerHTML = renderDetail(action, d.dims, nameEnc) || '
No breakdown logged.
'; } catch (e) { det.innerHTML = `
✗ ${esc(e.message)}
`; } } function sec(title, html) { return `
${title}
${html}
`; } function renderDetail(action, dims, nameEnc) { const S = []; if (action === "dwarf" && dims.item) { // pro Item (egal Level): geschaetzte Versuche; Erfolg(rarity-Farbe)+Fail(rot) const byItem = {}; dims.item.forEach((r) => { const e = byItem[r.key] = byItem[r.key] || { su: 0, att: 0, rarity: r.rarity }; e.su += r.successes; e.att += r.attempts; }); const rows = Object.entries(byItem).map(([k, v]) => ({ key: k, ...v })).sort((a, b) => b.att - a.att); const max = Math.max(1, ...rows.map((r) => r.att)); S.push(sec("Items you tried to forge — click one for the level split", rows.map((r) => splitBar(`${esc(r.key)}`, [{ v: r.su, color: rarityColor(r.rarity), label: "forged" }, { v: Math.max(0, r.att - r.su), color: RED, label: "burned" }], max, { clickable: true, onclick: `forgeDwarfItem(this,'${nameEnc}','${enc(r.key)}')` })).join(""))); } else if (action === "enchant") { if (dims.enchant) { // je Typ ein Balken: L1 · L2 · L3 · Fail (Fail einheitlich hinten) const byType = {}; dims.enchant.forEach((r) => { const t = byType[r.key] = byType[r.key] || { fail: 0, lv: {} }; if (r.level === 0) t.fail += r.attempts; else t.lv[r.level] = (t.lv[r.level] || 0) + r.successes; }); const rows = Object.entries(byType).map(([t, v]) => ({ t, ...v, total: v.fail + Object.values(v.lv).reduce((a, b) => a + b, 0) })).sort((a, b) => b.total - a.total); const max = Math.max(1, ...rows.map((r) => r.total)); const encKey = `
${sw(ENCH_LV[1])}L1 ${sw(ENCH_LV[2])}L2 ${sw(ENCH_LV[3])}L3 ${sw(RED)}fail
`; S.push(sec("Enchants — level reached vs fail (bar = attempts)", encKey + rows.map((r) => splitBar( `${esc(r.t)}`, [{ v: r.lv[1] || 0, color: ENCH_LV[1], label: "L1" }, { v: r.lv[2] || 0, color: ENCH_LV[2], label: "L2" }, { v: r.lv[3] || 0, color: ENCH_LV[3], label: "L3" }, { v: r.fail, color: RED, label: "fail" }], max)).join(""))); } if (dims.item) { const max = Math.max(1, ...dims.item.map((r) => r.successes)); S.push(sec("By item (successful enchants)", dims.item.slice(0, 25).map((r) => splitBar(esc(r.key), [{ v: r.successes, color: BLUE, label: "enchanted" }], max)).join(""))); } } else if (action === "smelt" && dims.ore) { const max = Math.max(1, ...dims.ore.map((r) => r.attempts)); S.push(sec("Smelting by ore (bar = attempts)", dims.ore.slice().sort((a, b) => b.attempts - a.attempts).map((r) => splitBar(`${esc(r.key)}`, [{ v: r.successes, color: ORE_COLOR[r.key] || GREEN, label: "smelted" }, { v: r.attempts - r.successes, color: RED, label: "fail" }], max)).join(""))); } else if (action === "downgrade" && dims.level) { const rows = dims.level.slice().sort((a, b) => a.level - b.level); const max = Math.max(1, ...rows.map((r) => r.attempts)); S.push(sec("Downgrades — pieces recovered vs burned (bar = attempts)", rows.map((r) => { const avg = r.attempts ? r.extra / r.attempts : 0; return splitBar(`L${r.level} → L${Math.max(1, r.level - 1)} · ⌀ ${avg.toFixed(1)} pcs`, [{ v: r.successes, color: GREEN, label: "downgraded" }, { v: r.attempts - r.successes, color: RED, label: "burned" }], max); }).join("") + `
Downgrading one Level-N item returns up to N copies at Level N−1; a burn returns none. “⌀ pcs” = average pieces recovered per attempt.
`)); } else if ((action === "arena" || action === "nightmare") && dims.drop) { const rows = dims.drop.slice().sort((a, b) => b.successes - a.successes); const max = Math.max(1, ...rows.map((r) => r.successes)); S.push(sec((action === "nightmare" ? "Nightmare Arena loot" : "Arena loot") + " — items found", rows.map((r) => splitBar(`${esc(r.key)} L${r.level}`, [{ v: r.successes, color: rarityColor(r.rarity), label: "found" }], max, { breakdown: false })).join(""))); } else if (action === "elf" && dims.input) { // Nur EIN Set, benannt nach dem legendären Ergebnis (Input-Versuche = Legendary-Versuche). const rows = dims.input.slice() .map((r) => ({ out: ELF_MAP[String(r.key).toLowerCase()] || r.key, att: r.attempts, su: r.successes })) .sort((a, b) => b.att - a.att); const max = Math.max(1, ...rows.map((r) => r.att)); S.push(sec("Elf combines — legendary made vs burned (bar = attempts)", rows.map((r) => splitBar(`${esc(r.out)}`, [{ v: r.su, color: rarityColor("Legendary"), label: "combined" }, { v: Math.max(0, r.att - r.su), color: RED, label: "burned" }], max)).join(""))); } else if (action === "pet_enhance" && dims.pet) { // nach Versuchsanzahl sortiert (meiste Versuche oben) const rows = dims.pet.slice().sort((a, b) => b.attempts - a.attempts); const max = Math.max(1, ...rows.map((r) => r.attempts)); S.push(sec("Pet enhancing — by attempts (bar = attempts)", rows.map((r) => splitBar(`${esc(r.key)} ${fPct0(r.successes / r.attempts)}`, [{ v: r.successes, color: BLUE, label: "enhanced" }, { v: r.attempts - r.successes, color: RED, label: "fail" }], max)).join(""))); } return S.join(""); } // Dwarf-Item -> Level-Aufteilung (nur versuchte Level, 1 oben) async function forgeDwarfItem(barEl, nameEnc, itemEnc) { let host = barEl.nextElementSibling; if (host && host.classList.contains("fdwarf-levels")) { host.remove(); return; } host = document.createElement("div"); host.className = "fdwarf-levels"; host.innerHTML = '
⟳…
'; barEl.after(host); try { // nameEnc === "@@combine" -> kombinierte Mehr-Account-Ansicht (names=), sonst ein Account. const q = nameEnc === "@@combine" ? `names=${encodeURIComponent(combineNames)}` : `name=${nameEnc}`; const d = await api(`/player-detail?${q}&action=dwarf&limit=1500`); const item = decodeURIComponent(itemEnc); const rows = (d.dims.item || []).filter((r) => r.key === item) .map((r) => ({ level: r.level, rarity: r.rarity, su: r.successes, att: r.attempts })) .sort((a, b) => a.level - b.level); const max = Math.max(1, ...rows.map((r) => r.att)); host.innerHTML = rows.map((r) => splitBar(`L${r.level}`, [{ v: r.su, color: rarityColor(r.rarity), label: "forged" }, { v: Math.max(0, r.att - r.su), color: RED, label: "burned" }], max)).join(""); } catch (e) { host.innerHTML = `
✗ ${esc(e.message)}
`; } } // ════════════════ LEADERBOARDS-TAB ════════════════ let injected = false, tabReady = false, meta = null, itemsLoaded = false, boardLoaded = false; const lb = { action: "dwarf", sort: "attempts", q: "", offset: 0, limit: 25 }; let lbSeq = 0, lbTimer = null; function injectIntoLeaderboardTab() { if (injected) return; injected = true; const lt = $("tab-leaderboard"); if (!lt) return; const live = document.createElement("div"); live.id = "lb-live-wrap"; while (lt.firstChild) live.appendChild(lt.firstChild); const bar2 = document.createElement("div"); bar2.className = "forge-subtabs lb-topsubs"; bar2.innerHTML = ` `; const forge = document.createElement("div"); forge.id = "lb-forge-wrap"; forge.style.display = "none"; forge.innerHTML = forgeTabHTML(); const mob = document.createElement("div"); mob.id = "lb-mob-wrap"; mob.style.display = "none"; mob.innerHTML = (typeof window.mobTabHTML === "function") ? window.mobTabHTML() : ""; lt.appendChild(bar2); lt.appendChild(live); lt.appendChild(forge); lt.appendChild(mob); } function forgeTabHTML() { return `

Forge & Action Stats

${NAME_NOTE}${LEGEND_NOTE}
`; } function forgeOnLeaderboardOpen() { injectIntoLeaderboardTab(); } function lbSubtab(which) { document.querySelectorAll(".lb-topsubs .forge-subtab").forEach((b) => b.classList.toggle("active", b.dataset.lbsub === which)); $("lb-live-wrap").style.display = which === "live" ? "" : "none"; $("lb-forge-wrap").style.display = which === "forge" ? "" : "none"; const mw = $("lb-mob-wrap"); if (mw) mw.style.display = which === "mob" ? "" : "none"; if (which === "forge") loadForgeTab(); if (which === "mob" && typeof window.mobOnShow === "function") window.mobOnShow(); } async function loadForgeTab() { if (tabReady) return; tabReady = true; try { meta = await api("/meta"); const upd = meta.data_through ? `data through ${meta.data_through} (${ago(meta.data_through)})` : ""; $("forge-meta-summary").innerHTML = `${fInt(meta.player_count)} names · ${fInt(+meta.meta.total_events)} events${upd ? " · " + upd : ""}`; const actsOrdered = (meta.actions || []).slice().sort((a, b) => { const ia = LOOKUP_ORDER.indexOf(a), ib = LOOKUP_ORDER.indexOf(b); return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib); }); $("forge-lb-action").innerHTML = actsOrdered.map((a) => ``).join(""); syncSortOptions(); } catch (e) { $("forge-meta-summary").textContent = "✗ " + e.message; } forgeSubtab("board"); // Leaderboard ist die Standardansicht } function syncSortOptions() { const action = $("forge-lb-action").value, dwarf = action === "dwarf"; const drop = action === "arena" || action === "nightmare"; const opts = []; if (drop) opts.push(["legendary", "Most legendarys"]); // Arena/Nightmare: nach Legendary-Drops opts.push(["attempts", "Most attempts"]); if (dwarf) opts.push(["gain", "Highest value gain"], ["loss", "Biggest value loss"]); opts.push(["luckiest", "Luckiest"], ["unluckiest", "Unluckiest"]); // fuer alle const sel = $("forge-lb-sort"), cur = sel.value; sel.innerHTML = opts.map(([v, l]) => ``).join(""); sel.value = opts.some((o) => o[0] === cur) ? cur : opts[0][0]; } async function loadForgeLeaderboard(reset) { const action = $("forge-lb-action").value; syncSortOptions(); const sort = $("forge-lb-sort").value, q = $("forge-lb-q").value.trim(); if (reset) lb.offset = 0; Object.assign(lb, { action, sort, q }); const seq = ++lbSeq, body = $("forge-lb-body"); body.innerHTML = '
⟳ Loading…
'; try { const d = await api(`/leaderboard?action=${action}&sort=${sort}&q=${encodeURIComponent(q)}&offset=${lb.offset}&limit=${lb.limit}`); if (seq !== lbSeq) return; if (!d.rows.length) { body.innerHTML = '
No names found.
'; } else body.innerHTML = `${d.is_arena ? "" : ""}${d.has_value ? "" : ""}${ d.rows.map((r, i) => { const rank = r.rank != null ? r.rank : (lb.offset + i + 1); const cnt = d.has_rate ? `
${fInt(r.successes)} / ${fInt(r.attempts)}${fPct0(r.rate)}
` : fInt(r.attempts); let c3; if (d.is_arena) c3 = ``; else c3 = r.luck_z != null ? `` : ""; const val = d.has_value ? `` : ""; return `${c3}${val}`; }).join("")}
#Name ${d.has_rate ? "Success / Attempts" : "Runs"}LegendaryLuckValue
${fInt(r.legendary)} ${fPct(r.legendary_ratio)}${r.luck_z >= 0 ? "+" : ""}${r.luck_z.toFixed(2)}–${r.value_known ? fVal(r.value_net || 0) : "–"}
${fInt(rank)} ${esc(r.name)}${cnt}
`; $("forge-lb-pageinfo").textContent = d.rows.length ? `${lb.offset + 1}–${lb.offset + d.rows.length}` : "0"; $("forge-lb-prev").disabled = lb.offset <= 0; $("forge-lb-next").disabled = !d.has_more; } catch (e) { body.innerHTML = `
✗ ${esc(e.message)}
`; } } function forgeLbPage(dir) { lb.offset = Math.max(0, lb.offset + dir * lb.limit); loadForgeLeaderboard(false); } function forgeLbSearch() { clearTimeout(lbTimer); lbTimer = setTimeout(() => loadForgeLeaderboard(true), 350); } // 1. Klick: Aktion-Stats inline; 2. Klick: wieder zu. Voller Lookup via kleinem Button. async function forgeLbClickName(td, nameEnc, action) { const tr = td.parentElement; const nxt = tr.nextElementSibling; if (nxt && nxt.classList.contains("lb-inline")) { nxt.remove(); return; } // 2. Klick: schliessen document.querySelectorAll("#forge-lb-body .lb-inline").forEach((x) => x.remove()); const cols = tr.children.length; const row = document.createElement("tr"); row.className = "lb-inline"; row.innerHTML = `
⟳…
`; tr.after(row); const box = row.querySelector(".lb-inline-box"); try { const d = await api(`/player-detail?name=${nameEnc}&action=${action}`); box.innerHTML = `
${esc(ACTION_LABEL[action] || action)} — ${esc(decodeURIComponent(nameEnc))}
` + (renderDetail(action, d.dims, nameEnc) || '
No breakdown.
'); } catch (e) { box.innerHTML = `
✗ ${esc(e.message)}
`; } } function forgeOpenPlayer(nameEnc) { const name = decodeURIComponent(nameEnc), inp = $("ai-name"); if (inp) inp.value = name; if (typeof switchTab === "function") switchTab("info"); if (typeof aiLive === "function") aiLive(); } function forgeSubtab(which) { document.querySelectorAll("#lb-forge-wrap .pb > .forge-subtabs .forge-subtab").forEach((b) => b.classList.toggle("active", b.dataset.fsub === which)); ["totals", "board"].forEach((s) => { const el = $("forge-sub-" + s); if (el) el.style.display = which === s ? "" : "none"; }); if (which === "totals") forgeTotalsInit(); if (which === "board" && !boardLoaded) { boardLoaded = true; loadForgeLeaderboard(true); } } // ── Luck-Badge (geteilt: Spieler-Lookup + Combine-Ansicht) ── function luckBadgeHTML(o) { if (!o) return ""; const p = o.luck_percentile, z = o.luck_z; const pv = Math.round((p >= 0.5 ? p : 1 - p) * 100); const PLABEL = { dwarf: "forge", elf: "elf", pet_enhance: "pet", arena: "arena", nightmare: "nightmare" }; const across = (o.parts || []).map((x) => PLABEL[x] || x).join(" · "); const vc = o.value_luck_coins; const coins = (vc != null) ? ` · ${fVal(vc)} vs expected` : ""; return `
${p >= 0.5 ? "Lucky" : "Unlucky"} ${p >= 0.5 ? "luckier" : "unluckier"} than ${pv}% · z=${z >= 0 ? "+" : ""}${z.toFixed(2)}${coins}${across ? " · across " + across : ""}
`; } // ════════════════ TOTALS (globale Stats je Aktion, wie die Item-Tabelle) ════════════════ let totalsInit = false; function forgeTotalsInit() { if (totalsInit) return; totalsInit = true; const sel = $("forge-totals-action"); if (!sel) return; // Dwarf (Item-Tabellen) + alle Breakdown-Aktionen; Dwarf steht per LOOKUP_ORDER vorn. const have = (meta && meta.actions) ? meta.actions : LOOKUP_ORDER; const acts = have.filter((a) => a === "dwarf" || TOTAL_DIM[a]).slice() .sort((a, b) => LOOKUP_ORDER.indexOf(a) - LOOKUP_ORDER.indexOf(b)); if (!acts.includes("dwarf")) acts.unshift("dwarf"); sel.innerHTML = acts.map((a) => ``).join(""); sel.value = "dwarf"; loadForgeTotals(); } function totalsHead(action) { const t = (meta && meta.totals || []).find((x) => x.action === action); if (!t) return ""; const rate = (t.has_rate && t.attempts) ? ` · ${fPct0(t.successes / t.attempts)} success` : ""; return t.has_rate ? `${fInt(t.attempts)} attempts · ${fInt(t.successes)} ${SUCCESS_VERB[action] || "ok"}${rate} — everyone combined` : `${fInt(t.attempts)} ${SUCCESS_VERB[action] || "events"} — everyone combined`; } async function loadForgeTotals() { const sel = $("forge-totals-action"); if (!sel || !sel.value) return; const action = sel.value; const head = $("forge-totals-head"), body = $("forge-totals-body"), dwarfBox = $("forge-totals-dwarf"); try { window.smTrack && smTrack("stat:totals_" + action); } catch (e) {} // Dwarf = die Item-Tabellen (Raster + Drill), nicht ein flacher Breakdown. if (action === "dwarf") { body.style.display = "none"; body.innerHTML = ""; dwarfBox.style.display = ""; head.innerHTML = totalsHead("dwarf"); if (!itemsLoaded) loadForgeItems(); return; } dwarfBox.style.display = "none"; body.style.display = ""; const dim = TOTAL_DIM[action] || "drop"; body.innerHTML = '
⟳ Loading…
'; head.innerHTML = ""; try { const d = await api(`/breakdown?action=${action}&dim=${dim}&limit=500`); head.innerHTML = totalsHead(action); const dims = {}; dims[dim] = (d.rows || []).map((r) => ({ key: r.key, level: r.level, rarity: r.rarity, attempts: r.attempts, successes: r.successes, extra: 0 })); body.innerHTML = renderDetail(action, dims, "") || '
No data logged yet.
'; } catch (e) { body.innerHTML = `
✗ ${esc(e.message)}
`; } } // ════════════════ COMBINE ACCOUNTS (Dwarf-Multi: mehrere Accounts zusammen) ════════════════ let combineNames = ""; async function loadForgeCombined() { const inp = $("forge-combine-input"); if (!inp) return; const raw = (inp.value || "").trim(); combineNames = raw; const luck = $("forge-combine-luck"), body = $("forge-combine-body"); if (!raw) { body.innerHTML = '
Enter at least one name.
'; luck.innerHTML = ""; return; } body.innerHTML = '
⟳ Loading…
'; luck.innerHTML = ""; try { window.smTrack && smTrack("lookup:combine"); } catch (e) {} try { const d = await api(`/player?names=${encodeURIComponent(raw)}`); luck.innerHTML = luckBadgeHTML(d.overall); const ord = (x) => { const i = LOOKUP_ORDER.indexOf(x.action); return i < 0 ? 99 : i; }; const acts = (d.actions || []).slice().sort((a, b) => ord(a) - ord(b)); body.innerHTML = `
Combined: ${esc(d.name)}
` + (acts.map((a) => combinedActionCard(a)).join("") || '
No forge actions logged.
'); } catch (e) { body.innerHTML = /No forge stats/i.test(e.message) ? '
No forge activity logged for these accounts.
' : `
✗ ${esc(e.message)}
`; } } function combinedActionCard(a) { let counts; const verb = SUCCESS_VERB[a.action] || "ok"; if (!a.has_rate) counts = `${fInt(a.successes || a.attempts)} ${verb}`; else counts = `${fInt(a.successes)} ${verb} · ${fInt(a.fails)} failed`; let extra = ""; if (a.luck_pct != null) { const s = a.luck_pct >= 0 ? "+" : ""; extra += `luck ${s}${a.luck_pct.toFixed(1)}%`; } if (a.value_luck_coins != null) extra += `value ${fVal(a.value_luck_coins)}`; if ((a.action === "arena" || a.action === "nightmare") && a.legendary != null) extra += `${fInt(a.legendary)} legendary`; return `
${esc(ACTION_LABEL[a.action] || a.action)} ${counts} ${extra}▸
`; } async function forgeCombinedToggle(head, action) { const det = head.parentElement.querySelector(".forge-action-detail"); const caret = head.querySelector(".forge-caret"); if (det.style.display !== "none") { det.style.display = "none"; caret.textContent = "▸"; return; } caret.textContent = "▾"; det.style.display = ""; det.innerHTML = '
⟳…
'; try { const d = await api(`/player-detail?names=${encodeURIComponent(combineNames)}&action=${action}&limit=1500`); // "@@combine" als Quelle -> Dwarf-Items bleiben klickbar (Level-Verteilung wie im Einzel-Lookup). det.innerHTML = renderDetail(action, d.dims, "@@combine") || '
No breakdown logged.
'; } catch (e) { det.innerHTML = `
✗ ${esc(e.message)}
`; } } // ── Item Tables: Grid + Drill (overview/rarity/level/cell -> item -> players) ── let drillStack = []; async function loadForgeItems() { itemsLoaded = true; await ensureRates(); const grid = $("forge-items-grid"); grid.innerHTML = '
⟳ Loading…
'; try { const d = await api(`/items?action=dwarf`); const cells = {}, lvTot = {}; let maxL = 1; d.grid.forEach((g) => { if (!g.rarity || g.level < 1) return; const k = g.rarity + "|" + g.level; (cells[k] = cells[k] || { att: 0, su: 0 }); cells[k].att += g.attempts; cells[k].su += g.successes; lvTot[g.level] = (lvTot[g.level] || 0) + g.attempts; }); for (let l = 1; l <= 40; l++) if ((lvTot[l] || 0) >= 100) maxL = l; let h = '
'; for (let l = 1; l <= maxL; l++) h += ``; h += ""; RARITIES.forEach((rr) => { h += ``; for (let l = 1; l <= maxL; l++) { const c = cells[rr + "|" + l]; if (c && c.att) h += ``; else h += ''; } h += ""; }); h += "
Rarity ╲ Lv${l}
${rr}${fShort(c.att)}${fPct0(c.su / c.att)}·
Big number = forge ATTEMPTS (exact); below = success rate. Click a cell, a rarity (row) or a level (column) — or an item — to drill in.
"; grid.innerHTML = h; forgeDrillReset("all", null, null); // Overview } catch (e) { grid.innerHTML = `
✗ ${esc(e.message)}
`; } } // Overview ist immer die Basis; Zellen/Zeilen/Spalten setzen darauf auf -> "back" fuehrt // zurueck bis zur Ansicht mit ALLEN Items. function forgeDrillReset(scope, rarity, level) { drillStack = scope === "all" ? [] : [() => drillDist("all", null, null)]; drillPush(() => drillDist(scope, rarity, level)); } function drillPush(fn) { drillStack.push(fn); fn(); } function drillBack() { drillStack.pop(); const f = drillStack[drillStack.length - 1]; if (f) f(); } function drillHead(title) { const back = drillStack.length > 1 ? `` : ""; return `
${back}${title}
`; } async function drillDist(scope, rarity, level) { const drill = $("forge-items-drill"); drill.innerHTML = '
⟳…
'; const qp = []; if (rarity) qp.push("rarity=" + rarity); if (level != null) qp.push("level=" + level); try { const d = await api(`/item-dist?${qp.join("&")}&limit=400`); const title = scope === "all" ? "All items by forge attempts (any level)" : scope === "rarity" ? `${rarity} — items by attempts (all levels)` : scope === "level" ? `Level ${level} — items by attempts (all rarities)` : `${rarity} · Level ${level}`; const sub = `
${fInt(d.tot_attempts)} attempts · ${fPct0(d.tot_successes / d.tot_attempts)} success
`; const max = Math.max(1, ...d.items.map((r) => r.attempts)); // In einer ZELLE (Rarität+Level fix) direkt zur Spielerverteilung; sonst erst Level-Split. const cell = scope === "cell"; drill.innerHTML = drillHead(title + sub) + (d.items.map((r) => splitBar( `${esc(r.item)}`, [{ v: r.successes, color: rarityColor(r.rarity), label: "forged" }, { v: Math.max(0, r.attempts - r.successes), color: RED, label: "burned" }], max, { clickable: true, onclick: cell ? `forgeDrillPlayers('${enc(r.item)}',${level},'${r.rarity}')` : `forgeDrillItem('${enc(r.item)}','${r.rarity}')` })).join("") || '
No items.
'); } catch (e) { drill.innerHTML = `
✗ ${esc(e.message)}
`; } } function forgeDrillItem(itemEnc, rarity) { drillPush(() => drillItemLevels(itemEnc, rarity)); } async function drillItemLevels(itemEnc, rarity) { const drill = $("forge-items-drill"); drill.innerHTML = '
⟳…
'; const item = decodeURIComponent(itemEnc); try { const d = await api(`/item-levels?item=${encodeURIComponent(item)}&rarity=${rarity}`); const max = Math.max(1, ...d.levels.map((r) => r.attempts)); drill.innerHTML = drillHead(`${esc(item)} — attempts by level (click a level for players)`) + (d.levels.map((r) => splitBar(`L${r.level}`, [{ v: r.successes, color: rarityColor(rarity), label: "forged" }, { v: r.fails, color: RED, label: "burned" }], max, { clickable: true, onclick: `forgeDrillPlayers('${enc(item)}',${r.level},'${rarity}')` })).join("") || '
No levels.
'); } catch (e) { drill.innerHTML = `
✗ ${esc(e.message)}
`; } } function forgeDrillPlayers(itemEnc, level, rarity) { drillPush(() => drillPlayers(itemEnc, level, rarity)); } async function drillPlayers(itemEnc, level, rarity) { const drill = $("forge-items-drill"); drill.innerHTML = '
⟳…
'; const item = decodeURIComponent(itemEnc); try { const d = await api(`/item-leaderboard?item=${encodeURIComponent(item)}&level=${level}&rarity=${rarity}&limit=100`); const t = d.tot || { attempts: 0, successes: 0 }; const head = drillHead(`Top forgers of ${esc(item)} · L${level} ${fInt(t.attempts)} attempts · ${fPct0(t.successes / (t.attempts || 1))} success`); if (!d.rows.length) { const msg = t.attempts ? `Forged ${fInt(t.attempts)}× (${fInt(t.successes)} success, ${fInt(t.attempts - t.successes)} burned). The forger(s) have almost no other logged activity, so they aren’t listed individually.` : "No forge attempts recorded for this combination."; drill.innerHTML = head + `
${msg}
`; return; } const max = Math.max(1, ...d.rows.map((r) => r.attempts || r.successes)); drill.innerHTML = head + d.rows.map((r, i) => splitBar( `${i + 1}. ${esc(r.name)}`, [{ v: r.successes, color: rarityColor(rarity), label: "forged" }, { v: Math.max(0, (r.attempts || r.successes) - r.successes), color: RED, label: "burned" }], max)).join("") + `
Top ${d.rows.length}${d.has_more ? "+" : ""} forgers by attempts, out of ${fInt(t.attempts)} total attempts. Coloured = forged, red = burned.
`; } catch (e) { drill.innerHTML = `
✗ ${esc(e.message)}
`; } } function injectCSS() { if ($("forge-stats-css")) return; const s = document.createElement("style"); s.id = "forge-stats-css"; s.textContent = ` #ai-forge-panel .ph{display:flex;align-items:center;gap:8px} .ai-forge-luck,.forge-meta-summary{margin-left:auto;font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--text3,#8ea);text-align:right} .forge-luck-badge{padding:5px 12px;border-radius:9px;border:1px solid rgba(255,255,255,.14);line-height:1.2} .forge-luck-badge .flb-big{font-size:14px;font-weight:800;display:block}.forge-luck-badge .flb-sub{font-size:9px;opacity:.8} .forge-luck-badge.f-lucky{background:rgba(0,230,118,.13);border-color:rgba(0,230,118,.4);color:var(--green,#00e676)} .forge-luck-badge.f-unlucky{background:rgba(255,64,80,.13);border-color:rgba(255,64,80,.4);color:var(--red,#ff5064)} .forge-luck-badge.f-neutral{background:rgba(255,255,255,.06);color:var(--text2,#cde)} .forge-namenote{font-size:10px;color:var(--text3,#8ea);background:rgba(255,255,255,.04);border-left:2px solid var(--accent,#00c8ff);padding:5px 9px;border-radius:0 6px 6px 0;margin-bottom:10px} .forge-legend{font-size:10px;color:var(--text2,#cde);background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:6px;padding:6px 10px;margin-bottom:11px;line-height:1.75} .lg-sw{display:inline-block;width:9px;height:9px;border-radius:2px;vertical-align:middle;margin:0 3px 2px 5px} .fdet-key{font-size:9px;color:var(--text3,#8ea);margin:-3px 0 5px} .forge-action{border:1px solid rgba(255,255,255,.08);border-radius:9px;margin-bottom:6px;background:rgba(255,255,255,.02)} .forge-action-head{display:flex;align-items:center;gap:10px;padding:9px 11px;cursor:pointer;flex-wrap:wrap} .forge-action-head:hover{background:rgba(255,255,255,.04)} .forge-action-name{font-weight:700;min-width:105px} .forge-action-counts{font-family:'Share Tech Mono',monospace;font-size:12px} .forge-action-chips{display:flex;gap:5px;flex-wrap:wrap;margin-left:auto} .forge-caret{color:var(--text3,#8ea);font-size:11px;width:12px;text-align:center} .forge-chip{font-family:'Share Tech Mono',monospace;font-size:10px;padding:2px 7px;border-radius:20px;background:rgba(255,255,255,.06);white-space:nowrap} .forge-chip.f-lucky{background:rgba(0,230,118,.14);color:var(--green,#00e676)} .forge-chip.f-unlucky{background:rgba(255,64,80,.14);color:var(--red,#ff5064)} .forge-chip.f-legend{background:rgba(255,176,32,.16);color:#ffb020} .forge-action-detail{padding:8px 12px 12px;border-top:1px solid rgba(255,255,255,.06)} .fdet-sec{margin:10px 0} .fdet-title{font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:1px;text-transform:uppercase;color:var(--text3,#8ea);margin-bottom:5px} .fdet-lv{font-size:9px;color:var(--text3,#8ea)} .fbar{display:flex;align-items:center;gap:8px;margin:2px 0;font-size:11.5px} .fbar-click{cursor:pointer;border-radius:5px;padding:1px 3px;margin:1px -3px} .fbar-click:hover{background:rgba(0,200,255,.10)} .fbar-l{flex:0 0 34%;max-width:34%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right} .fbar-track{flex:1;display:flex;align-items:center;height:18px;background:rgba(255,255,255,.05);border-radius:4px;overflow:hidden;min-width:0} .fbar-fill{display:flex;height:100%;border-radius:4px;overflow:hidden;min-width:0} .sbar-seg{height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;min-width:0} .sbar-num{font-size:10px;font-family:'Share Tech Mono',monospace;color:#fff;text-shadow:0 0 3px #000,0 1px 2px #000;white-space:nowrap;padding:0 1px} .fbar-after{flex:0 0 auto;margin-left:5px;font-size:10px;font-family:'Share Tech Mono',monospace;white-space:nowrap;color:var(--text3,#8ea)} .fbar-after b{font-weight:700} .fbar-go{flex:0 0 auto;color:var(--accent,#00c8ff);font-size:13px;font-weight:700;width:14px;text-align:center;opacity:.8} .fdwarf-levels{margin:2px 0 8px 6%;padding-left:10px;border-left:2px solid rgba(255,255,255,.1)} .forge-tbl{width:100%;border-collapse:collapse;font-size:11.5px;margin-top:4px} .forge-tbl th{text-align:left;font-family:'Share Tech Mono',monospace;font-size:9px;letter-spacing:1px;text-transform:uppercase;color:var(--text3,#8ea);padding:3px 8px;border-bottom:1px solid rgba(255,255,255,.1)} .forge-tbl td{padding:4px 8px;border-bottom:1px solid rgba(255,255,255,.04)} .forge-tbl tbody tr:hover{background:rgba(255,255,255,.03)} .forge-note{font-size:9.5px;color:var(--text3,#8ea);margin-top:6px;line-height:1.4;font-style:italic} .forge-empty{padding:14px;text-align:center;color:var(--text3,#8ea);font-family:'Share Tech Mono',monospace;font-size:11px} .forge-subtabs{display:flex;gap:6px;margin-bottom:12px;border-bottom:1px solid rgba(255,255,255,.08);flex-wrap:wrap} .lb-topsubs{margin-bottom:14px} .forge-subtab{background:none;border:none;border-bottom:2px solid transparent;color:var(--text3,#8ea);padding:7px 14px;cursor:pointer;font-weight:600;font-size:13px} .forge-subtab.active{color:var(--accent,#00c8ff);border-bottom-color:var(--accent,#00c8ff)} .forge-controls{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;margin-bottom:10px} .forge-controls label{font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--text3,#8ea);display:flex;flex-direction:column;gap:3px} .forge-controls select,.forge-controls input{background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.14);color:var(--text,#eef);border-radius:6px;padding:5px 8px;font-size:12px} .forge-controls select option{background:#12161c;color:#e8eef7} .forge-controls input{margin-left:auto;min-width:150px} .forge-lb-name{color:var(--accent,#00c8ff);cursor:pointer;font-weight:600}.forge-lb-name:hover{text-decoration:underline} .forge-rank{color:var(--text3,#8ea);font-family:'Share Tech Mono',monospace} .lb-count{display:flex;flex-direction:column;line-height:1.15}.lb-mini{font-size:9px;color:var(--text3,#8ea)} .lb-inline-box{background:rgba(0,0,0,.22);border:1px solid rgba(0,200,255,.2);border-radius:8px;padding:8px 12px;margin:2px 0} .lb-inline-head{font-weight:700;font-size:12px;margin-bottom:6px;display:flex;align-items:center;gap:8px} .flookup{background:rgba(0,200,255,.14);border:1px solid rgba(0,200,255,.35);color:var(--accent,#00c8ff);border-radius:5px;padding:2px 8px;cursor:pointer;font-size:11px;white-space:nowrap;font-weight:600} .flookup:hover{background:rgba(0,200,255,.24)} .forge-pager{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:10px} .forge-pager button{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);color:var(--text,#eef);border-radius:6px;padding:5px 12px;cursor:pointer;font-size:12px} .forge-pager button:disabled{opacity:.35;cursor:default} .forge-pager span{font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--text3,#8ea)} .forge-scroll{overflow-x:auto} .forge-items td,.forge-items th{text-align:center;white-space:nowrap} .forge-rlabel{text-align:left!important;font-weight:700}.frow,.fcol{cursor:pointer}.frow:hover,.fcol:hover{background:rgba(0,200,255,.14)} .forge-cell{cursor:pointer;padding:4px 6px!important;border-radius:4px;min-width:58px} .forge-cell:hover{background:rgba(0,200,255,.12)!important} .forge-cell-n{display:block;font-weight:800;font-size:13px}.forge-cell-rate{display:block;font-family:'Share Tech Mono',monospace;font-size:8px;color:var(--text3,#8ea)} .forge-cell-empty{color:rgba(255,255,255,.12);text-align:center} .forge-drill-head{margin:12px 0 6px;font-weight:700;font-size:13px;display:flex;align-items:center;gap:8px;flex-wrap:wrap} .fdh-sub{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--text3,#8ea);font-weight:400} .fback{background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);color:var(--text,#eef);border-radius:5px;padding:2px 9px;cursor:pointer;font-size:11px} .f-lucky{color:var(--green,#00e676)}.f-unlucky{color:var(--red,#ff5064)}.f-neutral{color:var(--text2,#cde)}.f-legend{color:#ffb020} .r-common{color:#9fb0c3}.r-rare{color:#4ea3ff}.r-epic{color:#b060ff}.r-legendary{color:#ffb020} .forge-combine-intro{font-size:11px;color:var(--text2,#cde);line-height:1.5;margin-bottom:10px} .forge-combine-go{white-space:nowrap} .forge-combine-toggle{background:rgba(0,200,255,.12);border:1px solid rgba(0,200,255,.3);color:var(--accent,#00c8ff);border-radius:14px;padding:3px 11px;cursor:pointer;font-size:11px;font-weight:600;white-space:nowrap} .forge-combine-toggle:hover{background:rgba(0,200,255,.22)} #ai-forge-combine-box{border:1px solid rgba(0,200,255,.18);border-radius:8px;padding:10px 12px;margin-bottom:12px;background:rgba(0,200,255,.03)} .forge-combine-names{font-family:'Share Tech Mono',monospace;font-size:10px;color:var(--text3,#8ea);margin:2px 0 10px} #forge-totals-head{margin:-4px 0 10px} #forge-combine-luck{margin-bottom:10px}`; document.head.appendChild(s); } function injectSkeleton() { const sub = $("ai-forge-panel"); if (sub) sub.innerHTML = `

Forge & Action Stats

`; } // Combine-Box im Lookup auf/zu; beim Oeffnen den aktuellen Lookup-Namen vorbefuellen. function forgeToggleCombineBox() { const box = $("ai-forge-combine-box"); if (!box) return; const show = box.style.display === "none"; box.style.display = show ? "" : "none"; if (show) { const inp = $("forge-combine-input"); if (inp) { if (!inp.value) { const n = $("ai-name"); if (n && n.value) inp.value = n.value.trim(); } inp.focus(); } } } Object.assign(window, { loadForgeStats, forgeHideStats, forgeToggleDetail, forgeDwarfItem, forgeOnLeaderboardOpen, lbSubtab, loadForgeTab, forgeSubtab, loadForgeLeaderboard, forgeLbPage, forgeLbSearch, forgeLbClickName, forgeOpenPlayer, loadForgeItems, forgeDrillReset, forgeDrillBack: drillBack, forgeDrillItem, forgeDrillPlayers, loadForgeTotals, loadForgeCombined, forgeCombinedToggle, forgeToggleCombineBox, }); injectCSS(); injectSkeleton(); initBarMeasure(); })();