Files
quiz/app/static/js/quiz.js
T

607 lines
21 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
let currentSessionId = null;
let currentQuestionNumber = 1;
let totalQuestions = 10;
let currentScore = 0;
let currentStreak = 0;
let attemptsUsed = 0;
const maxAttempts = 3;
let selectedDifficulty = "all";
let selectedCategory = "all";
let lastQuestionData = null;
let timerStartTime = null;
let timerInterval = null;
let elapsedSeconds = 0;
const CATEGORY_LABELS = {
de: {
all: "✨ Alle Kategorien",
books: "📖 Buchwissen",
movies: "🎬 Filmwissen",
creatures: "🐉 Magische Geschöpfe",
spells: "🪄 Zaubersprüche & Tränke",
characters: "🧙‍♂️ Charaktere",
hogwarts: "🏰 Hogwarts & Orte",
quidditch: "🧹 Quidditch",
lore: "🔮 Artefakte & Lore",
general: "⚡ Allgemein",
harry_potter: "⚡ Harry Potter"
},
en: {
all: "✨ All Categories",
books: "📖 Book Lore",
movies: "🎬 Movie Trivia",
creatures: "🐉 Magical Creatures",
spells: "🪄 Spells & Potions",
characters: "🧙‍♂️ Characters",
hogwarts: "🏰 Hogwarts & Places",
quidditch: "🧹 Quidditch",
lore: "🔮 Artifacts & Lore",
general: "⚡ General",
harry_potter: "⚡ Harry Potter"
}
};
const UI_TEXT = {
de: {
pageTitle: "Harry Potter Wissensquiz",
pageSubtitle: "Teste dein Wissen über Bücher, Filme, Geschöpfe und Zaubersprüche – direkt & ohne Umwege.",
diffLabel: "Schwierigkeitsgrad",
diffAll: "Alle Stufen",
diffEasy: "Leicht",
diffMed: "Mittel",
diffHard: "Schwer (Experte)",
catLabel: "Themengebiet wählen",
catAll: "Alle Kategorien",
catBooks: "Buchwissen",
catMovies: "Filmwissen",
catCreatures: "Magische Geschöpfe",
catSpells: "Zaubersprüche & Tränke",
catChars: "Charaktere",
catHogwarts: "Hogwarts & Orte",
catQuidditch: "Quidditch",
catLore: "Artefakte & Lore",
dashboardRules: "<span>🎯 10 Fragen pro Runde</span><span>·</span><span>🔥 Streak-Zähler</span><span>·</span><span>💡 Bis zu 3 Versuche je Frage</span>",
startQuizBtn: "⚡ Quiz jetzt starten ➔",
startBtnCasting: "⚡ Runde wird geladen...",
pointsLabel: "Pkt.",
questionLabel: "Frage",
ofLabel: "/",
attemptLabel: "Versuch",
checkBtn: "✓ Antwort prüfen",
hintBtn: "💡 Tipp anzeigen",
hintHeading: "💡 Hinweis:",
placeholder: "Deine Antwort eingeben...",
noHint: "Kein Tipp vorhanden.",
hintFailed: "Tipp konnte nicht geladen werden.",
wrongFeedback: "Nicht ganz! Noch {attempts} Versuch(e).",
exhaustedWrong: "Leider nicht gelöst (3 Fehlversuche).",
solutionLabel: "Richtige Antwort:",
startError: "Fehler beim Starten des Quiz: ",
networkError: "Netzwerkfehler: "
},
en: {
pageTitle: "Harry Potter Trivia Quiz",
pageSubtitle: "Test your knowledge across books, movies, creatures, and spells — straight to the action.",
diffLabel: "Difficulty Level",
diffAll: "All Levels",
diffEasy: "Easy",
diffMed: "Medium",
diffHard: "Hard (Expert)",
catLabel: "Select Topic",
catAll: "All Categories",
catBooks: "Book Lore",
catMovies: "Movie Trivia",
catCreatures: "Magical Creatures",
catSpells: "Spells & Potions",
catChars: "Characters",
catHogwarts: "Hogwarts & Places",
catQuidditch: "Quidditch",
catLore: "Artifacts & Lore",
dashboardRules: "<span>🎯 10 Questions per Round</span><span>·</span><span>🔥 Answer Streak</span><span>·</span><span>💡 Up to 3 Attempts</span>",
startQuizBtn: "⚡ Start Quiz Now ➔",
startBtnCasting: "⚡ Loading Round...",
pointsLabel: "Pts",
questionLabel: "Question",
ofLabel: "/",
attemptLabel: "Attempt",
checkBtn: "✓ Check Answer",
hintBtn: "💡 Show Hint",
hintHeading: "💡 Hint:",
placeholder: "Type your answer...",
noHint: "No hint available.",
hintFailed: "Could not load hint.",
wrongFeedback: "Not quite! {attempts} attempt(s) left.",
exhaustedWrong: "Out of attempts (3 failed tries).",
solutionLabel: "Correct answer:",
startError: "Error starting quiz: ",
networkError: "Network error: "
}
};
// Web Audio API Sound Generator for crisp trivia feedback
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) {
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
if (type === 'magic') {
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.12, audioCtx.currentTime + index * 0.07);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + index * 0.07 + 0.3);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(audioCtx.currentTime + index * 0.07);
osc.stop(audioCtx.currentTime + index * 0.07 + 0.3);
});
} else if (type === 'wrong') {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(180, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(95, audioCtx.currentTime + 0.22);
gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.22);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.22);
}
}
function selectDifficulty(diff) {
selectedDifficulty = diff;
document.querySelectorAll(".diff-chip").forEach(chip => {
chip.classList.toggle("active", chip.getAttribute("data-diff") === diff);
});
}
function selectCategory(cat) {
selectedCategory = cat;
document.querySelectorAll(".cat-chip").forEach(chip => {
chip.classList.toggle("active", chip.getAttribute("data-cat") === cat);
});
}
function updateStreakUI(animate = false) {
const badge = document.getElementById("streakBadge");
if (!badge) return;
badge.innerText = currentStreak >= 2 ? `🔥 ${currentStreak} Streak` : `🔥 ${currentStreak}`;
badge.classList.toggle("streak-hot", currentStreak >= 2);
if (animate) {
badge.classList.remove("streak-pop");
void badge.offsetWidth;
badge.classList.add("streak-pop");
}
}
function onLanguageChange(lang) {
applyQuizTranslations(lang);
}
function applyQuizTranslations(lang) {
const t = UI_TEXT[lang] || UI_TEXT.de;
const setTxt = (id, val) => {
const el = document.getElementById(id);
if (el && val) el.innerText = val;
};
setTxt("pageTitle", t.pageTitle);
setTxt("pageSubtitle", t.pageSubtitle);
setTxt("diffLabel", t.diffLabel);
setTxt("diffAllText", t.diffAll);
setTxt("diffEasyText", t.diffEasy);
setTxt("diffMedText", t.diffMed);
setTxt("diffHardText", t.diffHard);
setTxt("catLabel", t.catLabel);
setTxt("catAllText", t.catAll);
setTxt("catBooksText", t.catBooks);
setTxt("catMoviesText", t.catMovies);
setTxt("catCreaturesText", t.catCreatures);
setTxt("catSpellsText", t.catSpells);
setTxt("catCharsText", t.catChars);
setTxt("catHogwartsText", t.catHogwarts);
setTxt("catQuidditchText", t.catQuidditch);
setTxt("catLoreText", t.catLore);
const rulesEl = document.getElementById("dashboardRules");
if (rulesEl) rulesEl.innerHTML = t.dashboardRules;
const startQuizBtn = document.getElementById("startQuizBtn");
if (startQuizBtn && !startQuizBtn.disabled) startQuizBtn.innerText = t.startQuizBtn;
setTxt("checkBtn", t.checkBtn);
setTxt("hintBtn", t.hintBtn);
setTxt("hintBtnMC", t.hintBtn);
setTxt("hintHeading", t.hintHeading);
const answerInput = document.getElementById("answerInput");
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 startQuizBtn = document.getElementById("startQuizBtn");
const t = UI_TEXT[currentLang] || UI_TEXT.de;
if (startQuizBtn) {
startQuizBtn.disabled = true;
startQuizBtn.innerText = t.startBtnCasting;
}
playSound('magic');
sessionQuestionIds = [];
currentStreak = 0;
updateStreakUI(false);
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,
category: selectedCategory,
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;
const introCard = document.getElementById("introCard");
if (introCard) {
introCard.classList.add("hidden");
}
document.getElementById("gameCard").classList.remove("hidden");
startTimer();
renderQuestion(data);
} catch (err) {
alert(t.startError + err.message);
if (startQuizBtn) {
startQuizBtn.disabled = false;
startQuizBtn.innerText = t.startQuizBtn;
}
}
}
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;
const catMap = CATEGORY_LABELS[currentLang] || CATEGORY_LABELS.de;
document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`;
document.getElementById("pointsCounter").innerText = `🏆 ${currentScore} ${t.pointsLabel}`;
const rawCat = (qData.category || "general").toLowerCase();
document.getElementById("badgeCategory").innerText = catMap[rawCat] || `⚡ ${rawCat.toUpperCase()}`;
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 = currentLang === "en" ? "Hard" : "Schwer";
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;
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 = `🏆 ${currentScore} ${t.pointsLabel}`;
const frageBox = document.getElementById("frageBox");
const feedbackBox = document.getElementById("feedbackBox");
if (result.correct) {
playSound('magic');
currentStreak += 1;
updateStreakUI(true);
if (clickedBtn) {
clickedBtn.classList.remove("choice-selected");
clickedBtn.classList.add("choice-correct");
}
frageBox.classList.add("sparkle-effect");
if (result.question_id) {
recordQuestionMastered(result.question_id);
}
const pts = result.points_earned || 100;
const streakBonusText = currentStreak >= 3 ? ` · 🔥 ${currentStreak}er Serie!` : "";
const ptsText = (currentLang === "en")
? `✓ Correct! +${pts} Points${currentStreak >= 3 ? ` · 🔥 ${currentStreak} Streak!` : ""}`
: `✓ Richtig! +${pts} Punkte${streakBonusText}`;
feedbackBox.innerHTML = `<span class="feedback-correct">${ptsText}</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);
}
}, 1100);
} else {
playSound('wrong');
currentStreak = 0;
updateStreakUI(false);
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) {
if (result.question_id) {
recordQuestionWrong(result.question_id);
}
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 {
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();
}
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)}`;
}
document.addEventListener("DOMContentLoaded", () => {
const input = document.getElementById("answerInput");
if (input) {
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
checkAnswer();
}
});
}
applyQuizTranslations(currentLang);
});