/** * minigames/flappy.js * 🐦 Flappy Bird — Klicke oder Leertaste, um durch die Röhren zu fliegen! */ window.MG_flappy = (function() { const ID = 'flappy'; const EMOJI = '🐦'; const NAME = 'Flappy Bird'; const DESC = 'Klick oder Leertaste, um zu fliegen. 3 Hindernisse schaffen = Sieg!'; const CONTROLS = 'Klick / Leertaste'; const MULTI = 1; function run(wrap, W, H, cfg, onDone) { const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); const theme = cfg.theme || { primary: '#06b6d4' }; const WIN_PIPES = cfg.winPipes || 3; const GRAV = 0.5, JUMP = -8, PW = 38, GAP = 85; let bird = { y: H / 2, vy: 0 }; let pipes = [{ x: W, gap: Math.random() * (H - 100) + 30 }]; let score = 0; let dead = false; let started = false; let stopped = false; let raf, endTimer; const jump = () => { if (!dead) { bird.vy = JUMP; started = true; } }; const onKey = e => { if (e.code === 'Space') { e.preventDefault(); jump(); } }; document.addEventListener('keydown', onKey); canvas.addEventListener('click', jump); // Touch canvas.addEventListener('touchstart', e => { e.preventDefault(); jump(); }, { passive: false }); function loop() { if (stopped) return; raf = requestAnimationFrame(loop); ctx.fillStyle = '#050508'; ctx.fillRect(0, 0, W, H); // Hintergrund-Gradient const grad = ctx.createLinearGradient(0, 0, 0, H); grad.addColorStop(0, '#0a0a1a'); grad.addColorStop(1, '#050508'); ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H); if (started && !dead) { bird.vy += GRAV; bird.y += bird.vy; pipes.forEach(p => p.x -= 2.8); if (pipes[pipes.length - 1].x < W - 170) pipes.push({ x: W, gap: Math.random() * (H - 100) + 30 }); pipes = pipes.filter(p => p.x > -PW); pipes.forEach(p => { if (p.x + PW < 50 && !p.passed) { p.passed = true; score++; } }); if (bird.y < 0 || bird.y + 20 > H) dead = true; pipes.forEach(p => { if (50 < p.x + PW && 70 > p.x && (bird.y < p.gap || bird.y + 20 > p.gap + GAP)) dead = true; }); if (dead && !endTimer) endTimer = setTimeout(() => onDone(score >= WIN_PIPES), 1000); if (score >= WIN_PIPES && !dead && !endTimer) endTimer = setTimeout(() => onDone(true), 400); } // Röhren zeichnen pipes.forEach(p => { // Röhren-Körper MGAPI.roundRect(ctx, p.x, 0, PW, p.gap - 8, 4, `${theme.primary}cc`, null); MGAPI.roundRect(ctx, p.x - 3, p.gap - 12, PW + 6, 12, 4, `${theme.primary}ee`, null); MGAPI.roundRect(ctx, p.x, p.gap + GAP + 8, PW, H - p.gap - GAP - 8, 4, `${theme.primary}cc`, null); MGAPI.roundRect(ctx, p.x - 3, p.gap + GAP, PW + 6, 12, 4, `${theme.primary}ee`, null); }); // Vogel ctx.shadowColor = dead ? '#ef4444' : theme.primary; ctx.shadowBlur = 12; ctx.fillStyle = dead ? '#ef4444' : theme.primary; ctx.beginPath(); ctx.ellipse(50 + 14, bird.y + 10, 14, 10, bird.vy * 0.04, 0, Math.PI * 2); ctx.fill(); // Auge ctx.shadowBlur = 0; ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(50 + 20, bird.y + 7, 4, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(50 + 21, bird.y + 7, 2, 0, Math.PI * 2); ctx.fill(); // HUD MGAPI.text(ctx, `🐦 ${score} / ${WIN_PIPES}`, 8, 14, { align: 'left', size: 13, color: theme.primary }); if (!started) { MGAPI.text(ctx, 'Klicken oder Leertaste', W / 2, H / 2 + 40, { size: 13, color: 'rgba(255,255,255,0.7)' }); } if (dead) { MGAPI.resultScreen(ctx, W, H, score >= WIN_PIPES, score >= WIN_PIPES ? `${score} Hindernisse geschafft!` : `Nur ${score} — flieg weiter!`); } } raf = requestAnimationFrame(loop); return { stop() { stopped = true; cancelAnimationFrame(raf); clearTimeout(endTimer); document.removeEventListener('keydown', onKey); canvas.removeEventListener('click', jump); }, }; } 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, winPipes: 3 }, won => MGAPI.onResult(won)); } return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; })();