Stop bodies blinking on their way in

They spawn BEHIND the far baseline, at negative z, and the frames that step off
distance walked used a plain %, which keeps the sign of the dividend:
floor(-1.5 / 0.25) % 4 is -2, so the pick asked for 'z_g-1'. drawStatic draws
nothing for a sprite that does not exist, so the body flickered until it walked
past z=0 — worst on exactly the ones Oliver noticed, because only the heavy
bodies step off distance. The chapter boss had it too, spawning at z=-1.2 and
falling through to an undefined frame name.

One positive-modulo helper at the four picks. Verified across the whole approach
from z=-1.6: every frame resolves to a real sprite now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-31 19:47:38 +02:00
1 parent 85e08ab4b8
commit a7835d367c
2 files changed
+10 -5

No files matched your search

+1 -1
View File
@@ -97,6 +97,6 @@
<p id="build"></p> <p id="build"></p>
</div> </div>
</div> </div>
<script type="module" src="main.js?v=58"></script> <script type="module" src="main.js?v=59"></script>
</body> </body>
</html> </html>
+9 -4
View File
@@ -258,6 +258,11 @@ function drawBoy(b) {
ctx.drawImage(img, -w / 2, -h, w, h); ctx.drawImage(img, -w / 2, -h, w, h);
ctx.restore(); ctx.restore();
} }
// Frames stepped by DISTANCE WALKED need a POSITIVE modulo. Bodies spawn BEHIND the far baseline at
// negative z, and JS keeps the sign of the dividend: floor(-1.5 / 0.25) % 4 is -2, so the pick asked
// for 'z_g-1' — a sprite that does not exist, and drawStatic silently draws nothing. That is the
// blinking Oliver saw on the way in, on exactly the bodies that step off distance: brutes and bosses.
const stepFrame = (v, n) => ((Math.floor(v) % n) + n) % n; // 0..n-1, whatever the sign of v
const ZFRAMES = { a: 4, b: 4, f: 4, d: 4, g: 4 }; // 4-frame walk cycles where sheets exist; the rest stay 2 const ZFRAMES = { a: 4, b: 4, f: 4, d: 4, g: 4 }; // 4-frame walk cycles where sheets exist; the rest stay 2
// The rogue ball machine, on its own generated sheet: a dented hopper cart on wheels with a barrel // The rogue ball machine, on its own generated sheet: a dented hopper cart on wheels with a barrel
// aimed down-court, and a second frame with the muzzle flashing. Oliver called the first, procedural // aimed down-court, and a second frame with the muzzle flashing. Oliver called the first, procedural
@@ -288,7 +293,7 @@ function drawZombie(zb) {
if (!SPR['z_' + v + N]) N = 2; // frames still loading (or sheet rejected): 2-frame fallback if (!SPR['z_' + v + N]) N = 2; // frames still loading (or sheet rejected): 2-frame fallback
// same CYCLE frequency as before — more frames subdivide the stride, they don't speed it up // same CYCLE frequency as before — more frames subdivide the stride, they don't speed it up
let frame = rk && zb.pose === 'swing' ? 2 let frame = rk && zb.pose === 'swing' ? 2
: zb.variant === 'g' ? Math.floor(zb.z / (TUNE.bruteStride * 2 / N)) % N + 1 // heaviest body: step off DISTANCE : zb.variant === 'g' ? stepFrame(zb.z / (TUNE.bruteStride * 2 / N), N) + 1 // heaviest body: step off DISTANCE
// WALKED, like its wound lurch below, // WALKED, like its wound lurch below,
// or the feet slide (same cycle length) // or the feet slide (same cycle length)
: Math.floor(zb.walkT * (zb.variant === 'f' ? 6 : 2.6) * N / 2) % N + 1; : Math.floor(zb.walkT * (zb.variant === 'f' ? 6 : 2.6) * N / 2) % N + 1;
@@ -307,7 +312,7 @@ function drawZombie(zb) {
// was pure foot-slide. This replaced a procedural squash-and-sway that read as exactly what // was pure foot-slide. This replaced a procedural squash-and-sway that read as exactly what
// it was: fake. The comedy is in the gait, and the gait has to be drawn. // it was: fake. The comedy is in the gait, and the gait has to be drawn.
const b = stage + 'b'; const b = stage + 'b';
name = SPR[b] && Math.floor(zb.z / TUNE.bruteStride) % 2 ? b : stage; name = SPR[b] && stepFrame(zb.z / TUNE.bruteStride, 2) ? b : stage;
h *= SPR[stage].height / SPRITE_H; // height ALWAYS off the A frame, or the size pops each step h *= SPR[stage].height / SPRITE_H; // height ALWAYS off the A frame, or the size pops each step
} else if (SPR.z_gw) name = 'z_gw'; // a missing asset degrades to the last skin that loaded } else if (SPR.z_gw) name = 'z_gw'; // a missing asset degrades to the last skin that loaded
} }
@@ -336,10 +341,10 @@ function drawZombie(zb) {
const idx = f >= 1 ? 0 : Math.min(nStage, Math.max(1, Math.ceil((1 - f) * nStage))); const idx = f >= 1 ? 0 : Math.min(nStage, Math.max(1, Math.ceil((1 - f) * nStage)));
const wanted = zb.windT > 0 ? A.throws[Math.min(idx, A.throws.length - 1)] const wanted = zb.windT > 0 ? A.throws[Math.min(idx, A.throws.length - 1)]
: idx ? A.wounds[idx - 1] : idx ? A.wounds[idx - 1]
: A.walk[Math.floor(zb.z / TUNE.bruteStride) % 2]; : A.walk[stepFrame(zb.z / TUNE.bruteStride, 2)];
// a design whose sheet has not been generated (or has not loaded) degrades to design 1 rather // a design whose sheet has not been generated (or has not loaded) degrades to design 1 rather
// than vanishing, since drawStatic silently draws nothing for a missing sprite // than vanishing, since drawStatic silently draws nothing for a missing sprite
const stage = SPR[wanted] ? wanted : BOSS_ART[0].walk[Math.floor(zb.z / TUNE.bruteStride) % 2]; const stage = SPR[wanted] ? wanted : BOSS_ART[0].walk[stepFrame(zb.z / TUNE.bruteStride, 2)];
if (SPR[stage]) { name = stage; h *= SPR[stage].height / SPRITE_H; } // height ALWAYS off the frame, or the size pops if (SPR[stage]) { name = stage; h *= SPR[stage].height / SPRITE_H; } // height ALWAYS off the frame, or the size pops
} }
drawStatic(name, zb.x, zb.z, h, Math.floor(zb.walkT * 5.2) % 2); drawStatic(name, zb.x, zb.z, h, Math.floor(zb.walkT * 5.2) % 2);