edu-boardgame-generator/minigames/catch.js

142 lines
5.1 KiB
JavaScript

/**
* minigames/catch.js
* 🍎 Äpfel fangen — Bewege den Korb und fange fallende Früchte!
*/
window.MG_catch = (function() {
const ID = 'catch';
const EMOJI = '🍎';
const NAME = 'Äpfel fangen';
const DESC = 'Bewege den Korb mit ← → oder der Maus und fange 5 Äpfel!';
const CONTROLS = '← → / A D / Maus';
const MULTI = 3;
function run(wrap, W, H, cfg, onDone) {
const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H);
const theme = cfg.theme || { primary: '#22c55e' };
const WIN = cfg.winCatch || 20;
const FRUITS = ['🍎','🍊','🍋','🍇','🍓','🍑'];
const BASKET_W = 70, BASKET_H = 24;
let basket = { x: W / 2 - BASKET_W / 2 };
let items = [];
let score = 0;
let missed = 0;
let frame = 0;
let stopped = false;
let raf, endTimer, result = null;
const keys = {};
let mouseX = null;
const onKey = e => { keys[e.code] = e.type === 'keydown'; };
const onMouse = e => {
const rect = canvas.getBoundingClientRect();
mouseX = (e.clientX - rect.left) * (canvas.width / rect.width / (window.devicePixelRatio || 1)) - BASKET_W / 2;
};
const onTouch = e => {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
mouseX = (e.touches[0].clientX - rect.left) * (canvas.width / rect.width / (window.devicePixelRatio || 1)) - BASKET_W / 2;
};
document.addEventListener('keydown', onKey);
document.addEventListener('keyup', onKey);
canvas.addEventListener('mousemove', onMouse);
canvas.addEventListener('touchmove', onTouch, { passive: false });
function spawnItem() {
items.push({
x: Math.random() * (W - 40) + 20,
y: -30,
vy: 3.2 + Math.random() * 1.2 + score * 0.22, // schneller + mehr Zuwachs
emoji: FRUITS[Math.floor(Math.random() * FRUITS.length)],
size: 26 + Math.random() * 10,
});
}
function loop() {
if (stopped) return;
raf = requestAnimationFrame(loop);
frame++;
if (mouseX !== null) {
basket.x += (mouseX - basket.x) * 0.18;
} else {
if (keys['ArrowLeft'] || keys['KeyA']) basket.x -= 5.5;
if (keys['ArrowRight'] || keys['KeyD']) basket.x += 5.5;
}
basket.x = Math.max(0, Math.min(W - BASKET_W, basket.x));
const spawnRate = Math.max(22, 55 - score * 3); // häufiger spawnen
if (frame % spawnRate === 0 && !result) spawnItem();
items.forEach(it => { it.y += it.vy; });
items = items.filter(it => {
const basketTop = H - 50 - BASKET_H;
if (it.y + it.size / 2 > basketTop &&
it.x > basket.x && it.x < basket.x + BASKET_W) {
score++;
if (score >= WIN && !endTimer)
endTimer = setTimeout(() => { result = 'win'; setTimeout(() => onDone(true), 900); }, 200);
return false;
}
if (it.y > H + 20) {
missed++;
if (missed >= 4 && score < WIN && !endTimer)
endTimer = setTimeout(() => { result = 'lose'; setTimeout(() => onDone(false), 900); }, 200);
return false;
}
return true;
});
ctx.fillStyle = '#050508';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, H - 50); ctx.lineTo(W, H - 50); ctx.stroke();
ctx.font = '28px serif'; ctx.textAlign = 'center';
items.forEach(it => ctx.fillText(it.emoji, it.x, it.y + it.size));
const bx = basket.x, by = H - 50 - BASKET_H;
MGAPI.roundRect(ctx, bx, by, BASKET_W, BASKET_H, 6, `${theme.primary}33`, theme.primary);
for (let i = 1; i < 4; i++) {
ctx.strokeStyle = `${theme.primary}66`; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(bx+(BASKET_W/4)*i, by); ctx.lineTo(bx+(BASKET_W/4)*i, by+BASKET_H); ctx.stroke();
}
ctx.font = '22px serif';
ctx.fillText('🧺', bx + BASKET_W / 2, by + BASKET_H + 20);
MGAPI.text(ctx, `🍎 ${score} / ${WIN} 💔 ${missed} / 4`, W / 2, 18, { size: 13, color: theme.primary });
if (!result)
MGAPI.text(ctx, '← → oder Maus bewegen', W / 2, H - 10, { size: 10, color: 'rgba(255,255,255,0.3)' });
if (result)
MGAPI.resultScreen(ctx, W, H, result === 'win',
result === 'win' ? `${score} Früchte gefangen!` : `${missed} verpasst — nächstes Mal!`);
}
raf = requestAnimationFrame(loop);
return {
stop() {
stopped = true;
cancelAnimationFrame(raf);
clearTimeout(endTimer);
document.removeEventListener('keydown', onKey);
document.removeEventListener('keyup', onKey);
canvas.removeEventListener('mousemove', onMouse);
canvas.removeEventListener('touchmove', onTouch);
},
};
}
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 };
})();