/** * Bestenliste (Leaderboard) verwalten und anzeigen. * Dieses Modul lädt die Top-10-Spieler vom Server. Falls der aktuell angemeldete * Benutzer nicht in diesen Top-10 vertreten ist, lädt es zusätzlich dessen * persönliches Bestergebnis und zeigt dieses optisch getrennt darunter an. */ /** * Formatiert Sekunden in ein lesbares MM:SS Format (z.B. 75 Sekunden -> "1:15"). * Bei ungültigen Werten wird ein Strich ausgegeben. * @param {number} seconds - Anzahl Sekunden. * @returns {string} Formatiertes Zeit-String. */ function formatTime(seconds) { if (typeof seconds !== "number" || Number.isNaN(seconds)) { return "-"; } const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`; } /** * Holt die aktuellen Authentifizierungsdaten aus dem globalen Auth-Modul. * @returns {Object|null} Auth-Objekt (mit username) oder null. */ function getLoggedInAuth() { if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") { return null; } const auth = window.AppAuth.getAuth(); if (!auth || !auth.username) { return null; } return auth; } /** * Normalisiert einen Benutzernamen (Trimming und Kleinschreibung) für robuste Vergleiche. * @param {string} username - Der Benutzername. * @returns {string} Der bereinigte Benutzername. */ function normalizeUsername(username) { return String(username ?? "").trim().toLowerCase(); } /** * Ermittelt den normalisierten Benutzernamen des aktuell angemeldeten Benutzers. * @returns {string|null} Der bereinigte Name oder null. */ function getLoggedInUsername() { const auth = getLoggedInAuth(); if (!auth) { return null; } return normalizeUsername(auth.username); } /** * Ermittelt den anzuzeigenden Rang eines Bestenlisten-Eintrags. * Nutzt vorrangig den vom Backend gelieferten Platz ("place"), andernfalls den Listenindex. * @param {Object} entry - Der Listeneintrag. * @param {number} index - Der Listenindex. * @returns {number} Der anzuzeigende Rang. */ function getDisplayedRank(entry, index) { const place = Number(entry?.place); if (!Number.isNaN(place) && place > 0) { return place; } return index + 1; } /** * Ermittelt das beste Ergebnis aus einer Liste von Scores. * Sortiert nach Score (absteigend) und bei Gleichstand nach Zeit (aufsteigend). * @param {Array} entries - Eine Liste von Score-Einträgen. * @returns {Object|null} Der beste Score-Eintrag. */ function getBestScoreEntry(entries) { return entries .slice() .sort((a, b) => { const scoreA = Number(a.score ?? 0); const scoreB = Number(b.score ?? 0); if (scoreB !== scoreA) { return scoreB - scoreA; } const timeA = Number(a.time ?? Number.MAX_SAFE_INTEGER); const timeB = Number(b.time ?? Number.MAX_SAFE_INTEGER); return timeA - timeB; })[0] ?? null; } /** * Holt das beste Ergebnis des aktuell angemeldeten Benutzers aus dem Backend. * @param {string} username - Der Benutzername des eingeloggten Spielers. * @returns {Promise} Das beste Spielergebnis oder null. */ async function getCurrentUserLeaderboardEntry(username) { if (!window.ScoreService || !username) { return null; } const scoreService = new window.ScoreService(window.config); const result = await scoreService.getScoreByName(username); if (!result.ok || !Array.isArray(result.body) || result.body.length === 0) { return null; } return getBestScoreEntry(result.body); } /** * Rendert die Bestenliste im DOM. * Hebt die Zeile des aktuell angemeldeten Benutzers farblich hervor. * Falls extraUserEntry übergeben wird (wenn der User nicht in den Top-10 ist), * wird am Ende eine Trennzeile und der Eintrag des Benutzers angehängt. * @param {Array} entries - Die Top-10 Leaderboard-Einträge. * @param {Object|null} extraUserEntry - Der separate Bestenlisten-Eintrag des angemeldeten Benutzers. */ function renderLeaderboard(entries, extraUserEntry = null) { const tableBody = document.getElementById("leaderboard-body"); if (!tableBody) { return; } const loggedInUsername = getLoggedInUsername(); tableBody.innerHTML = ""; // Rendern der Top-10 entries.forEach((entry, index) => { const row = document.createElement("tr"); const rowUsername = normalizeUsername(entry.username); // Zeile grün hervorheben, wenn es sich um den eigenen Account handelt if (loggedInUsername && rowUsername === loggedInUsername) { row.classList.add("leaderboard-row-current-user"); } row.innerHTML = ` ${getDisplayedRank(entry, index)} ${entry.username ?? "-"} ${formatTime(entry.time)} min ${entry.score ?? "-"} `; tableBody.appendChild(row); }); // Wenn der User eingeloggt ist, aber nicht in den Top-10 vertreten war, hängen wir ihn unten an if (extraUserEntry) { // Optische Lücke (Leerzeile) einfügen const spacerRow = document.createElement("tr"); spacerRow.classList.add("leaderboard-row-gap"); spacerRow.innerHTML = ''; tableBody.appendChild(spacerRow); // Zeile des angemeldeten Benutzers anhängen const userRow = document.createElement("tr"); userRow.classList.add("leaderboard-row-current-user"); userRow.classList.add("leaderboard-row-current-user-extra"); userRow.innerHTML = ` ${getDisplayedRank(extraUserEntry, 0)} ${extraUserEntry.username ?? "-"} ${formatTime(extraUserEntry.time)} min ${extraUserEntry.score ?? "-"} `; tableBody.appendChild(userRow); } } /** * Lädt die Top-10-Bestenliste vom Backend und prüft, * ob der angemeldete Benutzer separat geladen werden muss. */ async function loadTopTenLeaderboard() { const leaderboardService = new window.LeaderboardService(window.config); // Holt die Bestenliste beginnend bei Rang 1 (Offset 0) mit maximal 10 Einträgen const result = await leaderboardService.getLeaderboard(0, 10); if (!result.ok || !Array.isArray(result.body)) { renderLeaderboard([]); return; } const auth = getLoggedInAuth(); let extraUserEntry = null; if (auth && auth.username) { const loggedInUsername = normalizeUsername(auth.username); // Prüfen, ob der angemeldete Benutzer bereits in den Top-10 vorhanden ist const isInTopTen = result.body.some( (entry) => normalizeUsername(entry.username) === loggedInUsername, ); // Wenn er nicht in den Top-10 ist, rufen wir sein bestes Ergebnis separat ab if (!isInTopTen) { extraUserEntry = await getCurrentUserLeaderboardEntry(auth.username); } } renderLeaderboard(result.body, extraUserEntry); } /** * Globale Initialisierungsfunktion, die von navigation.js aufgerufen wird, * sobald die leaderboard.html-Teilseite geladen wurde. */ window.initLeaderboardPage = function initLeaderboardPage() { loadTopTenLeaderboard().catch((error) => { console.error("Fehler beim Laden des Leaderboards:", error); renderLeaderboard([]); }); };