Phase 8: bomb juice — the explosion is finally FELT
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
5a7f71fdba
commit
a3bd45e2f1
5 files changed
+19
-3
No files matched your search
Binary file not shown.
@@ -467,6 +467,8 @@ export function update(dt) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (G.flash) { G.flash.t -= dt; if (G.flash.t <= 0) G.flash = null; }
|
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);
|
crowd.cheer = Math.max(0, crowd.cheer - dt);
|
||||||
G.idleT += dt;
|
G.idleT += dt;
|
||||||
const w = BOUNCE_IDLE.tempo;
|
const w = BOUNCE_IDLE.tempo;
|
||||||
|
|||||||
@@ -233,6 +233,10 @@ function drawMgRacket() { // horde: the player braces the racket forward and up-
|
|||||||
// ------------------------------------------------------------------ frame ----
|
// ------------------------------------------------------------------ frame ----
|
||||||
export function render() {
|
export function render() {
|
||||||
ctx.setTransform(RES, 0, 0, RES, 0, 0); // logical W x H coordinates at RES density
|
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();
|
const pal = palette();
|
||||||
// stands: tiered crowd from top of screen down to just above the far court
|
// 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)
|
// gore decals: what remains of the visiting team (flat ground layer, then far-side mounds)
|
||||||
for (const d of gore) {
|
for (const d of gore) {
|
||||||
const p = proj(d.x, 0, d.z);
|
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();
|
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
|
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.mode !== 'eaten') drawChar(P); // there is no P anymore
|
||||||
if (G.horde && G.mode === 'splatter') drawMgRacket(); // rambo racket over the player
|
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
|
// HUD — scoreboard
|
||||||
ctx.fillStyle = 'rgba(12,16,24,0.82)'; ctx.fillRect(6, 6, 132, 30);
|
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);
|
ctx.strokeStyle = '#5a6a85'; ctx.strokeRect(6.5, 6.5, 131, 29);
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export const G = {
|
|||||||
overT: 0,
|
overT: 0,
|
||||||
hitstop: 0,
|
hitstop: 0,
|
||||||
flash: null, // {text, t}
|
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,
|
idleT: 0,
|
||||||
outMarker: null, // {x, z, out, t} — where your last shot lands
|
outMarker: null, // {x, z, out, t} — where your last shot lands
|
||||||
boy: null, // ball boy sprinting across: {x, z, dir, runT}
|
boy: null, // ball boy sprinting across: {x, z, dir, runT}
|
||||||
|
|||||||
+6
-2
@@ -101,6 +101,7 @@ function tickHorde(dt) {
|
|||||||
export function explode(x, z) {
|
export function explode(x, z) {
|
||||||
for (let i = zombies.length - 1; i >= 0; i--) // backwards: splatter() splices the array
|
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);
|
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();
|
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);
|
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);
|
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,
|
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,
|
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) {
|
export function updateZombies(dt) {
|
||||||
@@ -200,7 +204,7 @@ export function updateZombies(dt) {
|
|||||||
const boy = G.boy; // the boy mops any gore he passes over
|
const boy = G.boy; // the boy mops any gore he passes over
|
||||||
for (let i = gore.length - 1; i >= 0; i--) {
|
for (let i = gore.length - 1; i >= 0; i--) {
|
||||||
const d = gore[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);
|
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
|
for (let i = piles.length - 1; i >= 0; i--) { // piles shave far slower than stains — net growth wins
|
||||||
|
|||||||
Reference in new issue
Block a user