Two frames each: the run cycles land where the zombies always were

The live bench session's verdict, in Oliver's words: "this looks better than
anything before." Six and eight real phases at speed read as flicker on a
far-court body — the zombies never had this problem because their walks are
FOUR well-chosen frames at a calm rate, and the fix was to stop out-drawing
them. Each opponent now runs a TWO-frame true leg-swap pair — his frames 1+2
of the grid roll, hers 7+8 of the face-transplant cycle, both picked live on
the bench with SPR swaps — at TUNE.runHz2 = 1.7 (the old hardcoded 4.5 for
short cycles was most of a stride per display frame; that is the strobe).

The tuning rig ships too: TUNE.runN (frame-count override, 0 = probe),
T.SPR for live frame swaps, and the bench's living slots — the next cadence
question gets answered by watching, not rolling. Twelve run sprites become
four; the SPR list says why. SPRV 124, main.js v248.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-14 14:54:11 +02:00
1 parent 90d7ac1096
commit 40c74be4aa
16 files changed
+13 -9

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

+1 -1
View File
@@ -494,6 +494,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="src/main.js?v=247"></script> <script type="module" src="src/main.js?v=248"></script>
</body> </body>
</html> </html>
+3
View File
@@ -41,6 +41,9 @@ export const TUNE = {
// the dive is what buys the pace its net clearance // the dive is what buys the pace its net clearance
serveHardErr: 1.6,// how much a hard serve multiplies the timing scatter: pace is paid for in serveHardErr: 1.6,// how much a hard serve multiplies the timing scatter: pace is paid for in
// faults, which is what makes tap = safe second serve a real decision // faults, which is what makes tap = safe second serve a real decision
runN: 0, // frame-count override for run cycles (0 = probe per character sheet) — bench tuning knob
runHz2: 1.7, // stride cycles/s for TWO-frame pairs (the opponents). Tuned live on the bench
// with Oliver 2026-08-14 — the old hardcoded 4.5 was strobe; 1.7 reads as running.
runHz: 2.4, // stride cycles/s for the rolled 6/8-frame run sets. The old hardcoded 4.5 was runHz: 2.4, // stride cycles/s for the rolled 6/8-frame run sets. The old hardcoded 4.5 was
// tuned on 2-4 near-duplicate frames; on eight real phases it strobed // tuned on 2-4 near-duplicate frames; on eight real phases it strobed
faceFlip: 0.13, // s an auto-steered direction must persist before the body mirrors (earnFlip). faceFlip: 0.13, // s an auto-steered direction must persist before the body mirrors (earnFlip).
+2 -2
View File
@@ -7,7 +7,7 @@ import { TUNE, COURTS, TOURS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, W
import { P, O, ball, score, G, setupPoint, selectCourt, selectTour } from './state.js'; import { P, O, ball, score, G, setupPoint, selectCourt, selectTour } from './state.js';
import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZombies, splatPoint, deliverTin, selectWeapon, grantWeapon, showModal, dismissModal, flickTarget, dragServe, toggleTestMode } from './gameplay.js'; import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZombies, splatPoint, deliverTin, selectWeapon, grantWeapon, showModal, dismissModal, flickTarget, dragServe, toggleTestMode } from './gameplay.js';
import { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench, FALL, benchFall } from './beats.js'; import { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench, FALL, benchFall } from './beats.js';
import { fitViewport, render, proj, RES, SPRV, LOAD } from './render.js'; import { fitViewport, render, proj, RES, SPRV, LOAD, SPR } from './render.js';
import { voice, ambient, menuMusic, menuDuck } from './audio.js'; import { voice, ambient, menuMusic, menuDuck } from './audio.js';
import { zombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer , spawnBarrel } from './zombies.js'; import { zombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer , spawnBarrel } from './zombies.js';
import { cv } from './dom.js'; import { cv } from './dom.js';
@@ -556,6 +556,6 @@ if (location.hash === '#bench') setTimeout(() => { start(null); intoSplatter();
requestAnimationFrame(frame); requestAnimationFrame(frame);
// exposed for in-browser sanity checks and simulated playtests // exposed for in-browser sanity checks and simulated playtests
window.T = { TUNE, ball, P, O, score, G, zombies, keyPressed, laserScene, laserFire, pause: v => { paused = v !== false; }, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, spawnBarrel, grantWeapon, toggleTestMode, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer, splatPoint, solveShot, inServeBox, inCourt, proj, setupPoint, forceZombies, deliverTin, selectWeapon, showModal, dismissModal, dragServe, startDuel, resolveInterlude, flickTarget, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, window.T = { TUNE, ball, P, O, score, G, zombies, keyPressed, SPR, laserScene, laserFire, pause: v => { paused = v !== false; }, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, spawnBarrel, grantWeapon, toggleTestMode, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer, splatPoint, solveShot, inServeBox, inCourt, proj, setupPoint, forceZombies, deliverTin, selectWeapon, showModal, dismissModal, dragServe, startDuel, resolveInterlude, flickTarget, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch,
stats, step: dt => update(dt), render, press: keyPressed, FALL, benchFall, stats, step: dt => update(dt), render, press: keyPressed, FALL, benchFall,
get phase() { return G.phase; }, set phase(v) { G.phase = v; } }; get phase() { return G.phase; }, set phase(v) { G.phase = v; } };
+7 -6
View File
@@ -221,16 +221,16 @@ function farB(z) {
// Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the // Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the
// menu screens hang <img> tags on the same files, and two copies of this number is one copy too // menu screens hang <img> tags on the same files, and two copies of this number is one copy too
// many: the one that does not get bumped is the one that serves a cached 404. // many: the one that does not get bumped is the one that serves a cached 404.
export const SPRV = 123; export const SPRV = 124;
// The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was // The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was
// already requested at import — it just makes the wait visible and holds the menu back until the // already requested at import — it just makes the wait visible and holds the menu back until the
// last sheet has landed. See the onerror note below for the other half of "sometimes missing". // last sheet has landed. See the onerror note below for the other half of "sometimes missing".
export const LOAD = { done: 0, total: 0 }; export const LOAD = { done: 0, total: 0 };
const SPR = {}; export const SPR = {};
for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_run3','p_run4','p_run5','p_run6','p_swing1','p_swing2','p_die1','p_die2','p_die3','p_die4','p_die5', for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_run3','p_run4','p_run5','p_run6','p_swing1','p_swing2','p_die1','p_die2','p_die3','p_die4','p_die5',
'p_pull', // the serve wind-up: the racket dropping behind him while the pull is drawn 'p_pull', // the serve wind-up: the racket dropping behind him while the pull is drawn
'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_swing1','o_swing2', 'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_swing1','o_swing2',
'o_run1','o_run2','o_run3','o_run4','o_run5','o_run6', // the rolled 6-frame cycle (a 2x6 grid roll is TWO cycles — one row IS the cycle, never stitch the halves) 'o_run1','o_run2', // TWO frames, a true leg-swap pair — the whole 2026-08-14 tuning session's verdict: fewer, better-chosen frames at a calm rate beat every 6/8-frame roll ('this looks better than anything before')
'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4', 'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4',
'x_crowd','x_crowd2','x_ads', // CENTRE COURT (grass) — also the fallback set 'x_crowd','x_crowd2','x_ads', // CENTRE COURT (grass) — also the fallback set
'x_crowd_c','x_crowd_c2','x_ads_c', // TERRE ROUGE (clay) 'x_crowd_c','x_crowd_c2','x_ads_c', // TERRE ROUGE (clay)
@@ -290,7 +290,7 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
// could actually see was blue popping in after three frames of white. // could actually see was blue popping in after three frames of white.
'p_die1_f','p_die2_f','p_die3_f','p_die4_f','p_die5_f', 'p_die1_f','p_die2_f','p_die3_f','p_die4_f','p_die5_f',
'o_idle1_f','o_idle2_f','o_bounce_f','o_bounce2_f','o_toss_f','o_smash_f', 'o_idle1_f','o_idle2_f','o_bounce_f','o_bounce2_f','o_toss_f','o_smash_f',
'o_run1_f','o_run2_f','o_run3_f','o_run4_f','o_run5_f','o_run6_f','o_run7_f','o_run8_f','o_swing1_f','o_swing2_f', 'o_run1_f','o_run2_f','o_swing1_f','o_swing2_f', // her pair likewise (frames 7+8 of the face-transplant cycle)
'z_c1_f','z_c2_f','z_c3_f','z_c4_f','z_cw1_f','z_cw2_f','z_cw1b_f','z_cw2b_f', 'z_c1_f','z_c2_f','z_c3_f','z_c4_f','z_cw1_f','z_cw2_f','z_cw1b_f','z_cw2b_f',
'z_r1_f','z_r2_f','z_r3_f','z_r4_f', 'z_r1_f','z_r2_f','z_r3_f','z_r4_f',
'z_rw1_f','z_rw1b_f','z_rw1s_f','z_rw2_f','z_rw2b_f','z_rw2s_f', 'z_rw1_f','z_rw1b_f','z_rw1s_f','z_rw2_f','z_rw2b_f','z_rw2s_f',
@@ -1488,11 +1488,12 @@ function drawChar(ch) {
// The run cycle at whatever count the character's sheet carries: the _runroll.py cycles are 6 // The run cycle at whatever count the character's sheet carries: the _runroll.py cycles are 6
// (one call, one row, one character — see assets/_runroll.py for why the old per-frame art // (one call, one row, one character — see assets/_runroll.py for why the old per-frame art
// flickered); un-rolled characters fall back to 4 or 2. // flickered); un-rolled characters fall back to 4 or 2.
const runN = SPR[pre + 'run8'] ? 8 : SPR[pre + 'run6'] ? 6 : SPR[pre + 'run4'] ? 4 : 2; // TUNE.runN: live override for the bench's tuning sessions (0 = probe the sheet as always)
const runN = TUNE.runN || (SPR[pre + 'run8'] ? 8 : SPR[pre + 'run6'] ? 6 : SPR[pre + 'run4'] ? 4 : 2);
// The rolled 6/8-frame cycles run at their own STRIDE RATE (TUNE.runHz, cycles/s). The old 4.5 // The rolled 6/8-frame cycles run at their own STRIDE RATE (TUNE.runHz, cycles/s). The old 4.5
// was tuned on 2-4 near-duplicate frames, where it read as vigour — on eight real phases it was // was tuned on 2-4 near-duplicate frames, where it read as vigour — on eight real phases it was
// a strobe ("very flashy and unnatural"), most of a full stride every display frame. // a strobe ("very flashy and unnatural"), most of a full stride every display frame.
if (ch.pose === 'run') name = pre + 'run' + (Math.floor(ch.runT * (runN >= 6 ? TUNE.runHz : 4.5) * runN) % runN + 1); if (ch.pose === 'run') name = pre + 'run' + (Math.floor(ch.runT * (runN >= 6 ? TUNE.runHz : TUNE.runHz2) * runN) % runN + 1);
// (the receiver used to flip to run1/run2 here as a "split-step" — it read as a man sprinting on // (the receiver used to flip to run1/run2 here as a "split-step" — it read as a man sprinting on
// the spot, because the run frames are a full leaning stride, not a ready-hop. He idles now.) // the spot, because the run frames are a full leaning stride, not a ready-hop. He idles now.)
if (ch.pose === 'swing') name = pre + (ch.poseT < 0.15 ? 'swing1' : 'swing2'); if (ch.pose === 'swing') name = pre + (ch.poseT < 0.15 ? 'swing1' : 'swing2');