let currentSessionId = null; let currentQuestionNumber = 1; let totalQuestions = 10; let currentScore = 0; let attemptsUsed = 0; const maxAttempts = 3; let selectedDifficulty = "all"; let lastQuestionData = null; let timerStartTime = null; let timerInterval = null; let elapsedSeconds = 0; const UI_TEXT = { de: { pageTitle: "✨ Magisches Hogwarts-Escape-Quiz ✨", pageSubtitle: "10 knifflige Rätsel aus der Zauberwelt warten auf dich", envelopeLetter: "Hogwarts-Schule für Hexerei und Zauberei", introDesc: "Ein verzauberter Brief aus Hogwarts schwebt vor dir in der Luft.
Bist du bereit, die Prüfungen abzulegen und deine Hauspunkte zu sichern?", diffLabel: "Schwierigkeitsgrad wählen:", diffAll: "Alle gemischt", diffEasy: "Leicht", diffMed: "Mittel", diffHard: "Schwer", startBtn: "📜 Siegel brechen & Brief öffnen", startBtnCasting: "✨ Zauber wird gewirkt...", pointsLabel: "🏆 Hauspunkte", questionLabel: "Frage", ofLabel: "von", attemptLabel: "Versuch", checkBtn: "✨ Antwort prüfen", hintBtn: "💡 Hinweis anzeigen", hintHeading: "📜 Notiz aus der verbotenen Abteilung:", placeholder: "Deine Antwort hier eingeben...", noHint: "Kein Tipp vorhanden.", hintFailed: "Tipp konnte nicht geladen werden.", correctFeedback: "✨ Richtig! 1 Hauspunkt für dein Haus! ✨", wrongFeedback: "Leider falsch! Noch {attempts} Versuch(e).", exhaustedWrong: "Leider nicht richtig (3 Fehlversuche).", solutionLabel: "Richtige Lösung:", startError: "Fehler beim Starten des Quiz: ", networkError: "Netzwerkfehler: " }, en: { pageTitle: "✨ Magical Hogwarts Escape Quiz ✨", pageSubtitle: "10 tricky wizarding world puzzles await you", envelopeLetter: "Hogwarts School of Witchcraft and Wizardry", introDesc: "An enchanted letter from Hogwarts is floating in the air before you.
Are you ready to take the exams and secure House Points?", diffLabel: "Choose difficulty level:", diffAll: "All mixed", diffEasy: "Easy", diffMed: "Medium", diffHard: "Hard", startBtn: "📜 Break Seal & Open Letter", startBtnCasting: "✨ Casting spell...", pointsLabel: "🏆 House Points", questionLabel: "Question", ofLabel: "of", attemptLabel: "Attempt", checkBtn: "✨ Check Answer", hintBtn: "💡 Show Hint", hintHeading: "📜 Note from the Restricted Section:", placeholder: "Type your answer here...", noHint: "No hint available.", hintFailed: "Could not load hint.", correctFeedback: "✨ Correct! 1 House Point for your House! ✨", wrongFeedback: "Incorrect! {attempts} attempt(s) remaining.", exhaustedWrong: "Unfortunately incorrect (3 failed attempts).", solutionLabel: "Correct solution:", startError: "Error starting quiz: ", networkError: "Network error: " } }; // Web Audio API Sound Generator for magic chimes and buzzers const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); function playSound(type) { if (audioCtx.state === 'suspended') { audioCtx.resume(); } if (type === 'magic') { // Magical bright arpeggio chime const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6 notes.forEach((freq, index) => { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; gain.gain.setValueAtTime(0.15, audioCtx.currentTime + index * 0.08); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + index * 0.08 + 0.35); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(audioCtx.currentTime + index * 0.08); osc.stop(audioCtx.currentTime + index * 0.08 + 0.35); }); } else if (type === 'wrong') { // Soft error buzz const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(150, audioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.25); gain.gain.setValueAtTime(0.1, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.25); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.25); } } function selectDifficulty(diff) { selectedDifficulty = diff; document.querySelectorAll(".diff-chip").forEach(chip => { chip.classList.toggle("active", chip.getAttribute("data-diff") === diff); }); } function onLanguageChange(lang) { applyQuizTranslations(lang); } function applyQuizTranslations(lang) { const t = UI_TEXT[lang] || UI_TEXT.de; const pageTitle = document.getElementById("pageTitle"); const pageSubtitle = document.getElementById("pageSubtitle"); const envelopeLetter = document.getElementById("envelopeLetter"); const introDesc = document.getElementById("introDesc"); const diffLabel = document.getElementById("diffLabel"); const diffAllText = document.getElementById("diffAllText"); const diffEasyText = document.getElementById("diffEasyText"); const diffMedText = document.getElementById("diffMedText"); const diffHardText = document.getElementById("diffHardText"); const startBtn = document.getElementById("startBtn"); const checkBtn = document.getElementById("checkBtn"); const hintBtn = document.getElementById("hintBtn"); const hintBtnMC = document.getElementById("hintBtnMC"); const hintHeading = document.getElementById("hintHeading"); const answerInput = document.getElementById("answerInput"); if (pageTitle) pageTitle.innerText = t.pageTitle; if (pageSubtitle) pageSubtitle.innerText = t.pageSubtitle; if (envelopeLetter) envelopeLetter.innerText = t.envelopeLetter; if (introDesc) introDesc.innerHTML = t.introDesc; if (diffLabel) diffLabel.innerText = t.diffLabel; if (diffAllText) diffAllText.innerText = t.diffAll; if (diffEasyText) diffEasyText.innerText = t.diffEasy; if (diffMedText) diffMedText.innerText = t.diffMed; if (diffHardText) diffHardText.innerText = t.diffHard; if (startBtn && !startBtn.disabled) startBtn.innerText = t.startBtn; if (checkBtn) checkBtn.innerText = t.checkBtn; if (hintBtn) hintBtn.innerText = t.hintBtn; if (hintBtnMC) hintBtnMC.innerText = t.hintBtn; if (hintHeading) hintHeading.innerText = t.hintHeading; if (answerInput) answerInput.placeholder = t.placeholder; if (lastQuestionData) { renderQuestion(lastQuestionData); } } let sessionQuestionIds = []; function getRecentQuestionIds() { try { const stored = localStorage.getItem("quiz_recent_qids"); return stored ? JSON.parse(stored) : []; } catch(e) { return []; } } function getWrongQuestionIds() { try { const stored = localStorage.getItem("quiz_wrong_qids"); return stored ? JSON.parse(stored) : []; } catch(e) { return []; } } function recordQuestionWrong(qid) { if (!qid) return; try { const wrong = getWrongQuestionIds(); if (!wrong.includes(qid)) { wrong.push(qid); if (wrong.length > 30) wrong.shift(); localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong)); } } catch(e) {} } function recordQuestionMastered(qid) { if (!qid) return; try { let wrong = getWrongQuestionIds(); if (wrong.includes(qid)) { wrong = wrong.filter(id => id !== qid); localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong)); } } catch(e) {} } function saveRecentQuestions(ids) { if (!ids || ids.length === 0) return; try { let recent = getRecentQuestionIds(); recent = Array.from(new Set([...ids, ...recent])).slice(0, 20); localStorage.setItem("quiz_recent_qids", JSON.stringify(recent)); } catch(e) {} } async function startGame() { const envelope = document.getElementById("envelope"); const startBtn = document.getElementById("startBtn"); const t = UI_TEXT[currentLang] || UI_TEXT.de; startBtn.disabled = true; startBtn.innerText = t.startBtnCasting; envelope.classList.add("envelope-opened"); sessionQuestionIds = []; // Start background music if enabled or first click const bgm = document.getElementById("bgm"); if (bgm && bgm.paused) { bgm.play().then(() => { const icon = document.getElementById("audioIcon"); const text = document.getElementById("audioText"); if (icon && text) { icon.innerText = "🔊"; text.innerText = (currentLang === "de") ? "Musik: An" : "Music: On"; } localStorage.setItem("bgm_enabled", "true"); }).catch(() => {}); } try { const recentIds = getRecentQuestionIds(); const wrongIds = getWrongQuestionIds(); const res = await fetch("/api/game/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ lang: currentLang, difficulty: selectedDifficulty, recent_ids: recentIds, wrong_ids: wrongIds }) }); if (!res.ok) { throw new Error(currentLang === "en" ? "Could not start game session." : "Konnte Spiel nicht starten."); } const data = await res.json(); currentSessionId = data.session_id; currentQuestionNumber = data.question_number; totalQuestions = data.total_questions; currentScore = data.score; attemptsUsed = data.attempts_used; setTimeout(() => { document.getElementById("introCard").classList.add("hidden"); document.getElementById("gameCard").classList.remove("hidden"); startTimer(); renderQuestion(data); }, 1100); } catch (err) { alert(t.startError + err.message); startBtn.disabled = false; startBtn.innerText = t.startBtn; envelope.classList.remove("envelope-opened"); } } function startTimer() { timerStartTime = Date.now(); elapsedSeconds = 0; if (timerInterval) clearInterval(timerInterval); timerInterval = setInterval(updateTimer, 1000); updateTimer(); } function updateTimer() { if (!timerStartTime) return; elapsedSeconds = Math.floor((Date.now() - timerStartTime) / 1000); const m = Math.floor(elapsedSeconds / 60); const s = elapsedSeconds % 60; const badge = document.getElementById("timerBadge"); if (badge) { badge.innerText = `⏱️ ${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } } function renderQuestion(qData) { lastQuestionData = qData; if (qData.id && !sessionQuestionIds.includes(qData.id)) { sessionQuestionIds.push(qData.id); } const t = UI_TEXT[currentLang] || UI_TEXT.de; document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`; document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`; document.getElementById("badgeCategory").innerText = `⚡ ${qData.category.replace("_", " ").toUpperCase()}`; // Difficulty badge const badgeDiff = document.getElementById("badgeDifficulty"); if (badgeDiff) { const diff = qData.difficulty || "medium"; badgeDiff.className = `badge badge-diff badge-diff-${diff}`; let diffText = t.diffMed; let diffIcon = "🟡"; if (diff === "easy") { diffText = t.diffEasy; diffIcon = "🟢"; } else if (diff === "hard") { diffText = t.diffHard; diffIcon = "🔴"; } badgeDiff.innerText = `${diffIcon} ${diffText}`; } document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${attemptsUsed + 1} ${t.ofLabel} ${maxAttempts}`; document.getElementById("frageText").innerText = qData.question; const textGroup = document.getElementById("textAnswerGroup"); const choiceGroup = document.getElementById("choiceAnswerGroup"); const choiceContainer = document.getElementById("choiceOptionsContainer"); const input = document.getElementById("answerInput"); document.getElementById("hintBox").classList.add("hidden"); document.getElementById("feedbackBox").innerHTML = ""; if (qData.question_type === "multiple_choice" && qData.options && qData.options.length > 0) { textGroup.classList.add("hidden"); choiceGroup.classList.remove("hidden"); choiceContainer.innerHTML = ""; const letters = ["A", "B", "C", "D", "E", "F"]; qData.options.forEach((opt, idx) => { const btn = document.createElement("button"); btn.className = "choice-btn"; btn.type = "button"; btn.innerHTML = `${letters[idx] || "•"} ${opt}`; btn.onclick = () => submitAnswer(opt, btn); choiceContainer.appendChild(btn); }); } else { choiceGroup.classList.add("hidden"); textGroup.classList.remove("hidden"); input.value = ""; input.placeholder = t.placeholder; input.disabled = false; document.getElementById("checkBtn").disabled = false; input.focus(); } const hintBtn = document.getElementById("hintBtn"); if (hintBtn) hintBtn.disabled = false; const hintBtnMC = document.getElementById("hintBtnMC"); if (hintBtnMC) hintBtnMC.disabled = false; updateProgress(); } function updateProgress() { const percent = ((currentQuestionNumber - 1) / totalQuestions) * 100; document.getElementById("progressBar").style.width = percent + "%"; } function checkAnswer() { const input = document.getElementById("answerInput"); const answer = input.value.trim(); if (!answer) { input.focus(); return; } submitAnswer(answer); } async function submitAnswer(answer, clickedBtn = null) { const checkBtn = document.getElementById("checkBtn"); const input = document.getElementById("answerInput"); const t = UI_TEXT[currentLang] || UI_TEXT.de; if (checkBtn) checkBtn.disabled = true; if (input) input.disabled = true; // Disable all choice buttons const choiceBtns = document.querySelectorAll(".choice-btn"); choiceBtns.forEach(b => b.disabled = true); if (clickedBtn) { clickedBtn.classList.add("choice-selected"); } try { const res = await fetch("/api/game/check", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: currentSessionId, answer: answer }) }); if (!res.ok) { throw new Error(currentLang === "en" ? "Answer check failed." : "Fehler bei der Antwortprüfung."); } const result = await res.json(); currentScore = result.score; document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`; const frageBox = document.getElementById("frageBox"); const feedbackBox = document.getElementById("feedbackBox"); if (result.correct) { // Success! playSound('magic'); if (clickedBtn) { clickedBtn.classList.remove("choice-selected"); clickedBtn.classList.add("choice-correct"); } frageBox.classList.add("sparkle-effect"); // Mark question as mastered in history if (result.question_id) { recordQuestionMastered(result.question_id); } const pts = result.points_earned || 100; const ptsText = (currentLang === "en") ? `✨ Correct! +${pts} House Points! ✨` : `✨ Richtig! +${pts} Hauspunkte für dein Haus! ✨`; feedbackBox.innerHTML = `${ptsText}`; setTimeout(() => { frageBox.classList.remove("sparkle-effect"); if (result.is_finished) { finishGame(); } else if (result.next_question) { currentQuestionNumber = result.next_question.question_number; attemptsUsed = 0; renderQuestion(result.next_question); } }, 1200); } else { // Incorrect playSound('wrong'); if (clickedBtn) { clickedBtn.classList.remove("choice-selected"); clickedBtn.classList.add("choice-wrong"); } frageBox.classList.add("shake-effect"); setTimeout(() => frageBox.classList.remove("shake-effect"), 500); attemptsUsed = result.attempts_used; document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`; if (result.attempts_left === 0) { // Exhausted all attempts -> record for spaced repetition learning if (result.question_id) { recordQuestionWrong(result.question_id); } feedbackBox.innerHTML = `
${t.exhaustedWrong}
${t.solutionLabel} ${result.revealed_answer}
`; setTimeout(() => { if (result.is_finished) { finishGame(); } else if (result.next_question) { currentQuestionNumber = result.next_question.question_number; attemptsUsed = 0; renderQuestion(result.next_question); } }, 2200); } else { // Still attempts remaining const msg = t.wrongFeedback.replace("{attempts}", result.attempts_left); feedbackBox.innerHTML = `${msg}`; if (checkBtn) checkBtn.disabled = false; if (input) { input.disabled = false; input.select(); } // Re-enable choice buttons choiceBtns.forEach(b => b.disabled = false); } } } catch (err) { alert(t.networkError + err.message); if (checkBtn) checkBtn.disabled = false; if (input) input.disabled = false; choiceBtns.forEach(b => b.disabled = false); } } async function requestHint() { if (!currentSessionId) return; const hintBox = document.getElementById("hintBox"); const hintText = document.getElementById("hintText"); const t = UI_TEXT[currentLang] || UI_TEXT.de; try { const res = await fetch(`/api/game/hint?session_id=${encodeURIComponent(currentSessionId)}`); const data = await res.json(); hintText.innerText = data.hint || t.noHint; hintBox.classList.remove("hidden"); } catch (err) { hintText.innerText = t.hintFailed; hintBox.classList.remove("hidden"); } } function finishGame() { if (timerInterval) clearInterval(timerInterval); saveRecentQuestions(sessionQuestionIds); document.getElementById("progressBar").style.width = "100%"; window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}&diff=${encodeURIComponent(selectedDifficulty)}`; } // Enter key to submit document.addEventListener("DOMContentLoaded", () => { const input = document.getElementById("answerInput"); if (input) { input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); checkAnswer(); } }); } applyQuizTranslations(currentLang); });