edu-boardgame-generator/minigames/basketball.js

176 lines
5.8 KiB
JavaScript

/**
* minigames/basketball.js
* 🏀 Basketball — Wirf den Ball zum richtigen Zeitpunkt!
*/
window.MG_basketball = (function() {
const ID = 'basketball';
const EMOJI = '🏀';
const NAME = 'Basketball';
const DESC = 'Drücke zum richtigen Zeitpunkt, um den Ball zu werfen!';
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: '#f97316' };
const WIN_BASKETS = cfg.winBaskets || 1;
const HOOP_X = W * 0.72;
const HOOP_Y = H * 0.28;
const HOOP_W = 54;
const BALL_R = 18;
const BALL_START = { x: W * 0.18, y: H * 0.72 };
let ballX = BALL_START.x;
let ballY = BALL_START.y;
let vx = 0, vy = 0;
let flying = false;
let score = 0;
let misses = 0;
let stopped = false;
let raf, endTimer;
let result = null;
let power = 0;
let powerDir = 1;
const powerMax = 100;
function shoot() {
if (flying || result) return;
const dx = HOOP_X - BALL_START.x;
const dy = HOOP_Y - BALL_START.y - 20;
const speed = 8.5 + power * 0.12; // schneller: war 4 + 0.09
const angle = Math.atan2(dy, dx) - 0.18 - (power - 50) * 0.005;
vx = Math.cos(angle) * speed;
vy = Math.sin(angle) * speed;
flying = true;
ballX = BALL_START.x;
ballY = BALL_START.y;
}
const onAction = e => {
if (e.type === 'keydown' && e.code !== 'Space') return;
if (e.type === 'keydown') e.preventDefault();
shoot();
};
document.addEventListener('keydown', onAction);
canvas.addEventListener('click', onAction);
canvas.addEventListener('touchstart', e => { e.preventDefault(); shoot(); }, { passive: false });
function reset() {
flying = false;
ballX = BALL_START.x;
ballY = BALL_START.y;
}
function drawHoop() {
ctx.fillStyle = '#e5e7eb';
ctx.fillRect(HOOP_X + HOOP_W * 0.4, HOOP_Y - 50, 8, 50);
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.ellipse(HOOP_X + HOOP_W / 2, HOOP_Y, HOOP_W / 2, 8, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.lineWidth = 1.5;
for (let i = 0; i <= 4; i++) {
const nx = HOOP_X + (HOOP_W / 4) * i;
ctx.beginPath();
ctx.moveTo(nx, HOOP_Y + 8);
ctx.lineTo(HOOP_X + HOOP_W / 2 + (i - 2) * 3, HOOP_Y + 36);
ctx.stroke();
}
}
function drawPower() {
if (flying || result) return;
const bx = 12, by = H - 28, bw = W * 0.45, bh = 14;
MGAPI.roundRect(ctx, bx, by, bw, bh, 6, 'rgba(255,255,255,0.1)', 'rgba(255,255,255,0.2)');
const fill = Math.max(0, Math.min(1, power / powerMax));
const color = fill < 0.4 ? '#22c55e' : fill < 0.7 ? '#f59e0b' : '#ef4444';
MGAPI.roundRect(ctx, bx + 2, by + 2, (bw - 4) * fill, bh - 4, 4, color, null);
MGAPI.text(ctx, 'Kraft', bx + bw / 2, by + bh / 2, { size: 10, color: '#fff' });
}
function loop() {
if (stopped) return;
raf = requestAnimationFrame(loop);
ctx.fillStyle = '#050508';
ctx.fillRect(0, 0, W, H);
MGAPI.roundRect(ctx, 0, H - 10, W, 10, 0, '#1e293b', null);
drawHoop();
if (!flying && !result) {
power += powerDir * 2.2; // Powerbar etwas schneller (war 1.8)
if (power >= powerMax) powerDir = -1;
if (power <= 0) powerDir = 1;
}
if (flying) {
vy += 0.38;
ballX += vx;
ballY += vy;
const inHoopX = ballX > HOOP_X && ballX < HOOP_X + HOOP_W;
const inHoopY = Math.abs(ballY - HOOP_Y) < 18;
if (inHoopX && inHoopY && vy > 0) {
score++;
if (score >= WIN_BASKETS && !endTimer)
endTimer = setTimeout(() => { result = 'win'; setTimeout(() => onDone(true), 900); }, 300);
reset();
}
if (ballY > H + 20 || ballX > W + 20 || ballX < -20) {
misses++;
if (misses >= 6 && score < WIN_BASKETS && !endTimer)
endTimer = setTimeout(() => { result = 'lose'; setTimeout(() => onDone(false), 900); }, 300);
reset();
}
}
// Ball
ctx.shadowColor = theme.primary;
ctx.shadowBlur = 15;
ctx.fillStyle = theme.primary;
ctx.beginPath();
ctx.arc(ballX, ballY, BALL_R, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = 'rgba(0,0,0,0.35)';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(ballX, ballY, BALL_R, 0, Math.PI * 2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(ballX - BALL_R, ballY); ctx.lineTo(ballX + BALL_R, ballY); ctx.stroke();
drawPower();
MGAPI.text(ctx, `🏀 ${score} / ${WIN_BASKETS}`, W / 2, 18, { size: 14, color: theme.primary });
if (!flying && !result)
MGAPI.text(ctx, 'Klick oder Leertaste zum Werfen', W / 2, H - 48,
{ size: 11, color: 'rgba(255,255,255,0.45)' });
if (result)
MGAPI.resultScreen(ctx, W, H, result === 'win',
result === 'win' ? `${score} Körbe getroffen!` : `Nur ${score} — weiter üben!`);
}
raf = requestAnimationFrame(loop);
return {
stop() {
stopped = true;
cancelAnimationFrame(raf);
clearTimeout(endTimer);
document.removeEventListener('keydown', onAction);
canvas.removeEventListener('click', onAction);
},
};
}
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 };
})();