- editor.html: 5-step game editor with live Python code split-screen - game.html: playable board game engine - codegen.js: Python live-code generator (extracted from editor) - logo.png: PH Weingarten logo (extracted from Base64) - minigames/_api.js: shared Mini-Game API (makeCanvas, onResult, helpers) - 12 Mini-Games as individual modules (launch + preview API): snake, flappy, memory, quiz, reaction, basketball, catch, maze, simon, typing, puzzle, spotdiff - README.md with architecture docs and deployment guide Refactoring highlights: - editor.html: 251 KB → 102 KB (-59%) - Mini-games fully decoupled, each ~100-200 lines - All 12 games now have working launch() + preview() - Maze uses recursive backtracker algorithm - spotdiff uses canvas-drawn scene with 5 differences
129 lines
4.5 KiB
JavaScript
129 lines
4.5 KiB
JavaScript
/**
|
|
* 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 };
|
|
|
|
})();
|