# _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')