The reaction lands where the ball does

- praise was firing at RELEASE, half a second before impact (measured:
  32-frame flight, word on frame 0) — it now arms on the ball and pops
  on the first body it meets, or its first bounce
- DOUBLE KILL / MASSACRE / RETURNED rise off the thing that earned them,
  as world-anchored pop texts — the announcement band is for milestones
- the band got rows: a card can no longer print over the flash line
- GAME N flash deleted (the scoreboard bar already says it), CHANGEOVER
  folded into the countdown panel

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-08 20:50:46 +02:00
1 parent 9476f354c2
commit 1337048a85
7 files changed
+91 -30

No files matched your search

+17 -5
View File
@@ -21,13 +21,21 @@ export const extras = []; // triple-serve side balls {x, y, z, vx, vy, vz, pow,
// theirs and they hurt YOU. Swat one with a swing and it becomes an `extras` entry flying back
// up-court, which is how a parry turns into a real weapon for free.
export const shots = []; // {x, y, z, vx, vy, vz, g, t}
// world-anchored reaction pops: DOUBLE KILL rises off the corpse that earned it, PERFECT lands on
// the impact — the answer to "what did I just do" appears where you are already looking, not in
// the announcement band. Render draws them, gameplay's update ticks them, killed in resetZombies.
export const pops = []; // {x, y, z, text, col, t, T}
export function popText(x, y, z, text, col) {
pops.push({ x, y, z, text, col, t: 0.85, T: 0.85 });
if (pops.length > 10) pops.shift();
}
let spawnT = 1.5, moanT = 3;
// Every body, decal and projectile on the court, plus this module's own timers. Called by
// gameplay.restartRun — a restart is in-place now, not a page reload.
export function resetZombies() {
for (const a of [zombies, gore, piles, parts, pellets, extras, shots]) a.length = 0;
for (const a of [zombies, gore, piles, parts, pellets, extras, shots, pops]) a.length = 0;
spawnT = 1.5; moanT = 3; mgTick = 0; lastBarrel = null;
}
@@ -598,6 +606,7 @@ export function meleeSwipe(x, z, range) {
}
function splatter(i, fanfare = true) { // the MAIN ball hit a zombie
const zx = zombies[i].x, zy = zombieH(zombies[i]) * 0.9, zz = zombies[i].z; // for the pops — killZombie splices
killZombie(i, !!G.horde);
ball.vx *= TUNE.zPierce; ball.vz *= TUNE.zPierce; // keep enough pace to plow through the next
if (!fanfare) return; // machine-gun pellets splatter but skip the streak fanfare / bomb meter
@@ -608,9 +617,9 @@ function splatter(i, fanfare = true) { // the MAIN ball hit a zombie
G.hitstop = Math.max(G.hitstop, Math.min(0.09, 0.03 + 0.012 * ball.kills));
// escalating multi-kill fanfare — one shot, ≥2 zombies
// ...and the stat is banked on the TRANSITION to 2, so a massacre of six is one multi-kill
if (ball.kills === 2) { multiKill(); G.flash = { text: 'DOUBLE KILL', t: 0.9 }; cheer(1.6, 0.45); voice('double_kill', 0.4); }
else if (ball.kills === 3) { G.flash = { text: 'TRIPLE KILL', t: 0.9 }; cheer(2.2, 0.6); voice('triple_kill', 0.4); }
else if (ball.kills >= 4) { G.flash = { text: `MASSACRE ×${ball.kills}`, t: 0.9 }; cheer(2.8, 0.7); voice('unbelievable', 0.4);
if (ball.kills === 2) { multiKill(); popText(zx, zy, zz, 'DOUBLE KILL'); cheer(1.6, 0.45); voice('double_kill', 0.4); }
else if (ball.kills === 3) { popText(zx, zy, zz, 'TRIPLE KILL'); cheer(2.2, 0.6); voice('triple_kill', 0.4); }
else if (ball.kills >= 4) { popText(zx, zy, zz, `MASSACRE ×${ball.kills}`); cheer(2.8, 0.7); voice('unbelievable', 0.4);
G.slowT = Math.max(G.slowT, 0.35); } // a massacre borrows the boss's slow-mo — the infra already runs every frame
}
@@ -953,6 +962,9 @@ export function updateZombies(dt) {
CAUSE = ball.wid; // re-stamped per body: burnTick above shares this loop and moves it
const d = Math.hypot(ball.x - zb.x, ball.z - zb.z);
if (d < hitR(zb) + ballSweep && !(zb.hitT > 0)) { // brute and boss are bigger targets
// the serve's deferred praise pops on the FIRST body the ball meets — this is the impact
// the word was waiting for (armed in hitServe, see praiseWord)
if (ball.praise) { popText(zb.x, zombieH(zb) * 0.8, zb.z, ball.praise, UI.warm); ball.praise = null; }
if (ball.bomb) { // detonate on first contact, whatever the pace
explode(ball.x, ball.z); ball.bomb = false;
spentBall(); break; // zombies mutated — stop this frame's loop
@@ -1122,7 +1134,7 @@ export function updateZombies(dt) {
const sp = Math.hypot(s.vx, s.vz) * 1.15;
extras.push({ x: s.x, y: s.y, z: s.z, g: TUNE.gravity, pow: TUNE.parryPow, t: 2.5,
vx: Math.sin(ang) * sp, vy: 4.2, vz: -Math.cos(ang) * sp });
pock(0.85, 1300); G.flash = { text: 'RETURNED', t: 0.6 }; cheer(0.8, 0.3);
pock(0.85, 1300); popText(s.x, s.y + 0.4, s.z, 'RETURNED'); cheer(0.8, 0.3);
done = true;
}
if (!done && dHit < TUNE.shotHitR && s.y < TUNE.shotHitY) { hurtPlayer(TUNE.machDmg); done = true; }