const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const W = canvas.width; const H = canvas.height; // --- Constants --- const GRAVITY = 0.45; const FLAP_FORCE = -8.5; const PIPE_SPEED = 2.8; const PIPE_WIDTH = 60; const PIPE_GAP = 160; const PIPE_INTERVAL = 90; // frames between pipes const BIRD_X = 90; const BIRD_R = 18; // collision radius (slightly smaller than visual) // --- Palette --- const SKY_TOP = '#4ec0e4'; const SKY_BOT = '#87ceeb'; const GROUND_COL = '#ded895'; const PIPE_COL = '#4caf50'; const PIPE_DARK = '#388e3c'; const BIRD_BODY = '#ffd700'; const BIRD_WING = '#ffb300'; const BIRD_EYE = '#fff'; const BIRD_PUPIL = '#222'; const BIRD_BEAK = '#ff8c00'; // --- State --- let bird, pipes, score, frame, state, bestScore; // states: 'idle' | 'playing' | 'dead' function init() { bird = { y: H / 2, vy: 0, angle: 0, wingPhase: 0 }; pipes = []; score = 0; frame = 0; state = 'idle'; bestScore = bestScore || 0; } // --- Input --- function flap() { if (state === 'idle') { state = 'playing'; } if (state === 'playing') { bird.vy = FLAP_FORCE; bird.wingPhase = 0; } if (state === 'dead') { init(); } } document.addEventListener('keydown', e => { if (e.code === 'Space') { e.preventDefault(); flap(); }}); canvas.addEventListener('click', flap); canvas.addEventListener('touchstart', e => { e.preventDefault(); flap(); }, { passive: false }); // --- Pipe helpers --- function spawnPipe() { const minTop = 80; const maxTop = H - 120 - PIPE_GAP; const topH = Math.floor(Math.random() * (maxTop - minTop + 1)) + minTop; pipes.push({ x: W + 10, topH, passed: false }); } // --- Collision --- function circleRect(cx, cy, r, rx, ry, rw, rh) { const nearX = Math.max(rx, Math.min(cx, rx + rw)); const nearY = Math.max(ry, Math.min(cy, ry + rh)); const dx = cx - nearX, dy = cy - nearY; return dx * dx + dy * dy < r * r; } function checkCollision() { const GROUND_Y = H - 80; if (bird.y + BIRD_R >= GROUND_Y) return true; if (bird.y - BIRD_R <= 0) return true; for (const p of pipes) { const cap = 6; // pipe cap extra width if ( circleRect(BIRD_X, bird.y, BIRD_R, p.x - cap, 0, PIPE_WIDTH + cap * 2, p.topH) || circleRect(BIRD_X, bird.y, BIRD_R, p.x - cap, p.topH + PIPE_GAP, PIPE_WIDTH + cap * 2, H) ) return true; } return false; } // --- Draw helpers --- function drawSky() { const grad = ctx.createLinearGradient(0, 0, 0, H); grad.addColorStop(0, SKY_TOP); grad.addColorStop(1, SKY_BOT); ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H); } function drawGround() { const gy = H - 80; ctx.fillStyle = GROUND_COL; ctx.fillRect(0, gy, W, 80); ctx.fillStyle = '#8bc34a'; ctx.fillRect(0, gy, W, 12); } function drawPipe(p) { const capH = 24, capW = PIPE_WIDTH + 12; const capX = p.x - 6; // bottom pipe const botY = p.topH + PIPE_GAP; ctx.fillStyle = PIPE_COL; ctx.fillRect(p.x, botY, PIPE_WIDTH, H - botY); ctx.fillStyle = PIPE_DARK; ctx.fillRect(p.x, botY, 6, H - botY); // bottom cap ctx.fillStyle = PIPE_COL; ctx.fillRect(capX, botY, capW, capH); ctx.fillStyle = PIPE_DARK; ctx.fillRect(capX, botY, 6, capH); // top pipe ctx.fillStyle = PIPE_COL; ctx.fillRect(p.x, 0, PIPE_WIDTH, p.topH); ctx.fillStyle = PIPE_DARK; ctx.fillRect(p.x, 0, 6, p.topH); // top cap ctx.fillStyle = PIPE_COL; ctx.fillRect(capX, p.topH - capH, capW, capH); ctx.fillStyle = PIPE_DARK; ctx.fillRect(capX, p.topH - capH, 6, capH); } function drawBird() { ctx.save(); ctx.translate(BIRD_X, bird.y); const angle = Math.min(Math.max(bird.vy * 3, -30), 80) * Math.PI / 180; ctx.rotate(angle); // wing const wingY = 4 + Math.sin(bird.wingPhase) * 8; ctx.fillStyle = BIRD_WING; ctx.beginPath(); ctx.ellipse(-4, wingY, 10, 6, -0.4, 0, Math.PI * 2); ctx.fill(); // body ctx.fillStyle = BIRD_BODY; ctx.beginPath(); ctx.ellipse(0, 0, 22, 18, 0, 0, Math.PI * 2); ctx.fill(); // white belly ctx.fillStyle = '#fff8dc'; ctx.beginPath(); ctx.ellipse(4, 4, 12, 9, 0.3, 0, Math.PI * 2); ctx.fill(); // eye ctx.fillStyle = BIRD_EYE; ctx.beginPath(); ctx.arc(9, -5, 7, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = BIRD_PUPIL; ctx.beginPath(); ctx.arc(11, -5, 4, 0, Math.PI * 2); ctx.fill(); // shine ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(12, -7, 1.5, 0, Math.PI * 2); ctx.fill(); // beak ctx.fillStyle = BIRD_BEAK; ctx.beginPath(); ctx.moveTo(18, -2); ctx.lineTo(28, 2); ctx.lineTo(18, 6); ctx.closePath(); ctx.fill(); ctx.restore(); } function drawScore() { ctx.fillStyle = '#fff'; ctx.strokeStyle = '#333'; ctx.lineWidth = 4; ctx.font = 'bold 42px Segoe UI, Arial'; ctx.textAlign = 'center'; ctx.strokeText(score, W / 2, 70); ctx.fillText(score, W / 2, 70); } function drawOverlay(title, sub) { // panel ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.beginPath(); ctx.roundRect(W / 2 - 140, H / 2 - 120, 280, 240, 16); ctx.fill(); ctx.textAlign = 'center'; ctx.fillStyle = '#ffd700'; ctx.font = 'bold 36px Segoe UI, Arial'; ctx.fillText(title, W / 2, H / 2 - 68); if (state === 'dead') { ctx.fillStyle = '#fff'; ctx.font = '20px Segoe UI, Arial'; ctx.fillText(`Score: ${score}`, W / 2, H / 2 - 20); ctx.fillText(`Best: ${bestScore}`, W / 2, H / 2 + 14); } ctx.fillStyle = '#87ceeb'; ctx.font = '18px Segoe UI, Arial'; ctx.fillText(sub, W / 2, H / 2 + 60); ctx.fillStyle = 'rgba(255,255,255,0.15)'; ctx.beginPath(); ctx.roundRect(W / 2 - 100, H / 2 + 74, 200, 42, 10); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = 'bold 16px Segoe UI, Arial'; ctx.fillText('Space / Click / Tap', W / 2, H / 2 + 100); } // --- Main loop --- function update() { if (state === 'playing') { bird.vy += GRAVITY; bird.y += bird.vy; bird.wingPhase += 0.25; // Spawn pipes if (frame % PIPE_INTERVAL === 0) spawnPipe(); // Move pipes & score for (const p of pipes) { p.x -= PIPE_SPEED; if (!p.passed && p.x + PIPE_WIDTH < BIRD_X) { p.passed = true; score++; if (score > bestScore) bestScore = score; } } // Remove off-screen pipes pipes = pipes.filter(p => p.x + PIPE_WIDTH + 20 > 0); if (checkCollision()) { state = 'dead'; } frame++; } if (state === 'idle') { bird.y = H / 2 + Math.sin(frame * 0.05) * 10; frame++; } } function render() { drawSky(); pipes.forEach(drawPipe); drawGround(); drawBird(); if (state === 'playing' || state === 'dead') drawScore(); if (state === 'idle') drawOverlay('Flappy Bird', 'Get ready!'); if (state === 'dead') drawOverlay('Game Over', 'Try again?'); } function loop() { update(); render(); requestAnimationFrame(loop); } init(); loop();