Phase 6: racket zombies (rally mini-boss)

Survive a horde and a racket zombie takes up residence on the far court
(variant 'r', one per horde survived) — the tennis rally skill lives on
inside zombie mode. It reuses the turned-CPU z_c sprite (already holds a
racket) and a slimmed copy of the tennis AI: holds station deep, chases a
ball laterally at TUNE.racketSpeed (2.8), and returns your SOFT shots
(pow < zSplatQ) as live lastHit:'O' balls with quality capped at
racketQMax (0.55) and racketFlub (0.2) whiffs. trySwing gained an
optional quality-cap arg (defaulted, so tennis is untouched).

It's a mini-boss with TUNE.racketHp (3): only splat-quality balls hurt it
(-1 hp, staggers the ball; the killing blow runs the full splatter). Soft
balls pass through for it to rally; melee and pellets don't touch it. Its
return that gets past your baseline is a point for THEM; you can rally it
back with the existing player hit. A red HP bar floats over its head.

Verified via window.T: endHorde spawns exactly one (hp 3, z 3.5); three
splat hits kill it (2→1→dead); a pow-0.3 ball is returned toward you; an
'O' ball past your baseline scores for THEM; melee spares the boss but
cleaves the shambler beside it; tennis still 12/11/12 and a normal
shambler is still one-shot; renders with its HP bar; 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 09:18:43 +02:00
1 parent bfaafe6727
commit 41a6d2a034
4 files changed
+62 -6

No files matched your search

+16 -2
View File
@@ -78,8 +78,13 @@ function firePellet() {
pellets.push({ x: P.x, y: 0.9, z: P.z, vx: Math.sin(a) * s, vz: -Math.cos(a) * s, t: 1.2 });
pock(0.25, 1400); // rapid fire tick; if it clips on device, gate to every 2nd pellet
}
function spawnRacketZombie() { // one takes up residence on the far court each horde you survive
zombies.push({ x: (Math.random() * 2 - 1) * 2.5, z: 3.5, v: 0, walkT: Math.random() * 9,
variant: 'r', side: 'O', facing: 1, pose: 'idle', poseT: 0, swingT: -1, swingCool: 0, runT: 0, hp: TUNE.racketHp });
}
function endHorde() {
G.horde = null; pellets.length = 0;
spawnRacketZombie(); // survive the wave → a racket zombie stays to rally you
setupPoint(); // serve loop resumes; any zombies still standing are yours to mop up
voice('quiet_dark', 0.5, 0.85);
}
@@ -127,10 +132,11 @@ export function updateZombies(dt) {
const zb = zombies[i];
zb.walkT += dt;
zb.z += zb.v * dt;
if (G.mode === 'splatter' && zb.variant !== 'r' && zb.z > COURT.netZ) // on YOUR half they home in on you
if (zb.variant === 'r') { /* the racket boss holds station — its AI owns its x */ }
else if (G.mode === 'splatter' && zb.z > COURT.netZ) // on YOUR half they home in on you
zb.x += Math.sign(P.x - zb.x) * Math.min(Math.abs(P.x - zb.x), zb.v * 0.7 * dt);
else
zb.x += Math.sin(zb.walkT * 1.7) * 0.15 * dt; // far half / racket boss: aimless shamble
zb.x += Math.sin(zb.walkT * 1.7) * 0.15 * dt; // far half: aimless shamble
if (G.mode === 'splatter' && Math.hypot(zb.x - P.x, zb.z - P.z) < 0.7) { // it got you
gore.push({ x: P.x, z: P.z, r: 0.9, a: 0.9 });
for (let k = 0; k < 30; k++) parts.push({
@@ -150,6 +156,14 @@ export function updateZombies(dt) {
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 (zb.variant === 'r') { // mini-boss: splat-quality wounds it; soft balls it rallies (racketAI)
if ((ball.pow || 0) >= TUNE.zSplatQ) {
zb.hp--; splat(0.5); pock(0.6, 300);
ball.vx *= 0.3; ball.vz *= 0.3; ball.lastHit = 'X'; // spent on the hit
if (zb.hp <= 0) { splatter(i); continue; } // killing blow: full gore + point
}
continue; // soft balls pass through, for the AI to return
}
if ((ball.pow || 0) >= TUNE.zSplatQ) { splatter(i); continue; }
zb.z -= TUNE.zPushback; // weak sitters only stagger them
pock(0.6, 300);