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
+23
-19
@@ -22,6 +22,7 @@ export function spawnZombie(x, z, variant) {
|
||||
const zb = { x, z, v: TUNE.zSpeed * ramp * (0.8 + Math.random() * 0.4), walkT: Math.random() * 9,
|
||||
homeOff: (Math.random() * 2 - 1) * 0.8, variant }; // personal lane offset so they don't stack into a column
|
||||
if (variant === 'f') { zb.v = Math.min(TUNE.runnerVMax, zb.v * TUNE.runnerSpeed); shriek(); } // shriek = the fairness contract; capped so late runners stay reactable
|
||||
if (variant === 'a' || variant === 'b') zb.hp = TUNE.shamblerHp; // classic shamblers take a wound before they drop — only the runner is one-shot glass
|
||||
if (variant === 'g') { zb.v *= 0.55; zb.hp = TUNE.bruteHp; } // slow, but soaks hits
|
||||
if (variant === 's') zb.shieldT = 0; // chair up; > 0 = seconds of stagger left
|
||||
zombies.push(zb);
|
||||
@@ -67,22 +68,16 @@ function killZombie(i, quiet) {
|
||||
splat();
|
||||
sfx('headshot-' + (Math.random() < 0.5 ? 1 : 2), 0.6); // random headshot crack layered on the gore burst
|
||||
sendBoy(zb.x, zb.z); // a boy sprints out to mop
|
||||
if (!G.horde) splatPoint('P', quiet); // a horde is survival — it doesn't score, so it can't chain into itself
|
||||
if (!G.horde && !G.clearing) splatPoint('P', quiet); // horde = survival, clearing = mopping up an already-won game — neither scores
|
||||
}
|
||||
|
||||
// game over: the surviving wave collapses where it stands — no points, no piles, just bodies.
|
||||
// This is what makes a game END visibly instead of bleeding into the next one.
|
||||
export function sweepCourt() {
|
||||
let n = 0;
|
||||
for (let i = zombies.length - 1; i >= 0; i--) {
|
||||
const zb = zombies[i];
|
||||
if (zb.variant === 'r') continue; // the opponent keeps its residence
|
||||
zombies.splice(i, 1); n++;
|
||||
for (let k = 0; k < 6; k++) parts.push({
|
||||
x: zb.x, y: 0.5, z: zb.z, t: 0.4, dark: k % 2 === 0,
|
||||
vx: (Math.random() * 2 - 1) * 1.5, vy: 0.4 + Math.random(), vz: (Math.random() * 2 - 1) * 1.5 });
|
||||
}
|
||||
if (n) splat(0.4);
|
||||
// a small dark spatter the instant a multi-hit shambler is wounded — subtle impact pop, NOT the
|
||||
// kill spray (the ongoing thin trickle is bleed()'s job). Kept low per the settled blood feel.
|
||||
function woundBurst(zb) {
|
||||
for (let k = 0; k < 5; k++) parts.push({
|
||||
x: zb.x + (Math.random() * 2 - 1) * 0.15, y: 1.4, z: zb.z,
|
||||
vx: (Math.random() * 2 - 1) * 1.6, vy: 0.6 + Math.random() * 1.4, vz: (Math.random() * 2 - 1) * 1.6,
|
||||
t: 0.35 + Math.random() * 0.3, dark: Math.random() < 0.6 });
|
||||
}
|
||||
|
||||
// swishing the racket cleaves any shambler in reach (the racket-zombie boss shrugs melee off)
|
||||
@@ -164,6 +159,7 @@ function bleed(zb) {
|
||||
if (zb.variant === 'g' && zb.hp < TUNE.bruteHp) wounds = [[-0.3, 1.95, 1.1]]; // brute: torn-off arm
|
||||
else if (zb.variant === 'r' && zb.hp <= 1) wounds = [[0.05, 1.5, 2.4], [0.4, 1.15, 0.9]]; // headless: neck spurt + torn arm
|
||||
else if (zb.variant === 'r' && zb.hp === 2) wounds = [[0.4, 1.2, 1.0]]; // racket: torn-off arm
|
||||
else if ((zb.variant === 'a' || zb.variant === 'b') && zb.hp < TUNE.shamblerHp) wounds = [[0.28, 1.35, 0.85]]; // shambler: gashed shoulder
|
||||
else return;
|
||||
for (const w of wounds) {
|
||||
if (Math.random() > 0.4) continue; // sparse: most frames this wound emits nothing
|
||||
@@ -186,7 +182,7 @@ export function updateZombies(dt) {
|
||||
if ((G.mgCool -= dt) <= 0) { firePellet(); G.mgCool = 1 / TUNE.mgRate; }
|
||||
if (G.mgT <= 0) setupPoint(); // ammo dry — the serve loop resumes
|
||||
}
|
||||
if (!G.horde && !G.changeover && !G.interlude) { // breathers and duels get a quiet court
|
||||
if (!G.horde && !G.changeover && !G.interlude && !G.clearing) { // breathers, duels AND the mop-up after a won game get a quiet court (no new spawns)
|
||||
spawnT -= dt;
|
||||
// the spawn director: the match's age (games finished) sets both crowd size and tempo
|
||||
const cap = Math.min(TUNE.zMaxCap, TUNE.zMax + G.runGames);
|
||||
@@ -199,7 +195,7 @@ export function updateZombies(dt) {
|
||||
|
||||
for (let i = zombies.length - 1; i >= 0; i--) {
|
||||
const zb = zombies[i];
|
||||
if (!zb) continue; // a kill can cascade into sweepCourt (game won mid-loop) and gut the array under us
|
||||
if (!zb) continue; // a bomb detonation splices several out mid-loop and can gut the array under us
|
||||
zb.walkT += dt;
|
||||
zb.z += zb.v * dt;
|
||||
bleed(zb); // open wounds bleed every frame they're standing
|
||||
@@ -270,6 +266,14 @@ export function updateZombies(dt) {
|
||||
continue;
|
||||
}
|
||||
if (zb.variant === 'f') { splatter(i); continue; } // the sprinter is GLASS — any touch, any quality, it bursts
|
||||
if (zb.variant === 'a' || zb.variant === 'b') { // classic shamblers soak a splat-quality wound before they drop
|
||||
if ((ball.pow || 0) >= TUNE.zSplatQ) {
|
||||
if (--zb.hp <= 0) { splatter(i); continue; } // the second splat is the killing blow
|
||||
woundBurst(zb); splat(0.5); pock(0.6, 280); // first splat: a bloody wound, not a death
|
||||
zb.z -= TUNE.zPushback * 0.5; ball.vx *= 0.3; ball.vz *= 0.3; ball.lastHit = 'X'; continue; // ball spent on the wound
|
||||
}
|
||||
zb.z -= TUNE.zPushback; pock(0.6, 300); ball.vx *= 0.6; ball.vz *= 0.6; ball.lastHit = 'X'; continue; // soft ball just staggers
|
||||
}
|
||||
// chair-down shield zombies (and anything hit from behind) obey the normal rules:
|
||||
if ((ball.pow || 0) >= TUNE.zSplatQ) { splatter(i); continue; }
|
||||
zb.z -= TUNE.zPushback; // weak sitters only stagger them
|
||||
@@ -277,7 +281,7 @@ export function updateZombies(dt) {
|
||||
ball.vx *= 0.6; ball.vz *= 0.6; ball.lastHit = 'X'; // spent — one stagger per ball
|
||||
}
|
||||
}
|
||||
if (zb.z >= COURT.L + 0.7) { zombies.splice(i, 1); if (G.mode !== 'eaten' && !G.horde) splatPoint('O'); } // it reached your baseline
|
||||
if (zb.z >= COURT.L + 0.7) { zombies.splice(i, 1); if (G.mode !== 'eaten' && !G.horde && !G.clearing) splatPoint('O'); } // it reached your baseline (during a mop-up, a straggler that reaches the line just despawns — no point for them)
|
||||
}
|
||||
|
||||
for (let i = pellets.length - 1; i >= 0; i--) { // machine-gun rounds: flat, fast, one-shot kills
|
||||
@@ -288,7 +292,7 @@ export function updateZombies(dt) {
|
||||
const zb = zombies[j];
|
||||
if (!zb || zb.variant === 'r') continue;
|
||||
if (Math.hypot(zb.x - pe.x, zb.z - pe.z) < (zb.variant === 'g' ? 0.75 : 0.55)) {
|
||||
if (zb.variant === 'g' && --zb.hp > 0) pock(0.5, 260); // the brute soaks pellets too
|
||||
if (zb.hp > 1) { zb.hp--; pock(0.5, 260); } // brutes AND shamblers soak pellets — wound, don't kill
|
||||
else killZombie(j, true);
|
||||
hit = true; break;
|
||||
}
|
||||
@@ -308,7 +312,7 @@ export function updateZombies(dt) {
|
||||
if (Math.hypot(zb.x - e.x, zb.z - e.z) < (zb.variant === 'g' ? 0.75 : 0.55)) {
|
||||
if (zb.variant === 's' && e.vz < 0 && !(zb.shieldT > 0)) { zb.shieldT = TUNE.shieldDown; pock(0.9, 1800); } // chair blocks these too
|
||||
else if (e.pow >= TUNE.zSplatQ || zb.variant === 'f') {
|
||||
if (zb.variant === 'g' && --zb.hp > 0) splat(0.4); // wounds the brute
|
||||
if (zb.hp > 1) { zb.hp--; splat(0.4); } // wounds brutes AND shamblers
|
||||
else killZombie(j, !!G.horde);
|
||||
} else { zb.z -= TUNE.zPushback; pock(0.5, 300); }
|
||||
dead = true; break; // a side ball is spent on its first touch
|
||||
|
||||
Reference in new issue
Block a user