const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); const scoreEl = document.getElementById('score'); const heartsEl = document.getElementById('hearts'); const keys = new Set(); const gravity = 1900; const groundY = 420; const worldWidth = 6000; const waterGaps = [ { x: 700, width: 130 }, { x: 1300, width: 150 }, { x: 2100, width: 170 }, { x: 3000, width: 140 }, { x: 3900, width: 200 } ]; const state = { running: true, dead: false, deadReason: '', lastTs: performance.now(), score: 0, stepCarry: 0, cameraX: 0, hearts: 3, hitCooldown: 0, ants: [ { x: 1000, y: groundY - 20, width: 22, height: 20, vx: -65, alive: true }, { x: 1750, y: groundY - 20, width: 22, height: 20, vx: -70, alive: true }, { x: 2600, y: groundY - 20, width: 22, height: 20, vx: -80, alive: true }, { x: 3400, y: groundY - 20, width: 22, height: 20, vx: -75, alive: true } ], pterodactyls: [], pteroSpawnTimer: 0, meteors: [], meteorSpawnTimer: 0, player: { x: 160, y: 0, width: 64, height: 76, standingHeight: 76, duckHeight: 48, vx: 0, vy: 0, speed: 300, jumpPower: 760, onGround: true, ducking: false } }; state.player.y = groundY - state.player.height; window.addEventListener('keydown', (e) => { if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) { e.preventDefault(); } keys.add(e.key); if (e.key === 'ArrowUp' && state.player.onGround) { state.player.vy = -state.player.jumpPower; state.player.onGround = false; } }); window.addEventListener('keyup', (e) => keys.delete(e.key)); function drawBackground() { ctx.fillStyle = '#8ddcff'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#88d34f'; ctx.fillRect(0, groundY, canvas.width, canvas.height - groundY); // Water gaps for (const gap of waterGaps) { const sx = gap.x - state.cameraX; if (sx + gap.width < 0 || sx > canvas.width) continue; ctx.fillStyle = '#2e8bcf'; ctx.fillRect(sx, groundY, gap.width, canvas.height - groundY); } } function drawTRex(p) { const sx = p.x - state.cameraX; ctx.fillStyle = '#35535f'; ctx.fillRect(sx, p.y, p.width, p.height); ctx.fillStyle = '#27404a'; ctx.fillRect(sx + p.width - 12, p.y + 14, 7, 7); ctx.fillRect(sx + 10, p.y + p.height - 8, 14, 8); ctx.fillRect(sx + p.width - 24, p.y + p.height - 8, 14, 8); } function drawAnts() { ctx.fillStyle = '#5a1e0d'; for (const ant of state.ants) { if (!ant.alive) continue; const sx = ant.x - state.cameraX; if (sx + ant.width < 0 || sx > canvas.width) continue; ctx.fillRect(sx, ant.y, ant.width, ant.height); ctx.fillRect(sx - 4, ant.y + 6, 4, 4); ctx.fillRect(sx + ant.width, ant.y + 6, 4, 4); } } function drawPterodactyls() { ctx.fillStyle = '#6e5a8a'; for (const p of state.pterodactyls) { const sx = p.x - state.cameraX; if (sx + p.width < 0 || sx > canvas.width) continue; ctx.fillRect(sx, p.y, p.width, p.height); ctx.fillRect(sx - 8, p.y + 6, 8, 4); ctx.fillRect(sx + p.width, p.y + 6, 8, 4); } } function drawMeteors() { ctx.fillStyle = '#6b4d2e'; for (const m of state.meteors) { const sx = m.x - state.cameraX; if (sx + m.width < 0 || sx > canvas.width) continue; ctx.fillRect(sx, m.y, m.width, m.height); } } function intersects(a, b) { return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y; } function updateHeartsUI() { heartsEl.textContent = '❤'.repeat(state.hearts) + '🖤'.repeat(Math.max(0, 3 - state.hearts)); } function kill(reason) { state.running = false; state.dead = true; state.deadReason = reason; } function applyHit(reason) { if (state.hitCooldown > 0 || state.dead) return; state.hearts -= 1; state.hitCooldown = 1.0; if (state.hearts <= 0) { kill(reason); } } function isOverWater(player) { const centerX = player.x + player.width / 2; return waterGaps.some((g) => centerX >= g.x && centerX <= g.x + g.width); } function updateAnts(dt) { const p = state.player; const prevBottom = p.y + p.height - p.vy * dt; for (const ant of state.ants) { if (!ant.alive) continue; ant.x += ant.vx * dt; if (ant.x < 200) ant.vx = Math.abs(ant.vx); if (ant.x > worldWidth - 200) ant.vx = -Math.abs(ant.vx); if (intersects(p, ant)) { const playerBottom = p.y + p.height; const stomped = p.vy > 0 && prevBottom <= ant.y + 4 && playerBottom >= ant.y; if (stomped) { ant.alive = false; p.vy = -380; } else { applyHit('A swarm of ants got you.'); } } } } function updatePterodactyls(dt) { if (state.score <= 100) return; state.pteroSpawnTimer -= dt; if (state.pteroSpawnTimer <= 0) { const altitude = groundY - (state.player.standingHeight - 24); state.pterodactyls.push({ x: state.cameraX + canvas.width + 80, y: altitude, width: 54, height: 26, vx: -280 }); state.pteroSpawnTimer = 2.2; } const p = state.player; for (const flyer of state.pterodactyls) { flyer.x += flyer.vx * dt; if (intersects(p, flyer)) { applyHit('Pterodactyl collision. Duck next time!'); } } state.pterodactyls = state.pterodactyls.filter((f) => f.x + f.width > state.cameraX - 150); } function updateMeteors(dt) { if (state.score <= 250) return; state.meteorSpawnTimer -= dt; if (state.meteorSpawnTimer <= 0) { const spawnX = state.cameraX + 80 + Math.random() * (canvas.width - 160); state.meteors.push({ x: spawnX, y: -30, width: 24, height: 30, vy: 420 }); state.meteorSpawnTimer = 1.5; } const p = state.player; for (const m of state.meteors) { m.y += m.vy * dt; if (intersects(p, m)) { applyHit('Meteor strike!'); m.y = canvas.height + 100; } } state.meteors = state.meteors.filter((m) => m.y < canvas.height + 80); } function updatePlayer(dt) { const p = state.player; let move = 0; if (keys.has('ArrowLeft')) move -= 1; if (keys.has('ArrowRight')) move += 1; p.ducking = keys.has('ArrowDown') && p.onGround; const targetHeight = p.ducking ? p.duckHeight : p.standingHeight; if (targetHeight !== p.height) { p.y += p.height - targetHeight; p.height = targetHeight; } p.vx = move * p.speed; const oldX = p.x; p.x += p.vx * dt; p.x = Math.max(0, Math.min(worldWidth - p.width, p.x)); if (p.x > oldX) { state.stepCarry += p.x - oldX; while (state.stepCarry >= 8) { state.score += 1; state.stepCarry -= 8; } } p.vy += gravity * dt; p.y += p.vy * dt; if (p.y + p.height >= groundY) { p.y = groundY - p.height; p.vy = 0; p.onGround = true; if (isOverWater(p)) { kill('SPLASH! Water is instant death.'); } } else { p.onGround = false; } state.cameraX = Math.max(0, Math.min(worldWidth - canvas.width, p.x - 240)); } function drawDeathText() { if (!state.dead) return; ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#fff'; ctx.font = 'bold 42px Arial'; ctx.fillText('Game Over', 360, 220); ctx.font = '24px Arial'; ctx.fillText(state.deadReason, 280, 270); } function tick(ts) { const dt = Math.min((ts - state.lastTs) / 1000, 0.05); state.lastTs = ts; if (state.running) { state.hitCooldown = Math.max(0, state.hitCooldown - dt); updatePlayer(dt); updateAnts(dt); updatePterodactyls(dt); updateMeteors(dt); } ctx.clearRect(0, 0, canvas.width, canvas.height); drawBackground(); drawAnts(); drawPterodactyls(); drawMeteors(); drawTRex(state.player); drawDeathText(); scoreEl.textContent = `Score: ${state.score}`; updateHeartsUI(); requestAnimationFrame(tick); } requestAnimationFrame((ts) => { state.lastTs = ts; updateHeartsUI(); requestAnimationFrame(tick); });