Phase 4: bomb balls
Ball boys deliver a bomb ball every TUNE.bombEvery (8) splatter kills, up to TUNE.bombMax (3) in stock (G.bombs / G.killMeter in state). The next serve auto-consumes one (hitServe, splatter-only) — no extra input, keeping one-handed play. A bomb detonates on first zombie contact (regardless of pace) or, if it misses everyone, on its first bounce: explode() splatters everything within TUNE.bombRadius (2.5m), each kill feeding ball.kills so the Phase-2 fanfare escalates naturally, and leaves a crater — big stain, size-3 pile, 40 particles, 0.1s hitstop, boom(). New synth boom() in audio.js. Bomb ball renders black with a flickering orange fuse; the HUD shows stocked bombs as black/orange pips by the YOU line. Both explode sites set phase='over' so the serve loop doesn't stall on the killed ball. Alternative delivery ideas Oliver floated but not built: umpire throws it, or court pickup. Verified via window.T: 8th kill earns a bomb (meter resets); a serve consumes one and renders #111 black + orange fuse; a pow-0 bomb amid 4 clustered zombies kills all 4 with 'MASSACRE ×4', boom, hitstop 0.1, crater stain (r1.4) + size-4 pile; a bomb that misses still craters on first bounce; bombs never leak into tennis (mode guard); 3 HUD pips; no console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
ad4c8041b9
commit
06025bdccf
6 files changed
+67
-5
No files matched your search
+26
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
import { TUNE, COURT } from './config.js';
|
||||
import { G, P, ball, sendBoy } from './state.js';
|
||||
import { splat, moan, pock, voice } from './audio.js';
|
||||
import { splat, moan, pock, voice, boom } from './audio.js';
|
||||
import { splatPoint, cheer } from './gameplay.js';
|
||||
|
||||
export const zombies = []; // {x, z, v, walkT, variant: 'a'|'b'|'c'}
|
||||
@@ -43,6 +43,27 @@ function splatter(i) {
|
||||
if (ball.kills === 2) { G.flash = { text: 'DOUBLE KILL', t: 0.9 }; cheer(1.6, 0.45); voice('double_kill', 0.4); }
|
||||
else if (ball.kills === 3) { G.flash = { text: 'TRIPLE KILL', t: 0.9 }; cheer(2.2, 0.6); voice('triple_kill', 0.4); }
|
||||
else if (ball.kills >= 4) { G.flash = { text: `MASSACRE ×${ball.kills}`, t: 0.9 }; cheer(2.8, 0.7); voice('unbelievable', 0.4); }
|
||||
// bomb-ball reward: every bombEvery kills a boy delivers one (up to bombMax in stock)
|
||||
if (++G.killMeter >= TUNE.bombEvery) {
|
||||
G.killMeter = 0;
|
||||
if (G.bombs < TUNE.bombMax) { G.bombs++; sendBoy(); voice('new_balls', 0.2); G.flash = { text: 'BOMB BALL', t: 0.9 }; }
|
||||
}
|
||||
}
|
||||
|
||||
// bomb ball: everything inside bombRadius splatters at once (each kill still ++ball.kills so
|
||||
// the Phase-2 fanfare escalates), plus a crater — big stain, size-3 pile, extra particles, boom.
|
||||
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.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);
|
||||
else { piles.push({ x, z, size: 3, seed: Math.random() * 1e4, blobs: null, builtSize: 0 }); if (piles.length > 30) piles.shift(); }
|
||||
for (let k = 0; k < 40; k++) parts.push({
|
||||
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();
|
||||
}
|
||||
|
||||
export function updateZombies(dt) {
|
||||
@@ -77,6 +98,10 @@ export function updateZombies(dt) {
|
||||
if (ball.active && ball.lastHit === 'P' && ball.y < 1.9) {
|
||||
const d = Math.hypot(ball.x - zb.x, ball.z - zb.z);
|
||||
if (d < 0.55) {
|
||||
if (ball.bomb) { // detonate on first contact, whatever the pace
|
||||
explode(ball.x, ball.z); ball.active = false; ball.bomb = false;
|
||||
G.phase = 'over'; G.overT = TUNE.splatterOverT; break; // zombies mutated — stop this frame's loop
|
||||
}
|
||||
if ((ball.pow || 0) >= TUNE.zSplatQ) { splatter(i); continue; }
|
||||
zb.z -= TUNE.zPushback; // weak sitters only stagger them
|
||||
pock(0.6, 300);
|
||||
|
||||
Reference in new issue
Block a user