add prevention of ctrl+c ctrl + v
This commit is contained in:
parent
ad64bae67b
commit
82f8e46cde
793
js/play.js
793
js/play.js
@ -1,379 +1,436 @@
|
||||
(function() {
|
||||
// --- Konfiguration ---
|
||||
const MEMORIZE_TIME_SECONDS = 15;
|
||||
(function () {
|
||||
// --- Konfiguration ---
|
||||
const MEMORIZE_TIME_SECONDS = 15;
|
||||
|
||||
// Bausteine fuer den zufaelligen 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."
|
||||
]
|
||||
// Bausteine fuer den zufaelligen 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.",
|
||||
],
|
||||
};
|
||||
|
||||
let timerInterval;
|
||||
let currentTime = 0;
|
||||
|
||||
// Der aktuell angezeigte Text muss bis zur Auswertung stabil bleiben.
|
||||
let currentGameText = "";
|
||||
let lastGeneratedText = "";
|
||||
|
||||
// DOM-Referenzen werden erst gesetzt, nachdem pages/play.html dynamisch geladen wurde.
|
||||
let phaseStart;
|
||||
let phaseMemorize;
|
||||
let phaseInput;
|
||||
let phaseResult;
|
||||
let targetTextDisplay;
|
||||
let timerDisplay;
|
||||
let userTextInput;
|
||||
let resultScore;
|
||||
let resultOriginal;
|
||||
let resultInput;
|
||||
let gameStatus;
|
||||
let scoreSaveFeedback;
|
||||
let btnSubmitScore;
|
||||
|
||||
// --- Funktionen ---
|
||||
|
||||
function getRandomItem(items) {
|
||||
return items[Math.floor(Math.random() * items.length)];
|
||||
}
|
||||
|
||||
// Erstellt pro Runde zwei zufaellige Saetze plus Schluss-Satz und vermeidet direkte Wiederholungen.
|
||||
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;
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
if (!phaseStart || !phaseMemorize) return;
|
||||
|
||||
// Startansicht ausblenden und den neu generierten Text fuer die Lernphase anzeigen.
|
||||
phaseStart.classList.add("d-none");
|
||||
phaseMemorize.classList.remove("d-none");
|
||||
|
||||
if (gameStatus) {
|
||||
gameStatus.textContent = "Lernphase";
|
||||
gameStatus.className = "badge fs-6 px-3 py-2";
|
||||
gameStatus.style.backgroundColor = "#ffd166";
|
||||
gameStatus.style.color = "#1b1b2f";
|
||||
}
|
||||
|
||||
currentGameText = generateGameText();
|
||||
if (targetTextDisplay) targetTextDisplay.textContent = currentGameText;
|
||||
|
||||
// Nach Ablauf des Timers wird automatisch zur Eingabe gewechselt.
|
||||
currentTime = MEMORIZE_TIME_SECONDS;
|
||||
if (timerDisplay) timerDisplay.textContent = currentTime;
|
||||
|
||||
timerInterval = setInterval(() => {
|
||||
currentTime--;
|
||||
if (timerDisplay) timerDisplay.textContent = currentTime;
|
||||
|
||||
if (currentTime <= 0) {
|
||||
endMemorizePhase();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function endMemorizePhase() {
|
||||
clearInterval(timerInterval);
|
||||
|
||||
if (!phaseMemorize || !phaseInput) return;
|
||||
|
||||
// Text verschwindet, Eingabefeld erscheint: ab hier zaehlt nur noch das Gedaechtnis.
|
||||
phaseMemorize.classList.add("d-none");
|
||||
phaseInput.classList.remove("d-none");
|
||||
|
||||
if (gameStatus) {
|
||||
gameStatus.textContent = "Eingabe";
|
||||
gameStatus.className = "badge fs-6 px-3 py-2";
|
||||
gameStatus.style.backgroundColor = "#4a6fa5";
|
||||
gameStatus.style.color = "#fff";
|
||||
}
|
||||
|
||||
if (userTextInput) {
|
||||
userTextInput.value = "";
|
||||
userTextInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Entfernt alles, was beim Vergleichen nicht zaehlen soll.
|
||||
function normalizeWord(word) {
|
||||
return word.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
|
||||
}
|
||||
|
||||
// Behält die sichtbaren Woerter separat, damit Satzzeichen in der Ergebnisanzeige erhalten bleiben.
|
||||
function getWords(text) {
|
||||
return text.split(/\s+/).filter((word) => word.length > 0);
|
||||
}
|
||||
|
||||
function calculateScore(original, input) {
|
||||
if (!original || !input) return 0;
|
||||
|
||||
// Score-Regel: gleiche Woerter an gleicher Position, Satzzeichen und Grossschreibung ignoriert.
|
||||
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;
|
||||
const limit = Math.min(cleanOriginal.length, cleanInput.length);
|
||||
|
||||
for (let i = 0; i < limit; i++) {
|
||||
if (cleanInput[i] === cleanOriginal[i]) {
|
||||
correctWords++;
|
||||
}
|
||||
}
|
||||
|
||||
return correctWords;
|
||||
}
|
||||
|
||||
// Baut ein einzelnes farbiges Wort-Label fuer den Ergebnisvergleich.
|
||||
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;
|
||||
}
|
||||
|
||||
// Markiert Original und Eingabe nach derselben Positionslogik wie calculateScore().
|
||||
function renderWordComparison(original, input) {
|
||||
if (!resultOriginal || !resultInput) return;
|
||||
|
||||
const originalWords = getWords(original);
|
||||
const inputWords = getWords(input);
|
||||
|
||||
resultOriginal.innerHTML = "";
|
||||
resultInput.innerHTML = "";
|
||||
|
||||
// Original: rot, wenn das eingegebene Wort an dieser Position fehlt oder falsch ist.
|
||||
originalWords.forEach((word, index) => {
|
||||
const isCorrect =
|
||||
normalizeWord(word) === normalizeWord(inputWords[index] || "");
|
||||
resultOriginal.appendChild(createWordBadge(word, isCorrect));
|
||||
});
|
||||
|
||||
// Eingabe: rot, wenn das Wort nicht zum Originalwort an derselben Position passt.
|
||||
inputWords.forEach((word, index) => {
|
||||
const isCorrect =
|
||||
normalizeWord(word) === normalizeWord(originalWords[index] || "");
|
||||
resultInput.appendChild(createWordBadge(word, isCorrect));
|
||||
});
|
||||
}
|
||||
|
||||
function getAuth() {
|
||||
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.AppAuth.getAuth();
|
||||
}
|
||||
|
||||
function getScoreService() {
|
||||
if (!window.config || !window.ScoreService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new window.ScoreService(window.config);
|
||||
}
|
||||
|
||||
function showScoreSaveFeedback(message, type) {
|
||||
if (!scoreSaveFeedback) return;
|
||||
|
||||
scoreSaveFeedback.className = "alert alert-" + type + " mb-4";
|
||||
scoreSaveFeedback.textContent = message;
|
||||
scoreSaveFeedback.classList.remove("d-none");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Auth-Daten kommen aus login.js; der ScoreService setzt daraus die Backend-Header.
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
async function submitScore() {
|
||||
if (!userTextInput) return;
|
||||
|
||||
const userInput = userTextInput.value.trim();
|
||||
if (!userInput) {
|
||||
alert("Bitte geben Sie einen Text ein.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (btnSubmitScore) {
|
||||
btnSubmitScore.disabled = true;
|
||||
btnSubmitScore.textContent = "Wird ausgewertet...";
|
||||
}
|
||||
|
||||
const score = calculateScore(currentGameText, userInput);
|
||||
|
||||
// Ergebnis sofort anzeigen; das Speichern im Backend passiert danach asynchron.
|
||||
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";
|
||||
}
|
||||
|
||||
if (resultScore) resultScore.textContent = score;
|
||||
renderWordComparison(currentGameText, userInput);
|
||||
|
||||
// Genau dieser Rundentext wird gespeichert, damit Leaderboard/Score-Details nachvollziehbar bleiben.
|
||||
const scoreData = {
|
||||
score: score,
|
||||
time: MEMORIZE_TIME_SECONDS,
|
||||
text: currentGameText,
|
||||
userWrittenText: userInput,
|
||||
};
|
||||
|
||||
let timerInterval;
|
||||
let currentTime = 0;
|
||||
console.log("Score bereit zum Senden:", scoreData);
|
||||
|
||||
// Der aktuell angezeigte Text muss bis zur Auswertung stabil bleiben.
|
||||
let currentGameText = "";
|
||||
let lastGeneratedText = "";
|
||||
try {
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DOM-Referenzen werden erst gesetzt, nachdem pages/play.html dynamisch geladen wurde.
|
||||
let phaseStart;
|
||||
let phaseMemorize;
|
||||
let phaseInput;
|
||||
let phaseResult;
|
||||
let targetTextDisplay;
|
||||
let timerDisplay;
|
||||
let userTextInput;
|
||||
let resultScore;
|
||||
let resultOriginal;
|
||||
let resultInput;
|
||||
let gameStatus;
|
||||
let scoreSaveFeedback;
|
||||
let btnSubmitScore;
|
||||
window.initPlayPage = function initPlayPage() {
|
||||
clearInterval(timerInterval);
|
||||
|
||||
// --- Funktionen ---
|
||||
// Die Navigation laedt play.html per fetch; deshalb werden die Elemente erst hier gesucht.
|
||||
phaseStart = document.getElementById("phaseStart");
|
||||
phaseMemorize = document.getElementById("phaseMemorize");
|
||||
phaseInput = document.getElementById("phaseInput");
|
||||
phaseResult = document.getElementById("phaseResult");
|
||||
|
||||
function getRandomItem(items) {
|
||||
return items[Math.floor(Math.random() * items.length)];
|
||||
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");
|
||||
|
||||
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.");
|
||||
}
|
||||
});
|
||||
|
||||
if (userTextInput) {
|
||||
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());
|
||||
}
|
||||
|
||||
// Erstellt pro Runde zwei zufaellige Saetze plus Schluss-Satz und vermeidet direkte Wiederholungen.
|
||||
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;
|
||||
if (targetTextDisplay) {
|
||||
targetTextDisplay.addEventListener("copy", (e) => e.preventDefault());
|
||||
targetTextDisplay.style.userSelect = "none";
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
if (!phaseStart || !phaseMemorize) return;
|
||||
|
||||
// Startansicht ausblenden und den neu generierten Text fuer die Lernphase anzeigen.
|
||||
phaseStart.classList.add('d-none');
|
||||
phaseMemorize.classList.remove('d-none');
|
||||
|
||||
if(gameStatus) {
|
||||
gameStatus.textContent = "Lernphase";
|
||||
gameStatus.className = "badge fs-6 px-3 py-2";
|
||||
gameStatus.style.backgroundColor = "#ffd166";
|
||||
gameStatus.style.color = "#1b1b2f";
|
||||
}
|
||||
|
||||
currentGameText = generateGameText();
|
||||
if(targetTextDisplay) targetTextDisplay.textContent = currentGameText;
|
||||
|
||||
// Nach Ablauf des Timers wird automatisch zur Eingabe gewechselt.
|
||||
currentTime = MEMORIZE_TIME_SECONDS;
|
||||
if(timerDisplay) timerDisplay.textContent = currentTime;
|
||||
|
||||
timerInterval = setInterval(() => {
|
||||
currentTime--;
|
||||
if(timerDisplay) timerDisplay.textContent = currentTime;
|
||||
|
||||
if (currentTime <= 0) {
|
||||
endMemorizePhase();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function endMemorizePhase() {
|
||||
clearInterval(timerInterval);
|
||||
|
||||
if (!phaseMemorize || !phaseInput) return;
|
||||
|
||||
// Text verschwindet, Eingabefeld erscheint: ab hier zaehlt nur noch das Gedaechtnis.
|
||||
phaseMemorize.classList.add('d-none');
|
||||
phaseInput.classList.remove('d-none');
|
||||
|
||||
if(gameStatus) {
|
||||
gameStatus.textContent = "Eingabe";
|
||||
gameStatus.className = "badge fs-6 px-3 py-2";
|
||||
gameStatus.style.backgroundColor = "#4a6fa5";
|
||||
gameStatus.style.color = "#fff";
|
||||
}
|
||||
|
||||
if(userTextInput) {
|
||||
userTextInput.value = "";
|
||||
userTextInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Entfernt alles, was beim Vergleichen nicht zaehlen soll.
|
||||
function normalizeWord(word) {
|
||||
return word.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
|
||||
}
|
||||
|
||||
// Behält die sichtbaren Woerter separat, damit Satzzeichen in der Ergebnisanzeige erhalten bleiben.
|
||||
function getWords(text) {
|
||||
return text.split(/\s+/).filter(word => word.length > 0);
|
||||
}
|
||||
|
||||
function calculateScore(original, input) {
|
||||
if (!original || !input) return 0;
|
||||
|
||||
// Score-Regel: gleiche Woerter an gleicher Position, Satzzeichen und Grossschreibung ignoriert.
|
||||
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;
|
||||
const limit = Math.min(cleanOriginal.length, cleanInput.length);
|
||||
|
||||
for (let i = 0; i < limit; i++) {
|
||||
if (cleanInput[i] === cleanOriginal[i]) {
|
||||
correctWords++;
|
||||
}
|
||||
}
|
||||
|
||||
return correctWords;
|
||||
}
|
||||
|
||||
// Baut ein einzelnes farbiges Wort-Label fuer den Ergebnisvergleich.
|
||||
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;
|
||||
}
|
||||
|
||||
// Markiert Original und Eingabe nach derselben Positionslogik wie calculateScore().
|
||||
function renderWordComparison(original, input) {
|
||||
if (!resultOriginal || !resultInput) return;
|
||||
|
||||
const originalWords = getWords(original);
|
||||
const inputWords = getWords(input);
|
||||
|
||||
resultOriginal.innerHTML = "";
|
||||
resultInput.innerHTML = "";
|
||||
|
||||
// Original: rot, wenn das eingegebene Wort an dieser Position fehlt oder falsch ist.
|
||||
originalWords.forEach((word, index) => {
|
||||
const isCorrect = normalizeWord(word) === normalizeWord(inputWords[index] || "");
|
||||
resultOriginal.appendChild(createWordBadge(word, isCorrect));
|
||||
});
|
||||
|
||||
// Eingabe: rot, wenn das Wort nicht zum Originalwort an derselben Position passt.
|
||||
inputWords.forEach((word, index) => {
|
||||
const isCorrect = normalizeWord(word) === normalizeWord(originalWords[index] || "");
|
||||
resultInput.appendChild(createWordBadge(word, isCorrect));
|
||||
});
|
||||
}
|
||||
|
||||
function getAuth() {
|
||||
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.AppAuth.getAuth();
|
||||
}
|
||||
|
||||
function getScoreService() {
|
||||
if (!window.config || !window.ScoreService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new window.ScoreService(window.config);
|
||||
}
|
||||
|
||||
function showScoreSaveFeedback(message, type) {
|
||||
if (!scoreSaveFeedback) return;
|
||||
|
||||
scoreSaveFeedback.className = "alert alert-" + type + " mb-4";
|
||||
scoreSaveFeedback.textContent = message;
|
||||
scoreSaveFeedback.classList.remove("d-none");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Auth-Daten kommen aus login.js; der ScoreService setzt daraus die Backend-Header.
|
||||
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");
|
||||
}
|
||||
|
||||
async function submitScore() {
|
||||
if (!userTextInput) return;
|
||||
|
||||
const userInput = userTextInput.value.trim();
|
||||
if (!userInput) {
|
||||
alert("Bitte geben Sie einen Text ein.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (btnSubmitScore) {
|
||||
btnSubmitScore.disabled = true;
|
||||
btnSubmitScore.textContent = "Wird ausgewertet...";
|
||||
}
|
||||
|
||||
const score = calculateScore(currentGameText, userInput);
|
||||
|
||||
// Ergebnis sofort anzeigen; das Speichern im Backend passiert danach asynchron.
|
||||
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";
|
||||
}
|
||||
|
||||
if (resultScore) resultScore.textContent = score;
|
||||
renderWordComparison(currentGameText, userInput);
|
||||
|
||||
// Genau dieser Rundentext wird gespeichert, damit Leaderboard/Score-Details nachvollziehbar bleiben.
|
||||
const scoreData = {
|
||||
score: score,
|
||||
time: MEMORIZE_TIME_SECONDS,
|
||||
text: currentGameText,
|
||||
userWrittenText: userInput
|
||||
};
|
||||
|
||||
console.log("Score bereit zum Senden:", scoreData);
|
||||
|
||||
try {
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.initPlayPage = function initPlayPage() {
|
||||
clearInterval(timerInterval);
|
||||
|
||||
// Die Navigation laedt play.html per fetch; deshalb werden die Elemente erst hier gesucht.
|
||||
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');
|
||||
|
||||
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.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
};
|
||||
})();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user