The twelve-rung ladder: director, champion, physio and mascot walk on

Oliver's four picks, each rolled off its own settled BOSS frame through the
family pipeline (rows in _famroll.py) — the rung and the chapter boss are the
same person. matchFoe decoupled from bossArt's cycling on his call: the match
ladder is 12 with the champion capping rung 12; chapters keep their ten.

The champion refused to bulk through four rolls (words lost, then a new
pair-reference in char_ref only half-won), so his mass is stature: OPPONENTS
rows carry an optional world height h read by zombieH — champion 2.1 m, the
Bulk 2.0 m — and the serve aims at the body actually drawn. HEAD_CROP measured
for all four; ramp test extended to 12 rungs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-13 18:37:43 +02:00
1 parent 99a87115b3
commit 9a49acff51
130 files changed
+98 -22

No files matched your search

+28 -12
View File
@@ -756,9 +756,10 @@ export const bossArt = ch => ch % 10 === 0 ? BOSS_ART[BOSS_ART.length - 1]
// is a property of the table rather than something the code has to arrange.
// 1-2 the ones who fetch and sweep. Slow, wild, forgiving.
// 3-4 the ones who keep order. They start covering ground.
// 5-6 the ones who judge. Width arrives: they begin painting lines.
// 7-8 the ones who actually played. The club pro's own body, and the brute behind him.
// 9-10 management, and then the man whose name is on the honours board.
// 5 the entertainment. The costume still fits.
// 6-8 the ones who cover and judge. Width arrives: they begin painting lines.
// 9-10 the ones who actually played. The club pro's own body, and the brute behind him.
// 11-12 management, and then the man whose name is on the honours board.
const OPPONENTS = [
{ short: 'SHAMBLER', name: 'THE SHAMBLER', fam: 'sh',
line: 'HE WAS HERE BEFORE ANY OF THIS.', speed: 0.72, pace: 0.72, wide: 0.75, flub: 1.35 },
@@ -768,25 +769,36 @@ const OPPONENTS = [
line: 'HE MOWED THIS LAWN FOR THIRTY YEARS.', speed: 0.9, pace: 0.88, wide: 0.9, flub: 1.05 },
{ short: 'STEWARD', name: 'THE STEWARD', fam: 'st',
line: 'HE IS ASKING YOU TO LEAVE.', speed: 0.98, pace: 0.95, wide: 1.0 },
// 5-6: the entertainment, and then width arrives with the ones who judge
{ short: 'MASCOT', name: 'THE MASCOT', fam: 'ma',
line: 'THE CROWD STILL LOVES HIM.', speed: 1.02, pace: 0.98, wide: 1.05, flub: 1.0 },
{ short: 'RUNNER', name: 'THE RUNNER', fam: 'ru',
line: 'HE COVERS THE COURT. ALL OF IT.', speed: 1.22, pace: 1.0, wide: 1.05, flub: 0.95 },
{ short: 'JUDGE', name: 'THE LINE JUDGE', fam: 'lj',
line: 'EVERY BALL OF YOURS IS OUT.', speed: 1.05, pace: 1.05, wide: 1.28, flub: 0.85 },
{ short: 'PHYSIO', name: 'THE PHYSIO', fam: 'ph',
line: 'HE KNOWS EXACTLY WHERE IT HURTS.', speed: 1.1, pace: 1.1, wide: 1.22, flub: 0.82 },
{ short: 'CLUB PRO', name: 'THE CLUB PRO',
line: 'HE GAVE LESSONS ON THIS COURT.', speed: 1.12, pace: 1.15, wide: 1.15, flub: 0.8 },
{ short: 'THE BULK', name: 'THE BULK', fam: 'br',
{ short: 'THE BULK', name: 'THE BULK', fam: 'br', h: 2.0,
line: 'NOTHING GETS PAST HIM TWICE.', speed: 1.0, pace: 1.3, wide: 1.2, flub: 0.75 },
{ short: 'DIRECTOR', name: 'THE DIRECTOR',
{ short: 'DIRECTOR', name: 'THE DIRECTOR', fam: 'dr',
line: 'HE CAN HAVE YOU REMOVED.', speed: 1.22, pace: 1.22, wide: 1.25, flub: 0.65 },
{ short: 'CHAMPION', name: 'THE CHAMPION',
// 12: the man whose name is on the honours board. `h` is WORLD height (zombieH reads it, so the
// serve aims at the body actually drawn): four family rolls could not make the art itself bulky
// against the pose sheet — see _famroll.py's pair-reference note — so his size is stature.
{ short: 'CHAMPION', name: 'THE CHAMPION', fam: 'ch', h: 2.1,
line: 'HE RETURNS EVERYTHING.', speed: 1.35, pace: 1.3, wide: 1.3, flub: 0.5 },
];
// Who opponent N is. The chapter roster's own order is reused wholesale (bossArt), so the champion
// lands on opponent 10 exactly as he lands on chapter 10 and the two rosters can never drift apart.
// Who opponent N is. This used to reuse the chapter roster's cycling (bossArt) so the two lists
// could never drift — DECOUPLED 2026-08-13 on Oliver's call: the match ladder is 12 rungs with the
// champion capping rung 12, while the chapter bosses stay a 10-design rotation with the champion
// on every tenth. The lists share CHARACTERS (a rung and a boss are the same person, anchored on
// the same art), just not arithmetic. The ladder cycles past 12 exactly as bossArt cycles past 10.
export const matchFoe = n => {
const art = bossArt(n), i = BOSS_ART.indexOf(art);
return { name: art.name, key: 'L' + i, fam: 'r', ...OPPONENTS[i] };
const i = (n - 1) % OPPONENTS.length;
return { key: 'L' + i, fam: 'r', ...OPPONENTS[i] };
};
// ---- THE SPRITE BENCH's stops (back room only, see beats.startBench). Each is either an ANIMATED
@@ -821,7 +833,7 @@ export const BENCH_STAGES = ['HEALTHY', 'ARM OFF', 'HEADLESS'];
// that (15 to 48 away from z_br1, where a good frame scores under 8.5) and refuses them, which is
// the gate working, not the gate being wrong. Re-registering it is one entry here; see the note at
// the top of _master.py for the fix its next attempt needs.
export const ZFAMS = ['r', 'br', 'sh', 'bb', 'st', 'ru', 'gk', 'lj'];
export const ZFAMS = ['r', 'br', 'sh', 'bb', 'st', 'ru', 'gk', 'lj', 'dr', 'ch', 'ph', 'ma'];
// HEAD CROPS for the opponent intro card — a straight source-pixel crop out of each family's own
// healthy frame (z_<fam>1.png), so the card's icon is the body on court, never separate art.
@@ -838,6 +850,10 @@ export const HEAD_CROP = {
ru: { x: 14, y: 0, w: 57, h: 55 },
gk: { x: 6, y: 0, w: 73, h: 65 },
lj: { x: 6, y: 0, w: 74, h: 56 },
dr: { x: 10, y: 0, w: 62, h: 49 },
ch: { x: 3, y: 0, w: 68, h: 63 },
ph: { x: 12, y: 0, w: 63, h: 58 },
ma: { x: 0, y: 0, w: 124, h: 81 }, // the mascot's head IS the ball rim too — hand-deepened
};
// THE RACKET FAMILY'S THREE STAGES, as a FRACTION of the pool — never against literal hp, the rule
@@ -1007,7 +1023,7 @@ export const MACH_H = 1.55; // was 1.15 — waist high read as a prop you could
export const zombieH = zb => zb.variant === 'o' ? TUNE.barrelH
: zb.variant === 'g' ? 2.7 : zb.variant === 'd' ? 2.15
: zb.variant === 'k' ? 3.2 : zb.variant === 'h' ? 2.4
: zb.variant === 'r' ? 1.85 : zb.variant === 'm' ? MACH_H
: zb.variant === 'r' ? ((zb.foe && zb.foe.h) || 1.85) : zb.variant === 'm' ? MACH_H
: zb.variant === 'B' ? 1.8 * (zb.scale || 1.5) : 1.8;
// Where on that body the ball is aimed, and where the aim mark is drawn. Solving the shot to the
// GROUND under a shambler meant the ball came down past it and struck at the ankles — the wound is
+1 -1
View File
@@ -221,7 +221,7 @@ function farB(z) {
// 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
// many: the one that does not get bumped is the one that serves a cached 404.
export const SPRV = 108;
export const SPRV = 109;
// 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
// last sheet has landed. See the onerror note below for the other half of "sometimes missing".