The opponent is complete: 3 wound stages x 11 states, and a gate for wounds that never happened

The serve and the fall now exist at every wound stage, so a one-armed opponent
still serves and a headless one still goes down, instead of reverting to a
whole body to do it. 33 frames, no gaps.

THE BALL IS OUT OF THE SERVE SPRITES. Oliver: it was painted into three frames
and the game draws the ball itself, so every one of them would have ridden
along at the wrong place, the wrong time and in the wrong number. Hands open
and empty now, poses unchanged as if a ball were there.

THE FOURTH GATE, and the one nothing else could see: "copy this exactly, change
ONE thing" loses when the change is a REMOVAL. Taking a head off worked every
time — it is dramatic. Taking an ARM off mostly did not: five of seven wound
frames came back as the reference faithfully redrawn, 0.02-0.05 of their own
silhouette different, and the ladder quietly HEALED the body the score had just
maimed. Frame count, background, clipping, detail, kit and stride all pass that
sheet. A batch now declares how far it must move from the sheet it was drawn
from — a copy scores 0.02-0.05, a real amputation 0.12-0.38, a decapitation
0.27-0.75 — and the whole set now measures 0.18-0.65.

AND THE BACKGROUND RULE MOVED TO THE FRONT OF EVERY PROMPT. Six rejections in
eleven rolls were "background is not magenta", and the generator was not
glitching: it was painting the zombies ON A TENNIS COURT. The chained restyle
subjects are long, and the magenta instruction sat behind three hundred words
of "keep everything exactly as the reference", where it stopped being honoured.
Same sentence, said before anything can bury it.

One design consequence kept rather than papered over: the missing arm IS the
tossing arm, so a wounded opponent tosses off the racket face the way one-armed
players actually do. The wound changes how he plays, which is what slice 3 has
been asking for.

Known and NOT fixed: the shorts hue drifts from -9 to -19 down the ladder. Each
stage is measured against the one before it, so a legal 4-point hop three times
over is a 14-point total nobody authorised. The fix is to anchor every stage to
the healthy frame with a total budget instead of a per-hop one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 12:45:26 +02:00
1 parent 4bbdf6357a
commit 5c0db0a22e
24 files changed
+181 -24

No files matched your search

+5 -1
View File
@@ -200,7 +200,11 @@ function benchApply() {
const zb = zombies.find(z => z.variant === 'r'); if (!zb) return;
zb.hpMax = BENCH_HP;
zb.hp = Math.max(1, Math.round(BENCH_HP * [1, 0.5, 0.2][G.bench.stage]));
zb.showFrame = BENCH[G.bench.i].spr || null;
// ...and the named states take the wound stage with them: z_r<suffix> at full health,
// z_rw1<suffix> / z_rw2<suffix> below it. render falls back to the healthy frame if a stage has
// not been drawn yet, and the HUD says so — a bench must never imply art that does not exist.
const suf = BENCH[G.bench.i].spr;
zb.showFrame = suf ? (G.bench.stage ? 'z_rw' + G.bench.stage : 'z_r') + suf : null;
}
// the bench's own controls, taken before anything else reads the key: SPACE / a tap steps to the
+10 -7
View File
@@ -705,13 +705,16 @@ export const BENCH = [
{ name: 'WALK CYCLE', walk: true },
{ name: 'READY STANCE', pose: 'idle' },
{ name: 'FOREHAND', pose: 'swing' },
{ name: 'SERVE · BOUNCE', spr: 'z_rbounce' },
{ name: 'SERVE · TOSS', spr: 'z_rtoss' },
{ name: 'SERVE · STRIKE', spr: 'z_rserve' },
{ name: 'FALL 1 · BUCKLING', spr: 'z_rfall1' },
{ name: 'FALL 2 · KNEES', spr: 'z_rfall2' },
{ name: 'FALL 3 · DOWN', spr: 'z_rfall3' },
{ name: 'RISE', spr: 'z_rrise' },
// `spr` is a SUFFIX, not a filename: the bench builds z_r<suffix> / z_rw1<suffix> / z_rw2<suffix>
// from the wound stage, so the same left/right control walks the serve and the fall through their
// damage stages exactly as it walks the shuffle through them.
{ name: 'SERVE · BOUNCE', spr: 'bounce' },
{ name: 'SERVE · TOSS', spr: 'toss' },
{ name: 'SERVE · STRIKE', spr: 'serve' },
{ name: 'FALL 1 · BUCKLING', spr: 'fall1' },
{ name: 'FALL 2 · KNEES', spr: 'fall2' },
{ name: 'FALL 3 · DOWN', spr: 'fall3' },
{ name: 'RISE', spr: 'rise' },
];
export const BENCH_STAGES = ['HEALTHY', 'ARM OFF', 'HEADLESS'];
+18 -6
View File
@@ -195,7 +195,7 @@ export function proj(x, y, 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 = 98;
export const SPRV = 100;
// 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".
@@ -247,6 +247,11 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
// serve (slice 7, alternating service) and the fall between points (slice 4).
// Loaded now so the sprite bench can show them the day they are drawn.
'z_rbounce','z_rtoss','z_rserve','z_rfall1','z_rfall2','z_rfall3','z_rrise',
// ...and the same seven at each wound stage: a one-armed opponent still has to serve
// and still has to go down, and reverting to the healthy body to do it would undo the
// damage the score just did
'z_rw1bounce','z_rw1toss','z_rw1serve','z_rw1fall1','z_rw1fall2','z_rw1fall3','z_rw1rise',
'z_rw2bounce','z_rw2toss','z_rw2serve','z_rw2fall1','z_rw2fall2','z_rw2fall3','z_rw2rise',
'w_ball','w_bomb','w_triple','w_fire','w_gun', // weapon-rack icons
'w_beacon','w_laser','w_rocket','w_flamer', // ...and the four that come out of a barrel
'z_barrel1','z_barrel2', // the weapon barrel, two frames so it ROLLS
@@ -687,7 +692,13 @@ function drawZombie(zb) {
// THE BENCH OVERRIDE, last of all: it names a frame outright, so states the game cannot reach
// yet (the serve, the fall) can be looked at the day they are generated rather than the day they
// are wired up. Only the back room's sprite bench ever sets this.
if (zb.showFrame && SPR[zb.showFrame]) name = zb.showFrame;
if (zb.showFrame) {
// a wound stage that has not been drawn yet degrades to the HEALTHY frame rather than to the
// walk cycle — the bench prints what it actually drew, so the substitution is visible
const heal = zb.showFrame.replace(/^z_rw\d/, 'z_r');
if (SPR[zb.showFrame]) name = zb.showFrame;
else if (SPR[heal]) name = heal;
}
// ...and only NOW the tour swap, so the height read below comes off the frame actually drawn. The
// branches above all test the men's names (which always exist), so the wound logic is untouched.
name = SK(name);
@@ -787,10 +798,11 @@ function drawBenchHud() {
// ...and say so when the stop has no wound variant. The serve and the fall exist for the HEALTHY
// body only so far, so a stage label there would be claiming art that has not been drawn — and the
// whole reason this screen exists is to stop the art and the wiring lying to each other.
const named = !!BENCH[b.i].spr;
shadowText(named ? 'HEALTHY ONLY · NO WOUND VARIANT YET'
: BENCH_STAGES[b.stage] + ' · ' + (b.i + 1) + '/' + BENCH.length,
W / 2, y + 24, named ? UI.dim : UI.warm);
// FELL BACK means the stage asked for a frame that has not been drawn and got the healthy one —
// said out loud, because a bench that quietly substitutes art is worse than no bench.
const fell = zb && zb.showFrame && zb.drawn && zb.drawn !== zb.showFrame;
shadowText(BENCH_STAGES[b.stage] + ' · ' + (b.i + 1) + '/' + BENCH.length
+ (fell ? ' · FELL BACK' : ''), W / 2, y + 24, fell ? UI.bad : UI.warm);
// the file on screen, and the hp fraction the wound stage was read from — the two numbers you
// need to tell "the art is wrong" from "the wiring picked the wrong art"
ctx.font = F(8);