226 lines
6.8 KiB
JavaScript
226 lines
6.8 KiB
JavaScript
/**
|
|
* 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<Object>} 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<Object|null>} 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<Object>} 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 = `
|
|
<td>${getDisplayedRank(entry, index)}</td>
|
|
<td>${entry.username ?? "-"}</td>
|
|
<td>${formatTime(entry.time)} min</td>
|
|
<td>${entry.score ?? "-"}</td>
|
|
`;
|
|
|
|
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 = '<td colspan="4"></td>';
|
|
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 = `
|
|
<td>${getDisplayedRank(extraUserEntry, 0)}</td>
|
|
<td>${extraUserEntry.username ?? "-"}</td>
|
|
<td>${formatTime(extraUserEntry.time)} min</td>
|
|
<td>${extraUserEntry.score ?? "-"}</td>
|
|
`;
|
|
|
|
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([]);
|
|
});
|
|
};
|
|
|