176 lines
5.8 KiB
JavaScript
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 };
|
|
|
|
})();
|