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