commit e1c8e971fd82560846986d165de17ea9067d81b4 Author: Flappy Dev Date: Sat Jun 6 14:42:47 2026 +0000 Add Flappy Bird game - HTML5 Canvas implementation diff --git a/game.js b/game.js new file mode 100644 index 0000000..3b69766 --- /dev/null +++ b/game.js @@ -0,0 +1,279 @@ +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(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..bf99fb5 --- /dev/null +++ b/index.html @@ -0,0 +1,37 @@ + + + + + + Flappy Bird + + + + +
Space / Click / Tap to flap
+ + +