diff --git a/js/game.js b/js/game.js index b248ca1..efe280d 100644 --- a/js/game.js +++ b/js/game.js @@ -1,16 +1,30 @@ 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, @@ -33,7 +47,6 @@ window.addEventListener('keydown', (e) => { e.preventDefault(); } keys.add(e.key); - if (e.key === 'ArrowUp' && state.player.onGround) { state.player.vy = -state.player.jumpPower; state.player.onGround = false; @@ -46,21 +59,44 @@ function drawBackground() { 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(p.x, p.y, p.width, p.height); + ctx.fillRect(sx, p.y, p.width, p.height); ctx.fillStyle = '#27404a'; - ctx.fillRect(p.x + p.width - 12, p.y + 14, 7, 7); // eye - ctx.fillRect(p.x + 10, p.y + p.height - 8, 14, 8); // foot1 - ctx.fillRect(p.x + p.width - 24, p.y + p.height - 8, 14, 8); // foot2 + 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; @@ -74,7 +110,7 @@ function updatePlayer(dt) { p.vx = move * p.speed; const oldX = p.x; p.x += p.vx * dt; - p.x = Math.max(0, Math.min(canvas.width - p.width, p.x)); + p.x = Math.max(0, Math.min(worldWidth - p.width, p.x)); if (p.x > oldX) { state.stepCarry += p.x - oldX; @@ -91,26 +127,47 @@ function updatePlayer(dt) { 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) { - if (!state.running) return; const dt = Math.min((ts - state.lastTs) / 1000, 0.05); state.lastTs = ts; - updatePlayer(dt); + 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); });