/** * minigames/reaction.js * ⚡ Reaktionstest — Drück den Knopf so schnell wie möglich! */ window.MG_reaction = (function() { const ID = 'reaction'; const EMOJI = '⚡'; const NAME = 'Reaktionstest'; const DESC = 'Wenn der Kreis GRÜN wird — so schnell wie möglich klicken!'; const CONTROLS = 'Klick / Leertaste'; const MULTI = 3; function run(wrap, W, H, cfg, onDone) { const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); const theme = cfg.theme || { primary: '#ef4444' }; const ROUNDS = 3; const WIN_MS = 600; let phase = 'wait'; let waitEnd = 0; let reStart = 0; let times = []; let best = Infinity; let stopped = false; let raf, endTimer; function nextWait() { phase = 'wait'; waitEnd = performance.now() + (2 + Math.random() * 3) * 1000; } nextWait(); const react = () => { if (phase === 'ready') { const t = performance.now() - reStart; times.push(t); best = Math.min(best, t); if (times.length >= ROUNDS) { clearTimeout(endTimer); endTimer = setTimeout(() => onDone(best < WIN_MS), 800); phase = 'done'; } else { nextWait(); } } else if (phase === 'wait') { phase = 'toosoon'; setTimeout(nextWait, 1000); } }; const onKey = e => { if (e.code === 'Space') { e.preventDefault(); react(); } }; document.addEventListener('keydown', onKey); canvas.addEventListener('click', react); function loop(ts) { if (stopped) return; raf = requestAnimationFrame(loop); if (phase === 'wait' && ts > waitEnd) { phase = 'ready'; reStart = performance.now(); } // Hintergrund const bg = phase === 'ready' ? '#14532d' : phase === 'toosoon' ? '#7f1d1d' : phase === 'done' ? '#0f0e17' : '#050508'; ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H); const cx = W / 2, cy = H / 2; if (phase === 'wait') { // Pulsierender Kreis (rot = warten) const pulse = 0.85 + 0.15 * Math.sin(ts * 0.003); ctx.beginPath(); ctx.arc(cx, cy, 60 * pulse, 0, Math.PI * 2); ctx.fillStyle = 'rgba(239,68,68,0.2)'; ctx.fill(); ctx.strokeStyle = '#ef444488'; ctx.lineWidth = 3; ctx.stroke(); MGAPI.text(ctx, '⏳', cx, cy - 6, { size: 36 }); MGAPI.text(ctx, 'Warte...', cx, cy + 44, { size: 14, color: '#a7a3c2' }); MGAPI.text(ctx, `${times.length} / ${ROUNDS} Runden`, cx, H - 20, { size: 12, color: '#64748b' }); } if (phase === 'ready') { // Leuchtender grüner Kreis ctx.shadowColor = '#22c55e'; ctx.shadowBlur = 40; ctx.beginPath(); ctx.arc(cx, cy, 65, 0, Math.PI * 2); ctx.fillStyle = 'rgba(22,163,74,0.4)'; ctx.fill(); ctx.strokeStyle = '#22c55e'; ctx.lineWidth = 4; ctx.stroke(); ctx.shadowBlur = 0; MGAPI.text(ctx, '⚡', cx, cy - 8, { size: 44 }); MGAPI.text(ctx, 'JETZT!', cx, cy + 48, { size: 22, family: "'Fredoka One',cursive", color: '#fff', shadow: '#22c55e', shadowBlur: 15 }); MGAPI.text(ctx, 'Klick oder Leertaste!', cx, cy + 78, { size: 13, color: 'rgba(255,255,255,0.6)' }); } if (phase === 'toosoon') { MGAPI.text(ctx, '😅', cx, cy - 10, { size: 48 }); MGAPI.text(ctx, 'Zu früh!', cx, cy + 46, { size: 22, family: "'Fredoka One',cursive", color: '#fca5a5' }); } if (phase === 'done') { MGAPI.resultScreen(ctx, W, H, best < WIN_MS, best < WIN_MS ? `${Math.round(best)} ms — super schnell!` : `${Math.round(best)} ms — noch etwas langsam`); } // Statistik if (times.length > 0 && phase !== 'done') { const last = times[times.length - 1]; MGAPI.text(ctx, `Letzte: ${Math.round(last)} ms | Beste: ${Math.round(best)} ms`, cx, H - 14, { size: 11, color: 'rgba(255,255,255,0.4)' }); } } raf = requestAnimationFrame(loop); return { stop() { stopped = true; cancelAnimationFrame(raf); clearTimeout(endTimer); document.removeEventListener('keydown', onKey); canvas.removeEventListener('click', react); }, }; } function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; })();