/* TechTacular – IT-Quizduell im Jeopardy-Stil. Board kommt aus /api/techtacular/board (ohne Lösung), Prüfung + Score serverseitig. */ (() => { 'use strict'; const GAME = 'techtacular'; const boardEl = document.getElementById('tt-board'); const newBtn = document.getElementById('tt-new'); const finishBtn = document.getElementById('tt-finish'); const cancelBtn = document.getElementById('tt-cancel'); const scoreEl = document.getElementById('tt-score'); const answeredEl= document.getElementById('tt-answered'); const timeEl = document.getElementById('tt-time'); const bestEl = document.getElementById('tt-best'); const qOverlay = document.getElementById('tt-question'); const qCat = document.getElementById('tt-q-cat'); const qPoints = document.getElementById('tt-q-points'); const qText = document.getElementById('tt-q-text'); const qAnswers = document.getElementById('tt-q-answers'); const qFeedback = document.getElementById('tt-q-feedback'); const qNext = document.getElementById('tt-q-next'); const resultEl = document.getElementById('tt-result'); const resultScore = document.getElementById('tt-result-score'); const resultMax = document.getElementById('tt-result-max'); const resultDetail= document.getElementById('tt-result-detail'); const resultHigh = document.getElementById('tt-result-high'); const againBtn = document.getElementById('tt-again'); let board = null; let previousIds = []; // Frage-IDs der Vorrunde -> werden im nächsten Board vermieden let tiles = []; // Tile-Zustände {data, categoryTitle, answered, correct, el} let answeredCount = 0; let score = 0; let seconds = 0; let timerId = null; let running = false; let current = null; // gerade offene Kachel const submitted = []; // {questionId, answerId} für /finish function formatTime(s) { return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; } function startTimer() { if (timerId) return; timerId = setInterval(() => { seconds++; timeEl.textContent = formatTime(seconds); }, 1000); } function stopTimer() { clearInterval(timerId); timerId = null; } function setIdle(message) { boardEl.style.gridTemplateColumns = ''; boardEl.innerHTML = `
${message}
`; } function updateHud() { scoreEl.textContent = String(score); answeredEl.textContent = `${answeredCount} / ${tiles.length}`; } function renderBoard() { boardEl.innerHTML = ''; boardEl.style.gridTemplateColumns = `repeat(${board.categories.length}, 1fr)`; tiles = []; board.categories.forEach((category) => { const col = document.createElement('div'); col.className = 'tt-col'; const title = document.createElement('div'); title.className = 'tt-cat-title'; title.textContent = category.title; col.appendChild(title); category.tiles.forEach((tileData) => { const tileEl = document.createElement('button'); tileEl.type = 'button'; tileEl.className = 'tt-tile'; tileEl.textContent = String(tileData.points); const state = { data: tileData, categoryTitle: category.title, answered: false, correct: false, el: tileEl }; tileEl.addEventListener('click', () => openTile(state)); tiles.push(state); col.appendChild(tileEl); }); boardEl.appendChild(col); }); } function openTile(state) { if (!running || state.answered || current) return; current = state; qCat.textContent = state.categoryTitle; qPoints.textContent = String(state.data.points); qText.textContent = state.data.text; qFeedback.textContent = ''; qFeedback.className = 'tt-q-feedback'; qNext.hidden = true; qAnswers.innerHTML = ''; state.data.answers.forEach((answer) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'tt-answer'; btn.textContent = `Was ist ${answer.text}?`; btn.addEventListener('click', () => chooseAnswer(state, answer.id, btn)); qAnswers.appendChild(btn); }); qOverlay.hidden = false; } async function chooseAnswer(state, answerId, btn) { if (state.answered) return; state.answered = true; // sofort sperren (eine Antwort pro Frage) const buttons = Array.from(qAnswers.querySelectorAll('.tt-answer')); buttons.forEach((b) => { b.disabled = true; }); submitted.push({ questionId: state.data.questionId, answerId }); try { const res = await fetch('/api/techtacular/answer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ questionId: state.data.questionId, answerId }) }); const data = await res.json(); state.correct = data.correct; // richtige Antwort grün, eigene falsche rot markieren state.data.answers.forEach((answer, i) => { if (answer.id === data.correctAnswerId) buttons[i].classList.add('correct'); }); if (!data.correct) btn.classList.add('wrong'); const correctTerm = (state.data.answers.find((a) => a.id === data.correctAnswerId) || {}).text || ''; if (data.correct) { score += data.points; qFeedback.textContent = `Richtig! +${data.points}. ${data.explanation || ''}`.trim(); qFeedback.className = 'tt-q-feedback correct'; } else { qFeedback.textContent = `Falsch – richtig: „Was ist ${correctTerm}?“. ${data.explanation || ''}`.trim(); qFeedback.className = 'tt-q-feedback wrong'; } updateHud(); } catch (err) { qFeedback.textContent = 'Antwort konnte nicht geprüft werden.'; qFeedback.className = 'tt-q-feedback wrong'; console.error(err); } qNext.hidden = false; } function closeQuestion() { if (!current) return; const state = current; current = null; qOverlay.hidden = true; state.el.classList.add('answered', state.correct ? 'correct' : 'wrong'); state.el.textContent = state.correct ? `✓ ${state.data.points}` : '✗'; answeredCount++; updateHud(); if (answeredCount === tiles.length) { finishGame(); } } async function finishGame() { if (!running) return; running = false; stopTimer(); finishBtn.hidden = true; cancelBtn.hidden = true; try { const res = await fetch('/api/techtacular/finish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ durationSeconds: seconds, answers: submitted }) }); const data = await res.json(); resultScore.textContent = String(data.score); resultMax.textContent = String(data.maxScore); resultDetail.textContent = `${data.correctCount} von ${data.totalCount} Fragen richtig · ${formatTime(seconds)}`; resultHigh.hidden = !data.newHighscore; bestEl.textContent = String(data.highscore); resultEl.hidden = false; } catch (err) { console.error('Runde konnte nicht gespeichert werden:', err); } } function cancelGame() { if (!running) return; if (answeredCount > 0 && !window.confirm( 'Aktuelle Runde wirklich abbrechen? Sie wird nicht gespeichert.')) { return; } running = false; current = null; stopTimer(); qOverlay.hidden = true; finishBtn.hidden = true; cancelBtn.hidden = true; setIdle('Runde abgebrochen – klicke „Neues Spiel“ für ein frisches Board.'); } async function loadStats() { try { const res = await fetch(`/api/games/${GAME}/stats`); if (!res.ok) return; const stats = await res.json(); bestEl.textContent = stats.highscore > 0 ? String(stats.highscore) : '–'; } catch (err) { /* egal */ } } async function newGame() { stopTimer(); seconds = 0; score = 0; answeredCount = 0; current = null; submitted.length = 0; timeEl.textContent = '0:00'; resultEl.hidden = true; qOverlay.hidden = true; const excludeParam = previousIds.length ? ('?exclude=' + previousIds.join(',')) : ''; try { const res = await fetch('/api/techtacular/board' + excludeParam); if (!res.ok) throw new Error(`HTTP ${res.status}`); board = await res.json(); } catch (err) { console.error('Board konnte nicht geladen werden:', err); setIdle('Board konnte nicht geladen werden.'); return; } if (!board.categories || !board.categories.length) { setIdle('Noch keine Kategorien vorhanden. Ein Admin kann sie unter „TechTacular verwalten“ anlegen.'); return; } renderBoard(); previousIds = board.categories.flatMap((c) => c.tiles.map((t) => t.questionId)); updateHud(); running = true; finishBtn.hidden = false; cancelBtn.hidden = false; startTimer(); } newBtn.addEventListener('click', newGame); againBtn.addEventListener('click', newGame); finishBtn.addEventListener('click', finishGame); cancelBtn.addEventListener('click', cancelGame); qNext.addEventListener('click', closeQuestion); loadStats(); newGame(); })();