(() => { const startBtn = document.getElementById('startBtn'); const timeEl = document.getElementById('time'); const progressEl = document.getElementById('progress'); const problemEl = document.getElementById('problem'); const optionsEl = document.getElementById('options'); const gameEl = document.getElementById('game'); const statusEl = document.getElementById('status'); const resultEl = document.getElementById('result'); const fxLayer = document.getElementById('fxLayer'); const btnColorClasses = ['c-yellow','c-orange','c-pink','c-purple','c-blue','c-teal']; const praise = ['Отлично! ⭐','Молодец! 🎉','Супер! 🌟','Здорово! 🥳','Так держать! 👍']; // decorative assets removed let sessionId = null; let totalQuestions = 20; let remaining = 60; let countdown = null; let inFlight = false; function setStatus(msg) { statusEl.textContent = msg || ''; } function showGame(show) { gameEl.classList.toggle('hidden', !show); } function showResult(score) { const { correct, total_answered, total } = score; resultEl.innerHTML = `
Результат: ${correct} из ${total}
Отвечено: ${total_answered}
`; resultEl.classList.remove('hidden'); document.getElementById('againBtn')?.addEventListener('click', () => { resultEl.classList.add('hidden'); start(); }); } function renderQuestion(q) { if (!q) return; problemEl.textContent = `${q.a} × ${q.b}`; optionsEl.innerHTML = ''; const colors = shuffle(btnColorClasses).slice(0, q.options.length); q.options.forEach((value, idx) => { const btn = document.createElement('button'); btn.className = `option-btn ${colors[idx % colors.length]}`; btn.textContent = String(value); btn.addEventListener('click', () => answer(value, btn)); optionsEl.appendChild(btn); }); progressEl.textContent = `${q.index}/${totalQuestions}`; } function shuffle(arr) { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function stopTimer() { if (countdown) { clearInterval(countdown); countdown = null; } } function startTimer(seconds) { remaining = seconds; timeEl.textContent = String(remaining); stopTimer(); countdown = setInterval(() => { remaining -= 1; if (remaining <= 0) { remaining = 0; timeEl.textContent = '0'; stopTimer(); } else { timeEl.textContent = String(remaining); } }, 1000); } async function start() { if (inFlight) return; inFlight = true; setStatus(''); showGame(false); startBtn.disabled = true; resultEl.classList.add('hidden'); try { const res = await fetch('/api/session/start', { method: 'POST' }); if (!res.ok) throw new Error('Не удалось начать сессию'); const data = await res.json(); sessionId = data.session_id; totalQuestions = data.total_questions; startTimer(data.remaining_seconds); renderQuestion(data.question); showGame(true); setStatus('Отвечайте как можно быстрее!'); } catch (e) { console.error(e); setStatus('Ошибка старта. Попробуйте ещё раз.'); } finally { startBtn.disabled = false; inFlight = false; } } function celebrateFromElement(el) { if (!fxLayer) return; const rect = el.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; const count = 40; for (let i = 0; i < count; i++) { const p = document.createElement('span'); p.className = 'confetti'; // origin p.style.left = `${x}px`; p.style.top = `${y}px`; // random trajectory const angle = Math.random() * Math.PI * 2; const distance = 60 + Math.random() * 80; const tx = Math.cos(angle) * distance; const ty = Math.sin(angle) * distance + 40 + Math.random() * 60; // gravity pull const rot = (Math.random() * 720 - 360).toFixed(0) + 'deg'; p.style.setProperty('--tx', `${tx.toFixed(0)}px`); p.style.setProperty('--ty', `${ty.toFixed(0)}px`); p.style.setProperty('--rot', rot); const hue = Math.floor(Math.random() * 360); p.style.backgroundColor = `hsl(${hue} 90% 55%)`; p.style.animation = `confetti ${650 + Math.random() * 400}ms ease-out forwards`; fxLayer.appendChild(p); setTimeout(() => p.remove(), 1200); } } function celebrateFullscreen() { if (!fxLayer) return; // quick flash overlay const flash = document.createElement('div'); flash.className = 'flash'; fxLayer.appendChild(flash); setTimeout(() => flash.remove(), 550); const vw = window.innerWidth; const vh = window.innerHeight; const count = Math.min(220, Math.floor((vw * vh) / 7000)); // scale with viewport for (let i = 0; i < count; i++) { const p = document.createElement('span'); p.className = 'confetti'; // random start across the screen const sx = Math.random() * vw; const sy = Math.random() * vh * 0.6 + vh * 0.1; // avoid extreme edges p.style.left = `${sx}px`; p.style.top = `${sy}px`; // random trajectory const angle = Math.random() * Math.PI * 2; const distance = 180 + Math.random() * 520; const tx = Math.cos(angle) * distance; const ty = Math.sin(angle) * distance + 80; // slight gravity bias const rot = (Math.random() * 1080 - 540).toFixed(0) + 'deg'; p.style.setProperty('--tx', `${tx.toFixed(0)}px`); p.style.setProperty('--ty', `${ty.toFixed(0)}px`); p.style.setProperty('--rot', rot); const hue = Math.floor(Math.random() * 360); p.style.backgroundColor = `hsl(${hue} 90% 55%)`; const dur = 1000 + Math.random() * 900; p.style.animation = `confetti ${dur}ms ease-out forwards`; fxLayer.appendChild(p); setTimeout(() => p.remove(), dur + 200); } } async function answer(value, clickedBtn) { if (!sessionId || inFlight) return; inFlight = true; // Disable all buttons to prevent double clicks const buttons = Array.from(optionsEl.querySelectorAll('button')); buttons.forEach(b => b.disabled = true); try { const res = await fetch(`/api/session/${sessionId}/answer`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ answer: value }), }); if (!res.ok) throw new Error('Не удалось отправить ответ'); const data = await res.json(); // feedback if (data.correct) { clickedBtn.classList.add('correct'); celebrateFullscreen(); setStatus(praise[Math.floor(Math.random() * praise.length)]); } else { clickedBtn.classList.add('wrong'); } // Small pause to show feedback await new Promise(r => setTimeout(r, data.correct ? 350 : 250)); if (data.finished) { stopTimer(); showGame(false); const reason = data.reason === 'timeout' ? 'Время вышло.' : 'Завершено!'; setStatus(reason); showResult(data.score); } else { timeEl.textContent = String(data.remaining_seconds); renderQuestion(data.question); } } catch (e) { console.error(e); setStatus('Ошибка ответа. Перезапустите сессию.'); } finally { inFlight = false; } } startBtn.addEventListener('click', start); // no decor positioning })();