swordmasters-optimizer / static /forge_stats.js
oettaM123's picture
Claude Opus 4.8
Forge: nightmare/arena split, totals+dwarf, combine-in-lookup, VIP, formulas
f4f6fd8
Raw History Blame Contribute Delete
54.7 kB
/* 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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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) => `<span class="sbar-seg" style="flex:0 0 ${total ? s.v / total * 100 : 0}%;background:${s.color}" title="${esc(s.label || "")}: ${fInt(s.v)}"><span class="sbar-num" data-c="${s.color}">${fInt(s.v)}</span></span>`).join("");
const arrow = opts.clickable ? '<span class="fbar-go">▸</span>' : "";
return `<div class="fbar${opts.clickable ? " fbar-click" : ""}"${opts.onclick ? ` onclick="${opts.onclick}"` : ""}>
<div class="fbar-l">${labelHtml}</div>
<div class="fbar-track"><div class="fbar-fill" style="flex:0 0 ${fillPct}%">${inner}</div></div>${arrow}</div>`;
}
// 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 ? `<b style="color:${n.dataset.c}">${n.textContent}</b>` : "";
}).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 = `<div class="forge-namenote">📌 Everything here is by <b>display name</b>, not account — a rename starts a fresh history.</div>`;
const sw = (c) => `<span class="lg-sw" style="background:${c}"></span>`;
const LEGEND_NOTE = `<div class="forge-legend">Each bar's length = <b>attempts</b>. Inside, a bar is split into its outcomes:
${sw("var(--green,#00e676)")}any <b>coloured</b> part = a success, ${sw(RED)}dark&nbsp;red = a fail. For enchants the coloured parts are the
<b>enchant level reached</b> (${sw(ENCH_LV[1])}L1 ${sw(ENCH_LV[2])}L2 ${sw(ENCH_LV[3])}L3); for dwarf/elf they're coloured by item rarity.</div>`;
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 = '<div class="forge-empty">⟳ Loading…</div>'; 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("") || '<div class="forge-empty">No forge actions logged.</div>');
} catch (e) {
luck.innerHTML = "";
body.innerHTML = /No forge stats/i.test(e.message)
? '<div class="forge-empty">No forge activity logged for this name yet.</div>'
: `<div class="forge-empty">✗ ${esc(e.message)}</div>`;
}
}
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 = `<b>${fInt(a.successes || a.attempts)}</b> ${verb}`;
else counts = `<b class="f-lucky">${fInt(a.successes)}</b> ${verb} · <b class="f-unlucky">${fInt(a.fails)}</b> 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 += `<span class="forge-chip ${luckClass(a.luck_percentile)}" title="wie weit ueber/unter dem erwarteten Erfolg (echte Einzel-Wahrscheinlichkeiten)">luck ${s}${a.luck_pct.toFixed(1)}%</span>`;
}
// 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 += `<span class="forge-chip ${luckClass(a.value_luck_percentile)}" title="Wert ueber/unter Erwartung — Input immer verloren, Output nur bei Erfolg (dwarf: Item L+1, elf: Legendary, pet: XPet)">value ${fVal(a.value_luck_coins)}</span>`;
}
if ((a.action === "arena" || a.action === "nightmare") && a.legendary != null) extra += `<span class="forge-chip f-legend">${fInt(a.legendary)} legendary</span>`;
return `<div class="forge-action">
<div class="forge-action-head" onclick="forgeToggleDetail(this,'${enc(name)}','${a.action}')">
<span class="forge-action-name">${esc(ACTION_LABEL[a.action] || a.action)}</span>
<span class="forge-action-counts">${counts}</span>
<span class="forge-action-chips">${extra}</span><span class="forge-caret">▸</span></div>
<div class="forge-action-detail" style="display:none"></div></div>`;
}
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 = '<div class="forge-empty">⟳…</div>';
try { const d = await api(`/player-detail?name=${nameEnc}&action=${action}&limit=1500`);
det.innerHTML = renderDetail(action, d.dims, nameEnc) || '<div class="forge-empty">No breakdown logged.</div>';
} catch (e) { det.innerHTML = `<div class="forge-empty">✗ ${esc(e.message)}</div>`; }
}
function sec(title, html) { return `<div class="fdet-sec"><div class="fdet-title">${title}</div>${html}</div>`; }
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(`<span class="${rcls(r.rarity)}">${esc(r.key)}</span>`,
[{ 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 = `<div class="fdet-key">${sw(ENCH_LV[1])}L1 ${sw(ENCH_LV[2])}L2 ${sw(ENCH_LV[3])}L3 ${sw(RED)}fail</div>`;
S.push(sec("Enchants — level reached vs fail (bar = attempts)", encKey + rows.map((r) => splitBar(
`<span style="color:${ENCH_COLOR[r.t] || "#8fd0ff"}">${esc(r.t)}</span>`,
[{ 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(`<span style="color:${ORE_COLOR[r.key] || "#ccc"}">${esc(r.key)}</span>`,
[{ 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} <span class="fdet-lv">→ L${Math.max(1, r.level - 1)} · ⌀ ${avg.toFixed(1)} pcs</span>`,
[{ v: r.successes, color: GREEN, label: "downgraded" }, { v: r.attempts - r.successes, color: RED, label: "burned" }], max);
}).join("")
+ `<div class="forge-note">Downgrading one Level-N item returns up to N copies at Level N−1; a burn returns none. “⌀ pcs” = average pieces recovered per attempt.</div>`));
} 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(`<span class="${rcls(r.rarity)}">${esc(r.key)}</span> <span class="fdet-lv">L${r.level}</span>`,
[{ 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(`<span class="r-legendary">${esc(r.out)}</span>`,
[{ 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)} <span class="fdet-lv">${fPct0(r.successes / r.attempts)}</span>`,
[{ 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 = '<div class="forge-empty">⟳…</div>'; 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 = `<div class="forge-empty">✗ ${esc(e.message)}</div>`; }
}
// ════════════════ 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 = `<button class="forge-subtab active" data-lbsub="live" onclick="lbSubtab('live')">Player Leaderboard</button>
<button class="forge-subtab" data-lbsub="forge" onclick="lbSubtab('forge')">Forge &amp; Actions</button>
<button class="forge-subtab" data-lbsub="mob" onclick="lbSubtab('mob')">Mob Kills</button>`;
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 `<div class="panel"><div class="ph"><h2>Forge &amp; Action Stats</h2>
<div id="forge-meta-summary" class="forge-meta-summary"></div></div>
<div class="pb">${NAME_NOTE}${LEGEND_NOTE}
<div class="forge-subtabs">
<button class="forge-subtab active" data-fsub="board" onclick="forgeSubtab('board')">Leaderboard</button>
<button class="forge-subtab" data-fsub="totals" onclick="forgeSubtab('totals')">Totals</button></div>
<div id="forge-sub-totals" style="display:none">
<div class="forge-controls">
<label>Show<select id="forge-totals-action" onchange="loadForgeTotals()"></select></label></div>
<div id="forge-totals-head" class="fdh-sub"></div>
<div id="forge-totals-dwarf" style="display:none"><div id="forge-items-grid"></div><div id="forge-items-drill"></div></div>
<div id="forge-totals-body"></div></div>
<div id="forge-sub-board">
<div class="forge-controls">
<label>Action<select id="forge-lb-action" onchange="loadForgeLeaderboard(true)"></select></label>
<label>Sort by<select id="forge-lb-sort" onchange="loadForgeLeaderboard(true)"></select></label>
<input type="text" id="forge-lb-q" placeholder="Search name…" oninput="forgeLbSearch()"></div>
<div id="forge-lb-body"></div>
<div class="forge-pager"><button id="forge-lb-prev" onclick="forgeLbPage(-1)">‹ Prev</button>
<span id="forge-lb-pageinfo"></span><button id="forge-lb-next" onclick="forgeLbPage(1)">Next ›</button></div></div>
</div></div>`;
}
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) => `<option value="${a}">${esc(ACTION_LABEL[a] || a)}</option>`).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]) => `<option value="${v}">${l}</option>`).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 = '<div class="forge-empty">⟳ Loading…</div>';
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 = '<div class="forge-empty">No names found.</div>'; }
else body.innerHTML = `<table class="forge-tbl forge-lb"><thead><tr><th>#</th><th>Name</th>
<th>${d.has_rate ? "Success / Attempts" : "Runs"}</th>${d.is_arena ? "<th>Legendary</th>" : "<th>Luck</th>"}${d.has_value ? "<th>Value</th>" : ""}</tr></thead><tbody>${
d.rows.map((r, i) => {
const rank = r.rank != null ? r.rank : (lb.offset + i + 1);
const cnt = d.has_rate ? `<div class="lb-count"><span>${fInt(r.successes)} / ${fInt(r.attempts)}</span><span class="lb-mini">${fPct0(r.rate)}</span></div>` : fInt(r.attempts);
let c3;
if (d.is_arena) c3 = `<td class="f-legend">${fInt(r.legendary)} <span class="lb-mini">${fPct(r.legendary_ratio)}</span></td>`;
else c3 = r.luck_z != null ? `<td class="${luckClass(r.luck_percentile)}">${r.luck_z >= 0 ? "+" : ""}${r.luck_z.toFixed(2)}</td>` : "<td>–</td>";
const val = d.has_value ? `<td class="${(r.value_net || 0) >= 0 ? "f-lucky" : "f-unlucky"}">${r.value_known ? fVal(r.value_net || 0) : "–"}</td>` : "";
return `<tr data-name="${esc(r.name)}"><td class="forge-rank">${fInt(rank)}</td>
<td class="forge-lb-name" onclick="forgeLbClickName(this,'${enc(r.name)}','${action}')">${esc(r.name)}</td><td>${cnt}</td>${c3}${val}</tr>`;
}).join("")}</tbody></table>`;
$("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 = `<div class="forge-empty">✗ ${esc(e.message)}</div>`; }
}
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 = `<td colspan="${cols}"><div class="lb-inline-box"><div class="forge-empty">⟳…</div></div></td>`;
tr.after(row);
const box = row.querySelector(".lb-inline-box");
try {
const d = await api(`/player-detail?name=${nameEnc}&action=${action}`);
box.innerHTML = `<div class="lb-inline-head"><button class="flookup" onclick="event.stopPropagation();forgeOpenPlayer('${nameEnc}')" title="Open the full player lookup">🔍 lookup</button><span>${esc(ACTION_LABEL[action] || action)} — ${esc(decodeURIComponent(nameEnc))}</span></div>`
+ (renderDetail(action, d.dims, nameEnc) || '<div class="forge-empty">No breakdown.</div>');
} catch (e) { box.innerHTML = `<div class="forge-empty">✗ ${esc(e.message)}</div>`; }
}
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 `<div class="forge-luck-badge ${luckClass(p)}" title="Wert-gewichtet: jedes Event zaehlt nach Coins-at-Risk"><span class="flb-big">${p >= 0.5 ? "Lucky" : "Unlucky"}</span>
<span class="flb-sub">${p >= 0.5 ? "luckier" : "unluckier"} than ${pv}% · z=${z >= 0 ? "+" : ""}${z.toFixed(2)}${coins}${across ? " · across " + across : ""}</span></div>`;
}
// ════════════════ 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) => `<option value="${a}">${a === "dwarf" ? "Dwarf" : esc(ACTION_LABEL[a] || a)}</option>`).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 = '<div class="forge-empty">⟳ Loading…</div>'; 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, "") || '<div class="forge-empty">No data logged yet.</div>';
} catch (e) { body.innerHTML = `<div class="forge-empty">✗ ${esc(e.message)}</div>`; }
}
// ════════════════ 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 = '<div class="forge-empty">Enter at least one name.</div>'; luck.innerHTML = ""; return; }
body.innerHTML = '<div class="forge-empty">⟳ Loading…</div>'; 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 = `<div class="forge-combine-names">Combined: <b>${esc(d.name)}</b></div>`
+ (acts.map((a) => combinedActionCard(a)).join("") || '<div class="forge-empty">No forge actions logged.</div>');
} catch (e) {
body.innerHTML = /No forge stats/i.test(e.message)
? '<div class="forge-empty">No forge activity logged for these accounts.</div>'
: `<div class="forge-empty">✗ ${esc(e.message)}</div>`;
}
}
function combinedActionCard(a) {
let counts; const verb = SUCCESS_VERB[a.action] || "ok";
if (!a.has_rate) counts = `<b>${fInt(a.successes || a.attempts)}</b> ${verb}`;
else counts = `<b class="f-lucky">${fInt(a.successes)}</b> ${verb} · <b class="f-unlucky">${fInt(a.fails)}</b> failed`;
let extra = "";
if (a.luck_pct != null) { const s = a.luck_pct >= 0 ? "+" : ""; extra += `<span class="forge-chip ${luckClass(a.luck_percentile)}">luck ${s}${a.luck_pct.toFixed(1)}%</span>`; }
if (a.value_luck_coins != null) extra += `<span class="forge-chip ${luckClass(a.value_luck_percentile)}">value ${fVal(a.value_luck_coins)}</span>`;
if ((a.action === "arena" || a.action === "nightmare") && a.legendary != null) extra += `<span class="forge-chip f-legend">${fInt(a.legendary)} legendary</span>`;
return `<div class="forge-action">
<div class="forge-action-head" onclick="forgeCombinedToggle(this,'${a.action}')">
<span class="forge-action-name">${esc(ACTION_LABEL[a.action] || a.action)}</span>
<span class="forge-action-counts">${counts}</span>
<span class="forge-action-chips">${extra}</span><span class="forge-caret">▸</span></div>
<div class="forge-action-detail" style="display:none"></div></div>`;
}
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 = '<div class="forge-empty">⟳…</div>';
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")
|| '<div class="forge-empty">No breakdown logged.</div>';
} catch (e) { det.innerHTML = `<div class="forge-empty">✗ ${esc(e.message)}</div>`; }
}
// ── 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 = '<div class="forge-empty">⟳ Loading…</div>';
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 = '<div class="forge-scroll"><table class="forge-tbl forge-items"><thead><tr><th>Rarity ╲ Lv</th>';
for (let l = 1; l <= maxL; l++) h += `<th class="fcol" onclick="forgeDrillReset('level',null,${l})">${l}</th>`;
h += "</tr></thead><tbody>";
RARITIES.forEach((rr) => { h += `<tr><td class="${rcls(rr)} forge-rlabel frow" onclick="forgeDrillReset('rarity','${rr}',null)">${rr}</td>`;
for (let l = 1; l <= maxL; l++) { const c = cells[rr + "|" + l];
if (c && c.att) h += `<td class="forge-cell" onclick="forgeDrillReset('cell','${rr}',${l})"><span class="forge-cell-n">${fShort(c.att)}</span><span class="forge-cell-rate">${fPct0(c.su / c.att)}</span></td>`;
else h += '<td class="forge-cell-empty">·</td>'; } h += "</tr>"; });
h += "</tbody></table></div><div class=\"forge-note\">Big number = forge ATTEMPTS (exact); below = success rate. Click a cell, a rarity (row) or a level (column) — or an item — to drill in.</div>";
grid.innerHTML = h;
forgeDrillReset("all", null, null); // Overview
} catch (e) { grid.innerHTML = `<div class="forge-empty">✗ ${esc(e.message)}</div>`; }
}
// 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 ? `<button class="fback" onclick="forgeDrillBack()">‹ back</button>` : "";
return `<div class="forge-drill-head">${back}${title}</div>`;
}
async function drillDist(scope, rarity, level) {
const drill = $("forge-items-drill"); drill.innerHTML = '<div class="forge-empty">⟳…</div>';
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" ? `<span class="${rcls(rarity)}">${rarity}</span> — items by attempts (all levels)`
: scope === "level" ? `Level ${level} — items by attempts (all rarities)`
: `<span class="${rcls(rarity)}">${rarity}</span> · Level ${level}`;
const sub = `<div class="fdh-sub">${fInt(d.tot_attempts)} attempts · ${fPct0(d.tot_successes / d.tot_attempts)} success</div>`;
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(
`<span class="${rcls(r.rarity)}">${esc(r.item)}</span>`,
[{ 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("")
|| '<div class="forge-empty">No items.</div>');
} catch (e) { drill.innerHTML = `<div class="forge-empty">✗ ${esc(e.message)}</div>`; }
}
function forgeDrillItem(itemEnc, rarity) { drillPush(() => drillItemLevels(itemEnc, rarity)); }
async function drillItemLevels(itemEnc, rarity) {
const drill = $("forge-items-drill"); drill.innerHTML = '<div class="forge-empty">⟳…</div>';
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(`<span class="${rcls(rarity)}">${esc(item)}</span> — 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("")
|| '<div class="forge-empty">No levels.</div>');
} catch (e) { drill.innerHTML = `<div class="forge-empty">✗ ${esc(e.message)}</div>`; }
}
function forgeDrillPlayers(itemEnc, level, rarity) { drillPush(() => drillPlayers(itemEnc, level, rarity)); }
async function drillPlayers(itemEnc, level, rarity) {
const drill = $("forge-items-drill"); drill.innerHTML = '<div class="forge-empty">⟳…</div>';
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 <span class="${rcls(rarity)}">${esc(item)}</span> · L${level} <span class="fdh-sub">${fInt(t.attempts)} attempts · ${fPct0(t.successes / (t.attempts || 1))} success</span>`);
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 + `<div class="forge-empty">${msg}</div>`; return;
}
const max = Math.max(1, ...d.rows.map((r) => r.attempts || r.successes));
drill.innerHTML = head + d.rows.map((r, i) => splitBar(
`<span class="forge-lb-name" onclick="event.stopPropagation();forgeOpenPlayer('${enc(r.name)}')">${i + 1}. ${esc(r.name)}</span>`,
[{ v: r.successes, color: rarityColor(rarity), label: "forged" }, { v: Math.max(0, (r.attempts || r.successes) - r.successes), color: RED, label: "burned" }],
max)).join("")
+ `<div class="forge-note">Top ${d.rows.length}${d.has_more ? "+" : ""} forgers by attempts, out of ${fInt(t.attempts)} total attempts. Coloured = forged, red = burned.</div>`;
} catch (e) { drill.innerHTML = `<div class="forge-empty">✗ ${esc(e.message)}</div>`; }
}
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 = `<div class="ph"><h2>Forge &amp; Action Stats</h2>
<button class="forge-combine-toggle" onclick="forgeToggleCombineBox()" title="Combine the forge stats of several accounts (e.g. your alts)">+ combine accounts</button>
<div id="ai-forge-luck" class="ai-forge-luck"></div></div>
<div class="pb">
<div id="ai-forge-combine-box" style="display:none">
<div class="forge-combine-intro">Combine the forge &amp; action stats of several accounts into one — e.g. your own alts. Names separated by commas.</div>
<div class="forge-controls">
<input type="text" id="forge-combine-input" placeholder="Name1, Name2, Name3…" onkeydown="if(event.key==='Enter')loadForgeCombined()">
<button class="fback forge-combine-go" onclick="loadForgeCombined()">Combine ›</button></div>
<div id="forge-combine-luck"></div>
<div id="forge-combine-body"></div></div>
<div id="ai-forge-body"></div></div>`;
}
// 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();
})();