DinoLand/js/game.js
2026-03-03 20:41:43 -06:00

310 lines
7.8 KiB
JavaScript

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);
});