diff --git a/assets/audio/order.m4a b/assets/audio/order.m4a new file mode 100644 index 0000000..54ab993 Binary files /dev/null and b/assets/audio/order.m4a differ diff --git a/gameplay.js b/gameplay.js index 51fb16c..9483778 100644 --- a/gameplay.js +++ b/gameplay.js @@ -467,6 +467,8 @@ export function update(dt) { return; } if (G.flash) { G.flash.t -= dt; if (G.flash.t <= 0) G.flash = null; } + if (G.shake && (G.shake.t -= dt) <= 0) G.shake = null; + if (G.boomFlash > 0) G.boomFlash -= dt; crowd.cheer = Math.max(0, crowd.cheer - dt); G.idleT += dt; const w = BOUNCE_IDLE.tempo; diff --git a/render.js b/render.js index 21c7c8e..f7dcf5c 100644 --- a/render.js +++ b/render.js @@ -233,6 +233,10 @@ function drawMgRacket() { // horde: the player braces the racket forward and up- // ------------------------------------------------------------------ frame ---- export function render() { ctx.setTransform(RES, 0, 0, RES, 0, 0); // logical W x H coordinates at RES density + if (G.shake) { // bomb kick: jitter the whole world (transform is reset before the HUD) + const m = G.shake.mag * G.shake.t / 0.35; + ctx.translate((Math.random() * 2 - 1) * m, (Math.random() * 2 - 1) * m); + } const pal = palette(); // stands: tiered crowd from top of screen down to just above the far court @@ -271,7 +275,7 @@ export function render() { // gore decals: what remains of the visiting team (flat ground layer, then far-side mounds) for (const d of gore) { const p = proj(d.x, 0, d.z); - ctx.fillStyle = `rgba(140,18,18,${d.a})`; + ctx.fillStyle = d.scorch ? `rgba(28,22,16,${d.a})` : `rgba(140,18,18,${d.a})`; // craters burn black-brown ctx.beginPath(); ctx.ellipse(p.x, p.y, p.s * d.r * 0.4, p.s * d.r * 0.16, 0, 0, 7); ctx.fill(); } drawPiles(false); // far-side piles (occluded by the net); near-side piles drawn after the net @@ -328,6 +332,9 @@ export function render() { if (G.mode !== 'eaten') drawChar(P); // there is no P anymore if (G.horde && G.mode === 'splatter') drawMgRacket(); // rambo racket over the player + ctx.setTransform(RES, 0, 0, RES, 0, 0); // un-shake: the HUD and overlays stay rock steady + if (G.boomFlash > 0) { ctx.fillStyle = `rgba(255,255,255,${Math.min(1, G.boomFlash / 0.08) * 0.85})`; ctx.fillRect(0, 0, W, H); } + // HUD — scoreboard ctx.fillStyle = 'rgba(12,16,24,0.82)'; ctx.fillRect(6, 6, 132, 30); ctx.strokeStyle = '#5a6a85'; ctx.strokeRect(6.5, 6.5, 131, 29); diff --git a/state.js b/state.js index 3a5932f..2a6876d 100644 --- a/state.js +++ b/state.js @@ -21,6 +21,9 @@ export const G = { overT: 0, hitstop: 0, flash: null, // {text, t} + shake: null, // {t, mag} — camera shake, decays like flash (bomb impact) + boomFlash: 0, // s of white flash frame left (bomb impact) + lastOrderT: -9, // idleT of the last 'Order' call — bomb chains mustn't spam the umpire idleT: 0, outMarker: null, // {x, z, out, t} — where your last shot lands boy: null, // ball boy sprinting across: {x, z, dir, runT} diff --git a/zombies.js b/zombies.js index fbb919b..661ea90 100644 --- a/zombies.js +++ b/zombies.js @@ -101,6 +101,7 @@ function tickHorde(dt) { export function explode(x, z) { for (let i = zombies.length - 1; i >= 0; i--) // backwards: splatter() splices the array if (Math.hypot(zombies[i].x - x, zombies[i].z - z) < TUNE.bombRadius) splatter(i); + gore.push({ x, z, r: 1.6, a: 1, scorch: true }); if (gore.length > 40) gore.shift(); // the crater — near-permanent gore.push({ x, z, r: 1.4, a: 0.9 }); if (gore.length > 40) gore.shift(); const pl = piles.find(p => Math.hypot(p.x - x, p.z - z) < 0.6); if (pl) pl.size = Math.min(4, pl.size + 3); @@ -109,7 +110,10 @@ export function explode(x, z) { x, y: 0.6, z, vx: (Math.random() * 2 - 1) * 6, vy: 1 + Math.random() * 5, vz: (Math.random() * 2 - 1) * 6, t: 0.5 + Math.random() * 0.7, dark: k % 2 === 0, }); - G.hitstop = 0.1; boom(); + G.hitstop = 0.18; boom(); + G.shake = { t: 0.35, mag: 6 }; // camera kick + G.boomFlash = 0.08; // one white frame + if (G.idleT - G.lastOrderT > 3) { voice('order', 0.35); G.lastOrderT = G.idleT; } // the umpire loses composure } export function updateZombies(dt) { @@ -200,7 +204,7 @@ export function updateZombies(dt) { const boy = G.boy; // the boy mops any gore he passes over for (let i = gore.length - 1; i >= 0; i--) { const d = gore[i]; - if (boy && Math.abs(boy.x - d.x) < 0.7 && Math.abs(boy.z - d.z) < 1.2) d.a -= dt * 1.6; + if (boy && Math.abs(boy.x - d.x) < 0.7 && Math.abs(boy.z - d.z) < 1.2) d.a -= dt * (d.scorch ? 0.16 : 1.6); // craters scar the lawn 10× longer if (d.a <= 0) gore.splice(i, 1); } for (let i = piles.length - 1; i >= 0; i--) { // piles shave far slower than stains — net growth wins