Make the bosses move, and put six of them and three new bodies on court

A chapter boss had no AI at all: it matched no branch in the variant chain and
fell through to the crowd's aimless wobble, so "the boss AI owns its x" was a
comment describing code that did not exist. Movement is now data on the BOSS_ART
row -- pace, shadow, surge, charge, bounce -- and bossAI is the one place it
happens. Every behaviour field is optional, so a row carrying only art behaves
exactly as a boss did before.

Three earlier drafts of those styles failed the same way: written as "walk toward
the player's x", which in splatter means arrive in four seconds and stand still,
because the player is rooted in pretoss for nearly all of a boss fight. Every
style now moves under its own power and only biases toward him. Measured over a
simulated minute each: 50-136 m travelled, none static.

Three new designs -- THE HEAD COACH (a 5-shot drill 0.14s apart), THE PHYSIO
(charges, and withdraws less than it charges, so it reaches you at 38s and that
is the fight's clock), THE SPONSOR MASCOT (gap 0, so the whole wall of five
leaves in ONE frame and is dodged rather than parried). Harder to kill: 16 -> 20
hp, 9 -> 10 per chapter, plus per-design multipliers.

Three new bodies: the ball boy (3 hp, 2 skins, from game 3), the bouncer (6/3,
game 8) and the roller man (9/4, game 15), each with a 4-frame walk and a drawn
two-frame lurch for every wound stage.

Three wound rules became one. The brute had a hardcoded stage array, everything
else a single skin behind 'abd'.includes(...), and the boss a fraction of hpMax.
The boss's rule reproduces the other two exactly -- verified: the brute still
draws z_gw/gw2/gw3/gw4 at hp 4/3/2/1 -- and keeps working when hp is not
one-per-skin, which all three new bodies need. So a multi-hit body is now a row
in config.ZWOUND and no code. bleed() reads the same call, which also gave the
steward the trickle his wound skin had always implied.

Fixes a real crash on the way past: killPlayer empties shots[] while the shots
loop is mid-iteration, so every remaining index read undefined and the frame
threw. The mascot's five-at-once volley makes it easy to hit; the shipped 3-shot
volley could always have hit it.

_regen.py gains two style blocks and a check tier. One constant used to say
"keep every colour exactly as the reference" -- right for a remaster, and it
fights every word of the subject otherwise. newart is for a new character, state
for the same character wounded or mid-throw; using newart for the wound sheets
licensed a repaint and got one, five came back with living tan skin. And the
detail gate ratchets against the file it replaces, so check: any exists for
re-rolling already-fine art to fix something the gate cannot see, such as colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-03 11:45:35 +02:00
1 parent 0774a2a31d
commit 4cda6bde76
64 files changed
+694 -78

No files matched your search

+39 -7
View File
@@ -21,21 +21,47 @@ SCRATCH = os.environ.get('SPLAT_SCRATCH', '/tmp/splatter-regen')
GEN = os.path.expanduser('~/.claude/skills/generate-image/scripts/generate_image.py')
PY = os.environ.get('SPLAT_PY', sys.executable)
STYLE = (
# The style block is in three parts because the middle one is a REDRAW instruction and only a redraw
# wants it. "Keep every pose and colour exactly as in the reference" is right when the job is
# remastering a sprite that already exists, and it fights every word of the subject when the job is a
# NEW character drawn in the reference's style — set "newart": true on the batch to drop it.
RES = (
"Redraw them as HIGH-RESOLUTION pixel art. The art pixels must be SMALL: build each figure out "
"of roughly 90 to 110 pixels of height instead of the reference's 40, so there is room for real "
"detail, and spend it — a face with teeth, sunken eyes and a jawline; individual fingers; "
"clothing with visible seams, hems, folds and frayed torn edges; laced shoes; four or five "
"distinct shades per material with fine hatched shading where the light falls away. Keep every "
"pose, every body proportion, every height, every facing and every wound EXACTLY as in the "
"reference — only the drawing resolution and the amount of fine detail change. Keep the same "
"colours. Hard black outline around each figure. The background must be ONE FLAT solid magenta "
"distinct shades per material with fine hatched shading where the light falls away. "
)
FIDELITY = (
"Keep every pose, every body proportion, every height, every facing and every wound EXACTLY as "
"in the reference — only the drawing resolution and the amount of fine detail change. Keep the "
"same colours. "
)
CANVAS = (
"Hard black outline around each figure. The background must be ONE FLAT solid magenta "
"#FF00FF across the entire canvas — no scenery, no gradient, no vignette. Keep at least 200 "
"pixels of empty magenta between figures and at least 150 pixels of empty magenta at every edge "
"of the canvas: no figure or limb may touch or run off an edge. No shadow, no ground line, no text. "
"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."
)
STYLE = RES + FIDELITY + CANVAS
NEW_STYLE = RES + (
"Match the reference's DRAWING STYLE, its overall figure height and its facing — but the "
"character, the clothes, the colours and the poses are the ones described above, not the "
"reference's. Do not copy the reference's body or outfit. "
) + CANVAS
# The third case, and the one that is easy to miss: the SAME character in a different state (wounded,
# mid-throw). "newart" is wrong for it — it licenses the generator to repaint the character, and it
# did: wound frames came back with living tan skin and a redesigned shirt while the healthy frame
# stayed a grey-green corpse. Set "state": true for those.
STATE_STYLE = RES + (
"This is the SAME character as the reference, not a new one. His colours, his clothing, his "
"build, his proportions and his height must be IDENTICAL to the reference — the only things "
"that change are the injuries and the pose described above. His skin stays exactly the same "
"dead grey-green as the reference: he is a walking corpse, never pink, never tan, never living "
"flesh. Do not redesign or recolour any garment. "
) + CANVAS
def batches():
with open(os.path.join(HERE, '_regen_batches.json')) as f:
@@ -147,7 +173,7 @@ 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']} {STYLE}"
prompt = f"{spec['subject']} {STATE_STYLE if spec.get('state') 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...')
@@ -174,7 +200,13 @@ def run(name, spec, dry=False):
# that already has blood on it starts high, so a genuinely finer wounded body gains less). A
# REPOSE — turning the player around, pointing the runner at the camera, matching a wound between
# two frames — is already fine art and only has to come back no worse.
bar = {'finer': 1.25, 'same': 0.85, 'loose': 0.7}[spec.get('check', 'finer')]
# 'any' turns the detail gate OFF, and there is exactly one job it is for: RE-ROLLING art that is
# already fine to fix something the gate cannot see, such as a colour. The bar is measured against
# the file being replaced, so once a good sheet installs it RATCHETS — a second roll of equal
# quality now has to beat the first, and three colour-fix re-rolls were rejected for being 0.39
# against a 0.46 that was itself only there because the first roll happened to be dense. Magenta
# and edge checks still apply; verify the thing you were actually fixing by measuring it.
bar = {'finer': 1.25, 'same': 0.85, 'loose': 0.7, 'any': 0.0}[spec.get('check', 'finer')]
if sum(after) / len(after) < sum(before) / len(before) * bar:
print(f'[{name}] REJECTED: not finer than what it replaces {before} -> {after}'); return False
if spec.get('palette_ref'):