317 lines
10 KiB
JavaScript
317 lines
10 KiB
JavaScript
(() => {
|
|
"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 = `<div class="leaderboard-status">Loading Hardcore leaderboard...</div>`;
|
|
return;
|
|
}
|
|
|
|
if (leaderboardState.error) {
|
|
box.innerHTML = `<div class="leaderboard-status leaderboard-status--error">${escapeHtml(leaderboardState.error)}</div>`;
|
|
return;
|
|
}
|
|
|
|
if (!leaderboardState.loaded) {
|
|
box.innerHTML = `<div class="leaderboard-status">Leaderboard data will load here.</div>`;
|
|
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 `<th><button type="button" class="leaderboard-sort" data-sort="${col.key}">${escapeHtml(col.label)}${marker}</button></th>`;
|
|
}).join("");
|
|
|
|
const body = pageRows.map((row, idx) => {
|
|
const rank = start + idx + 1;
|
|
return `<tr>
|
|
<td data-label="Rank">${rank}</td>
|
|
<td data-label="Player Name">${escapeHtml(row.name)}</td>
|
|
<td data-label="Points">${fmtNumber(row.points)}</td>
|
|
<td data-label="Level">${fmtNumber(row.level)}</td>
|
|
<td data-label="Status">${escapeHtml(row.status)}</td>
|
|
<td data-label="Class">${escapeHtml(row.class || "—")}</td>
|
|
<td data-label="SecID">${escapeHtml(row.secid || "—")}</td>
|
|
<td data-label="Kills">${fmtNumber(row.kills)}</td>
|
|
<td data-label="Playtime" data-sort-value="${row.playtime}">${escapeHtml(fmtPlaytime(row.playtime))}</td>
|
|
</tr>`;
|
|
}).join("");
|
|
|
|
box.innerHTML = `
|
|
<div class="leaderboard-table-wrap">
|
|
<table class="leaderboard-table">
|
|
<thead><tr>${head}</tr></thead>
|
|
<tbody>${body || `<tr><td colspan="9">No Hardcore leaderboard rows yet.</td></tr>`}</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="leaderboard-pager">
|
|
<button type="button" id="leaderboard-prev" ${leaderboardState.page <= 1 ? "disabled" : ""}>Previous</button>
|
|
<span>Page ${leaderboardState.page} of ${totalPages}</span>
|
|
<button type="button" id="leaderboard-next" ${leaderboardState.page >= totalPages ? "disabled" : ""}>Next</button>
|
|
</div>
|
|
`;
|
|
|
|
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]) => `<option value="${value}">${label}</option>`).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();
|
|
}
|
|
})();
|