edu-boardgame-generator/minigames/reaction.js

144 lines
4.6 KiB
JavaScript

/**
* 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 };
})();