Burn them to ashes, and give the burst back its bar

A burnt body does not burst. Fire kills by cooking, so there is nothing left to
spray: a body that dies alight -- flamethrower or flame ball -- leaves dark grey
ash instead of blood. Same decal, mound and particles, recoloured and smaller,
with a puff of smoke and a dull thud instead of the wet splat and the headshot
crack. The flame BALL has to mark itself, because that kill lands before the body
ever catches.

The flamethrower is a lighter, not a gun: no impact damage at all, it pierces,
and it only IGNITES -- the burn does the killing, so a lit crowd walks a few
steps and goes down together. 26 m of reach against a 22.8 m court, so it
crosses to the far baseline. The guard matters: hp -= 0 fell straight through to
killZombie, which made the cone delete the crowd on contact.

Every burst weapon had no progress bar and the cause was the spend. Arming spent
the tin at once, so the rack entry vanished -- and the bar draws on the slot
holding G.gunId, which no longer existed. The tin is spent when the burst ENDS
now, so icon, count and bar are all there while it runs, and every path that
ends a burst routes through endBurst or it would hand out free ammo.

Auto-fire movement again: the previous fix read moving as mx||mz||assist, and
finger-steering zeroes mx/mz, so sliding left and right still counted as standing
still. And the oval on his back was the mg racket -- its head projected almost
exactly onto the sprite; offset by facing it reads as held out beside him.

The laser's far edge is pinned in SCREEN space to the middle of the ad strip
rather than running out to beamLen on the ground, which landed on their top line.
beamLen goes back to being the kill range instead of a number tuned to one camera.

Beacon thumps, rocket swooshes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-03 15:05:56 +02:00
1 parent 91053b4c35
commit 6e88fd5bb9
6 files changed
+128 -30

No files matched your search

+21 -7
View File
@@ -592,7 +592,7 @@ export function startDuel() {
if (!zombies.some(z => z.variant === 'r')) spawnRacketZombie(); // the schedule guarantees an opponent
for (const z of zombies) if (z.variant === 'r') z.dueled = true; // a veteran can be challenged again — this marks the fresh duel
G.weapon = 0; // tennis rules for the duel: plain balls, no tins
G.mgT = 0; pellets.length = 0; // ...and a burst in progress is over. The umpire has restored order.
G.mgT = 0; endBurst(); pellets.length = 0; // ...and a burst in progress is over. The umpire has restored order.
G.interlude = { introT: 2.4 }; // the intro beat: serving blocked while the umpire announces
G.boy = null; // ...and the mop boy is waved off the court (sendBoy is barred for the duration)
ball.active = false; setupPoint();
@@ -614,7 +614,7 @@ export function startBossFight(chapter) {
if (G.bossFight) return;
G.bossFight = chapter;
zombies.length = 0; pellets.length = 0; extras.length = 0; shots.length = 0; // a clean stage
G.horde = null; G.mgT = 0; G.clearing = false;
G.horde = null; G.mgT = 0; endBurst(); G.clearing = false;
ball.active = false; setupPoint();
spawnChapterBoss(chapter);
zombieDuck(true); // the music drops out for it
@@ -934,7 +934,10 @@ function tryArmGun() {
if (!w || w.kind !== 'burst' || slotStock(G.weapon) <= 0) return false;
G.gunId = id;
G.mgT = w.len ?? TUNE.mgPickupLen;
spendSlot(G.weapon);
// The tin is spent when the burst ENDS, not when it starts. Spending on commit removed the rack
// entry immediately, so the countdown bar — which draws on the slot holding G.gunId — had no slot
// to draw on and every burst weapon ran with no progress bar at all. Oliver spotted it on the
// flamethrower; it was true of all four.
G.mgCool = 0;
ball.active = false; G.phase = 'rally';
P.pose = 'idle'; P.poseT = 0; P.swingT = -1;
@@ -953,6 +956,15 @@ export function serveToss() {
if (score.server === 'P' && G.phase === 'pretoss' && !ball.active) tossBall(P);
}
// The burst is over: NOW the tin is spent. Called from updateZombies when mgT runs out.
export function endBurst() {
const id = G.gunId;
G.gunId = null;
if (!id) return;
const slot = [0, 1, 2, 3, 4].find(i => slotId(i) === id);
if (slot !== undefined) spendSlot(slot);
}
// The laser fired from a TAP (the keyboard/two-stage path). The drag path solves its own direction
// and goes through hitServe; this one has no arc to read a lane off, so it takes the held flick aim
// if there is one and otherwise fires straight up-court.
@@ -1168,12 +1180,11 @@ export function update(dt) {
// to what the camera can see (portrait's world bounds are wider than its viewport)
const mgMode = G.horde || G.mgT > 0;
const xVis = Math.min(P_BOUNDS.x, (W / 2 - 14) / (TUNE.camF / (TUNE.camZ - P.z)));
let mgSteer = false;
let mgSteer = false, mgMoving = false;
if (mgMode && touchPX() !== null) {
mgSteer = true;
const tx = Math.max(-xVis, Math.min(xVis, touchPX() * xVis));
if (Math.abs(tx - P.x) > 0.04) { P.facing = tx > P.x ? 1 : -1; P.runT += dt; P.pose = 'run'; }
else P.pose = 'idle';
if (Math.abs(tx - P.x) > 0.04) { P.facing = tx > P.x ? 1 : -1; mgMoving = true; }
P.x = tx;
}
const slow = P.swingT >= 0 ? 0.35 : 1;
@@ -1202,7 +1213,10 @@ export function update(dt) {
const xLim = Math.max(xVis, COURT.W / 2);
P.x = Math.max(-xLim, Math.min(xLim, P.x));
P.z = Math.max(P_BOUNDS.zMin, Math.min(P_BOUNDS.zMax, P.z));
const moving = mx || mz || assist !== 0;
// mgMoving MUST be in here. Finger-steering zeroes mx/mz (they are the keyboard axes), so a
// player sliding left and right under auto-fire counted as standing still and never ran —
// "when he's moving left and right the run animation is not applied".
const moving = mgMoving || mx || mz || assist !== 0;
P.runT += moving ? dt : 0;
// Auto-fire used to be excluded here, which left the pose stuck on 'idle' — and render then
// faked a run cycle off G.idleT so he was not a statue. That fake is what read as "this hectic