Phase 7: the scoreboard is the progression — spawn director, changeovers, sets, permadeath
- Spawn director: gap and court cap scale with games finished (runGames); game 1 is three lonely shamblers, late runs approach zSpawnMin/zMaxCap - Changeover after odd games: 4.5s forced breather, no spawns, no serving (melee still works), boys mop, umpire calls 'Time.' (new time.m4a) - 6 games = a set; set end launches the horde (replaces the splatGames>=2 trigger) - Zombies taking a game sets shameGame (phase 9 withholds the delivery) - Eaten = the run is over: GAME, SET, MASSACRE summary with games/sets survived and best run (localStorage); HUD shows sets in splatter (SETS·GAMES PTS) - spentBall() helper so dead-ball paths can't clobber a changeover's beat - T surface: splatPoint + explode exposed for verify scripts Verified: game-1 cap 3 held 25s; changeover blocks taps+spawns then resumes; 6 games -> set+horde -> racket zombie; runGames=6 tempo ~6 spawns/10s; eaten writes best; tennis regression 11/12 CPU returns; no console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
25db84ed87
commit
5a7f71fdba
7 files changed
+80
-27
No files matched your search
+40
-7
@@ -180,21 +180,46 @@ function pointTo(side, reason) {
|
||||
|
||||
// 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.
|
||||
// The scoreboard IS the progression: games drive spawn pressure, 6 games = a set = a horde,
|
||||
// odd games end in a changeover breather.
|
||||
export function splatPoint(side, quiet = false) {
|
||||
const gameOver = advancePoint(side);
|
||||
if (gameOver) {
|
||||
G.runGames++;
|
||||
if (side === 'O') G.shameGame = true; // they took a game — Oliver pays at the next new-balls delivery
|
||||
voice(side === 'P' ? 'game_set_massacre' : 'game_opponent', 0.5); // game-win lines always speak
|
||||
cheer(2.2, 0.5);
|
||||
if (side === 'P' && ++G.splatGames >= 2) startHorde(); // 2nd splatter game (and every one after): the horde
|
||||
const i = side === 'P' ? 0 : 1;
|
||||
if (score.games[i] >= 6) { // set to the winner — and the court answers with a horde
|
||||
G.sets[i]++; score.games = [0, 0];
|
||||
startHorde();
|
||||
} else if ((score.games[0] + score.games[1]) % 2 === 1) startChangeover(); // real rule: after odd games
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
// changeover (after odd games, as the rules demand): a forced breather — no spawns,
|
||||
// no serving, the boys mop. Zombies already on court keep walking; it's calm, not safe.
|
||||
function startChangeover() {
|
||||
G.changeover = true;
|
||||
G.phase = 'over'; G.overT = TUNE.changeoverT;
|
||||
sendBoy();
|
||||
G.flash = { text: 'CHANGEOVER', t: 1.4 };
|
||||
}
|
||||
|
||||
// retire the splatter ball; a running changeover's long beat must not be clobbered short
|
||||
export function spentBall() {
|
||||
ball.active = false;
|
||||
G.phase = 'over';
|
||||
if (!G.changeover) G.overT = TUNE.splatterOverT;
|
||||
}
|
||||
|
||||
// the swarm keeps coming: no serving, the racket auto-fires, the thumb drag steers you
|
||||
function startHorde() {
|
||||
if (G.horde) return;
|
||||
G.changeover = false; // a cheat-forced horde mid-changeover must not leave spawns locked
|
||||
G.horde = { t: TUNE.hordeLen, spawnT: 0, mgT: 0 };
|
||||
ball.active = false; G.phase = 'rally'; // ball gone; 'rally' lets the movement block steer P
|
||||
P.pose = 'idle'; P.poseT = 0; P.swingT = -1; // clear the triggering shot's serve/swing pose
|
||||
@@ -246,8 +271,8 @@ function onBounce() {
|
||||
ball.bounces++;
|
||||
pock(0.35, 500);
|
||||
if (G.mode === 'splatter' && ball.bomb && ball.bounces === 1) { // a bomb that missed everyone still blows up
|
||||
explode(ball.x, ball.z); ball.active = false; ball.bomb = false;
|
||||
G.phase = 'over'; G.overT = TUNE.splatterOverT; return;
|
||||
explode(ball.x, ball.z); ball.bomb = false;
|
||||
spentBall(); return;
|
||||
}
|
||||
if (G.mode !== 'tennis') return; // no lines, no faults — the umpire has other concerns
|
||||
if (ball.bounces === 1) {
|
||||
@@ -401,6 +426,7 @@ export function keyPressed(code) {
|
||||
audio(); // unlock on first gesture
|
||||
if (G.mode === 'splatter') meleeSwipe(P.x, P.z, TUNE.meleeRange); // a swish cleaves adjacent shamblers
|
||||
if (G.horde) return; // horde: the racket auto-fires; a tap only swishes, never serves
|
||||
if (G.changeover) return; // the breather is mandatory — the melee swish above still works
|
||||
// splatter: a tap/flick during the brief 'over' beat re-arms the next serve immediately
|
||||
if (G.mode === 'splatter' && G.phase === 'over') { ball.active = false; setupPoint(); }
|
||||
if (G.phase === 'pretoss' && score.server === 'P') {
|
||||
@@ -558,14 +584,15 @@ export function update(dt) {
|
||||
if (ball.active && Math.abs(ball.vy) < 0.8) { // rolled out — dead ball
|
||||
ball.active = false;
|
||||
if (G.mode === 'splatter') { // hand the server a fresh one, fast — they're coming
|
||||
G.phase = 'over'; G.overT = TUNE.splatterOverT; sendBoy();
|
||||
spentBall(); sendBoy();
|
||||
if (Math.random() < 0.12) voice('new_balls', 0.3);
|
||||
} else if (ball.bounces === 1) pointTo(ball.lastHit, null);
|
||||
}
|
||||
}
|
||||
// a racket zombie's return that gets past you (crosses your baseline) is a point for THEM
|
||||
if (G.mode === 'splatter' && ball.active && ball.lastHit === 'O' && ball.z > COURT.L + 0.3) {
|
||||
ball.active = false; if (!G.horde) splatPoint('O'); G.phase = 'over'; G.overT = TUNE.splatterOverT;
|
||||
if (!G.horde) splatPoint('O'); // may start a changeover — spentBall respects its beat
|
||||
spentBall();
|
||||
}
|
||||
// splatter: retire spent or hopeless balls fast so the next serve comes right up.
|
||||
// z-bounds gate on travel direction — a serve born behind the baseline (P.z up to 26)
|
||||
@@ -574,7 +601,7 @@ export function update(dt) {
|
||||
((ball.lastHit === 'X' && ball.bounces >= 1)
|
||||
|| (ball.z < -2 && ball.vz < 0) || (ball.z > 25.5 && ball.vz > 0)
|
||||
|| Math.abs(ball.x) > 7.5)) {
|
||||
ball.active = false; G.phase = 'over'; G.overT = TUNE.splatterOverT;
|
||||
spentBall();
|
||||
}
|
||||
if (G.phase === 'toss' && ball.y <= 1.4 && ball.vy < 0) { // uncaught toss → retoss
|
||||
ball.active = false; G.phase = 'pretoss';
|
||||
@@ -595,5 +622,11 @@ export function update(dt) {
|
||||
}
|
||||
if (G.outMarker && (G.outMarker.t -= dt) <= 0) G.outMarker = null;
|
||||
|
||||
if (G.phase === 'over') { G.overT -= dt; if (G.overT <= 0) setupPoint(); }
|
||||
if (G.phase === 'over') {
|
||||
G.overT -= dt;
|
||||
if (G.overT <= 0) {
|
||||
if (G.changeover) { G.changeover = false; voice('time', 0, 0.95); } // "Time." — play resumes
|
||||
setupPoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user