468 lines
18 KiB
JavaScript
Executable File
468 lines
18 KiB
JavaScript
Executable File
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.<br>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.<br>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);
|
|
}
|
|
}
|
|
|
|
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");
|
|
|
|
// 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 res = await fetch("/api/game/start", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
lang: currentLang,
|
|
difficulty: selectedDifficulty
|
|
})
|
|
});
|
|
|
|
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;
|
|
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 = `<span class="choice-letter">${letters[idx] || "•"}</span> <span class="choice-text">${opt}</span>`;
|
|
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");
|
|
feedbackBox.innerHTML = `<span class="feedback-correct">${t.correctFeedback}</span>`;
|
|
|
|
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
|
|
feedbackBox.innerHTML = `
|
|
<div class="feedback-wrong">${t.exhaustedWrong}</div>
|
|
<div class="feedback-revealed">${t.solutionLabel} <strong>${result.revealed_answer}</strong></div>
|
|
`;
|
|
|
|
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 = `<span class="feedback-wrong">${msg}</span>`;
|
|
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);
|
|
document.getElementById("progressBar").style.width = "100%";
|
|
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}`;
|
|
}
|
|
|
|
// 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);
|
|
});
|