Files
psopeeps_site/site/placeholder-pages.js
2026-06-13 16:32:56 -04:00

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) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
}[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();
}
})();