(() => { "use strict"; function qs(sel) { return document.querySelector(sel); } function setText(id, text) { const el = qs(id); if (el) el.textContent = text; } const leaderboardState = { rows: [], sortKey: "points", sortDir: "desc", page: 1, pageSize: 10, loading: false, loaded: false, error: null, }; const leaderboardColumns = [ { key: "rank", label: "Rank", numeric: true }, { key: "name", label: "Player Name" }, { key: "points", label: "Points", numeric: true }, { key: "level", label: "Level", numeric: true }, { key: "status", label: "Status" }, { key: "class", label: "Class" }, { key: "secid", label: "SecID" }, { key: "kills", label: "Kills", numeric: true }, { key: "playtime", label: "Playtime", numeric: true }, ]; function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[ch])); } function fmtNumber(value) { const n = Number(value || 0); return Number.isFinite(n) ? n.toLocaleString() : "0"; } function fmtPlaytime(seconds) { const total = Number(seconds || 0); if (!Number.isFinite(total) || total <= 0) return "0h"; const hours = Math.floor(total / 3600); const minutes = Math.floor((total % 3600) / 60); if (hours <= 0) return `${minutes}m`; return minutes ? `${hours}h ${minutes}m` : `${hours}h`; } function normalizeLeaderboardRow(row, index) { return { originalRank: index + 1, name: row.PlayerName || row.CharacterName || row.character_name || "", points: Number(row.Points ?? row.TotalPoints ?? 0), level: Number(row.Level ?? row.level ?? 0), class: row.Class || row.character_class || "", secid: row.SecID || row.section_id || "", kills: Number(row.Kills ?? row.TotalKills ?? row.total_enemies_killed ?? 0), status: (row.Alive === false || row.alive === false) ? "Dead" : "Alive", playtime: Number(row.PlayTimeSeconds ?? row.play_time_seconds ?? 0), }; } async function fetchHardcoreLeaderboard() { leaderboardState.loading = true; leaderboardState.error = null; renderHardcoreLeaderboard(); const cacheBucket = Math.floor(Date.now() / 300000); const urls = [ `/generated/hardcore-leaderboard-points.json?v=${cacheBucket}`, "/api/hardcore/leaderboard/points", "/hardcore/leaderboard/points", ]; let lastError = null; for (const url of urls) { try { const res = await fetch(url, { credentials: "same-origin" }); if (!res.ok) { lastError = new Error(`${url}: HTTP ${res.status}`); continue; } const data = await res.json(); const rows = Array.isArray(data) ? data : (data.rows || data.characters || []); leaderboardState.rows = rows.map(normalizeLeaderboardRow); leaderboardState.loaded = true; leaderboardState.loading = false; leaderboardState.page = 1; renderHardcoreLeaderboard(); return; } catch (err) { lastError = err; } } leaderboardState.loading = false; leaderboardState.error = lastError ? String(lastError.message || lastError) : "Unable to load leaderboard."; renderHardcoreLeaderboard(); } function sortedLeaderboardRows() { const key = leaderboardState.sortKey; const dir = leaderboardState.sortDir === "asc" ? 1 : -1; const col = leaderboardColumns.find((c) => c.key === key); const numeric = !!col?.numeric; return [...leaderboardState.rows].sort((a, b) => { let av = key === "rank" ? a.originalRank : a[key]; let bv = key === "rank" ? b.originalRank : b[key]; if (numeric) { av = Number(av || 0); bv = Number(bv || 0); return (av - bv) * dir; } return String(av || "").localeCompare(String(bv || "")) * dir; }); } function renderHardcoreLeaderboard() { const box = qs("#leaderboard-placeholder"); if (!box) return; if (leaderboardState.loading) { box.innerHTML = `
Loading Hardcore leaderboard...
`; return; } if (leaderboardState.error) { box.innerHTML = `
${escapeHtml(leaderboardState.error)}
`; return; } if (!leaderboardState.loaded) { box.innerHTML = `
Leaderboard data will load here.
`; return; } const rows = sortedLeaderboardRows(); const pageSize = leaderboardState.pageSize; const totalPages = Math.max(1, Math.ceil(rows.length / pageSize)); leaderboardState.page = Math.min(Math.max(1, leaderboardState.page), totalPages); const start = (leaderboardState.page - 1) * pageSize; const pageRows = rows.slice(start, start + pageSize); const head = leaderboardColumns.map((col) => { const active = leaderboardState.sortKey === col.key; const marker = active ? (leaderboardState.sortDir === "asc" ? " ▲" : " ▼") : ""; return ``; }).join(""); const body = pageRows.map((row, idx) => { const rank = start + idx + 1; return ` ${rank} ${escapeHtml(row.name)} ${fmtNumber(row.points)} ${fmtNumber(row.level)} ${escapeHtml(row.status)} ${escapeHtml(row.class || "—")} ${escapeHtml(row.secid || "—")} ${fmtNumber(row.kills)} ${escapeHtml(fmtPlaytime(row.playtime))} `; }).join(""); box.innerHTML = `
${head}${body || ``}
No Hardcore leaderboard rows yet.
Page ${leaderboardState.page} of ${totalPages}
`; box.querySelectorAll("[data-sort]").forEach((btn) => { btn.addEventListener("click", () => { const key = btn.getAttribute("data-sort"); if (leaderboardState.sortKey === key) { leaderboardState.sortDir = leaderboardState.sortDir === "asc" ? "desc" : "asc"; } else { leaderboardState.sortKey = key; leaderboardState.sortDir = key === "name" || key === "class" || key === "secid" || key === "status" ? "asc" : "desc"; } leaderboardState.page = 1; renderHardcoreLeaderboard(); }); }); qs("#leaderboard-prev")?.addEventListener("click", () => { leaderboardState.page -= 1; renderHardcoreLeaderboard(); }); qs("#leaderboard-next")?.addEventListener("click", () => { leaderboardState.page += 1; renderHardcoreLeaderboard(); }); } function updateLeaderboards() { const mode = qs("#leaderboard-mode")?.value || "hardcore"; const pageSizeWrap = qs("#leaderboard-page-size-wrap"); if (pageSizeWrap) pageSizeWrap.hidden = mode !== "hardcore"; if (mode === "hardcore") { if (!leaderboardState.loaded && !leaderboardState.loading) { fetchHardcoreLeaderboard(); } else { renderHardcoreLeaderboard(); } return; } const labels = { cmode: "CMode leaderboard placeholder.", "hardcore-cmode": "Hardcore CMode leaderboard placeholder.", }; setText("#leaderboard-placeholder", labels[mode] || "Leaderboard data will load here."); } function updateDrops() { const mode = qs("#drops-mode")?.value || "peeps"; const version = qs("#drops-version")?.value || "v1"; const versionWrap = qs("#drops-version-wrap"); const epWrap = qs("#drops-episode-wrap"); if (!versionWrap || !epWrap) return; if (mode === "hardcore") { versionWrap.hidden = true; epWrap.hidden = false; setText("#drops-placeholder", "Hardcore drop table placeholder."); return; } versionWrap.hidden = false; epWrap.hidden = version !== "v4"; setText("#drops-placeholder", `Peeps ${version.toUpperCase()} drop table placeholder.`); } function updateBestiaryEpisodes(version) { const ep = qs("#bestiary-episode"); if (!ep) return; const eps = version === "v4" ? [["ep1", "Episode 1"], ["ep2", "Episode 2"], ["ep4", "Episode 4"]] : [["ep1", "Episode 1"], ["ep2", "Episode 2"]]; ep.innerHTML = eps.map(([value, label]) => ``).join(""); } function updateBestiary() { const version = qs("#bestiary-version")?.value || "v2"; const epWrap = qs("#bestiary-episode-wrap"); const bpWrap = qs("#bestiary-bp-wrap"); if (!epWrap || !bpWrap) return; epWrap.hidden = version === "v2"; bpWrap.hidden = !(version === "v2" || version === "v4"); if (version === "v3" || version === "v4") { updateBestiaryEpisodes(version); } setText("#bestiary-placeholder", `${version.toUpperCase()} bestiary placeholder.`); } function bind() { qs("#leaderboard-mode")?.addEventListener("change", updateLeaderboards); qs("#leaderboard-page-size")?.addEventListener("change", (event) => { leaderboardState.pageSize = Number(event.target.value || 10); leaderboardState.page = 1; renderHardcoreLeaderboard(); }); qs("#drops-mode")?.addEventListener("change", updateDrops); qs("#drops-version")?.addEventListener("change", updateDrops); qs("#bestiary-version")?.addEventListener("change", updateBestiary); qs("#bestiary-episode")?.addEventListener("change", updateBestiary); qs("#bestiary-bp")?.addEventListener("change", updateBestiary); updateLeaderboards(); updateDrops(); updateBestiary(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bind); } else { bind(); } })();