Add heart color states, ant +50 popups, and action SFX
This commit is contained in:
parent
20f4f4fbc2
commit
df3f1e513e
89
js/game.js
89
js/game.js
@ -14,6 +14,46 @@ const gravity = 1900;
|
|||||||
const groundY = 420;
|
const groundY = 420;
|
||||||
const worldWidth = 6000;
|
const worldWidth = 6000;
|
||||||
|
|
||||||
|
let audioCtx = null;
|
||||||
|
|
||||||
|
function ensureAudioCtx() {
|
||||||
|
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playSfx(type) {
|
||||||
|
try {
|
||||||
|
ensureAudioCtx();
|
||||||
|
const now = audioCtx.currentTime;
|
||||||
|
const osc = audioCtx.createOscillator();
|
||||||
|
const gain = audioCtx.createGain();
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(audioCtx.destination);
|
||||||
|
|
||||||
|
const presets = {
|
||||||
|
jump: { f0: 420, f1: 680, dur: 0.1, wave: 'triangle', vol: 0.06 },
|
||||||
|
squish: { f0: 180, f1: 120, dur: 0.12, wave: 'square', vol: 0.07 },
|
||||||
|
duck: { f0: 240, f1: 180, dur: 0.06, wave: 'sine', vol: 0.05 },
|
||||||
|
ptero_spawn: { f0: 520, f1: 460, dur: 0.1, wave: 'sawtooth', vol: 0.05 },
|
||||||
|
meteor_spawn: { f0: 300, f1: 200, dur: 0.14, wave: 'sawtooth', vol: 0.06 },
|
||||||
|
meteor_land: { f0: 140, f1: 70, dur: 0.18, wave: 'square', vol: 0.08 },
|
||||||
|
hit: { f0: 260, f1: 140, dur: 0.1, wave: 'square', vol: 0.06 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const p = presets[type];
|
||||||
|
if (!p) return;
|
||||||
|
osc.type = p.wave;
|
||||||
|
osc.frequency.setValueAtTime(p.f0, now);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(Math.max(40, p.f1), now + p.dur);
|
||||||
|
gain.gain.setValueAtTime(p.vol, now);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.0001, now + p.dur);
|
||||||
|
osc.start(now);
|
||||||
|
osc.stop(now + p.dur);
|
||||||
|
} catch (_) {
|
||||||
|
// ignore audio errors in unsupported environments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const waterGaps = [
|
const waterGaps = [
|
||||||
{ x: 700, width: 130 },
|
{ x: 700, width: 130 },
|
||||||
{ x: 1300, width: 150 },
|
{ x: 1300, width: 150 },
|
||||||
@ -43,6 +83,7 @@ function createInitialState() {
|
|||||||
pteroSpawnTimer: 0,
|
pteroSpawnTimer: 0,
|
||||||
meteors: [],
|
meteors: [],
|
||||||
meteorSpawnTimer: 0,
|
meteorSpawnTimer: 0,
|
||||||
|
floatingTexts: [],
|
||||||
player: {
|
player: {
|
||||||
x: 160,
|
x: 160,
|
||||||
y: groundY - 76,
|
y: groundY - 76,
|
||||||
@ -79,6 +120,7 @@ window.addEventListener('keydown', (e) => {
|
|||||||
if (e.key === 'ArrowUp' && state.player.onGround && state.running) {
|
if (e.key === 'ArrowUp' && state.player.onGround && state.running) {
|
||||||
state.player.vy = -state.player.jumpPower;
|
state.player.vy = -state.player.jumpPower;
|
||||||
state.player.onGround = false;
|
state.player.onGround = false;
|
||||||
|
playSfx('jump');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('keyup', (e) => keys.delete(e.key));
|
window.addEventListener('keyup', (e) => keys.delete(e.key));
|
||||||
@ -175,10 +217,37 @@ function drawMeteors() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addFloatingText(x, y, text, color = '#ffe066') {
|
||||||
|
state.floatingTexts.push({ x, y, text, color, ttl: 0.8, vy: -36 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFloatingTexts(dt) {
|
||||||
|
for (const t of state.floatingTexts) {
|
||||||
|
t.ttl -= dt;
|
||||||
|
t.y += t.vy * dt;
|
||||||
|
}
|
||||||
|
state.floatingTexts = state.floatingTexts.filter((t) => t.ttl > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFloatingTexts() {
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = 'bold 22px Arial';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
for (const t of state.floatingTexts) {
|
||||||
|
const alpha = Math.max(0, Math.min(1, t.ttl / 0.8));
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.fillStyle = t.color;
|
||||||
|
ctx.fillText(t.text, t.x - state.cameraX, t.y);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
const intersects = (a, b) => a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
const intersects = (a, b) => 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() {
|
function updateHeartsUI() {
|
||||||
heartsEl.textContent = '❤'.repeat(state.hearts) + '🖤'.repeat(Math.max(0, 3 - state.hearts));
|
const active = '<span class="heart heart-active">❤</span>'.repeat(state.hearts);
|
||||||
|
const lost = '<span class="heart heart-lost">❤</span>'.repeat(Math.max(0, 3 - state.hearts));
|
||||||
|
heartsEl.innerHTML = active + lost;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTopScores() {
|
async function loadTopScores() {
|
||||||
@ -215,6 +284,7 @@ function applyHit(reason) {
|
|||||||
if (state.hitCooldown > 0 || state.dead) return;
|
if (state.hitCooldown > 0 || state.dead) return;
|
||||||
state.hearts -= 1;
|
state.hearts -= 1;
|
||||||
state.hitCooldown = 1;
|
state.hitCooldown = 1;
|
||||||
|
playSfx('hit');
|
||||||
if (state.hearts <= 0) kill(reason);
|
if (state.hearts <= 0) kill(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,6 +309,9 @@ function updateAnts(dt) {
|
|||||||
if (stomped) {
|
if (stomped) {
|
||||||
ant.alive = false;
|
ant.alive = false;
|
||||||
p.vy = -380;
|
p.vy = -380;
|
||||||
|
state.score += 50;
|
||||||
|
addFloatingText(ant.x + ant.width / 2, ant.y - 10, '+50', '#ffe066');
|
||||||
|
playSfx('squish');
|
||||||
} else {
|
} else {
|
||||||
applyHit('A swarm of ants got you.');
|
applyHit('A swarm of ants got you.');
|
||||||
}
|
}
|
||||||
@ -286,6 +359,7 @@ function updatePterodactyls(dt) {
|
|||||||
height: lane.height,
|
height: lane.height,
|
||||||
vx: -280
|
vx: -280
|
||||||
});
|
});
|
||||||
|
playSfx('ptero_spawn');
|
||||||
state.pteroSpawnTimer = 2.2;
|
state.pteroSpawnTimer = 2.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,13 +378,20 @@ function updateMeteors(dt) {
|
|||||||
|
|
||||||
if (state.meteorSpawnTimer <= 0) {
|
if (state.meteorSpawnTimer <= 0) {
|
||||||
const spawnX = state.cameraX + 80 + Math.random() * (canvas.width - 160);
|
const spawnX = state.cameraX + 80 + Math.random() * (canvas.width - 160);
|
||||||
state.meteors.push({ x: spawnX, y: -30, width: 24, height: 30, vy: 420 });
|
state.meteors.push({ x: spawnX, y: -30, width: 24, height: 30, vy: 420, landed: false });
|
||||||
|
playSfx('meteor_spawn');
|
||||||
state.meteorSpawnTimer = 1.5;
|
state.meteorSpawnTimer = 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = state.player;
|
const p = state.player;
|
||||||
for (const m of state.meteors) {
|
for (const m of state.meteors) {
|
||||||
m.y += m.vy * dt;
|
m.y += m.vy * dt;
|
||||||
|
|
||||||
|
if (!m.landed && m.y + m.height >= groundY) {
|
||||||
|
m.landed = true;
|
||||||
|
playSfx('meteor_land');
|
||||||
|
}
|
||||||
|
|
||||||
if (intersects(p, m)) {
|
if (intersects(p, m)) {
|
||||||
applyHit('Meteor strike!');
|
applyHit('Meteor strike!');
|
||||||
m.y = canvas.height + 100;
|
m.y = canvas.height + 100;
|
||||||
@ -325,7 +406,9 @@ function updatePlayer(dt) {
|
|||||||
if (keys.has('ArrowLeft')) move -= 1;
|
if (keys.has('ArrowLeft')) move -= 1;
|
||||||
if (keys.has('ArrowRight')) move += 1;
|
if (keys.has('ArrowRight')) move += 1;
|
||||||
|
|
||||||
|
const wasDucking = p.ducking;
|
||||||
p.ducking = keys.has('ArrowDown') && p.onGround;
|
p.ducking = keys.has('ArrowDown') && p.onGround;
|
||||||
|
if (!wasDucking && p.ducking) playSfx('duck');
|
||||||
const targetHeight = p.ducking ? p.duckHeight : p.standingHeight;
|
const targetHeight = p.ducking ? p.duckHeight : p.standingHeight;
|
||||||
if (targetHeight !== p.height) {
|
if (targetHeight !== p.height) {
|
||||||
p.y += p.height - targetHeight;
|
p.y += p.height - targetHeight;
|
||||||
@ -370,6 +453,7 @@ function tick(ts) {
|
|||||||
updateAnts(dt);
|
updateAnts(dt);
|
||||||
updatePterodactyls(dt);
|
updatePterodactyls(dt);
|
||||||
updateMeteors(dt);
|
updateMeteors(dt);
|
||||||
|
updateFloatingTexts(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
@ -377,6 +461,7 @@ function tick(ts) {
|
|||||||
drawAnts();
|
drawAnts();
|
||||||
drawPterodactyls();
|
drawPterodactyls();
|
||||||
drawMeteors();
|
drawMeteors();
|
||||||
|
drawFloatingTexts();
|
||||||
drawTRex(state.player);
|
drawTRex(state.player);
|
||||||
|
|
||||||
scoreEl.textContent = `Score: ${state.score}`;
|
scoreEl.textContent = `Score: ${state.score}`;
|
||||||
|
|||||||
@ -46,3 +46,10 @@ button, input {
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
ol { padding-left: 22px; }
|
ol { padding-left: 22px; }
|
||||||
|
|
||||||
|
.heart {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
.heart-active { color: #e02424; }
|
||||||
|
.heart-lost { color: #111; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user