Phase 5: horde waves + machine-gun racket

Winning your 2nd splatter game (G.splatGames) — or triple-tapping the
umpire in splatter (forceZombies now calls startHorde) — launches a 12s
horde (TUNE.hordeLen). During a horde: the match ball is gone and taps do
nothing; the racket auto-fires pellets straight up-court at TUNE.mgRate
(6/s, ±4° spread, 26 m/s, flat, 1.2s life) and the thumb DRAG steers you
(auto-assist disabled). Zombies pour in at TUNE.hordeRate (2.5/s) from the
far baseline and the sides, ignoring zMax; the speed ramp still applies.

Pellets live in a new zombies.js `pellets` array, kill on contact (always
splatter-quality). splatter() gains a fanfare flag — pellet kills make
gore/piles/points but skip the multi-kill streak so a horde doesn't spam
MASSACRE. splatPoint() gains a quiet flag: during a horde the umpire only
calls a score every >2s (game-win lines always speak); the HUD score keeps
ticking. Horde ends → setupPoint() resumes serving, voice('quiet_dark');
leftover zombies stay for you to mop up. Eaten stays lethal during hordes
(a zombie reaching you ends it and clears the swarm). Pellets render as
2×2 yellow tracers with a ground shadow; serve prompt suppressed while
G.horde. window.T exposes pellets + forceZombies.

NOTE for Oliver: per spec every game won after the 2nd triggers a horde,
and games won by the machine gun mid-horde count too, so play gets very
horde-heavy after game 2 — flag if you want a cooldown or a knob.

Verified via window.T: cheat + 2nd-game both launch the horde (1st does
not); ~29 spawns over exactly 12s; pellets stream (7 in flight) and clear
a lane, building piles/gore; 4 points scored but the umpire speaks once
(rate-limited); horde ends cleanly back to pretoss; eaten still fires
mid-horde; regression ladder green — tennis 11/12, Phase 1 flick-serve,
Phase 2 DOUBLE→TRIPLE, Phase 4 MASSACRE ×4; no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 4.8 committed 2026-07-11 22:26:22 +02:00
1 parent 06025bdccf
commit bf9553bd49
6 files changed
+84 -21

No files matched your search

+21 -8
View File
@@ -178,20 +178,32 @@ function pointTo(side, reason) {
G.serveNum = 1;
}
// splatter-mode scoring: points land mid-flight without interrupting play
export function splatPoint(side) {
// splatter-mode scoring: points land mid-flight without interrupting play.
// quiet (during a horde) rate-limits the umpire so he doesn't call every rapid-fire point.
export function splatPoint(side, quiet = false) {
const gameOver = advancePoint(side);
if (gameOver) {
voice(side === 'P' ? 'game_set_massacre' : 'game_opponent', 0.5);
voice(side === 'P' ? 'game_set_massacre' : 'game_opponent', 0.5); // game-win lines always speak
cheer(2.2, 0.5);
} else {
voice(scoreKey(), 0.45);
if (side === 'P') cheer(0.7, 0.25);
if (side === 'P' && ++G.splatGames >= 2) startHorde(); // 2nd splatter game (and every one after): the horde
} else if (!quiet || G.idleT - G.lastScoreT > 2) {
voice(scoreKey(), 0.45); G.lastScoreT = G.idleT;
if (side === 'P' && !quiet) cheer(0.7, 0.25);
}
}
// hidden shortcut: tapping the umpire three times summons them early
// the swarm keeps coming: no serving, the racket auto-fires, the thumb drag steers you
function startHorde() {
if (G.horde) return;
G.horde = { t: TUNE.hordeLen, spawnT: 0, mgT: 0 };
ball.active = false; G.phase = 'rally'; // ball gone; 'rally' lets the movement block steer P
voice('remain_seated', 0.3);
G.flash = { text: 'THEY KEEP COMING', t: 1.2 };
}
// hidden shortcut: tapping the umpire three times summons the horror early
export function forceZombies() {
if (G.mode === 'splatter') { startHorde(); return; } // in splatter the cheat forces a horde wave
if (G.mode !== 'tennis') return;
ball.active = false;
startTransition();
@@ -363,6 +375,7 @@ function trySwing(ch, isAI) {
export function keyPressed(code) {
if (code !== 'Space') return;
audio(); // unlock on first gesture
if (G.horde) return; // horde: the racket auto-fires, taps do nothing — the thumb drag steers you
// splatter: a tap/flick during the brief 'over' beat arms the next serve immediately —
// reset inline, then fall through to pretoss so the finger is the only cadence limit
if (G.mode === 'splatter' && G.phase === 'over') { ball.active = false; setupPoint(); }
@@ -421,7 +434,7 @@ export function update(dt) {
P.x += mx * TUNE.playerSpeed * slow * dt;
P.z += mz * TUNE.playerSpeed * slow * dt;
let assist = 0;
if (!mx && !mz && TUNE.autoAssist > 0 && ball.active) {
if (!mx && !mz && TUNE.autoAssist > 0 && ball.active && !G.horde) { // horde: you aim by hand, no assist
const chasing = ball.lastHit === 'O';
// no `slow` here: tapping must not freeze your legs, or anticipating a shot loses it
const rate = TUNE.playerSpeed * TUNE.autoAssist * dt * (chasing ? 1 : 0.55);