From a3bd45e2f1ac6229bb8e23cfec8b783fa9eda695 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Sun, 12 Jul 2026 09:57:31 +0200 Subject: [PATCH] =?UTF-8?q?Phase=208:=20bomb=20juice=20=E2=80=94=20the=20e?= =?UTF-8?q?xplosion=20is=20finally=20FELT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Camera shake (G.shake, decays over 0.35s; HUD stays steady via transform reset) - One white flash frame (G.boomFlash overlay) - Scorch crater: near-black stain the boys mop 10x slower — the lawn scars - Hitstop 0.1 -> 0.18 - Umpire loses composure: 'Order. Order, please.' (new order.m4a), rate-limited so bomb chains don't spam him Verified: explode kills cluster of 4 (MASSACRE x4 chains), shake/flash set and decay clean, 2nd bomb order-call suppressed, 1s of mopping erases blood (0.9->0) but not the crater (1->0.84); screenshot mid-shake; no console errors. Co-Authored-By: Claude Fable 5 --- assets/audio/order.m4a | Bin 0 -> 12227 bytes gameplay.js | 2 ++ render.js | 9 ++++++++- state.js | 3 +++ zombies.js | 8 ++++++-- 5 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 assets/audio/order.m4a diff --git a/assets/audio/order.m4a b/assets/audio/order.m4a new file mode 100644 index 0000000000000000000000000000000000000000..54ab9938aeb21b5b7171e159dccfd4be08b5551a GIT binary patch literal 12227 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 X6X&xNW|?QKN>j1XDtHYrt)2cCP@&A2 literal 0 HcmV?d00001 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