/** * Spiellogik (Lorem Ipsum Merk- und Schreibspiel). * Dieses Modul verwaltet die verschiedenen Phasen des Spiels (Startansicht, Einprägen, Eingabe, Auswertung), * berechnet die Punktzahl durch Wortvergleich und speichert das Ergebnis im Backend. * Unterstützt auch das Duell-System (Challenges) zwischen zwei Benutzern. */ (function () { // --- Konfiguration & Konstanten --- // Die Zeit in Sekunden, die der Spieler hat, um sich den Text einzuprägen. const MEMORIZE_TIME_SECONDS = 15; // Storage-Schlüssel, unter dem eine aktive Challenge im SessionStorage zwischengespeichert wird. const ACTIVE_CHALLENGE_STORAGE_KEY = "loremIpsumActiveChallenge"; // Präfix zur Kennzeichnung von eingebetteten JSON-Challenge-Daten in Textnachrichten. const CHALLENGE_DATA_PREFIX = "[[loremIpsumChallenge:"; // Bausteine für den zufälligen Rundentext. Alles bleibt lokal, damit das Spiel ohne Backend starten kann. const TEXT_PARTS = { subjects: [ "Der flinke Entwickler", "Die neugierige Studentin", "Ein mueder Professor", "Das kleine Frontend", "Der mutige Browser", "Eine schlaue Funktion", "Der vergessliche Server", "Die kreative Gruppe", ], actions: [ "sortiert leise", "debuggt geduldig", "vergleicht heimlich", "speichert vorsichtig", "rendert ploetzlich", "zaehlt konzentriert", "testet neugierig", "kompiliert langsam", ], objects: [ "sieben blaue Buttons", "drei lange Variablen", "neun goldene Woerter", "vier kaputte Formulare", "acht schnelle Requests", "zwei leuchtende Karten", "fuenf stille Fehlermeldungen", "sechs winzige Icons", ], places: [ "im hellen Dashboard", "unter dem dunklen Navbar", "neben dem alten Footer", "zwischen Login und Leaderboard", "vor dem ersten Kaffee", "waehrend der Lernphase", "hinter dem lokalen Server", "mitten im Semesterprojekt", ], endings: [ "Danach lacht der Code, weil alles endlich funktioniert.", "Am Ende merkt sich niemand die Semikolons, aber alle die Punkte.", "Kurz darauf blinkt die Konsole und behauptet, sie sei unschuldig.", "Spaeter landet der Score im Ranking und wartet auf Applaus.", "Dabei bleibt die Seite ruhig, obwohl der Timer dramatisch tickt.", "Zum Schluss gewinnt, wer die Woerter sauber in Reihenfolge bringt.", ], }; // --- Spielstatus Variablen --- let timerInterval; // Intervall-ID für den Memorize-Timer let currentTime = 0; // Aktueller Countdown-Stand let inputStartMs = null; // Zeitstempel (ms), wann der Benutzer mit der Eingabe begonnen hat // Der aktuell angezeigte Text muss bis zur Auswertung stabil bleiben. let currentGameText = ""; let lastGeneratedText = ""; // Verhindert direkt aufeinanderfolgende gleiche Texte // DOM-Referenzen (werden in initPlayPage gesucht, da play.html dynamisch nachgeladen wird) let phaseStart; // Start-Container (mit "Starten"-Button) let phaseMemorize; // Einpräge-Container (zeigt den Text und Countdown) let phaseInput; // Eingabe-Container (Textarea) let phaseResult; // Ergebnis-Container (Vergleich und erreichte Punkte) let targetTextDisplay; // Element zur Anzeige des zu merkenden Textes let timerDisplay; // Element zur Anzeige des Countdowns let userTextInput; // Textarea für die Benutzereingabe let resultScore; // Element zur Anzeige der Punktzahl im Ergebnis let resultOriginal; // Container für farblich markierten Originaltext let resultInput; // Container für farblich markierten eingegebenen Text let gameStatus; // Badge-Element zur Statusanzeige (z.B. "Lernphase", "Eingabe") let scoreSaveFeedback; // Container für Rückmeldungen beim Speichern let btnSubmitScore; // Button "Auswerten & Absenden" let activeChallenge = null; // Enthält geladene Challenge-Daten (falls Spiel aus Inbox gestartet) // --- Hilfsfunktionen --- /** * Wählt ein zufälliges Element aus einem Array aus. * @param {Array} items - Das Quell-Array. * @returns {*} Ein zufälliges Element. */ function getRandomItem(items) { return items[Math.floor(Math.random() * items.length)]; } /** * Erstellt pro Runde zwei zufällige Hauptsätze plus einen Schlusssatz. * Stellt sicher, dass sich zwei aufeinanderfolgende Runden-Texte unterscheiden. * @returns {string} Ein generierter Satz. */ function generateGameText() { let generatedText = ""; do { const firstSentence = [ getRandomItem(TEXT_PARTS.subjects), getRandomItem(TEXT_PARTS.actions), getRandomItem(TEXT_PARTS.objects), getRandomItem(TEXT_PARTS.places), ].join(" ") + "."; const secondSentence = [ getRandomItem(TEXT_PARTS.subjects), getRandomItem(TEXT_PARTS.actions), getRandomItem(TEXT_PARTS.objects), getRandomItem(TEXT_PARTS.places), ].join(" ") + "."; generatedText = firstSentence + " " + secondSentence + " " + getRandomItem(TEXT_PARTS.endings); } while (generatedText === lastGeneratedText); lastGeneratedText = generatedText; return generatedText; } /** * Startet eine neue Spielrunde. Blendet die Startphase aus und startet den Countdown. */ function startGame() { if (!phaseStart || !phaseMemorize) return; inputStartMs = null; // Zeitstempel zurücksetzen // Startansicht ausblenden und den Einpräge-Bildschirm anzeigen phaseStart.classList.add("d-none"); phaseMemorize.classList.remove("d-none"); // Status-Badge aktualisieren if (gameStatus) { gameStatus.textContent = "Lernphase"; gameStatus.className = "badge fs-6 px-3 py-2"; gameStatus.style.backgroundColor = "#ffd166"; gameStatus.style.color = "#1b1b2f"; } // Text ermitteln (aus Challenge oder neu generiert) und anzeigen currentGameText = getRoundText(); if (targetTextDisplay) targetTextDisplay.textContent = currentGameText; // Countdown-Timer starten currentTime = MEMORIZE_TIME_SECONDS; if (timerDisplay) timerDisplay.textContent = currentTime; timerInterval = setInterval(() => { currentTime--; if (timerDisplay) timerDisplay.textContent = currentTime; // Nach Ablauf des Timers automatisch in die Eingabephase wechseln if (currentTime <= 0) { endMemorizePhase(); } }, 1000); } /** * Beendet die Einprägephase, stoppt den Timer und öffnet das Eingabefeld. */ function endMemorizePhase() { clearInterval(timerInterval); if (!phaseMemorize || !phaseInput) return; // Text ausblenden (wichtig gegen Abschreiben!) und Eingabefeld zeigen phaseMemorize.classList.add("d-none"); phaseInput.classList.remove("d-none"); // Status-Badge anpassen if (gameStatus) { gameStatus.textContent = "Eingabe"; gameStatus.className = "badge fs-6 px-3 py-2"; gameStatus.style.backgroundColor = "#4a6fa5"; gameStatus.style.color = "#fff"; } // Fokus ins Textfeld setzen, damit der Spieler sofort tippen kann if (userTextInput) { userTextInput.value = ""; userTextInput.focus(); } // Zeitmessung für die Eingabedauer starten inputStartMs = Date.now(); } /** * Normalisiert ein einzelnes Wort für den fehlertoleranten Vergleich. * Konvertiert zu Kleinbuchstaben, löst Umlaute/ß auf und entfernt Satzzeichen. * @param {string} word - Das zu bereinigende Wort. * @returns {string} Das normalisierte Wort. */ function normalizeWord(word) { return word .toLowerCase() .replace(/ä/g, "ae") .replace(/ö/g, "oe") .replace(/ü/g, "ue") .replace(/ß/g, "ss") // Entfernt Satz- und Sonderzeichen .replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, ""); } /** * Zerlegt einen Text anhand von Leerzeichen in ein Array von Wörtern. * Leere Elemente (z.B. durch mehrfache Leerzeichen) werden ausgefiltert. * @param {string} text - Der Eingabetext. * @returns {Array} Liste der Wörter. */ function getWords(text) { return text.split(/\s+/).filter((word) => word.length > 0); } /** * Berechnet die Punktzahl auf Basis der korrekt geschriebenen Wörter. * Es wird die exakte Position der Wörter verglichen (Wort an Index i). * @param {string} original - Der vorgegebene Originaltext. * @param {string} input - Die Eingabe des Spielers. * @returns {number} Anzahl der übereinstimmenden Wörter. */ function calculateScore(original, input) { if (!original || !input) return 0; // Beide Texte in normalisierte Wort-Listen zerlegen const cleanOriginal = getWords(original) .map(normalizeWord) .filter((word) => word.length > 0); const cleanInput = getWords(input) .map(normalizeWord) .filter((word) => word.length > 0); let correctWords = 0; // Nur bis zum kürzeren der beiden Texte vergleichen, um Out-of-Bounds zu vermeiden const limit = Math.min(cleanOriginal.length, cleanInput.length); for (let i = 0; i < limit; i++) { if (cleanInput[i] === cleanOriginal[i]) { correctWords++; } } return correctWords; } /** * Erstellt ein farbiges Badge-Element für die Ergebnisanzeige. * @param {string} word - Das darzustellende Wort. * @param {boolean} isCorrect - Bestimmt die Farbe (Grün für korrekt, Rot für falsch). * @returns {HTMLElement} Das erstellte span-Element. */ function createWordBadge(word, isCorrect) { const badge = document.createElement("span"); badge.className = "badge me-1 mb-1 " + (isCorrect ? "text-bg-success" : "text-bg-danger"); badge.textContent = word; return badge; } /** * Visualisiert den Wortvergleich zwischen Original und Eingabe im Ergebnisbereich. * @param {string} original - Der Originaltext. * @param {string} input - Der eingegebene Text. */ function renderWordComparison(original, input) { if (!resultOriginal || !resultInput) return; const originalWords = getWords(original); const inputWords = getWords(input); resultOriginal.innerHTML = ""; resultInput.innerHTML = ""; // Originaltext-Badges: Rot markieren, wenn an dieser Position das Wort fehlt oder falsch eingegeben wurde originalWords.forEach((word, index) => { const isCorrect = normalizeWord(word) === normalizeWord(inputWords[index] || ""); resultOriginal.appendChild(createWordBadge(word, isCorrect)); }); // Eingegebene Badges: Rot markieren, wenn das Wort an dieser Stelle nicht zum Original passt inputWords.forEach((word, index) => { const isCorrect = normalizeWord(word) === normalizeWord(originalWords[index] || ""); resultInput.appendChild(createWordBadge(word, isCorrect)); }); } /** * Holt die aktuellen Authentifizierungsdaten. */ function getAuth() { if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") { return null; } return window.AppAuth.getAuth(); } /** * Holt eine Instanz des ScoreService. */ function getScoreService() { if (!window.config || !window.ScoreService) { return null; } return new window.ScoreService(window.config); } /** * Holt eine Instanz des ChallengeService. */ function getChallengeService() { if (!window.config || !window.ChallengeService) { return null; } return new window.ChallengeService(window.config); } /** * Holt eine Instanz des MessageService. */ function getMessageService() { if (!window.config || !window.MessageService) { return null; } return new window.MessageService(window.config); } /** * Liest eine im SessionStorage hinterlegte aktive Challenge aus. * @returns {Object|null} Challenge-Datenobjekt oder null. */ function readActiveChallenge() { // Challenge-Runden werden auf der Nachrichten-Seite gestartet. // Der gespeicherte Kontext entscheidet über den API-Fluss nach Spielende. const raw = sessionStorage.getItem(ACTIVE_CHALLENGE_STORAGE_KEY); if (!raw) { return null; } try { const challenge = JSON.parse(raw); if (!challenge || challenge.id === undefined || challenge.id === null || !challenge.opponent) { return null; } return challenge; } catch { return null; } } /** * Prüft, ob aktuell eine aktive Herausforderung vorliegt. */ function hasActiveChallenge() { return Boolean(activeChallenge && activeChallenge.id !== undefined && activeChallenge.id !== null); } /** * Prüft, ob es sich um die zweite Runde einer Challenge handelt. * In der zweiten Runde spielt der Herausforderer gegen den Score des Gegners. */ function isChallengeSecondRound() { return hasActiveChallenge() && activeChallenge.role === "challenger" && activeChallenge.opponentScore !== null && activeChallenge.opponentScore !== undefined; } /** * Prüft, ob es sich um die erste Runde einer Challenge handelt. * In der ersten Runde spielt der herausgeforderte User und sendet danach sein Ergebnis. */ function isChallengeFirstRound() { return hasActiveChallenge() && activeChallenge.role === "opponent"; } /** * Schreibt den aktuellen Challenge-Kontext in den SessionStorage. */ function writeActiveChallenge(challenge) { sessionStorage.setItem(ACTIVE_CHALLENGE_STORAGE_KEY, JSON.stringify(challenge)); } /** * Ermittelt den Text für die Spielrunde. * Bei Challenges müssen beide Spieler denselben Text erhalten, welcher aus dem Kontext ausgelesen wird. * @returns {string} Der Rundentext. */ function getRoundText() { if (!activeChallenge) { return generateGameText(); } // Wenn ein fester Challenge-Text existiert, wird dieser verwendet if (typeof activeChallenge.challengeText === "string" && activeChallenge.challengeText.trim()) { return activeChallenge.challengeText; } // Falls bei einer alten Challenge kein Text hinterlegt war, generieren wir einen und speichern ihn im Kontext const generatedText = generateGameText(); activeChallenge.challengeText = generatedText; writeActiveChallenge(activeChallenge); return generatedText; } /** * Zeigt Feedback zum Speicherstatus des Scores (z. B. "Score erfolgreich gespeichert"). * @param {string} message - Der Feedbacktext. * @param {string} type - Der Bootstrap-Alert-Typ (danger, warning, success, info). */ function showScoreSaveFeedback(message, type) { if (!scoreSaveFeedback) return; scoreSaveFeedback.className = "alert alert-" + type + " mb-4"; scoreSaveFeedback.textContent = message; scoreSaveFeedback.classList.remove("d-none"); } /** * Speichert das Ergebnis eines normalen Spiels im Backend. * @param {Object} scoreData - Das Score-Datenobjekt. */ async function saveScore(scoreData) { const auth = getAuth(); if (!auth || !auth.username || !auth.password) { showScoreSaveFeedback( "Score wurde nur lokal berechnet. Bitte einloggen, damit er im Leaderboard gespeichert wird.", "warning", ); return; } const scoreService = getScoreService(); if (!scoreService) { showScoreSaveFeedback( "Score-Service konnte nicht geladen werden.", "danger", ); return; } showScoreSaveFeedback("Score wird gespeichert...", "info"); const result = await scoreService.postScore( auth.username, auth.password, scoreData.score, scoreData.time, scoreData.text, scoreData.userWrittenText, ); if (result.ok) { const place = result.body && result.body.place ? " Platz " + result.body.place + "." : ""; showScoreSaveFeedback( "Score erfolgreich gespeichert." + place, "success", ); return; } if (result.status === 401) { showScoreSaveFeedback( "Score konnte nicht gespeichert werden: Login ist nicht gültig.", "danger", ); return; } showScoreSaveFeedback( "Score konnte nicht gespeichert werden (Status " + result.status + ").", "danger", ); } /** * Zeigt das Endergebnis eines Duells direkt im Browser an (Sieg, Niederlage, Unentschieden). * Wird nach der finalen Runde einer Challenge ausgeführt. * @param {number} score - Die eigene Punktzahl. */ function renderChallengeResult(score) { if (!scoreSaveFeedback || !isChallengeSecondRound()) { return; } const opponentScore = Number(activeChallenge.opponentScore); if (Number.isNaN(opponentScore)) { return; } const auth = getAuth(); const ownName = auth?.username || "Du"; const opponentName = activeChallenge.opponent; // Sieger ermitteln const result = score > opponentScore ? "winner" : score < opponentScore ? "loser" : "draw"; // Texte und Bilder je nach Ausgang des Spiels konfigurieren const headline = result === "winner" ? "Du gewinnst die Challenge" : result === "loser" ? opponentName + " gewinnt die Challenge" : "Unentschieden"; const outcomeText = result === "winner" ? "Gewonnen" : result === "loser" ? "Verloren" : "Unentschieden"; const outcomeImage = result === "winner" ? "image/sieg.png" : result === "loser" ? "image/verloren.png" : "image/unentschieden.png"; // Ergebnisgrafik zusammenbauen const graphic = document.createElement("div"); graphic.className = "play-challenge-result play-challenge-result-" + result; const outcomeHeader = document.createElement("div"); outcomeHeader.className = "challenge-outcome challenge-outcome-" + (result === "winner" ? "win" : result === "loser" ? "loss" : "draw"); const outcomeImg = document.createElement("img"); outcomeImg.src = outcomeImage; outcomeImg.alt = outcomeText; const outcomeLabel = document.createElement("strong"); outcomeLabel.textContent = outcomeText; outcomeHeader.append(outcomeImg, outcomeLabel); const title = document.createElement("div"); title.className = "play-challenge-result-title"; title.textContent = headline; const scores = document.createElement("div"); scores.className = "play-challenge-result-scores"; const ownScore = document.createElement("div"); ownScore.className = "play-challenge-result-score"; ownScore.innerHTML = ""; ownScore.querySelector("strong").textContent = ownName; ownScore.querySelector("span").textContent = score + " Punkte"; const otherScore = document.createElement("div"); otherScore.className = "play-challenge-result-score"; otherScore.innerHTML = ""; otherScore.querySelector("strong").textContent = opponentName; otherScore.querySelector("span").textContent = opponentScore + " Punkte"; scores.append(ownScore, otherScore); graphic.append(outcomeHeader, title, scores); // Grafik unter dem Speicher-Feedback einfügen scoreSaveFeedback.insertAdjacentElement("afterend", graphic); } /** * Sendet den Abschluss einer Challenge an das dafür vorgesehene Challenge-Backend. * Falls dieser Endpoint nicht existiert (altes Backend), liefert die Funktion false. * @param {Object} scoreData - Das Score-Datenobjekt. * @returns {Promise} True, falls das Speichern im Challenge-Backend erfolgreich war. */ async function completeChallenge(scoreData) { const auth = getAuth(); if (!auth || !hasActiveChallenge()) { return; } const challengeService = getChallengeService(); if (!challengeService) { showScoreSaveFeedback( "Score gespeichert, aber der Challenge-Service konnte nicht geladen werden.", "warning", ); return; } const result = await challengeService.completeChallenge( auth.username, auth.password, activeChallenge.id, scoreData.score, scoreData.time, scoreData.text, scoreData.userWrittenText, ); if (!result.ok) { console.warn("Challenge-Abschluss über API nicht möglich, nutze Nachrichten-Fallback. Status:", result.status); return false; } // Challenge erfolgreich abgeschlossen sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY); activeChallenge = null; showScoreSaveFeedback( "Challenge abgeschlossen. Beide User erhalten eine Ergebnisnachricht.", "success", ); if (typeof window.updateMessagesNavState === "function") { window.updateMessagesNavState(); } return true; } /** * Ablauf für die ERSTE Runde einer Challenge (der Geforderte hat gespielt). * Der Score wird normal gespeichert und der Herausforderer wird benachrichtigt. * @param {Object} scoreData - Die Score-Daten des Geforderten. */ async function finishFirstChallengeRound(scoreData) { // Score im normalen Ranking ablegen await saveScore(scoreData); // Dem Herausforderer eine Nachricht schicken mit den erreichten Punkten await notifyChallenger(scoreData); sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY); activeChallenge = null; showScoreSaveFeedback("Resultat wurde an den Herausforderer gesendet.", "success"); if (typeof window.updateMessagesNavState === "function") { window.updateMessagesNavState(); } } /** * Benachrichtigt den Herausforderer per Inbox-Nachricht über das eigene Ergebnis. * Da der Challenge-Ablauf über Nachrichten läuft, betten wir die Spieldaten * als JSON-String in die Textnachricht ein. * @param {Object} scoreData - Die erzielten Spieldaten. */ async function notifyChallenger(scoreData) { const auth = getAuth(); const messageService = getMessageService(); if (!auth || !messageService || !activeChallenge.challenger) { return; } // Zu übermittelnde Spieldaten strukturieren const challengeData = { id: activeChallenge.id, challenger: activeChallenge.challenger, opponent: auth.username, opponentScore: scoreData.score, challengeText: scoreData.text, }; // Betten das JSON verschlüsselt in das Nachrichtenfeld ein const messageText = CHALLENGE_DATA_PREFIX + JSON.stringify(challengeData) + "]]" + "\n" + auth.username + " hat gespielt und " + scoreData.score + " Punkte erreicht. Jetzt bist du dran."; const result = await messageService.postMessage( auth.username, auth.password, activeChallenge.challenger, "challenge", messageText, ); if (!result.ok) { showScoreSaveFeedback( "Challenge wurde gespeichert, aber die Nachricht an den Herausforderer konnte nicht gesendet werden.", "warning", ); } } /** * Ermittelt den Namen des Siegers auf Basis der Punktzahlen. * @param {number} challengerScore - Punkte des Herausforderers. * @param {number} opponentScore - Punkte des Gegners. * @returns {string} Der Gewinnername oder "draw" bei Gleichstand. */ function getChallengeWinner(challengerScore, opponentScore) { if (challengerScore > opponentScore) { return activeChallenge.challenger || getAuth()?.username || "Herausforderer"; } if (challengerScore < opponentScore) { return activeChallenge.opponent || "Gegner"; } return "draw"; } /** * Hilfsfunktion zum Senden der Abschlussnachricht (Challenge-Resultat) an den Gegner. * @param {string} recipient - Der Empfänger der Nachricht. * @param {Object} resultData - Das Resultatsobjekt. * @param {string} text - Der Nachrichtentext. * @returns {Promise} True bei Erfolg. */ async function sendChallengeResultMessage(recipient, resultData, text) { const auth = getAuth(); const messageService = getMessageService(); if (!auth || !messageService || !recipient) { return false; } const messageText = CHALLENGE_DATA_PREFIX + JSON.stringify(resultData) + "]]" + "\n" + text; const result = await messageService.postMessage( auth.username, auth.password, recipient, "challenge", messageText, ); return result.ok; } /** * Fallback-Ablauf für die ZWEITE Runde (Herausforderer beendet das Spiel), * falls die direkte Complete-API des Backends fehlschlägt. * Das Ergebnis wird berechnet und per Nachricht an den Gegner gesendet. * @param {Object} scoreData - Die Spieldaten des Herausforderers. */ async function finishFinalChallengeWithMessages(scoreData) { const auth = getAuth(); if (!auth || !activeChallenge) { return; } // Eigenen Score speichern await saveScore(scoreData); const opponentScore = Number(activeChallenge.opponentScore); const winner = getChallengeWinner(scoreData.score, opponentScore); // Resultat-JSON zusammenbauen const resultData = { id: activeChallenge.id, challenger: activeChallenge.challenger || auth.username, opponent: activeChallenge.opponent, challengerScore: scoreData.score, opponentScore: opponentScore, challengeText: scoreData.text, winner: winner, }; const winnerText = winner === "draw" ? "Unentschieden" : "Gewinner: " + winner; const resultText = "Challenge abgeschlossen. " + resultData.challenger + ": " + resultData.challengerScore + " Punkte, " + resultData.opponent + ": " + resultData.opponentScore + " Punkte. " + winnerText + "."; // Das Ergebnis per Message an den Gegner übermitteln const opponentMessageSent = await sendChallengeResultMessage( activeChallenge.opponent, resultData, resultText, ); sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY); activeChallenge = null; if (!opponentMessageSent) { showScoreSaveFeedback( "Score gespeichert, aber die Ergebnisnachricht an den Gegner konnte nicht gesendet werden.", "warning", ); return; } showScoreSaveFeedback( "Challenge abgeschlossen. Ergebnisnachricht wurde an den Gegner gesendet.", "success", ); if (typeof window.updateMessagesNavState === "function") { window.updateMessagesNavState(); } } /** * Beendet die Spielrunde, wertet die Eingabe aus, rendert die Fehler * und leitet die Speicherung je nach Spielmodus ein. */ async function submitScore() { if (!userTextInput) return; const userInput = userTextInput.value.trim(); if (!userInput) { alert("Bitte geben Sie einen Text ein."); return; } // Button sperren gegen Doppelklicks if (btnSubmitScore) { btnSubmitScore.disabled = true; btnSubmitScore.textContent = "Wird ausgewertet..."; } // Punktzahl berechnen const score = calculateScore(currentGameText, userInput); // Eingabebereich ausblenden und Ergebnisbereich einblenden if (phaseInput) phaseInput.classList.add("d-none"); if (phaseResult) phaseResult.classList.remove("d-none"); if (gameStatus) { gameStatus.textContent = "Abgeschlossen"; gameStatus.className = "badge fs-6 px-3 py-2"; gameStatus.style.backgroundColor = "#28a745"; gameStatus.style.color = "#fff"; } // Ergebnis anzeigen if (resultScore) resultScore.textContent = score; renderWordComparison(currentGameText, userInput); renderChallengeResult(score); // Dauer der Eingabe ermitteln (mindestens 1 Sekunde) const inputDurationSeconds = inputStartMs ? Math.max(1, Math.round((Date.now() - inputStartMs) / 1000)) : MEMORIZE_TIME_SECONDS; const scoreData = { score: score, time: inputDurationSeconds, text: currentGameText, userWrittenText: userInput, }; try { if (isChallengeFirstRound()) { // Erste Runde des Duells showScoreSaveFeedback("Resultat wird an den Herausforderer gesendet...", "info"); await finishFirstChallengeRound(scoreData); } else if (hasActiveChallenge()) { // Finale Runde des Duells showScoreSaveFeedback("Challenge-Resultat wird gesendet...", "info"); let completed = false; try { // Versuchen über die offizielle API abzuschließen completed = await completeChallenge(scoreData); } catch (error) { console.warn("Challenge-Abschluss fehlgeschlagen, nutze Nachrichten-Fallback.", error); } if (!completed) { // Falls API fehlschlägt, auf den Nachrichten-Ablauf ausweichen showScoreSaveFeedback("Backend-Abschluss nicht moeglich. Ergebnis wird ueber Nachrichten gesendet...", "info"); await finishFinalChallengeWithMessages(scoreData); } } else { // Normales Spiel ohne Duell await saveScore(scoreData); } } catch (error) { console.error("Fehler beim Speichern des Scores:", error); showScoreSaveFeedback( "Score konnte wegen eines technischen Fehlers nicht gespeichert werden.", "danger", ); } finally { if (btnSubmitScore) { btnSubmitScore.disabled = false; btnSubmitScore.textContent = "Auswerten & Absenden"; } } } /** * Globale Initialisierungsfunktion, aufgerufen von navigation.js * nach dem Laden von play.html. */ window.initPlayPage = function initPlayPage() { clearInterval(timerInterval); // Bestehende Timer vorsichtshalber stoppen activeChallenge = readActiveChallenge(); // Prüfen, ob wir aus einer Challenge gestartet wurden // DOM-Referenzen holen phaseStart = document.getElementById("phaseStart"); phaseMemorize = document.getElementById("phaseMemorize"); phaseInput = document.getElementById("phaseInput"); phaseResult = document.getElementById("phaseResult"); targetTextDisplay = document.getElementById("targetTextDisplay"); timerDisplay = document.getElementById("timerDisplay"); userTextInput = document.getElementById("userTextInput"); resultScore = document.getElementById("resultScore"); resultOriginal = document.getElementById("resultOriginal"); resultInput = document.getElementById("resultInput"); gameStatus = document.getElementById("gameStatus"); scoreSaveFeedback = document.getElementById("scoreSaveFeedback"); const btnStart = document.getElementById("btnStartGame"); btnSubmitScore = document.getElementById("btnSubmitScore"); const btnRestart = document.getElementById("btnRestart"); const btnLeaderboard = document.getElementById("btnLeaderboard"); // Falls eine Challenge aktiv ist, Hinweistext über den Gegner und dessen Score einblenden const challengeHint = document.getElementById("challengeHint"); if (challengeHint && activeChallenge) { const opponentScore = activeChallenge.opponentScore !== null && activeChallenge.opponentScore !== undefined ? " Score: " + activeChallenge.opponentScore + "." : ""; const hintPrefix = activeChallenge.role === "opponent" ? "Du spielst zuerst gegen " : "Finale Runde gegen "; challengeHint.textContent = hintPrefix + activeChallenge.opponent + "." + opponentScore; challengeHint.classList.remove("d-none"); } // Event-Listener binden if (btnStart) btnStart.addEventListener("click", startGame); if (btnSubmitScore) btnSubmitScore.addEventListener("click", submitScore); if (btnRestart) btnRestart.addEventListener("click", () => window.loadPage("play", "nav-play"), ); if (btnLeaderboard) btnLeaderboard.addEventListener("click", () => { const navLink = document.getElementById("nav-leaderboard"); if (navLink) { navLink.click(); // Nutzt die bestehende Navigation inklusive Active-State. } else { console.warn("Sidebar Link #nav-leaderboard nicht gefunden."); } }); // Anti-Cheating Maßnahmen im Eingabebereich einrichten if (userTextInput) { // Kopieren, Einfügen, Ausschneiden und Tastenkombinationen sperren userTextInput.addEventListener("keydown", (e) => { const key = e.key.toLowerCase(); if ( (e.ctrlKey || e.metaKey) && (key === "c" || key === "v" || key === "x" || key === "a") ) { e.preventDefault(); alert("Yeah, cheating is not sooo nice..."); } }); userTextInput.addEventListener("paste", (e) => e.preventDefault()); userTextInput.addEventListener("copy", (e) => e.preventDefault()); userTextInput.addEventListener("cut", (e) => e.preventDefault()); // Sobald getippt wird, startet die Zeitmessung (falls nicht schon geschehen) userTextInput.addEventListener("input", () => { if (!inputStartMs) { inputStartMs = Date.now(); } }); } // Textauswahl auf dem zu merkenden Text verhindern, um Kopieren via Maus zu blockieren if (targetTextDisplay) { targetTextDisplay.addEventListener("copy", (e) => e.preventDefault()); targetTextDisplay.style.userSelect = "none"; } }; })();