Revert splatter rapid-fire serve; fix horde pose + re-trigger

Oliver's feedback on the feel pass:

1. Rapid-fire serving was way too easy. Reverted splatter serving to the
   proper model: a tap does the toss ritual (apex timing = quality), a
   flick is a quick aimed serve. Dropped the multi-ball barrage and
   removed the extraBalls plumbing (array, update, render, hooks). Melee
   swish and zombie homing stay. If multi-ball is wanted back, it should
   require a proper serve per ball.
2. The player was stuck in the serve/swing pose when a horde started
   (the triggering shot's pose lingered) — startHorde now clears it.
3. A horde re-triggered immediately: machine-gun kills scored points, so
   games were won mid-horde and the next kill after it ended won another
   game → instant new horde. A horde is now survival: it doesn't score
   (splatPoint skipped while G.horde), so it can't chain. After a horde
   you must win a full game again to summon the next.

Verified via window.T: a tap tosses (not an instant serve) and the full
toss→serve→rally completes; melee still kills adjacent shamblers; pose is
idle/-1 at horde start; no scoring during a horde and the next kill after
scores 15 without restarting it; tennis 11/12, bomb consume, homing all
still green; 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-12 00:28:10 +02:00
1 parent 38035738ac
commit bfaafe6727
4 files changed
+25 -49

No files matched your search

+18 -15
View File
@@ -4,7 +4,7 @@ import { TUNE, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DE
import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy } from './state.js';
import { audio, pock, swish, applause, voice } from './audio.js';
import { axisX, axisZ, takeFlickAim } from './input.js';
import { updateZombies, spawnZombie, zombies, explode, meleeSwipe, stashExtraBall } from './zombies.js';
import { updateZombies, spawnZombie, zombies, explode, meleeSwipe } from './zombies.js';
// ------------------------------------------------------------- ballistics ----
// Launch ball from (x0,y0,z0) so it lands at (tx, tz) with horizontal speed s under gravity g.
@@ -197,6 +197,7 @@ 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
P.pose = 'idle'; P.poseT = 0; P.swingT = -1; // clear the triggering shot's serve/swing pose
voice('remain_seated', 0.3);
G.flash = { text: 'THEY KEEP COMING', t: 1.2 };
}
@@ -375,22 +376,24 @@ function trySwing(ch, isAI) {
export function keyPressed(code) {
if (code !== 'Space') return;
audio(); // unlock on first gesture
if (G.mode === 'splatter') {
// splatter is a shooter: every swish cleaves adjacent shamblers, and — outside a horde —
// fires a ball immediately. Serving while one still flies stashes it, so you can barrage.
meleeSwipe(P.x, P.z, TUNE.meleeRange);
if (G.horde) return; // horde: MG auto-fires, the tap only swishes (no serve)
if (ball.active) stashExtraBall(); // keep the in-flight ball alive
const flick = takeFlickAim(); // flick aims; a plain tap auto-targets the nearest zombie
ball.active = true; ball.collected = false; ball.bounces = 0; ball.firstLand = null;
ball.x = P.x + 0.35; ball.z = P.z; ball.y = 2.2; ball.g = TUNE.gravity;
hitServe(P, 0.62, flick);
return;
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
// 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') {
if (G.mode === 'splatter') {
const flick = takeFlickAim();
if (flick) { // flick = a quick aimed serve; a plain tap still does the full toss ritual
ball.active = true; ball.collected = false; ball.bounces = 0; ball.firstLand = null;
ball.x = P.x + 0.35; ball.z = P.z; ball.y = 2.2; ball.g = TUNE.gravity;
hitServe(P, 0.62, flick);
return;
}
}
tossBall(P); return;
}
// tennis: the toss ritual + rally swing (unchanged)
if (G.phase === 'pretoss' && score.server === 'P') { tossBall(P); return; }
if (G.phase === 'toss' && score.server === 'P') {
// serve timing: distance from apex = quality
// serve timing: distance from apex = quality — a real serve, not a free tap
const dy = Math.abs(ball.y - serveApexY());
if (ball.y < 2.0) return; // too low, let it drop (re-toss)
let q = Math.max(0.05, 1 - dy / 0.9);