Four new frames (p_pull1..4) turn the slingshot drag into the serve's wind-up: racket above the head, behind the head, behind the back, fully coiled, picked off pullFrac — the same 0..1 that sets the shot's power — so the gesture and the man are one movement and the release snaps to the smash. The two bounce frames are one drawing now. p_bounce2 is p_bounce with the free arm sheared up 12px and nothing else touched, so the swap cannot flicker, and it follows the ball instead of its rising phase; BOUNCE_IDLE was re-measured off the sprite so the ball tops out under the palm. p_toss had the tossed ball drawn into it — cut, canvas height kept, because the game scales him off img.height. The runner: hip centres sat -2.0/+7.1/-6.0/-0.1 px off the sprite centre, a 13px lateral jump at 12 frames a second, and Oliver's diagnosis was right that all four frames leaned the same way and lifted the same leg. Frames 3 and 4 mirrored, all four padded to one width with the hip on the centre line. assets/_derive.py holds the three derivations, since "the same drawing with one small difference" is the one thing the generator cannot draw. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
3.7 KiB
Python
72 lines
3.7 KiB
Python
# _derive.py — the frame fixes that must NOT go through the generator.
|
|
#
|
|
# python3 assets/_derive.py
|
|
#
|
|
# Three sprites are derived from other sprites by arithmetic, because the thing that matters about
|
|
# them is that they stay pixel-identical to their neighbour. Ask the generator for "the same drawing
|
|
# with one small difference" and it returns a different man, and the swap reads as a flicker — which
|
|
# is exactly the bug each of these fixes. Re-run this after regenerating p_bounce or the runner;
|
|
# every step is idempotent, so running it twice changes nothing.
|
|
import numpy as np
|
|
from PIL import Image
|
|
import os
|
|
|
|
A = os.path.dirname(os.path.abspath(__file__)) + '/'
|
|
load = lambda n: np.array(Image.open(A + n + '.png').convert('RGBA'))
|
|
save = lambda a, n: Image.fromarray(a, 'RGBA').save(A + n + '.png')
|
|
|
|
# 1. p_toss — the generator drew the tossed ball into the sprite; the game draws the real ball.
|
|
# The CANVAS keeps its height: drawChar scales the man off img.height, so re-cropping the dead
|
|
# space would shrink him.
|
|
a = load('p_toss')
|
|
rows = (a[..., 3] > 128).any(1)
|
|
cut = int(np.argmax(~rows)) # the ball floats clear above the raised hand
|
|
if cut and not rows[cut:cut + 8].any():
|
|
a[:cut] = 0; save(a, 'p_toss')
|
|
print(f'p_toss: ball removed (rows 0..{cut})')
|
|
else:
|
|
print('p_toss: already ball-free')
|
|
|
|
# 2. p_bounce2 — p_bounce with the free arm bent so the hand rises 12 px (0.10 m) to meet the ball
|
|
# at the top of its bounce. A per-column shear, pinned at the shoulder and rigid from the wrist out,
|
|
# so the forearm bends instead of the hand detaching.
|
|
X0, K, MAX = 96, 0.8, 12
|
|
b = load('p_bounce'); out = b.copy()
|
|
for x in range(X0, b.shape[1]):
|
|
s = min(MAX, round(K * (x - X0)))
|
|
if s: out[:-s, x] = b[s:, x]; out[-s:, x] = 0
|
|
save(out, 'p_bounce2')
|
|
print('p_bounce2: derived from p_bounce')
|
|
|
|
# 3. z_f1..4 — the runner, with `--runner`. This one is NOT idempotent (it mirrors, and it pads),
|
|
# so it is opt-in: run it exactly once on a freshly generated set of runner frames.
|
|
# Two defects, both measured. The generator drew all four frames leaning the same way and lifting
|
|
# the SAME leg, so the cycle never used his other side — mirroring frames 3 and 4 gives an
|
|
# alternating stride out of the poses that exist (he is front-on, so a mirror is free). And each
|
|
# frame is cropped to its own bbox and drawn centred on the body's x, so a swinging arm slid the
|
|
# whole man sideways: hip centres sat -2.0/+7.1/-6.0/-0.1 px off centre, a 13 px lateral jump at 12
|
|
# frames a second — that was the hard flicker. Pad every frame to one width with the HIP on the
|
|
# centre line. Padding is free: drawStatic derives the drawn width from img.width, so the figure
|
|
# inside keeps its scale and only its anchor moves.
|
|
import sys
|
|
if '--runner' not in sys.argv:
|
|
print('z_f1..4: skipped (pass --runner; it mirrors and pads, so it must run only once)')
|
|
raise SystemExit
|
|
NS = ['z_f1', 'z_f2', 'z_f3', 'z_f4']
|
|
for n in NS[2:]:
|
|
save(np.array(Image.open(A + n + '.png').convert('RGBA').transpose(Image.FLIP_LEFT_RIGHT)), n)
|
|
print(f'{n}: mirrored — the other leg leads')
|
|
ims = [load(n) for n in NS]
|
|
def hip(a): # midpoint of the hip band, not the mean: an arm
|
|
al = a[..., 3] > 128 # crossing the band would drag a mean sideways
|
|
xs = np.nonzero(al[int(al.shape[0] * .50):int(al.shape[0] * .62)].any(0))[0]
|
|
return (xs.min() + xs.max()) / 2
|
|
hips = [hip(a) for a in ims]
|
|
W = int(np.ceil(max(max(h, a.shape[1] - h) for h, a in zip(hips, ims)) * 2))
|
|
for n, a, h in zip(NS, ims, hips):
|
|
left = int(round(W / 2 - h))
|
|
o = np.zeros((a.shape[0], W, 4), np.uint8)
|
|
o[:, left:left + a.shape[1]] = a
|
|
save(o, n)
|
|
print(f'z_f1..4: hip-registered onto {W}px')
|