--variant now emits the whole ladder: nine batches, thirty-one frames

A variant used to stop at the healthy eleven, which is enough for a gore-mode
shambler and not enough for a MATCH opponent — that mode scores you on the
BODY, so an opponent with no wounded art would go from untouched to dead with
nothing in between.

  <key>_play / _serve / _fall       restyled off the master        11 frames
  <key>_w1  / _serve_w1 / _fall_w1  arm off, drawn from healthy    10 frames
  <key>_w2  / _serve_w2 / _fall_w2  headless, drawn from arm off   10 frames

Each stage is a restyle of THE STAGE ABOVE IT, in that order, because that
chaining is what finally made damage accumulate this morning: the missing arm
rides in the picture instead of having to win an argument with it. The helper
prints the batches in dependency order for exactly that reason.

Every gate the racket zombie's ladder needed is declared automatically — the
stride pair on both the healthy shuffle and each wound row, `changed` at 0.10
for an arm and 0.15 for a head (the generator will happily redraw the reference
and skip the amputation), and the pose-tolerant kit bounds with the shorts hue
kept tight, since hue is the measure that survives a pose change.

The wound rows carry three frames, not four: the shuffle doubles as the stance,
which is how drawZombie already reads them.

And the one-armed serve is generated, not hand-waved — the missing arm IS the
tossing arm, so the stump goes up and the racket hand flicks the ball off the
face, the way one-armed players actually serve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 12:56:53 +02:00
1 parent 7056945e62
commit 4bec0cb09c
1 file changed
+78 -10
+78 -10
View File
@@ -68,21 +68,89 @@ STRIDE_NOTE = ("In particular frames 2 and 3 must stay what they are in the refe
"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):
for src, dst in zip(refs, sprites): # placeholders: the density bar needs a baseline to beat
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
# pose-varied sheets move redness and shorts BRIGHTNESS on their own (a body face down
# shows a fraction of the shirt it shows standing); the shorts HUE is the measure that
# survives a pose change, so it stays the tight one. See the note in _regen.kit.
spec['palette_ref'] = refs[0]
spec['palette_tol'] = [26, 14, 99, 5]
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():
names = ['z_' + key + f[3:] for f in frames] # z_r1 -> z_br1, z_rtoss -> z_brtoss
for src, dst in zip(frames, names): # placeholders: the density bar needs a baseline
shutil.copy(os.path.join(HERE, src + '.png'), os.path.join(HERE, dst + '.png'))
spec = dict(sprites=names, ref_only=frames, newart=True, check='loose', anchor=0,
subject=RESTYLE.format(n=len(frames), who=who)
+ (STRIDE_NOTE if part == 'play' else ''))
if part == 'play': spec['stride'] = [[1, 2]] # the gate covers it from the first roll
B[f'{key}_{part}'] = spec
print(f'{key}_{part:6s} -> {", ".join(names)}')
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'\nnow run: python3 assets/_regen.py {key}_play {key}_serve {key}_fall --tries=4')
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: