lorem_ipsum/js/play.js

1018 lines
33 KiB
JavaScript

/**
* 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<string>} 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 = "<strong></strong><span></span>";
ownScore.querySelector("strong").textContent = ownName;
ownScore.querySelector("span").textContent = score + " Punkte";
const otherScore = document.createElement("div");
otherScore.className = "play-challenge-result-score";
otherScore.innerHTML = "<strong></strong><span></span>";
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<boolean>} 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<boolean>} 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";
}
};
})();