The agreed single pass did not land: br_w1 installed, br_serve and br_fall were refused by the palette gate at 12 to 22 away from z_br1, and everything drawn from them was correctly skipped rather than built on sand. 'br' comes out of ZFAMS. The art stays on disk and the nine batches stay in the JSON, so re-registering it later is one line — this is parked, not deleted, and nothing in the game referenced it (no opponent row names it). THE FINDING WORTH KEEPING, written at the top of _master.py: the brute's PLAY sheet is a good brute; its serve and fall sheets are each a slightly DIFFERENT brute. All three restyle from the master independently, so each invents the character afresh from the words, and "pale grey-green work shirt" lands somewhere new every time. The gate is not being fussy — it is measuring three different zombies. The fix is to hand the later sheets BOTH references: the master frame for the POSE and the variant's own first sheet for the CHARACTER, instead of the words for the character. Which is the lesson the wound stages already taught — a reference beats a description — applied one level up. Until that lands, a variant is reliable for its play sheet and a gamble for the other two. The racket zombie is untouched and complete: 33 frames, every gate green, and it is the opponent the match actually plays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
180 lines
11 KiB
Python
180 lines
11 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
|
|
|
|
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. ")
|
|
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):
|
|
# 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(refs, sprites):
|
|
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, restyled off the master
|
|
for part, frames in SHEETS.items():
|
|
batch(f'{key}_{part}', [K + f[3:] for f in frames], frames,
|
|
RESTYLE.format(n=len(frames), who=who) + (STRIDE_NOTE if part == 'play' else ''),
|
|
stride=[[1, 2]] if part == 'play' else None, newart=True)
|
|
|
|
# --- 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)
|