const GAMEMODES = [ { id: "overall", label: "Overall", icon: "assets/gamemodes/overall.png" }, { id: "vanilla", label: "Vanilla", icon: "assets/gamemodes/smaller-vanilla-pvp.png" }, { id: "mace", label: "Mace", icon: "assets/gamemodes/mace-pvp.png" }, { id: "axe", label: "Axe", icon: "assets/gamemodes/axe-pvp.png" }, { id: "sword", label: "Sword", icon: "assets/gamemodes/sword-pvp.png" }, { id: "smp", label: "SMP", icon: "assets/gamemodes/smp.png" }, { id: "diamondsmp", label: "Diamond SMP", icon: "assets/gamemodes/diamond-smp.png" }, { id: "uhc", label: "UHC", icon: "assets/gamemodes/uhc.png" }, { id: "pot", label: "Pot", icon: "assets/gamemodes/pot-pvp.png" }, { id: "nethop", label: "Neth OP", icon: "assets/gamemodes/neth-op.png" }, { id: "cart", label: "Cart", icon: "assets/gamemodes/cart-pvp.png" }, ]; const MODE_ICON_PATH = { vanilla: "assets/gamemodes/smaller-vanilla-pvp.png", mace: "assets/gamemodes/mace-pvp.png", axe: "assets/gamemodes/axe-pvp.png", sword: "assets/gamemodes/sword-pvp.png", smp: "assets/gamemodes/smp.png", diamondsmp: "assets/gamemodes/diamond-smp.png", uhc: "assets/gamemodes/uhc.png", pot: "assets/gamemodes/pot-pvp.png", nethop: "assets/gamemodes/neth-op.png", cart: "assets/gamemodes/cart-pvp.png", }; const MODE_LABELS = { vanilla: "Vanilla", mace: "Mace", axe: "Axe", sword: "Sword", smp: "SMP", diamondsmp: "Diamond SMP", uhc: "UHC", pot: "Pot", nethop: "Neth OP", cart: "Cart", }; const TIER_POINTS = { HT1: 60, LT1: 45, HT2: 30, LT2: 20, HT3: 10, LT3: 6, HT4: 4, LT4: 3, HT5: 2, LT5: 1, }; function getAvatarUrl(username) { return `https://render.crafty.gg/3d/bust/${encodeURIComponent(username)}`; } const state = { gamemode: "overall", page: 1, totalPages: 1, totalItems: 0, players: [], }; const profileCache = new Map(); const searchCache = new Map(); let activeSearchController = null; const tabsEl = document.getElementById("tabs"); const rankingListEl = document.getElementById("rankingList"); const listHeadEl = document.querySelector(".list-head"); const viewTitleEl = document.getElementById("viewTitle"); const statusLabelEl = document.getElementById("statusLabel"); const loadingStateEl = document.getElementById("loadingState"); const errorStateEl = document.getElementById("errorState"); const paginationEl = document.getElementById("pagination"); const pageInfoEl = document.getElementById("pageInfo"); const prevPageEl = document.getElementById("prevPage"); const nextPageEl = document.getElementById("nextPage"); const searchInputEl = document.getElementById("searchInput"); const searchDropdownEl = document.getElementById("searchDropdown"); const profileOverlayEl = document.getElementById("profileOverlay"); const profileModalEl = document.getElementById("profileModal"); const profileCloseBtnEl = document.getElementById("profileCloseBtn"); const profileLoadingEl = document.getElementById("profileLoading"); const profileErrorEl = document.getElementById("profileError"); const profileContentEl = document.getElementById("profileContent"); const profileAvatarEl = document.getElementById("profileAvatar"); const profileUsernameEl = document.getElementById("profileUsername"); const profileRegionEl = document.getElementById("profileRegion"); const profileTopPointsEl = document.getElementById("profileTopPoints"); const profilePositionEl = document.getElementById("profilePosition"); const profileTiersEl = document.getElementById("profileTiers"); function debounce(fn, delay = 250) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } function tierColorClass(tier) { if (!tier) { return ""; } return tier.toLowerCase(); } function buildTabs() { tabsEl.innerHTML = GAMEMODES.map( (mode) => ` ` ).join(""); tabsEl.querySelectorAll(".tab-btn").forEach((btn) => { btn.addEventListener("click", () => { const mode = btn.dataset.mode; if (mode === state.gamemode) { return; } state.gamemode = mode; state.page = 1; buildTabs(); loadRankings(); }); }); } function closeProfileModal() { profileOverlayEl.classList.add("hidden"); profileOverlayEl.setAttribute("aria-hidden", "true"); document.body.classList.remove("modal-open"); } function renderProfileModal(player) { profileAvatarEl.src = getAvatarUrl(player.username); profileAvatarEl.alt = `${player.username} avatar`; profileUsernameEl.textContent = player.username; profilePositionEl.textContent = `#${player.position}`; const profileTopClass = player.position === 1 ? "position-top-1" : player.position === 2 ? "position-top-2" : player.position === 3 ? "position-top-3" : ""; profilePositionEl.className = `position-rank ${profileTopClass}`.trim(); const region = String(player.region || "NA").toUpperCase(); profileRegionEl.textContent = region; profileRegionEl.className = `profile-region region-${region.toLowerCase()}`; profileTopPointsEl.textContent = `${player.totalPoints} points`; const sortedProfileTiers = Object.entries(player.tiers || {}) .filter(([, tier]) => tier) .map(([mode, tier]) => ({ mode, tier, points: TIER_POINTS[tier] || 0, })) .sort((a, b) => { if (b.points !== a.points) { return b.points - a.points; } return (MODE_LABELS[a.mode] || a.mode).localeCompare(MODE_LABELS[b.mode] || b.mode); }); const maxTierSlots = 10; const filledSlots = sortedProfileTiers.slice(0, maxTierSlots); const emptySlots = Math.max(maxTierSlots - filledSlots.length, 0); profileTiersEl.innerHTML = [ ...filledSlots.map( ({ mode, tier, points }) => `
No gamemode tiers assigned yet
`; } } async function openProfileModal(username) { profileOverlayEl.classList.remove("hidden"); profileOverlayEl.setAttribute("aria-hidden", "false"); document.body.classList.add("modal-open"); profileLoadingEl.classList.remove("hidden"); profileErrorEl.classList.add("hidden"); profileErrorEl.textContent = ""; profileContentEl.classList.add("hidden"); try { if (profileCache.has(username)) { renderProfileModal(profileCache.get(username)); profileContentEl.classList.remove("hidden"); return; } const response = await fetch(`/api/players/${encodeURIComponent(username)}`, { headers: { Accept: "application/json" }, }); if (!response.ok) { throw new Error(`Request failed (${response.status})`); } const player = await response.json(); profileCache.set(username, player); renderProfileModal(player); profileContentEl.classList.remove("hidden"); } catch (_error) { profileErrorEl.classList.remove("hidden"); profileErrorEl.textContent = "Failed to load player profile."; } finally { profileLoadingEl.classList.add("hidden"); } } function setLoading(loading) { loadingStateEl.classList.toggle("hidden", !loading); } function setError(message = "") { errorStateEl.classList.toggle("hidden", !message); errorStateEl.textContent = message; } function renderPagination() { if (state.gamemode !== "overall" || state.totalItems === 0) { paginationEl.classList.add("hidden"); return; } paginationEl.classList.remove("hidden"); pageInfoEl.textContent = `Page ${state.page} / ${state.totalPages} | ${state.totalItems} players`; prevPageEl.disabled = state.page <= 1; nextPageEl.disabled = state.page >= state.totalPages; } function visibleTiers(player) { return Object.entries(player.tiers || {}).filter(([, tier]) => tier); } function getTierNumber(tierValue) { const value = String(tierValue || ""); const maybeNumber = Number(value.slice(-1)); return Number.isInteger(maybeNumber) ? maybeNumber : null; } function getTierBand(tierValue) { return String(tierValue || "").toUpperCase().startsWith("HT") ? "HT" : "LT"; } function tierHeaderIconTemplate(tierNum) { if (tierNum > 3) { return ""; } return ` `; } function playerCardTemplate(player, index) { const globalIndex = (state.page - 1) * 20 + index + 1; let topClass = ""; if (globalIndex === 1) { topClass = "top-1"; } else if (globalIndex === 2) { topClass = "top-2"; } else if (globalIndex === 3) { topClass = "top-3"; } const tierEntries = visibleTiers(player) .map(([mode, tier]) => ({ mode, tier, points: TIER_POINTS[tier] || 0, })) .sort((a, b) => { if (b.points !== a.points) { return b.points - a.points; } return (MODE_LABELS[a.mode] || a.mode).localeCompare(MODE_LABELS[b.mode] || b.mode); }); const maxTierSlots = 10; const filledSlots = tierEntries.slice(0, maxTierSlots); const emptySlots = Math.max(maxTierSlots - filledSlots.length, 0); const tierSlotsHtml = [ ...filledSlots.map( (entry) => `No players currently have this rank yet
` : rows .map( (row) => ` ` ) .join("") }