Splatter: clear the wave by hand, tougher 2-hit shamblers, set cards, free depth
Feedback batch from a phone playtest: - Winning a game no longer auto-sweeps the court. The survivors stay and must be killed by hand (no new spawns, no scoring during the mop-up); only once the court is clear does play advance to the changeover / next game / set. Replaces sweepCourt() with a G.clearing gate + proceedAfterClear(). "CLEAR THE COURT". - Classic shamblers (a/b) now soak one splat-quality wound before they drop (shamblerHp=2) — swapping to new z_aw/z_bw wound skins that bleed the thin dynamic trickle plus a small impact pop. Only the runner stays one-shot glass; brute/boss unchanged. Ball, pellets and triple side-balls all respect hp. - Winning a SET shows a centered milestone card (a real pause) before the horde; one reusable drawCard() now backs both it and the reworded sudden-death card. - Player is no longer pinned to the baseline: free to advance up-court on keyboard (bounded at the net); touch keeps the baseline (drag is the serve-aim) and the horde/GUN burst re-brace there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
13416ee749
commit
6df0ddb00f
10 files changed
+91
-42
No files matched your search
+42
-13
@@ -5,7 +5,7 @@ import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy } fr
|
||||
import { audio, pock, swish, applause, voice, sfx, aww, zombieMusic, zombieDuck } from './audio.js';
|
||||
import { axisX, axisZ, takeFlickAim, touchPX } from './input.js';
|
||||
import { W } from './render.js'; // runtime-only cycle (render imports gameplay), same pattern as input/zombies
|
||||
import { updateZombies, spawnZombie, spawnRacketZombie, sweepCourt, zombies, explode, meleeSwipe, extras, parts } from './zombies.js';
|
||||
import { updateZombies, spawnZombie, spawnRacketZombie, zombies, explode, meleeSwipe, extras, parts } from './zombies.js';
|
||||
|
||||
// ------------------------------------------------------------- ballistics ----
|
||||
// Launch ball from (x0,y0,z0) so it lands at (tx, tz) with horizontal speed s under gravity g.
|
||||
@@ -236,23 +236,47 @@ export function splatPoint(side, quiet = false) {
|
||||
G.tinDue = true;
|
||||
voice(side === 'P' ? 'game_set_massacre' : 'game_opponent', 0.5); // game-win lines always speak
|
||||
if (side === 'P') cheer(2.2, 0.5); else aww(0.85); // your game = applause, theirs = a groan
|
||||
sweepCourt(); // the wave is OVER: survivors collapse where they stand — a game ends visibly
|
||||
ball.active = false; // the beat owns the court
|
||||
G.pendingGame = true; // 'GAME N' announces the next wave when play resumes
|
||||
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 { G.phase = 'over'; G.overT = TUNE.gameBeatT; } // even games get a real beat too, not a 0.1s blink
|
||||
if (score.games[i] >= 6) { G.sets[i]++; score.games = [0, 0]; G.afterClear = 'set'; } // set banked; a horde follows the card
|
||||
else if ((score.games[0] + score.games[1]) % 2 === 1) G.afterClear = 'changeover'; // real rule: after odd games
|
||||
else G.afterClear = 'game'; // even game: a beat, then the next wave
|
||||
G.clearWinner = side;
|
||||
if (side === 'P' && !zombies.some(z => z.variant === 'r'))
|
||||
spawnRacketZombie(); // every game you win, an opponent takes the far court (rally it, or splat it)
|
||||
// The wave does NOT auto-collapse anymore: the survivors stay and must be cleared BY HAND before
|
||||
// play advances — only no NEW shamblers spawn. When the last one is down, proceedAfterClear()
|
||||
// fires (see update()). If the winning shot already emptied the court, proceed at once.
|
||||
if (zombies.some(z => z.variant !== 'r')) {
|
||||
G.clearing = true; ball.active = false;
|
||||
G.flash = { text: 'CLEAR THE COURT', t: 1.8 };
|
||||
G.phase = 'over'; G.overT = TUNE.gameBeatT; // a short beat to register the game, then the serve loop resumes for the mop-up
|
||||
} else proceedAfterClear();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
// the court is mopped up (or was already clear): play advances to whatever the won game earned —
|
||||
// a changeover breather, a plain even-game beat, or (on a set) the centered SET card then a horde.
|
||||
export function proceedAfterClear() {
|
||||
G.clearing = false;
|
||||
const mode = G.afterClear, side = G.clearWinner;
|
||||
G.afterClear = null;
|
||||
if (mode === 'set') {
|
||||
G.card = { title: 'SET ' + (G.sets[0] + G.sets[1]), // the set just banked (sets already ++'d at game over)
|
||||
sub1: side === 'P' ? 'THE SET IS YOURS' : 'SET TO THEM',
|
||||
sub2: 'SETS ' + G.sets[0] + ' – ' + G.sets[1],
|
||||
color: side === 'P' ? '#e6ee3a' : '#ff5050', t: TUNE.setCardT };
|
||||
G.pendingHorde = true; // the horde begins when the card's pause ends
|
||||
ball.active = false; G.phase = 'over'; G.overT = TUNE.setCardT;
|
||||
} else if (mode === 'changeover') {
|
||||
G.pendingGame = true; startChangeover(); // 'GAME N' after the breather
|
||||
} else { // 'game': even-game beat, then GAME N announces the next wave
|
||||
G.pendingGame = true; ball.active = false; G.phase = 'over'; G.overT = TUNE.gameBeatT;
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@@ -326,6 +350,7 @@ function startHorde() {
|
||||
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
|
||||
P.z = HOME.P.z; // brace at the baseline — the horde is a hold-the-line stand (finger steers x only)
|
||||
voice('remain_seated', 0.3);
|
||||
G.flash = { text: 'THEY KEEP COMING', t: 1.2 };
|
||||
}
|
||||
@@ -540,6 +565,7 @@ function tryGunBurst() {
|
||||
G.mgT = TUNE.mgPickupLen; G.mgCool = 0;
|
||||
ball.active = false; G.phase = 'rally';
|
||||
P.pose = 'idle'; P.poseT = 0; P.swingT = -1;
|
||||
P.z = HOME.P.z; // brace at the baseline for the burst (finger steers x only)
|
||||
G.flash = { text: 'GUN', t: 1.0 };
|
||||
return true;
|
||||
}
|
||||
@@ -614,6 +640,7 @@ export function update(dt) {
|
||||
return;
|
||||
}
|
||||
if (G.flash) { G.flash.t -= dt; if (G.flash.t <= 0) G.flash = null; }
|
||||
if (G.card) { G.card.t -= dt; if (G.card.t <= 0) G.card = 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);
|
||||
@@ -666,9 +693,9 @@ export function update(dt) {
|
||||
if (mx) P.facing = mx > 0 ? 1 : -1;
|
||||
else if (Math.abs(assist) > 0.25) P.facing = assist > 0 ? 1 : -1; // hysteresis: small corrections don't flip facing
|
||||
}
|
||||
// splatter: you hold the baseline (a turret) — only side-to-side, no drifting up-court.
|
||||
// The duel is exempt: it's tennis again, so depth is yours to chase the rally.
|
||||
if (G.mode === 'splatter' && !G.interlude) P.z = HOME.P.z;
|
||||
// splatter: you START at the baseline but you're free to advance up-court (bounded at the net by
|
||||
// P_BOUNDS) — hunt the shamblers down, especially when mopping up a won game. Auto-assist still
|
||||
// recovers you toward the baseline between shots on touch; the duel is plain tennis as ever.
|
||||
if (P.swingT >= 0) {
|
||||
P.swingT += dt;
|
||||
if (G.mode === 'tennis' && ball.active && ball.lastHit === 'O') { // sample the approach for the miss log (t = what trySwing tests this frame)
|
||||
@@ -685,6 +712,7 @@ export function update(dt) {
|
||||
|
||||
// --- zombies + transition timeline
|
||||
if (G.mode !== 'tennis') updateZombies(dt);
|
||||
if (G.clearing && !zombies.some(z => z.variant !== 'r')) proceedAfterClear(); // the won-game wave is mopped up — advance
|
||||
if (G.mode === 'transition') transition(dt);
|
||||
|
||||
// --- interlude trigger: the court is clear, an opponent holds the far court, you're about
|
||||
@@ -778,7 +806,7 @@ export function update(dt) {
|
||||
if (G.mode === 'splatter' && ball.active && ball.lastHit === 'O' && ball.z > COURT.L + 0.3) {
|
||||
ball.active = false;
|
||||
if (G.interlude) resolveInterlude('O'); // sudden death: its passing shot ends the duel
|
||||
else { if (!G.horde) splatPoint('O'); spentBall(); }
|
||||
else { if (!G.horde && !G.clearing) splatPoint('O'); spentBall(); } // clearing = mop-up, no scoring either way
|
||||
}
|
||||
// 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)
|
||||
@@ -813,6 +841,7 @@ export function update(dt) {
|
||||
if (G.phase === 'over') {
|
||||
G.overT -= dt;
|
||||
if (G.overT <= 0) {
|
||||
if (G.pendingHorde) { G.pendingHorde = false; startHorde(); return; } // the set card's pause is over — release the horde
|
||||
if (G.changeover) { G.changeover = false; voice('time', 0, 0.95); } // "Time." — play resumes
|
||||
// auto-fire modes must NEVER re-enter the serve loop: an 'over' beat expiring into
|
||||
// setupPoint() -> pretoss left the player locked in the serve posture while the gun
|
||||
|
||||
Reference in new issue
Block a user