Fix the freeze; duel intro card + tennis lights; hold-to-aim; instant gun steering

THE FREEZE (root cause found): the game-winning kill fires INSIDE the zombie
update loop — killZombie -> splatPoint -> gameOver -> sweepCourt guts the
zombies array mid-iteration, the loop reads zombies[i] === undefined, throws,
and the un-rescheduled RAF chain dies: a permanent hard freeze. Crowded courts
(later games, brutes) make the mid-loop game-winner likelier — hence 'freezes
around game 3 when the fat zombies arrive'. Fixed at the root (null guards in
every loop whose body can cascade into a sweep: zombie walk loop, meleeSwipe,
explode, pellet + extras inner loops) AND made frame() reschedule RAF FIRST so
no single bad frame can ever kill the loop again.

Aim awareness: the reticle now appears the moment the thumb lands (mag 0 =
auto-target the most advanced zombie, red lock immediately). A pure HOLD
(>250ms, no drag) releases into the auto-target serve; dragging aims as
before; the serve prompt switches to 'RELEASE TO SERVE / RELEASE FIRES THE
BOMB - DRAG BACK TO CANCEL' while aiming.

Duel presentation: the interlude now opens with a 2.4s intro beat — serving
blocked, centered card (SUDDEN DEATH / ONE POINT - THE LINES ARE LIVE / WIN
IT FOR THE NEXT TIN), and the palette lerps back to the TENNIS colors for
the duration of the duel (duelLerp, 0.8s each way) — order visibly restored,
then the rot returns.

Machine-gun steering: absolute now — the finger x IS the player x, zero lag
(was sign-quantized velocity). Clamped to what the camera can see
((W/2-14)/proj-scale — portrait's world bounds are wider than its viewport),
keyboard steering clamped the same during auto-fire.

Verified: the exact pre-fix crash scenario (game-winning kill mid-loop,
9 zombies + brute) survives and sweeps clean; intro blocks taps then the
duel plays; hold-release fired at the auto-lock and won a tin end-to-end;
screenshot shows tennis-green court + centered card + GAME 2; MG jumps
P.x to the finger in one frame, clamped; tennis 12/12; no console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-07-12 16:59:48 +02:00
1 parent 8cfe9fdbcd
commit e75b3888de
6 files changed
+69 -27

No files matched your search

+13 -3
View File
@@ -20,7 +20,8 @@ const hexLerp = (a, b, t) => {
return `rgb(${Math.round(ar + (br - ar) * t)},${Math.round(ag + (bg - ag) * t)},${Math.round(ab + (bb - ab) * t)})`;
};
function palette() {
const t = G.mode === 'tennis' ? 0 : G.mode === 'splatter' ? 1 : Math.min(1, G.transT / 6);
// splatter lerps back toward the tennis palette while a duel runs — order, restored
const t = G.mode === 'tennis' ? 0 : G.mode === 'splatter' ? 1 - G.duelLerp : Math.min(1, G.transT / 6);
if (t === 0) return PAL.tennis;
if (t === 1) return PAL.splatter;
const m = {};
@@ -302,7 +303,7 @@ export function render() {
drawRing(ft.zt.x, ft.zt.z, '#ff5050');
const hp = proj(ft.zt.x, 2.2, ft.zt.z);
ctx.fillStyle = '#ff5050'; ctx.fillRect(hp.x - 1, hp.y - 4, 3, 3);
} else drawRing(ft.tx, ft.tz, '#f8e048');
} else if (ft.tx !== undefined) drawRing(ft.tx, ft.tz, '#f8e048');
}
// far-side drawables in z order (far → near), then the net, then the near side
@@ -401,7 +402,8 @@ export function render() {
const touch = matchMedia('(pointer: coarse)').matches;
const stock = G.bombs ? 'BOMB' : G.triples ? 'TRIPLE' : G.flames ? 'FIRE' : null;
const txt = G.mode === 'splatter'
? (stock ? `AIMED SERVES FIRE THE ${stock} · TAP KEEPS IT`
? (G.aim ? (stock ? `RELEASE FIRES THE ${stock} · DRAG BACK TO CANCEL` : 'RELEASE TO SERVE · DRAG BACK TO CANCEL')
: stock ? `AIMED SERVES FIRE THE ${stock} · TAP KEEPS IT`
: touch ? 'TAP TO SERVE · HOLD + DRAG TO AIM' : 'SPACE SERVES · ARROWS AIM')
: G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE')
: (touch ? 'TAP TO TOSS, TAP TO SERVE' : 'SPACE TO TOSS, SPACE TO SERVE');
@@ -411,6 +413,14 @@ export function render() {
ctx.font = 'bold 14px monospace'; ctx.fillStyle = '#ffffff';
ctx.fillText(G.flash.text, W / 2, 60 + Math.sin(G.flash.t * 20) * 1);
}
if (G.interlude && G.interlude.introT > 0) { // the duel card: a real pause, front and center
ctx.fillStyle = 'rgba(10,6,8,0.66)'; ctx.fillRect(0, H / 2 - 36, W, 64);
ctx.fillStyle = '#ff5050'; ctx.font = 'bold 18px monospace';
ctx.fillText('SUDDEN DEATH', W / 2, H / 2 - 26);
ctx.fillStyle = '#e8e8dc'; ctx.font = 'bold 8px monospace';
ctx.fillText('ONE POINT · THE LINES ARE LIVE', W / 2, H / 2 - 2);
ctx.fillText('WIN IT FOR THE NEXT TIN', W / 2, H / 2 + 12);
}
if (TUNE.missHud && G.lastMiss && G.idleT - G.lastMiss.at < 3.5) { // last miss reason, live
ctx.font = 'bold 8px monospace'; ctx.fillStyle = '#ff9060';
ctx.fillText(G.lastMiss.text.toUpperCase(), W / 2, H - 28);