Oliver, on the brute's fall frames: "now the fall zombie has a different skin tone.. man this is all so tiresome." Both halves were right. The skin IS twenty points lighter across that sheet, and the tiresome part is mine: I had been adding one hand-picked gate per drift I discovered — redness, then hue, then `changed`, then no-grids — which is a loop with no end, because naming features is not the same as comparing pictures. `kit` names four: shirt redness, shorts brightness, shorts hue, near-white fraction. Every character breaks a different one. The brute's shirt has no red in it at all, so its reference redness was 0 and blood scored 70-100 against a tolerance meant for a garment. A body face down shows a fraction of the shirt it shows standing, so two of the four move with POSE. And nothing in that list ever measured SKIN, which is exactly how a brute's flesh drifted across a whole sheet with six gates green. The palette gate asks the only question that generalises: are the big colour areas of this frame the same colours as the anchor's? Top SIX by area — the garments and the skin. Proportions ignored, so a pose change is invisible; blood is never in the top six, so a fresh wound cannot look like drift. CALIBRATED BEFORE IT WAS WIRED IN, which is the discipline I skipped all day. Frames known to be right score 0.0-8.5; the drifts we could actually see score 12.0-15.9. The threshold is 10, in the gap. Three formulations were measured and the two that overlapped were thrown away. And every sheet of a variant is now anchored to that variant's FIRST sheet, not to the stage above it. Chaining let a legal hop compound into a drift nobody authorised — the shorts walking -9 to -19 down the wound ladder was that, and so was this. Its first verdict on the existing brute: br_play is the character, and every other sheet sits 15 to 48 away from it. It would have caught all of this on the first roll. Also in: the GRID guard from the same session — the valley-cut rescue would slice a 2x2 sheet into four frames of a torso above someone else's boots, turning a loud failure into a silent one. Asked per cut PIECE, because a body is vertically continuous and a piece with a gap in it caught two of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
169 lines
11 KiB
Python
169 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).
|
|
#
|
|
# 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)
|