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, 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 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 isOverWater(player) { const centerX = player.x + player.width / 2; return waterGaps.some((g) => centerX >= g.x && centerX <= g.x + g.width); } 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) updatePlayer(dt); ctx.clearRect(0, 0, canvas.width, canvas.height); drawBackground(); drawTRex(state.player); drawDeathText(); scoreEl.textContent = `Score: ${state.score}`; updateHeartsUI(); requestAnimationFrame(tick); } requestAnimationFrame((ts) => { state.lastTs = ts; updateHeartsUI(); requestAnimationFrame(tick); });