diff --git a/assets/__pycache__/_regen.cpython-314.pyc b/assets/__pycache__/_regen.cpython-314.pyc index c4a3f2e..1cd98d0 100644 Binary files a/assets/__pycache__/_regen.cpython-314.pyc and b/assets/__pycache__/_regen.cpython-314.pyc differ diff --git a/assets/_regen.py b/assets/_regen.py index 3d3ec93..4c95f9c 100644 --- a/assets/_regen.py +++ b/assets/_regen.py @@ -55,6 +55,9 @@ CANVAS = ( "The canvas is a WIDE LANDSCAPE STRIP and every figure stands side by side in ONE SINGLE HORIZONTAL " "ROW on the same ground line — never a grid, never two rows, never stacked." ) +BG_FIRST = ("THE BACKGROUND OF THE OUTPUT IMAGE MUST BE ONE FLAT SOLID MAGENTA #FF00FF. No court, " + "no grass, no lines, no scenery, no sky, no gradient — nothing behind the figures but flat " + "magenta, edge to edge. ") STYLE = RES + FIDELITY + CANVAS NEW_STYLE = RES + ( "Match the reference's DRAWING STYLE, its overall figure height and its facing — but the " @@ -177,16 +180,40 @@ def kit_off(outs, ref_name, tol=(12, 10, 1.5, 4)): # opt-in per batch (`stride: [[i, j]]`) rather than applied to everything with two frames in it. STRIDE_H, STRIDE_TOP, STRIDE_LEG, STRIDE_HIP = 0.04, 0.20, 0.30, 0.62 -def stride_stats(pa, pb): +def _masks(pa, pb): a, b = Image.open(pa).convert('RGBA'), Image.open(pb).convert('RGBA') w, h = max(a.width, b.width), max(a.height, b.height) def m(im): c = Image.new('RGBA', (w, h)); c.paste(im, ((w - im.width) // 2, h - im.height)) return np.array(c)[..., 3] > 110 - ma, mb = m(a), m(b) + return a, b, m(a), m(b), h + +def _d(x, y): + return round(float((x ^ y).sum() / max(1, (x | y).sum())), 3) + +def stride_stats(pa, pb): + a, b, ma, mb, h = _masks(pa, pb) hip = int(h * STRIDE_HIP) - d = lambda x, y: round(float((x ^ y).sum() / max(1, (x | y).sum())), 3) - return round(a.height / max(1, b.height), 3), d(ma[:hip], mb[:hip]), d(ma[hip:], mb[hip:]) + return round(a.height / max(1, b.height), 3), _d(ma[:hip], mb[:hip]), _d(ma[hip:], mb[hip:]) + +# ---- DID THE CHANGE ACTUALLY HAPPEN? A chained restyle asks for one sheet redrawn with ONE THING +# different, and "copy this exactly" reliably beats "…but change that": removing a HEAD works every +# time (it is dramatic), removing an ARM mostly did not — five of seven wound frames came back as +# the reference redrawn, differing by 0.02-0.05 of their own silhouette, and the ladder silently +# healed the body it had just maimed. So a batch can declare how much it must differ from the sheet +# it was drawn FROM. Measured against the known numbers: a copy scores 0.02-0.05, a real amputation +# 0.12-0.38, a decapitation 0.27-0.75. Nothing else in the pipeline can see this — the art is +# beautiful, correctly coloured, correctly posed and completely wrong. +def changed_off(outs, refs, want): + for o, r in zip(outs, refs): + rp = os.path.join(HERE, r + '.png') + if not os.path.exists(rp): continue + _, _, ma, mb, _ = _masks(o, rp) + d = _d(ma, mb) + if d < want: + return (f'{os.path.basename(o)} is {r} redrawn — only {d:.3f} of the silhouette differs ' + f'(want >= {want}). The thing that was supposed to change did not change.') + return None def stride_off(outs, pairs): for i, j in pairs: @@ -245,7 +272,14 @@ def run(name, spec, dry=False): ref = ref_sheet(shown, f'{SCRATCH}/{name}_ref.png', spec.get('scale', 2.0), frames=len(sprites) + (1 if lead else 0)) sheet = f'{SCRATCH}/{name}_hi.png' - prompt = f"{spec['subject']} {STATE_STYLE if spec.get('state') else POSE_STYLE if spec.get('pose') else NEW_STYLE if spec.get('newart') else STYLE}" + # THE BACKGROUND RULE GOES FIRST, not only at the end of the style block. The chained restyle + # subjects are long, and with the magenta instruction sitting behind three hundred words of + # "keep everything exactly as the reference" the generator started painting the figures on a + # COURT — grey-blue, grey-green, dark green backgrounds, six rejections in eleven rolls. Same + # sentence, said before anything else has had a chance to bury it. + prompt = (BG_FIRST + spec['subject'] + ' ' + + (STATE_STYLE if spec.get('state') else POSE_STYLE if spec.get('pose') + else NEW_STYLE if spec.get('newart') else STYLE)) if dry: print(f'[{name}] would generate {len(sprites)} frames: {", ".join(sprites)}'); return True print(f'[{name}] generating {len(sprites)} frames...') @@ -287,6 +321,10 @@ def run(name, spec, dry=False): if spec.get('stride'): bad = stride_off(outs, spec['stride']) if bad: print(f'[{name}] REJECTED: {bad}'); return False + # a chained restyle declares how far it must move from the sheet it was drawn from + if spec.get('changed') and len(spec.get('ref_only', [])) == len(sprites): + bad = changed_off(outs, spec['ref_only'], spec['changed']) + if bad: print(f'[{name}] REJECTED: {bad}'); return False for o, s in zip(outs, sprites): if spec.get('check', 'finer') == 'finer': colour_match(o, s) Image.open(o).save(os.path.join(HERE, s + '.png')) diff --git a/assets/_regen_batches.json b/assets/_regen_batches.json index 3a0a48b..ca44270 100644 --- a/assets/_regen_batches.json +++ b/assets/_regen_batches.json @@ -1302,12 +1302,12 @@ "anchor": 0, "palette_ref": "z_r1", "palette_tol": [ - 12, - 10, + 20, + 14, 99, - 4 + 5 ], - "subject": "Three frames of the zombie SERVING. The FIRST reference image is the ZOMBIE TENNIS PLAYER this sheet is of — the same dead man, and he must be IDENTICAL to it in every frame: dirty yellow-blond hair, a RED CLOTH HEADBAND, a torn red short-sleeved tennis shirt, WARM CHARCOAL GREY tennis shorts above the knee (a slightly brownish dark grey, never blue-grey, never pale), bare grey-green dead legs, white socks and white tennis shoes, grey-green corpse skin, sunken eyes, a wooden racket with a pale strung head. Same build, same height, same colours, same drawing style. The REMAINING reference images are there ONLY to show the POSES to copy — never the character, never the clothes, never the skin. Copy each pose exactly. Frame 1: he stands at the baseline about to serve, bouncing the ball on the ground in front of him — copy reference image 2: upright, racket held low across the body, the free hand down with a tennis ball just under it. Frame 2: the TOSS — copy reference image 3: the free arm thrown STRAIGHT UP above the head with the ball leaving the fingertips, the racket dropping back behind the shoulder, weight rocking onto the back foot, the head tipped up watching the ball. Frame 3: the STRIKE — copy reference image 4: the racket at FULL STRETCH overhead at the moment of contact, the body extended upward on the toes, the free arm coming down across the chest. Output exactly 3 figures side by side in ONE horizontal row, all standing on the same ground line, with empty magenta between them. " + "subject": "Three frames of the zombie SERVING. The FIRST reference image is the ZOMBIE TENNIS PLAYER this sheet is of — the same dead man, and he must be IDENTICAL to it in every frame: dirty yellow-blond hair, a RED CLOTH HEADBAND, a torn red short-sleeved tennis shirt, WARM CHARCOAL GREY tennis shorts above the knee (a slightly brownish dark grey, never blue-grey, never pale), bare grey-green dead legs, white socks and white tennis shoes, grey-green corpse skin, sunken eyes, a wooden racket with a pale strung head. Same build, same height, same colours, same drawing style. The REMAINING reference images are there ONLY to show the POSES to copy — never the character, never the clothes, never the skin. Copy each pose exactly. Frame 1: he stands at the baseline about to serve, bouncing the ball on the ground in front of him — copy reference image 2: upright, racket held low across the body, the free hand down and EMPTY, palm open, as if he had just let a ball go. Frame 2: the TOSS — copy reference image 3: the free arm thrown STRAIGHT UP above the head the hand OPEN AND EMPTY at the top of the throw, the racket dropping back behind the shoulder, weight rocking onto the back foot, the head tipped up watching the ball. Frame 3: the STRIKE — copy reference image 4: the racket at FULL STRETCH overhead at the top of the swing, the body extended upward on the toes, the free arm coming down across the chest. Output exactly 3 figures side by side in ONE horizontal row, all standing on the same ground line, with empty magenta between them. THERE IS NO TENNIS BALL ANYWHERE IN ANY FRAME. Do not draw a ball in his hand, in the air, on the ground or on the racket — the ball is drawn separately by the game. His hands and the air around him are EMPTY, and the poses are the same as if a ball were there. " }, "racket_fall": { "sprites": [ @@ -1333,5 +1333,105 @@ 4 ], "subject": "Four frames of the zombie COLLAPSING AND GETTING BACK UP. The FIRST reference image is the ZOMBIE TENNIS PLAYER this sheet is of — the same dead man, and he must be IDENTICAL to it in every frame: dirty yellow-blond hair, a RED CLOTH HEADBAND, a torn red short-sleeved tennis shirt, WARM CHARCOAL GREY tennis shorts above the knee (a slightly brownish dark grey, never blue-grey, never pale), bare grey-green dead legs, white socks and white tennis shoes, grey-green corpse skin, sunken eyes, a wooden racket with a pale strung head. Same build, same height, same colours, same drawing style. The REMAINING reference images are there ONLY to show the POSES to copy — never the character, never the clothes, never the skin. Copy each pose exactly. Frame 1: the knees buckle — copy reference image 2: still upright but folding, one knee giving way, the racket arm flailing out for balance. Frame 2: DOWN ON HIS KNEES, doubled over, head low, one hand on the ground, still gripping the racket in the other — copy the crumpled posture of reference image 3. Frame 3: FLAT ON THE GROUND, face down, sprawled, the racket loose beside the outstretched hand — copy the collapsed posture of reference image 4. Draw him lying down: this figure is WIDE AND LOW, not standing. Frame 4: DRAGGING HIMSELF BACK UP — up on one knee with the other foot planted, one hand pushing off the ground, head coming up, the racket gripped again, halfway to standing. Output exactly 4 figures side by side in ONE horizontal row, all standing on the same ground line, with empty magenta between them. " + }, + "racket_serve_w1": { + "sprites": [ + "z_rw1bounce", + "z_rw1toss", + "z_rw1serve" + ], + "ref_only": [ + "z_rbounce", + "z_rtoss", + "z_rserve" + ], + "state": true, + "check": "loose", + "anchor": 0, + "palette_ref": "z_rbounce", + "palette_tol": [ + 26, + 14, + 99, + 5 + ], + "subject": "The reference image is a SPRITE SHEET of ONE zombie tennis player, 3 frames. REDRAW THE WHOLE SHEET with ONE THING CHANGED: his LEFT ARM — the one WITHOUT the racket — IS COMPLETELY GONE. He has ONLY ONE ARM in every frame: no left arm, no left hand, no left sleeve anywhere, just a ragged torn stump at the left shoulder gushing blood down his side, and his shirt slashed open across the ribs. Where the reference uses that arm, the stump is thrown out in its place instead. COUNT THE ARMS: there must be EXACTLY ONE ARM in every single frame — the one holding the racket. The other shoulder ends in a stump and nothing hangs from it. A frame with two arms in it is wrong. This is the whole change being asked for; do not copy the reference's second arm across. EVERYTHING ELSE STAYS EXACTLY AS IT IS IN THE REFERENCE — the same character, the same red shirt, the same WARM CHARCOAL shorts and their exact shade, the same grey-green skin, the same white socks and shoes, the same wooden racket, the same 3 poses in the same order, the same body positions, the same facing, the same heights. Output exactly 3 figures side by side in ONE horizontal row. Because he has only one arm he can no longer throw the ball up with a free hand: in the TOSS frame that arm's stump is flung upward instead and the racket hand flicks up in front of his face, as a one-armed player tosses off the racket face. THERE IS NO TENNIS BALL ANYWHERE IN ANY FRAME. Do not draw a ball in his hand, in the air, on the ground or on the racket — the ball is drawn separately by the game. His hands and the air around him are EMPTY, and the poses are the same as if a ball were there. ", + "changed": 0.1 + }, + "racket_serve_w2": { + "sprites": [ + "z_rw2bounce", + "z_rw2toss", + "z_rw2serve" + ], + "ref_only": [ + "z_rw1bounce", + "z_rw1toss", + "z_rw1serve" + ], + "state": true, + "check": "loose", + "anchor": 0, + "palette_ref": "z_rw1bounce", + "palette_tol": [ + 26, + 14, + 99, + 5 + ], + "subject": "The reference image is a SPRITE SHEET of ONE zombie tennis player, 3 frames. REDRAW THE WHOLE SHEET with ONE THING CHANGED: his HEAD IS COMPLETELY GONE — no head, no face, no hair, no headband anywhere above the shoulders, only a ragged gushing neck stump, so every figure is A CLEAR HEAD SHORTER than in the reference. He is ALREADY missing his left arm in the reference and it STAYS gone — he still has only one arm. EVERYTHING ELSE STAYS EXACTLY AS IT IS IN THE REFERENCE — the same character, the same red shirt, the same WARM CHARCOAL shorts and their exact shade, the same grey-green skin, the same white socks and shoes, the same wooden racket, the same 3 poses in the same order, the same body positions, the same facing, the same heights. Output exactly 3 figures side by side in ONE horizontal row. THERE IS NO TENNIS BALL ANYWHERE IN ANY FRAME. Do not draw a ball in his hand, in the air, on the ground or on the racket — the ball is drawn separately by the game. His hands and the air around him are EMPTY, and the poses are the same as if a ball were there. ", + "changed": 0.15 + }, + "racket_fall_w1": { + "sprites": [ + "z_rw1fall1", + "z_rw1fall2", + "z_rw1fall3", + "z_rw1rise" + ], + "ref_only": [ + "z_rfall1", + "z_rfall2", + "z_rfall3", + "z_rrise" + ], + "state": true, + "check": "loose", + "anchor": 0, + "palette_ref": "z_rfall1", + "palette_tol": [ + 26, + 14, + 99, + 5 + ], + "subject": "The reference image is a SPRITE SHEET of ONE zombie tennis player, 4 frames. REDRAW THE WHOLE SHEET with ONE THING CHANGED: his LEFT ARM — the one WITHOUT the racket — IS COMPLETELY GONE. He has ONLY ONE ARM in every frame: no left arm, no left hand, no left sleeve anywhere, just a ragged torn stump at the left shoulder gushing blood down his side, and his shirt slashed open across the ribs. Where the reference uses that arm, the stump is thrown out in its place instead. COUNT THE ARMS: there must be EXACTLY ONE ARM in every single frame — the one holding the racket. The other shoulder ends in a stump and nothing hangs from it. A frame with two arms in it is wrong. This is the whole change being asked for; do not copy the reference's second arm across. EVERYTHING ELSE STAYS EXACTLY AS IT IS IN THE REFERENCE — the same character, the same red shirt, the same WARM CHARCOAL shorts and their exact shade, the same grey-green skin, the same white socks and shoes, the same wooden racket, the same 4 poses in the same order, the same body positions, the same facing, the same heights. Output exactly 4 figures side by side in ONE horizontal row. THERE IS NO TENNIS BALL ANYWHERE IN ANY FRAME. Do not draw a ball in his hand, in the air, on the ground or on the racket — the ball is drawn separately by the game. His hands and the air around him are EMPTY, and the poses are the same as if a ball were there. ", + "changed": 0.1 + }, + "racket_fall_w2": { + "sprites": [ + "z_rw2fall1", + "z_rw2fall2", + "z_rw2fall3", + "z_rw2rise" + ], + "ref_only": [ + "z_rw1fall1", + "z_rw1fall2", + "z_rw1fall3", + "z_rw1rise" + ], + "state": true, + "check": "loose", + "anchor": 0, + "palette_ref": "z_rw1fall1", + "palette_tol": [ + 26, + 14, + 99, + 5 + ], + "subject": "The reference image is a SPRITE SHEET of ONE zombie tennis player, 4 frames. REDRAW THE WHOLE SHEET with ONE THING CHANGED: his HEAD IS COMPLETELY GONE — no head, no face, no hair, no headband anywhere above the shoulders, only a ragged gushing neck stump, so every figure is A CLEAR HEAD SHORTER than in the reference. He is ALREADY missing his left arm in the reference and it STAYS gone — he still has only one arm. EVERYTHING ELSE STAYS EXACTLY AS IT IS IN THE REFERENCE — the same character, the same red shirt, the same WARM CHARCOAL shorts and their exact shade, the same grey-green skin, the same white socks and shoes, the same wooden racket, the same 4 poses in the same order, the same body positions, the same facing, the same heights. Output exactly 4 figures side by side in ONE horizontal row. THERE IS NO TENNIS BALL ANYWHERE IN ANY FRAME. Do not draw a ball in his hand, in the air, on the ground or on the racket — the ball is drawn separately by the game. His hands and the air around him are EMPTY, and the poses are the same as if a ball were there. ", + "changed": 0.15 } } \ No newline at end of file diff --git a/assets/z_rbounce.png b/assets/z_rbounce.png index dd905bf..ab5fcc0 100644 Binary files a/assets/z_rbounce.png and b/assets/z_rbounce.png differ diff --git a/assets/z_rserve.png b/assets/z_rserve.png index 31177be..752bd50 100644 Binary files a/assets/z_rserve.png and b/assets/z_rserve.png differ diff --git a/assets/z_rtoss.png b/assets/z_rtoss.png index 4f52a5d..f734a56 100644 Binary files a/assets/z_rtoss.png and b/assets/z_rtoss.png differ diff --git a/assets/z_rw1bounce.png b/assets/z_rw1bounce.png new file mode 100644 index 0000000..5ac5dc9 Binary files /dev/null and b/assets/z_rw1bounce.png differ diff --git a/assets/z_rw1fall1.png b/assets/z_rw1fall1.png new file mode 100644 index 0000000..4cfd9b9 Binary files /dev/null and b/assets/z_rw1fall1.png differ diff --git a/assets/z_rw1fall2.png b/assets/z_rw1fall2.png new file mode 100644 index 0000000..357c365 Binary files /dev/null and b/assets/z_rw1fall2.png differ diff --git a/assets/z_rw1fall3.png b/assets/z_rw1fall3.png new file mode 100644 index 0000000..2728fe9 Binary files /dev/null and b/assets/z_rw1fall3.png differ diff --git a/assets/z_rw1rise.png b/assets/z_rw1rise.png new file mode 100644 index 0000000..04da578 Binary files /dev/null and b/assets/z_rw1rise.png differ diff --git a/assets/z_rw1serve.png b/assets/z_rw1serve.png new file mode 100644 index 0000000..ae4b51a Binary files /dev/null and b/assets/z_rw1serve.png differ diff --git a/assets/z_rw1toss.png b/assets/z_rw1toss.png new file mode 100644 index 0000000..bff98ff Binary files /dev/null and b/assets/z_rw1toss.png differ diff --git a/assets/z_rw2bounce.png b/assets/z_rw2bounce.png new file mode 100644 index 0000000..b3de2f9 Binary files /dev/null and b/assets/z_rw2bounce.png differ diff --git a/assets/z_rw2fall1.png b/assets/z_rw2fall1.png new file mode 100644 index 0000000..9fb05e3 Binary files /dev/null and b/assets/z_rw2fall1.png differ diff --git a/assets/z_rw2fall2.png b/assets/z_rw2fall2.png new file mode 100644 index 0000000..2b2b23a Binary files /dev/null and b/assets/z_rw2fall2.png differ diff --git a/assets/z_rw2fall3.png b/assets/z_rw2fall3.png new file mode 100644 index 0000000..fba7cb8 Binary files /dev/null and b/assets/z_rw2fall3.png differ diff --git a/assets/z_rw2rise.png b/assets/z_rw2rise.png new file mode 100644 index 0000000..e9841d2 Binary files /dev/null and b/assets/z_rw2rise.png differ diff --git a/assets/z_rw2serve.png b/assets/z_rw2serve.png new file mode 100644 index 0000000..0af574e Binary files /dev/null and b/assets/z_rw2serve.png differ diff --git a/assets/z_rw2toss.png b/assets/z_rw2toss.png new file mode 100644 index 0000000..2027848 Binary files /dev/null and b/assets/z_rw2toss.png differ diff --git a/index.html b/index.html index 137f827..10b3a1c 100644 --- a/index.html +++ b/index.html @@ -431,6 +431,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/beats.js b/src/beats.js index 246304c..80474ad 100644 --- a/src/beats.js +++ b/src/beats.js @@ -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 at full health, + // z_rw1 / z_rw2 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 diff --git a/src/config.js b/src/config.js index b36777b..3b6b9b3 100644 --- a/src/config.js +++ b/src/config.js @@ -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 / z_rw1 / z_rw2 + // 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']; diff --git a/src/render.js b/src/render.js index 8974daa..99ddd8a 100644 --- a/src/render.js +++ b/src/render.js @@ -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 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);