A match opponent is a FAMILY now, so a generated variant is one row and not dead art

--variant produces 31 frames; nothing could draw them. drawZombie hardcoded the
`z_r` prefix, the preloader spelled all 31 filenames out by hand, and the sprite
bench built its names from the same literal. A new zombie would have sat on disk.

  config.ZFAMS = ['r']        the families that exist
  config.famFrames(f)         the 31 names derived from a key

Everything that needs those names now derives them: the preloader spreads
ZFAMS.flatMap(famFrames), drawZombie reads `zb.fam` (unset = 'r', so the club
pro is untouched) and builds its wound lookups off it, and the bench walks
families on up/down. Adding an opponent is ONE ROW — which also retires 31
hand-typed filenames from the preload list, each of which was a chance to typo
one and have a body silently vanish for a frame. That is the oldest bug in
render.js and it is now impossible in this corner of it.

And the link that makes it worth anything: a LADDER RUNG can name a family.
`fam: 'br'` on a row in LADDER_TRAITS and that rung walks on as a different
body, all the way down its own wound ladder. So the whole path is now:

  _master.py --variant br "…"   ->  generate 31 frames
  ZFAMS += 'br'                 ->  the game loads them
  fam: 'br' on a ladder row     ->  that opponent is in the match

Verified: the bench still walks all 33 cells pulling the right derived name at
every wound stage, and a real match still opens on THE GROUNDSKEEPER wearing
family 'r' and drawing z_r1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 13:04:06 +02:00
1 parent 4bec0cb09c
commit e2487b784a
4 files changed
+52 -24

No files matched your search

+21 -1
View File
@@ -677,6 +677,9 @@ export const bossArt = ch => ch % 10 === 0 ? BOSS_ART[BOSS_ART.length - 1]
// pace scale on its returns' speed wide scale on how far off centre they may land
// flub scale on its whiff chance speed scale on how fast it covers the ground
// line the personality, said once, on the name plate as it walks on
// fam which SPRITE FAMILY this rung wears (config.ZFAMS). Absent = the club pro's body, 'r'.
// This is the payoff of _master.py --variant: generate an opponent, add its key to ZFAMS,
// name it on a rung, and that rung is a different body all the way down its wound ladder.
// short 8 characters, because that is the board's name slot — the match board names
// the thing you are playing instead of calling it THEM
const LADDER_TRAITS = [
@@ -695,7 +698,7 @@ const LADDER_TRAITS = [
// lands on rung 10 exactly as he lands on chapter 10 and the two rosters can never drift apart.
export const matchRung = n => {
const art = bossArt(n), i = BOSS_ART.indexOf(art);
return { name: art.name, key: 'L' + i, ...LADDER_TRAITS[i] };
return { name: art.name, key: 'L' + i, fam: 'r', ...LADDER_TRAITS[i] };
};
// ---- THE SPRITE BENCH's stops (back room only, see beats.startBench). Each is either an ANIMATED
@@ -718,6 +721,23 @@ export const BENCH = [
];
export const BENCH_STAGES = ['HEALTHY', 'ARM OFF', 'HEADLESS'];
// ---- MATCH-OPPONENT SPRITE FAMILIES. A family is a complete opponent: the eleven states at three
// wound stages, 31 frames, all named off one key (`r` -> z_r1, z_rtoss, z_rw1fall2 …). Generating a
// new one is `assets/_master.py --variant <key>`; USING it is one row here, because everything that
// needs the names — the preloader, drawZombie's wound lookup, the sprite bench — derives them from
// this table rather than spelling them out. That is the whole point: 31 hand-written filenames in a
// preload list is 31 chances to typo one and have a body silently vanish for a frame.
export const ZFAMS = ['r'];
const POSES = ['bounce', 'toss', 'serve', 'fall1', 'fall2', 'fall3', 'rise'];
export const famFrames = f => [
`z_${f}1`, `z_${f}2`, `z_${f}3`, `z_${f}4`, // stance, shuffle A, shuffle B, forehand
...POSES.map(s => `z_${f}${s}`),
// the wound rows carry no separate stance — the shuffle doubles as one, which is how drawZombie
// reads them (…w1 / …w1b are the stride pair, …w1s the forehand)
...[1, 2].flatMap(st => [`z_${f}w${st}`, `z_${f}w${st}b`, `z_${f}w${st}s`,
...POSES.map(s => `z_${f}w${st}${s}`)]),
];
// The roster, as the BODY COUNT screen reads it. Until now a variant's NAME lived only in the
// comment above its hp value — fine while nothing ever had to print one. The key is the `variant`
// char the zombie already carries, except that 'a' and 'b' fold together (they are one shambler in