Re-running --variant to rewrite a spec re-seeded EVERY placeholder from the
master, and placeholders are copies of the lean red-shirted man the poses came
from. That silently replaced a finished brute's bounce, toss, strike and four
fall frames with racket-zombie art. I restored z_br1..4 by hand afterwards and
did not notice the other seven were gone — so three subsequent passes were
regenerating sheets that already existed and had been good since this morning.
Recovered from 0e92c72, and variant() now refuses to seed over a frame that is
already there: a placeholder is a stand-in for art that does not exist yet, and
nothing else.
Then colour-matched every brute frame onto z_br1 rather than generating: five
moved from failing the palette gate to passing, with no generator calls at all.
That is the point of the correction being arithmetic — it applies retroactively
to art already on disk.
WHERE THE BRUTE HONESTLY STANDS:
HEALTHY 11/11 — stance, shuffle pair, forehand, bounce, toss, strike, and
the full fall. Consistent, colour-matched, done.
ARM OFF the play and serve frames read; the four FALL frames are standing
figures of the wrong build entirely.
HEADLESS not headless — z_brw2 still has its head, and is near-identical to
the arm-off frame it was drawn from. The `changed` gate passed it
because SOMETHING moved; it does not know that the something has to
be the head.
So about a third of the brute is right and it is the third that matters least —
the healthy row was always the easy one. The wound stages are the job.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
217 lines
14 KiB
Python
217 lines
14 KiB
Python
# _master.py — build THE MASTER SHEETS: the canonical pose set a new zombie is restyled FROM.
|
|
#
|
|
# python3 assets/_master.py # rebuild assets/_sheet_master_*.png from the installed frames
|
|
#
|
|
# WHY THIS EXISTS. A new zombie variant used to mean inventing a character AND a pose set, and the
|
|
# generator answers that by inventing a fresh figure per frame — which is how the walk cycles ended
|
|
# up as eleven unrelated illustrations. Handing it a FINISHED SHEET and asking for a restyle turns
|
|
# the job into a transform: the reference does the specifying and the prompt only says who to draw.
|
|
# Measured, the poses survive it intact (the brute restyle came back at upper 0.148 / legs 0.618
|
|
# against the master's own 0.157 / 0.565 — very slightly BETTER than what it copied).
|
|
#
|
|
# WHAT THE FIRST VARIANT SHOWED, and what the next attempt should change. The brute's PLAY sheet is
|
|
# a good brute. Its serve and fall sheets are each a slightly DIFFERENT brute, because all three
|
|
# restyle from the master independently — so each one invents the character afresh from the words,
|
|
# and "pale grey-green work shirt" lands somewhere new every time. The palette gate now measures the
|
|
# gap (15 to 48 against z_br1, where a good frame is under 8.5) and refuses them.
|
|
#
|
|
# The fix is to give the later sheets BOTH things: the master frame for the POSE and the variant's
|
|
# own first sheet for the CHARACTER, instead of the words for the character. That is the same lesson
|
|
# as the wound stages — a reference beats a description — applied one level up. Until then a variant
|
|
# is reliable for its play sheet and a gamble for the other two.
|
|
#
|
|
# WHY THREE SHEETS AND NOT ONE. The generator answers at a fixed width, around 1400-1800 px. At four
|
|
# figures across each gets 350-440 px, which carries the 90-110 px of figure height the style block
|
|
# asks for. All eleven on one strip would be ~130 px each and the detail gate would reject every
|
|
# roll. Four, four and three is the shape that fits — and it is already the shape of the three
|
|
# batches that made them.
|
|
import os, sys
|
|
from PIL import Image
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
MAGENTA = (255, 0, 255, 255)
|
|
|
|
# The master pose set, split the way it is generated. Keep these in step with config.BENCH — that is
|
|
# the screen these frames are judged on.
|
|
SHEETS = {
|
|
'play': ['z_r1', 'z_r2', 'z_r3', 'z_r4'], # stance, shuffle A, shuffle B, forehand
|
|
'serve': ['z_rbounce', 'z_rtoss', 'z_rserve'], # bounce, toss, strike
|
|
'fall': ['z_rfall1', 'z_rfall2', 'z_rfall3', 'z_rrise'], # buckle, knees, down, rise
|
|
}
|
|
|
|
def build(name, frames, scale=2, gap=90, pad=70):
|
|
ims = [Image.open(os.path.join(HERE, f + '.png')).convert('RGBA') for f in frames]
|
|
ims = [i.resize((i.width * scale, i.height * scale), Image.NEAREST) for i in ims]
|
|
w = sum(i.width for i in ims) + gap * (len(ims) - 1) + pad * 2
|
|
h = max(i.height for i in ims) + pad * 2
|
|
s = Image.new('RGBA', (w, h), MAGENTA)
|
|
x = pad
|
|
for i in ims:
|
|
s.paste(i, (x, h - pad - i.height), i) # feet on one ground line, as the game draws them
|
|
x += i.width + gap
|
|
out = os.path.join(HERE, f'_sheet_master_{name}.png')
|
|
s.save(out)
|
|
print(f'{os.path.basename(out):26s} {w}x{h} {len(frames)} frames ({", ".join(frames)})')
|
|
|
|
|
|
# ---- A NEW ZOMBIE OFF THE MASTER ------------------------------------------------------------
|
|
# python3 assets/_master.py --variant br "an ENORMOUS FAT ZOMBIE BRUTE: a huge sagging belly, a
|
|
# filthy stretched shirt, heavy brown work trousers…"
|
|
# python3 assets/_regen.py br_play br_serve br_fall --tries=4
|
|
#
|
|
# Writes three batches that RESTYLE the master sheets into a new character, seeds their placeholder
|
|
# PNGs, and declares the stride pair so the gate covers the new body from its first roll. The whole
|
|
# variant is then three generator calls and no pose design at all — which is the point of having a
|
|
# master: the reference specifies eleven poses that already work, and the prompt only says who.
|
|
import json, shutil, collections
|
|
|
|
# THE FIRST SHEET describes the character in WORDS, because at that point no picture of it exists.
|
|
RESTYLE = (
|
|
"The reference image is a {n}-FRAME SPRITE SHEET of one zombie tennis player. Redraw THE WHOLE "
|
|
"SHEET as a COMPLETELY DIFFERENT ZOMBIE: {who} "
|
|
"KEEP ALL {n} POSES EXACTLY AS THEY ARE IN THE REFERENCE — the same stance, the same steps with "
|
|
"the same leg forward in each, the same swing, the same facing, the same arm and racket "
|
|
"positions, the same order left to right, the same total height and the same distance from the "
|
|
"camera. Only the CHARACTER changes; the POSES do not. "
|
|
"There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and "
|
|
"the air are empty and the poses are the same as if a ball were there. "
|
|
"Output exactly {n} figures side by side in ONE horizontal row, all on the same ground line. ")
|
|
|
|
# ...AND EVERY SHEET AFTER IT DESCRIBES THE CHARACTER WITH A PICTURE OF IT. This is the whole fix.
|
|
# The brute's three sheets each restyled from the master independently and each re-derived "pale
|
|
# grey-green work shirt" from the same paragraph, so the game got three different brutes and the
|
|
# palette gate — correctly — refused two of them. A paragraph is a lossy specification of a
|
|
# character; the character is not.
|
|
#
|
|
# It is also the pattern that already worked: the racket zombie's own play sheet was generated from
|
|
# [z_c1, o_idle1, o_run1, o_swing1] — the character as an IMAGE, the poses as IMAGES — in one call,
|
|
# first time. I had it, and then did not use it for variants.
|
|
RESTYLE_KNOWN = (
|
|
"The FIRST reference image is THE CHARACTER — the exact zombie to draw, and the ONLY source for "
|
|
"who it is: its build, its bulk, its height, its clothing and every colour of it. Copy that "
|
|
"character exactly. "
|
|
"The REMAINING reference images are {n} POSES, and are there ONLY to show what the body is doing "
|
|
"— never who it is. "
|
|
"Draw THE CHARACTER FROM THE FIRST IMAGE in each of those {n} poses, in the same order, at the "
|
|
"same size and the same distance from the camera, all facing the same way. The character never "
|
|
"changes; only the pose does. "
|
|
"There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and "
|
|
"the air are empty and the poses are the same as if a ball were there. "
|
|
"Output exactly {n} figures side by side in ONE horizontal row, all on the same ground line. ")
|
|
STRIDE_NOTE = ("In particular frames 2 and 3 must stay what they are in the reference: ONE DRAWING "
|
|
"SHOWN TWICE with only the legs different — everything above the hips identical "
|
|
"between them, the legs clearly swapped, and the two frames NOT identical copies. ")
|
|
|
|
|
|
# ---- THE WOUND LADDER. A match opponent is scored ON ITS BODY: every point won takes a stage off
|
|
# it, so a variant that only exists whole would jump from untouched to dead with nothing between.
|
|
# Each stage is a restyle of THE STAGE ABOVE IT — that chaining is what finally made damage
|
|
# accumulate, because the missing arm then rides in the PICTURE instead of having to win an argument
|
|
# with it. The wound rows have no separate stance frame: the shuffle doubles as one, which is how
|
|
# drawZombie already reads them.
|
|
WOUND = (
|
|
"The reference image is a {n}-FRAME SPRITE SHEET of one zombie tennis player. REDRAW THE WHOLE "
|
|
"SHEET with ONE THING CHANGED: {change} "
|
|
"EVERYTHING ELSE STAYS EXACTLY AS IT IS IN THE REFERENCE — the same character, the same clothing "
|
|
"and its exact shades, the same skin, the same racket, the same {n} poses in the same order, the "
|
|
"same leg positions, the same facing, the same build. There is NO TENNIS BALL in any frame. "
|
|
"Output exactly {n} figures side by side in ONE horizontal row, all on the same ground line. ")
|
|
ARM_OFF = (
|
|
"the arm WITHOUT the racket is COMPLETELY GONE — torn off at the shoulder, leaving a ragged "
|
|
"stump gushing blood down that side and the clothing slashed open across the ribs. COUNT THE "
|
|
"ARMS: there must be EXACTLY ONE ARM in every single frame, the one holding the racket. Nothing "
|
|
"hangs from the other shoulder. A frame with two arms in it is wrong — this is the whole change "
|
|
"being asked for, so do not copy the reference's second arm across. Where the reference uses "
|
|
"that arm, the stump is thrown out in its place instead. ")
|
|
HEADLESS = (
|
|
"the HEAD IS COMPLETELY GONE — no head, no face, no hair anywhere above the shoulders, only a "
|
|
"ragged gushing neck stump, so every figure is A CLEAR HEAD SHORTER than in the reference. It is "
|
|
"ALREADY missing one arm in the reference and it STAYS missing — it still has only one arm. ")
|
|
# ...and because the missing arm IS the tossing arm, a wounded server has to toss the way one-armed
|
|
# players really do. The wound changing how it plays is the point, not a detail.
|
|
ONE_ARM_SERVE = (
|
|
"Because it has only one arm it can no longer throw the ball up with a free hand: in the TOSS "
|
|
"frame the stump is flung upward instead and the racket hand flicks up in front of the face, as "
|
|
"a one-armed player tosses off the racket face. ")
|
|
|
|
def variant(key, who):
|
|
bpath = os.path.join(HERE, '_regen_batches.json')
|
|
B = json.load(open(bpath, encoding='utf-8'), object_pairs_hook=collections.OrderedDict)
|
|
seed = lambda src, dst: shutil.copy(os.path.join(HERE, src + '.png'), os.path.join(HERE, dst + '.png'))
|
|
made = []
|
|
|
|
def batch(name, sprites, refs, subject, stride=None, changed=None, newart=False, lead=False):
|
|
# `lead` means the first reference is THE CHARACTER and not a pose — it is shown to the
|
|
# generator and is not one of the frames asked for, so it is skipped when pairing references
|
|
# to outputs (both for seeding and for the run-time baseline in _regen).
|
|
pose_refs = refs[1:] if lead else refs
|
|
# Only the sheets drawn off the MASTER are seeded here. A chained wound stage is seeded by
|
|
# _regen at run time, from whatever its reference has become by then — seeding it now would
|
|
# score it against a copy of the master, two steps better than the stage it follows.
|
|
if newart:
|
|
for src, dst in zip(pose_refs, sprites):
|
|
# NEVER OVERWRITE ART THAT EXISTS. Re-running --variant to rewrite a spec used to
|
|
# re-seed every placeholder from the master, which quietly replaced a finished brute's
|
|
# serve and fall sheets with copies of the lean man they were drawn from. A
|
|
# placeholder is only ever a stand-in for a frame that is not there yet.
|
|
if not os.path.exists(os.path.join(HERE, dst + '.png')):
|
|
seed(src, dst)
|
|
spec = dict(sprites=sprites, ref_only=list(refs), check='loose', anchor=0, subject=subject)
|
|
spec['newart' if newart else 'state'] = True
|
|
if stride: spec['stride'] = stride
|
|
if changed: spec['changed'] = changed
|
|
# EVERY SHEET OF A VARIANT IS PAINTED IN THE FIRST SHEET'S COLOURS. Anchoring each stage to
|
|
# the one before it let a legal hop compound into a drift nobody authorised; anchoring all of
|
|
# them to the healthy body means the whole variant, wounds and all, stays one character.
|
|
# The first sheet is what DEFINES the palette, so it is the only one not measured against it.
|
|
if sprites[0] != K + '1':
|
|
spec['palette_anchor'] = K + '1'
|
|
B[name] = spec
|
|
made.append(name)
|
|
print(f'{name:16s} <- {refs[0]}… {", ".join(sprites)}')
|
|
|
|
K = 'z_' + key
|
|
# --- the whole body. The PLAY sheet invents the character from the words; serve and fall are then
|
|
# drawn from the character it produced (K+'1' leads their reference list) plus the master's poses.
|
|
# That ordering is why the three must be run in the order printed at the end.
|
|
for part, frames in SHEETS.items():
|
|
first = part == 'play'
|
|
refs = frames if first else [K + '1'] + frames
|
|
batch(f'{key}_{part}', [K + f[3:] for f in frames], refs,
|
|
(RESTYLE.format(n=len(frames), who=who) + STRIDE_NOTE) if first
|
|
else RESTYLE_KNOWN.format(n=len(frames)),
|
|
stride=[[1, 2]] if first else None, newart=True, lead=not first)
|
|
|
|
# --- the two wound stages, each chained off the one above it
|
|
WALK = ['2', '3', '4'] # shuffle A, shuffle B, forehand — the wound rows' three frames
|
|
SERVE = ['bounce', 'toss', 'serve']
|
|
FALL = ['fall1', 'fall2', 'fall3', 'rise']
|
|
for st, change, extra, moved in ((1, ARM_OFF, ONE_ARM_SERVE, 0.10), (2, HEADLESS, '', 0.15)):
|
|
up = K + (f'w{st - 1}' if st > 1 else '') # the stage this one is drawn from
|
|
walk_src = [up + s for s in (['', 'b', 's'] if st > 1 else WALK)]
|
|
batch(f'{key}_w{st}', [f'{K}w{st}', f'{K}w{st}b', f'{K}w{st}s'], walk_src,
|
|
WOUND.format(n=3, change=change)
|
|
+ 'In particular frames 1 and 2 are ONE DRAWING SHOWN TWICE with only the legs '
|
|
'different — identical above the hips, legs clearly swapped, and NOT identical copies. ',
|
|
stride=[[0, 1]], changed=moved)
|
|
batch(f'{key}_serve_w{st}', [f'{K}w{st}' + s for s in SERVE], [up + s for s in SERVE],
|
|
WOUND.format(n=3, change=change) + extra, changed=moved)
|
|
batch(f'{key}_fall_w{st}', [f'{K}w{st}' + s for s in FALL], [up + s for s in FALL],
|
|
WOUND.format(n=4, change=change), changed=moved)
|
|
|
|
json.dump(B, open(bpath, 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
|
|
print(f'\n{len(made)} batches, 31 frames. Run them IN THIS ORDER — each wound stage is drawn '
|
|
f'from the stage above it, so an earlier one must be installed first:\n\n'
|
|
f' python3 assets/_regen.py {" ".join(made)} --tries=4\n')
|
|
|
|
if __name__ == '__main__':
|
|
if '--variant' in sys.argv:
|
|
i = sys.argv.index('--variant')
|
|
variant(sys.argv[i + 1], sys.argv[i + 2])
|
|
sys.exit()
|
|
for name, frames in SHEETS.items():
|
|
missing = [f for f in frames if not os.path.exists(os.path.join(HERE, f + '.png'))]
|
|
if missing:
|
|
print(f'[{name}] SKIPPED, missing: {", ".join(missing)}'); continue
|
|
build(name, frames)
|