feat: add hearts HUD and water-gap instant death hazard

This commit is contained in:
OpenClaw Engineer 2026-03-03 20:39:57 -06:00
parent 5b1d0affca
commit 5de53553ca

View File

@ -1,16 +1,30 @@
const canvas = document.getElementById('game'); const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score'); const scoreEl = document.getElementById('score');
const heartsEl = document.getElementById('hearts');
const keys = new Set(); const keys = new Set();
const gravity = 1900; const gravity = 1900;
const groundY = 420; 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 = { const state = {
running: true, running: true,
dead: false,
deadReason: '',
lastTs: performance.now(), lastTs: performance.now(),
score: 0, score: 0,
stepCarry: 0, stepCarry: 0,
cameraX: 0,
hearts: 3,
player: { player: {
x: 160, x: 160,
y: 0, y: 0,
@ -33,7 +47,6 @@ window.addEventListener('keydown', (e) => {
e.preventDefault(); e.preventDefault();
} }
keys.add(e.key); keys.add(e.key);
if (e.key === 'ArrowUp' && state.player.onGround) { if (e.key === 'ArrowUp' && state.player.onGround) {
state.player.vy = -state.player.jumpPower; state.player.vy = -state.player.jumpPower;
state.player.onGround = false; state.player.onGround = false;
@ -46,21 +59,44 @@ function drawBackground() {
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#88d34f'; ctx.fillStyle = '#88d34f';
ctx.fillRect(0, groundY, canvas.width, canvas.height - groundY); 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) { function drawTRex(p) {
const sx = p.x - state.cameraX;
ctx.fillStyle = '#35535f'; 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.fillStyle = '#27404a';
ctx.fillRect(p.x + p.width - 12, p.y + 14, 7, 7); // eye ctx.fillRect(sx + p.width - 12, p.y + 14, 7, 7);
ctx.fillRect(p.x + 10, p.y + p.height - 8, 14, 8); // foot1 ctx.fillRect(sx + 10, p.y + p.height - 8, 14, 8);
ctx.fillRect(p.x + p.width - 24, p.y + p.height - 8, 14, 8); // foot2 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) { function updatePlayer(dt) {
const p = state.player; const p = state.player;
let move = 0; let move = 0;
if (keys.has('ArrowLeft')) move -= 1; if (keys.has('ArrowLeft')) move -= 1;
if (keys.has('ArrowRight')) move += 1; if (keys.has('ArrowRight')) move += 1;
@ -74,7 +110,7 @@ function updatePlayer(dt) {
p.vx = move * p.speed; p.vx = move * p.speed;
const oldX = p.x; const oldX = p.x;
p.x += p.vx * dt; 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) { if (p.x > oldX) {
state.stepCarry += p.x - oldX; state.stepCarry += p.x - oldX;
@ -91,26 +127,47 @@ function updatePlayer(dt) {
p.y = groundY - p.height; p.y = groundY - p.height;
p.vy = 0; p.vy = 0;
p.onGround = true; 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) { function tick(ts) {
if (!state.running) return;
const dt = Math.min((ts - state.lastTs) / 1000, 0.05); const dt = Math.min((ts - state.lastTs) / 1000, 0.05);
state.lastTs = ts; state.lastTs = ts;
updatePlayer(dt); if (state.running) updatePlayer(dt);
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground(); drawBackground();
drawTRex(state.player); drawTRex(state.player);
drawDeathText();
scoreEl.textContent = `Score: ${state.score}`; scoreEl.textContent = `Score: ${state.score}`;
updateHeartsUI();
requestAnimationFrame(tick); requestAnimationFrame(tick);
} }
requestAnimationFrame((ts) => { requestAnimationFrame((ts) => {
state.lastTs = ts; state.lastTs = ts;
updateHeartsUI();
requestAnimationFrame(tick); requestAnimationFrame(tick);
}); });