diff --git a/PROJECT.md b/PROJECT.md index fbd76ae..39f3901 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -1768,16 +1768,50 @@ stops being something you police with gates and becomes a property of the reques the model's own guidance says: frames generated in bulk as a sheet hold their character; frames generated one at a time do not. +### THE POSE REFERENCE IS `assets/_pose_ref_family.png`, AND IT IS TRACKED + +**One file is the reference for every zombie tennis character there will ever be.** 31 frames of the +club pro in 9 row bands, exactly `_zsheet.layout()`'s order, on magenta. Every family in `ZFAMS` +except `r` itself was drawn off it. It lived in `assets/_refcheck/` — which is **gitignored** — so the +thing the entire pipeline rests on was untracked and unreproducible for as long as it existed. It is +`assets/_pose_ref_family.png` now, next to `_pose_ref_serve.png`, and it is committed. It ships in the +image via `COPY assets`, like every other source sheet; that is not worth a Dockerfile exception. + +**Nothing in this repo composed it.** `_regen.ref_sheet()` builds ONE row at ONE scale — it is not +what made a 9-row sheet. Whatever did was ad-hoc and is gone. Rebuilding the sheet therefore means +writing the composer INTO the repo, once, with the gate below. + +#### NEVER PUT THE REFERENCE THROUGH THE MODEL + +The version of this sheet that produced the six broken families was a GENERATED RE-DRAW of `z_r*` — +nine separate 4K calls, one per row band, stacked locally. That round trip is where both of Oliver's +sprite-bench faults came from, and `_famref.py` exists to make it impossible: the sheet is composed +from the sprites on disk, one scale, NEAREST, no API. See "The three defects" below for the numbers. + ### The four steps ```bash -# 1. the pose sheet — 31 existing frames, one PNG (any family that is already good; today z_r*) -# Composed locally, no API. See assets/_refcheck/brute_poses_in.png for the shape. -# 2. the character — ONE frame of the new body, upscaled NEAREST, on magenta -# 3. ONE call: poses from image 1, character from image 2 -# 4. cut it back down -python3 assets/_zsheet.py --out assets/_refcheck/sprites # review first -python3 assets/_zsheet.py --install # then install +# 1. THE POSE SHEET — composed from the 31 z_r* frames, one scale, NEAREST, gated. No API call. +uv run --with pillow --with numpy python assets/_famref.py +# 2+3. THE CHARACTER (one frame at 4x NEAREST on magenta) and ONE call: poses from image 1, +# character from image 2. Both live in _famroll.py; add a family with one row in its FAMS table. +uv run --with pillow --with requests python assets/_famroll.py +# 4. CUT IT BACK DOWN — review first, install second. +uv run --with pillow --with numpy python assets/_zsheet.py assets/_refcheck/fam_.png --out assets/_refcheck/rev_ +uv run --with pillow --with numpy python assets/_zsheet.py assets/_refcheck/fam_.png --install +``` + +`uv run --with …` is how these run: this machine's python has no PIL or numpy of its own, and the +previous session's interpreter was a uv ephemeral env. `SPLAT_PY` still overrides for `_regen.py`. + +**A frozen stride pair in the master blocks the whole pipeline, by design.** `_famref.py` refuses to +write the sheet if any of the three stride pairs has legs that barely move — because the model copies +what it is shown, so a frozen pair in the reference is a frozen pair in every family drawn off it. +Draw the missing twin first: + +```bash +uv run --with pillow --with numpy --with requests python assets/_stride.py z_rw1 # then z_rw2 +uv run --with pillow --with numpy --with requests python assets/_stride.py z_rw2 --example z_rw1 ``` Then: add the key to `config.ZFAMS`, put `fam: ''` on an `OPPONENTS` row, bump `SPRV` and @@ -1860,6 +1894,103 @@ magenta — check a new sheet. `z_brw1fall` was ONE DRAWING PASTED FOUR TIMES (0.011–0.055 apart, where a real fall sequence is 0.53–0.89). Run it before trusting any chained batch. +### The three defects the sprite bench found, and the fix (2026-08-12, ALL FIXED) + +Oliver walked the sprite bench and reported the same two faults on every family: the healthy walk +loses its head, and the wounded walk's feet do not move. All of it traced to `_pose_ref_family.png`, +and the pipeline had copied it faithfully — **which is the pipeline working.** Measured per figure as +the implied scale of the sheet against the `z_r*` PNG it was pasted from, width and height separately: + +``` +band 0 play 1 w2.339 h2.362 | 2 w2.341 h1.996 <-- the ONLY non-proportional figure in all 31 + 3 w2.345 h2.360 | 4 w2.333 h2.361 +band 1 serve 2.335 | band 2 fall 1.902 | band 3 w1 walk 2.34 | band 4 w1 serve 2.30 +band 5 w1 fall 1.922 | band 6 w2 walk 2.38 | band 7 w2 serve 2.047 | band 8 w2 fall 1.945 +``` + +**1. `z_r2` IS PASTED WITH ITS HEAD CLIPPED OFF.** Width scales at 2.341, height at 1.996 — 33 sprite +px missing off the top, width untouched. Every other figure is proportional to three decimals, so +this is a clip and it is the only one in the sheet. `z_r2.png` on disk is fine (123x225, head +present): the damage is in the composition, not the sprite. One bad paste, six families — and this is +the whole of "healthy walk loses its head", because drawZombie picks frames 2/3 as the walk pair. + +The last commit's walk-frame levelling makes it WORSE, and that is not the levelling being wrong: it +reads the missing head as a height error and scales the body up to compensate, so `z_sh2` is 147 px +wide against `z_sh3`'s 116. The walk flickers between a normal body and an oversized headless one. + +**2. THE SHEET IS NOT ONE SCALE, IT IS FIVE.** The fall bands sit at 81% of the standing bands and the +w2 serve band at 87%. That is the exact bug `_zsheet.py` was written to prevent on the way DOWN, +present on the way UP — the sheet tells the model a falling body is a fifth smaller than a standing +one. **`_zsheet.py`'s fall1 re-anchor and the walk levelling exist only to undo this.** Rebuild the +sheet at one global scale and both should become no-ops; that is the check that they were +compensations and not fudges. + +**3. THE WOUNDED STRIDE PAIR WAS NEVER DRAWN, and a rebuild will not fix it.** Leg-region silhouette +difference within the stride pair (feet-anchored, mass-centred, bottom third): + +| | healthy 2 <-> 3 | w1 <-> w1b | w2 <-> w2b | +|---|---|---|---| +| the reference sheet | **51.1%** | 14.3% | 14.5% | +| installed `r` | 51.0% | 15.2% | 14.8% | +| the six new families | 41–70% | 4.8–19.7% | 6.2–19.9% | + +`z_rw1b` genuinely has the same legs as `z_rw1` on disk, so the club pro has been sliding through both +wound stages since the day he shipped — `r` IS the reference, which is why its numbers match it +exactly. drawZombie is animating correctly (verified by instrumenting `drawImage` on the bench: it +alternates `z_rw1` <-> `z_rw1b` every 0.42 m of ground covered, the right frames at the right +cadence). This one needs the master's `w1b`/`w2b` REDRAWN with the legs swapped before a rebuild is +worth spending a roll on. + +**THE GATE THAT WAS MISSING IS ON THE INPUT.** `_zsheet.py` checks the roll that comes back — band +count, figure count, stacked bodies, edges, palette, heights — and nothing had ever checked the sheet +handed TO the model. `_famref.check()` is that gate: it re-reads the sheet it just wrote with +`_zsheet`'s own band and figure detection, and refuses one where any figure's width and height scales +disagree by more than 2% (the head clip was a 0.35 discrepancy) or where a stride pair is frozen. + +#### How each one was fixed + +| defect | fix | cost | +|---|---|---| +| 1 · head | `_famref.py` composes the sheet from the sprites on disk, one scale, NEAREST | no API call | +| 2 · five scales | same — one `k` for all 31 figures, derived once | no API call | +| 3 · frozen wounded stride | `_stride.py` draws the missing twin, shown the healthy pair as the example | 2 rolls | + +Defect 3 is the one that needed new art, and the thing that made it work after every previous attempt +failed was **showing the edit instead of describing it**: IMAGE 2 is the healthy stride pair side by +side, the same character performing exactly the change being asked for. Words alone ("identical above +the hips, legs clearly swapped") had returned near-identical drawings for as long as they were tried. +The headless twin then took the ARM-OFF pair as its example rather than the healthy one — a wounded +body is a closer picture of the edit, the same chaining the wound ladder itself uses. + +`_stride.py`'s gate measures three things and prints all three: legs moved (>=30%), body above the +hips did NOT (<=25%), and the racket bed survived (>=0.6x the base's area). **Its thresholds are +calibrated on stride pairs that already ship, not chosen** — the table is in the file. My first +guesses, 35 and 14, rejected `z_e1/z_e2` and `z_g1/z_g2`, both of which are accepted art. + +**And a stride twin FILLS the racket bed instead of keying it out** — the opposite of `_zsheet.py`, +for the opposite reason. `_zsheet` cuts a whole family, so every bed keys out together and the racket +is an open hoop throughout. A twin is half of a pair whose other half is already on disk with a +painted bed: the first rolls came back with 959 px of enclosed transparency against their base's 0, +which would have flickered the racket face empty/filled once per step. + +#### After the fix, measured on the installed sprites + +Every family re-rolled off the rebuilt reference. Leg difference within each stride pair: + +| fam | walk 2/3 | w1/w1b | w2/w2b | | fam | walk 2/3 | w1/w1b | w2/w2b | +|---|---|---|---|---|---|---|---|---| +| r | 51.1% | 68.1% | 36.7% | | ru | 55.9% | 59.1% | 38.7% | +| br | 40.7% | 42.0% | 26.8% | | gk | 41.1% | 44.6% | 30.6% | +| sh | 59.4% | 71.4% | 43.9% | | lj | 38.4% | 45.5% | 27.4% | +| bb | 56.5% | 67.1% | 43.2% | | st | 51.6% | 66.1% | 35.7% | +| | | | | | | *was 4.8–19.7%* | | | + +Every play row has its head, verified frame by frame in `#bench`, and all 24 family/wound-stage states +alternate a real pair (checked by instrumenting `drawImage` on the bench, not by eye). `br` and `lj` +sit at 26.8 / 27.4 on the headless pair — the hardest one, subtler than the rest but visibly moving; +worth a re-roll if they ever read as stiff. `lj` needed one re-roll: its first came back frozen on all +three pairs with the headless row 31 px short. + ### Still open - The style is close but not identical to the hand-made sprites: softer outline, blended shading diff --git a/assets/_famref.py b/assets/_famref.py new file mode 100644 index 0000000..b922436 --- /dev/null +++ b/assets/_famref.py @@ -0,0 +1,189 @@ +# _famref.py — compose THE POSE REFERENCE, `assets/_pose_ref_family.png`, from the installed z_r* +# frames. Locally. No API call. +# +# uv run --with pillow --with numpy python assets/_famref.py # write it, then gate it +# uv run --with pillow --with numpy python assets/_famref.py --check # gate the file on disk only +# +# WHY THIS EXISTS. The reference every zombie tennis family is drawn from used to be a GENERATED +# RE-DRAW of the racket family: nine separate 4K calls, one per row band, stacked locally +# (`gen4k.py`, session "Zombie sprite regeneration"). Both of the faults Oliver found on the sprite +# bench came out of that round trip, and both were then copied faithfully into all six families: +# +# THE HEAD. The play row's call came back with the shuffle-A figure DECAPITATED. Measured against +# the sprite it was drawn from, that one figure scaled 2.341 wide and 1.996 tall where all thirty +# others were proportional to three decimals — 33 sprite px missing off the top, width untouched. +# The local input to that call (`in_1_play.png`) was exact at 2.000x. The sheet lost the head, not +# the sprite: `z_r2.png` on disk has always been fine. +# +# THE SCALE. Nine independent calls returned nine independent sizes — the fall bands at 81% of the +# standing bands, the w2 serve band at 87%. `_zsheet.py`'s fall1 re-anchor and its walk-frame +# levelling exist ONLY to undo that, and the levelling is what turned a headless z_sh2 into a +# headless AND 13% oversized one: it read the missing head as a height error and scaled up. +# +# A reference composed from the sprites cannot have either fault. One scale, applied once, NEAREST; +# every figure is the art that is already on disk. That is the whole fix, and it costs no tokens. +# +# THE GATE IS ON THE INPUT, WHICH IS WHERE IT WAS MISSING. `_zsheet.py` checks what comes BACK from +# the model — band count, figure count, stacked bodies, edges, palette, heights — and nothing ever +# checked the sheet handed TO it. check() below re-reads the file it just wrote with _zsheet's own +# detection and refuses a sheet the cutter would misread or that has lost a figure's head. +import os, sys +import numpy as np +from PIL import Image + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _zsheet import layout, bands, key # the same layout and the same row splitter + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT = os.path.join(HERE, '_pose_ref_family.png') +FAM = 'r' # the club pro is the master. See PROJECT.md. +MAGENTA = (255, 0, 255) +SIZE = (3609, 6416) # 9:16, the aspect the images endpoint snaps to +MARGIN = 0.025 # a PROPORTION, never a pixel count — the model +GAP_X, GAP_Y = 60, 60 # picks its own resolution (see PROJECT.md) +STRIDE_MIN = 30.0 # % leg difference a real stride pair clears — calibrated in _stride.py + + +def bed_magenta(im): + """Repaint the racket's string bed magenta, so it cuts to an open hoop downstream. + + `_zsheet.py` deliberately INVERTS _fine.py's enclosed-magenta rule: the bed is meant to key out. + That only works if the reference SHOWS a magenta bed, because a picture beats a paragraph — the + prompt asking for one is not enough on its own. The installed z_r* frames have the bed painted + pale, so it is repainted here. The bed is the largest pale blob in the frame; the runners-up are + the shoes, at a third of its area or less on every one of the 31 frames. + """ + a = np.array(im.convert('RGBA')).astype(int) + R, G, B, A = a[..., 0], a[..., 1], a[..., 2], a[..., 3] + lo, hi = np.minimum(np.minimum(R, G), B), np.maximum(np.maximum(R, G), B) + pale = (A > 40) & (lo > 140) & (hi - lo < 50) + lab = np.zeros(pale.shape, int) + n = 0 + for y, x in zip(*np.nonzero(pale)): + if lab[y, x]: continue + n += 1 + stack = [(y, x)]; lab[y, x] = n + while stack: + cy, cx = stack.pop() + for dy in (-1, 0, 1): + for dx in (-1, 0, 1): + ny, nx = cy + dy, cx + dx + if 0 <= ny < pale.shape[0] and 0 <= nx < pale.shape[1] and pale[ny, nx] and not lab[ny, nx]: + lab[ny, nx] = n; stack.append((ny, nx)) + if not n: return im + areas = [(int((lab == i).sum()), i) for i in range(1, n + 1)] + best = max(areas)[1] + a[lab == best, :3] = MAGENTA + return Image.fromarray(a.astype('uint8'), 'RGBA') + + +def legs(im): + """Feet-anchored, mass-centred silhouette of the bottom third — the stride comparison surface. + + Normalised so a wider canvas cannot fake a difference: same metres-per-pixel for every frame, + centred on the silhouette's own horizontal centre of mass, anchored on the ground line. + """ + a = np.array(im.convert('RGBA')) + op = a[..., 3] > 40 + if not op.any(): return np.zeros((32, 64), bool) + h, w = op.shape + y0 = int(h * 0.66) + ys, xs = np.nonzero(op[y0:]) + cx = xs.mean() if len(xs) else w / 2 + W, H, mpp = 64, 32, w / 140 + gx = np.clip((cx + (np.arange(W) - W / 2) * mpp).round().astype(int), 0, w - 1) + gy = np.clip((h - 1 - (H - 1 - np.arange(H)) * ((h - y0) / H)).round().astype(int), 0, h - 1) + return op[np.ix_(gy, gx)] + + +def stride_diff(a, b): + """% of the union silhouette that differs. A real stride pair clears 33; a frozen one sits at 15.""" + u = (a | b).sum() + return round(float((a ^ b).sum()) / u * 100, 1) if u else 0.0 + + +def compose(): + rows = layout(FAM) + frames = {n: bed_magenta(Image.open(os.path.join(HERE, n + '.png'))) for row in rows for n in row} + # ONE scale for all 31 figures, derived from whichever axis binds. This is the entire point of the + # file: the old sheet had five scales because it was five separate rolls. + row_w = [sum(frames[n].width for n in row) + GAP_X * (len(row) - 1) for row in rows] + row_h = [max(frames[n].height for n in row) for row in rows] + cw, ch = max(row_w), sum(row_h) + GAP_Y * (len(rows) - 1) + k = min(SIZE[0] * (1 - 2 * MARGIN) / cw, SIZE[1] * (1 - 2 * MARGIN) / ch) + + sheet = Image.new('RGB', SIZE, MAGENTA) + y = round((SIZE[1] - ch * k) / 2) + for row, rh in zip(rows, row_h): + scaled = [(n, round(frames[n].width * k), round(frames[n].height * k)) for n in row] + x = round((SIZE[0] - (sum(w for _, w, _ in scaled) + round(GAP_X * k) * (len(row) - 1))) / 2) + ground = y + round(rh * k) # ONE ground line per band, bottom-aligned + for n, w, h in scaled: + sheet.paste(frames[n].resize((w, h), Image.NEAREST), (x, ground - h), frames[n].resize((w, h), Image.NEAREST)) + x += w + round(GAP_X * k) + y = ground + round(GAP_Y * k) + sheet.save(OUT) + print(f'{OUT} {sheet.size} scale {k:.3f} 31 figures, {len(rows)} bands') + return k + + +def check(): + """Re-read the written sheet the way _zsheet.py will, and refuse anything it would misread.""" + rows = layout(FAM) + a = key(Image.open(OUT).convert('RGB')) + bs = bands(a) + bad = [] + if len(bs) != len(rows): + sys.exit(f'GATE FAILED: {len(bs)} row bands, expected {len(rows)}') + # scale is read off the stance, exactly as _zsheet.py reads it + K = None + for (y0, y1), row in zip(bs, rows): + band = a[y0:y1] + cols = (band[..., 3] > 0).any(0) + spans, s = [], None + for x, v in enumerate(cols): + if v and s is None: s = x + elif not v and s is not None: + if x - s > 20: spans.append((s, x)) + s = None + if s is not None: spans.append((s, len(cols))) + if len(spans) != len(row): + sys.exit(f'GATE FAILED: row {row[0]}: {len(spans)} figures, expected {len(row)}') + for (x0, x1), n in zip(spans, row): + sub = band[:, x0:x1] + ys = np.nonzero((sub[..., 3] > 0).any(1))[0] + w, h = x1 - x0, int(ys.max() - ys.min() + 1) + src = Image.open(os.path.join(HERE, n + '.png')) + if K is None: K = h / src.height + sw, sh = w / src.width, h / src.height + # THE HEAD CLIP IS EXACTLY THIS TEST. A figure that lost its head keeps its width and + # loses height, so width and height stop agreeing — 2.341 against 1.996 is what the old + # sheet's z_r2 measured. Everything proportional to 2% is a figure that is all there. + if abs(sw - sh) > 0.02 * K: + bad.append(f'{n}: w{sw:.3f} h{sh:.3f} NOT PROPORTIONAL — a clipped figure') + elif abs(sh - K) > 0.02 * K: + bad.append(f'{n}: scale {sh:.3f} against the sheet\'s {K:.3f} — not one scale') + if bad: + sys.exit('GATE FAILED:\n ' + '\n '.join(bad)) + print(f'gate: 31 figures, {len(bs)} bands, every one proportional at one scale {K:.3f}') + + # ...and every stride pair, which the old sheet also got wrong. A frozen pair in the REFERENCE is + # a frozen pair in every family drawn off it — the model copies what it is shown, so this is the + # one place the check is worth anything. It was a warning while z_rw1b and z_rw2b still had the + # same legs as the frames they twin; `_stride.py` drew them, so it is a hard gate now. + L = {n: legs(Image.open(os.path.join(HERE, n + '.png'))) for n in + ('z_r2', 'z_r3', 'z_rw1', 'z_rw1b', 'z_rw2', 'z_rw2b')} + frozen = [] + for lbl, p, q in (('healthy 2<->3', 'z_r2', 'z_r3'), ('w1<->w1b', 'z_rw1', 'z_rw1b'), + ('w2<->w2b', 'z_rw2', 'z_rw2b')): + d = stride_diff(L[p], L[q]) + print(f' stride {lbl:14s} {d:5.1f}% {"ok" if d >= STRIDE_MIN else "FROZEN — legs barely move"}') + if d < STRIDE_MIN: frozen.append(f'{p}/{q} at {d:.1f}%') + if frozen: + sys.exit('GATE FAILED: frozen stride pair(s): ' + ', '.join(frozen) + + f' — need >={STRIDE_MIN}%. Draw the twin with _stride.py before rolling a family.') + + +if __name__ == '__main__': + if '--check' not in sys.argv: compose() + check() diff --git a/assets/_famroll.py b/assets/_famroll.py new file mode 100644 index 0000000..9ec14f7 --- /dev/null +++ b/assets/_famroll.py @@ -0,0 +1,125 @@ +# _famroll.py — roll ONE opponent family: 31 sprites, one call, one character. +# +# uv run --with pillow --with requests python assets/_famroll.py br # one family +# uv run --with pillow --with requests python assets/_famroll.py sh bb # several, in sequence +# +# Then cut it back down, review first and install second: +# uv run --with pillow --with numpy python assets/_zsheet.py assets/_refcheck/fam_br.png br --out assets/_refcheck/sprites_br +# uv run --with pillow --with numpy python assets/_zsheet.py assets/_refcheck/fam_br.png br --install +# +# WHY THIS IS IN THE REPO. Every family in ZFAMS except the club pro was rolled by a script that lived +# in a session scratchpad and is gone, off a reference in a gitignored directory. The pipeline PROJECT.md +# calls the one reliable way to make this game's art was, in practice, unreproducible. This is that +# script, tracked, reading the tracked reference that `_famref.py` composes. +# +# IMAGE 1 IS THE POSES, IMAGE 2 IS THE CHARACTER, and naming each image's job in the prompt is +# load-bearing: without it the majority wins and the roll comes back as whatever most of the pixels +# look like. See PROJECT.md, "What the model will and will not do". +import base64, os, sys +import requests +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +REF = os.path.join(HERE, '_pose_ref_family.png') +SCRATCH = os.path.join(HERE, '_refcheck') +MODEL = os.environ.get('SPLAT_MODEL', 'google/gemini-3-pro-image') + +# key -> (the frame the character is read off, the words that describe it). The frame is the source of +# truth for build and colour; the words only have to stop the model averaging it into a generic zombie. +FAMS = { + 'br': ('z_g1', 'an ENORMOUS FAT ZOMBIE BRUTE: a huge sagging belly over the belt, a filthy pale ' + 'grey-green work shirt straining over it, heavy olive-brown work trousers, thick ' + 'tree-trunk legs, scuffed brown boots, and a small bald head sunk low into massive ' + 'rounded shoulders. Far bulkier, heavier and wider than the figure in image 1'), + 'sh': ('z_a1', 'a lean SHAMBLING ZOMBIE in a filthy dark red tennis shirt and grey shorts, gaunt and stooped'), + 'bb': ('z_e1', 'a small young ZOMBIE BALL BOY in a pale tournament polo and shorts, slight and short'), + 'st': ('z_d1', 'a ZOMBIE STEWARD in a dark uniform jacket with a high collar, stiff and official'), + 'ru': ('z_f1', 'a wiry FAST ZOMBIE RUNNER in a sleeveless tennis top, lean and forward-leaning'), + 'gk': ('z_boss1', 'a heavy ZOMBIE GROUNDSKEEPER in stained work overalls and a shapeless hat'), + 'lj': ('z_judge1', 'a ZOMBIE LINE JUDGE in a dark blazer and pale trousers, upright and severe'), +} + + +def key(): + if os.environ.get('OPENROUTER_API_KEY'): return os.environ['OPENROUTER_API_KEY'] + for line in open(os.path.join(HERE, '..', '.env')): + if line.startswith('OPENROUTER_API_KEY='): + return line.split('=', 1)[1].strip().strip('"').strip("'") + sys.exit('no OPENROUTER_API_KEY') + + +def char_ref(k, spr): + """IMAGE 2: the character, at 4x NEAREST on magenta. Raw sprite pixels, no round trip — the same + rule the pose sheet now follows, and for the same reason.""" + im = Image.open(os.path.join(HERE, spr + '.png')).convert('RGBA') + big = im.resize((im.width * 4, im.height * 4), Image.NEAREST) + pad = round(big.height * 0.08) + out = Image.new('RGB', (big.width + 2 * pad, big.height + 2 * pad), (255, 0, 255)) + out.paste(big, (pad, pad), big) + p = os.path.join(SCRATCH, f'char_{k}.png') + os.makedirs(SCRATCH, exist_ok=True) + out.save(p) + return p + + +def prompt(who): + return ( + 'Take IMAGE 1 and redraw it with ONE change: the character.\n\n' + 'IMAGE 1 is a sprite sheet of a zombie tennis player: 31 figures in 9 rows on a flat magenta ' + 'background, first undamaged, then with one arm torn off, then with the head gone.\n\n' + f'IMAGE 2 is THE CHARACTER to draw instead: {who}. It is a walking corpse with dead ' + 'grey-green skin. Copy its build, its garments and every one of its colours exactly.\n\n' + 'Keep IMAGE 1 EXACTLY as it is in every other respect: the same 9 rows, the same number of ' + 'figures per row, the same order, the same poses, the same facing, the same sizes, the same ' + 'damage states in the same places. A figure with no head keeps no head; a figure missing an arm ' + 'keeps that arm missing with the same stump and blood. Only the body wearing the poses changes. ' + 'Do not carry over the previous character\'s shirt, shorts, shoes, hair or headband.\n\n' + # EVERY UNDAMAGED FIGURE HAS A HEAD, said out loud. The clause above ("a figure with no head + # keeps no head") is there for the wound rows and the old normalisation pass took it as licence + # to remove one from the play row — which is how six families came back with a decapitated + # shuffle frame. The reference no longer shows that; the prompt no longer implies it either. + 'EVERY FIGURE IN THE FIRST THREE ROWS HAS A HEAD ON ITS SHOULDERS, and so does every figure in ' + 'rows 4, 5 and 6. Only the last three rows are headless. Never remove a head that image 1 shows.\n\n' + 'MATCH THE DRAWING STYLE OF IMAGE 1 EXACTLY. Image 1 is hard-edged pixel art and your output ' + 'must be indistinguishable from it in technique: the same square pixel grid at the same pixel ' + 'size, the same hard black outline around every figure, the same flat blocks of colour, the ' + 'same stepped aliased edges, the same hand-placed dithering. No soft brushwork, no airbrushing, ' + 'no blur, no gradients, no anti-aliasing.\n\n' + 'The background is a solid, flat, uniform chroma key. Use EXACTLY hex color #FF00FF, one single ' + 'flat tone across the whole canvas behind every figure, with no panels or boxes of any other ' + 'colour. The inside of every racket head is that same EXACT #FF00FF. No text, no shadows, ' + 'no ground lines.') + + +def u(p): + return 'data:image/png;base64,' + base64.b64encode(open(p, 'rb').read()).decode() + + +def roll(k): + spr, who = FAMS[k] + ch = char_ref(k, spr) + r = requests.post('https://openrouter.ai/api/v1/images', + headers={'Authorization': f'Bearer {key()}', 'Content-Type': 'application/json'}, + # 4K and 9:16: the endpoint DEFAULTS to 1K and snaps the aspect to an enum, so both are + # spelled out. The reference is composed at 9:16 for exactly this reason. + json={'model': MODEL, 'prompt': prompt(who), 'resolution': '4K', 'aspect_ratio': '9:16', + 'input_references': [{'type': 'image_url', 'image_url': {'url': u(REF)}}, + {'type': 'image_url', 'image_url': {'url': u(ch)}}]}, + timeout=1800) + if r.status_code != 200: + print(f'{k}: HTTP {r.status_code} {r.text[:200]}'); return + d = r.json().get('data') or [] + if not d: + print(f'{k}: no image returned'); return + b = d[0].get('b64_json') or d[0].get('image_url', {}).get('url', '').split(',')[-1] + out = os.path.join(SCRATCH, f'fam_{k}.png') + open(out, 'wb').write(base64.b64decode(b)) + print(f'{k}: {out} {Image.open(out).size} (character from {spr})', flush=True) + + +if __name__ == '__main__': + keys = [a for a in sys.argv[1:] if not a.startswith('-')] + if not keys: sys.exit(f'usage: _famroll.py ... keys: {" ".join(FAMS)}') + for k in keys: + if k not in FAMS: sys.exit(f'unknown family {k}') + roll(k) diff --git a/assets/_pose_ref_family.png b/assets/_pose_ref_family.png new file mode 100644 index 0000000..c8a1d56 Binary files /dev/null and b/assets/_pose_ref_family.png differ diff --git a/assets/_stride.py b/assets/_stride.py new file mode 100644 index 0000000..018b04b --- /dev/null +++ b/assets/_stride.py @@ -0,0 +1,309 @@ +# _stride.py — draw the missing HALF of a stride pair. One frame in, its leg-swapped twin out. +# +# uv run --with pillow --with numpy --with requests python assets/_stride.py z_rw1 z_rw2 +# uv run --with pillow --with numpy --with requests python assets/_stride.py z_rw1 --check +# +# WHY. A walk cycle in this game is ONE DRAWING WITH THE LEGS MOVED, and every `…b` frame is that +# second half. The club pro's wound rows never got one: `z_rw1b` has the same legs as `z_rw1` (15.1% +# silhouette difference in the bottom third, where the healthy pair scores 51.1%), so a wounded +# opponent has slid through both damage stages since the day it shipped — and because the family +# pipeline draws every new body off the club pro, all seven families inherited it. Oliver, from the +# sprite bench: "arm off — feet are not moving. headless — feet are not moving." +# +# THE PROMPT SHOWS THE CHANGE RATHER THAN DESCRIBING IT. Every previous attempt at this asked in +# words ("identical above the hips, legs clearly swapped") and got two near-identical drawings back. +# IMAGE 2 here is the HEALTHY pair side by side — the same character performing exactly the edit being +# asked for. That is this project's own rule: a picture beats a paragraph. +# +# THE GATE IS THE POINT. A roll is installed only if the legs moved, the body above the hips did not, +# and the racket kept its bed. All three are measured and printed; a failure leaves the sprite alone. +import base64, os, sys +import numpy as np +import requests +from PIL import Image + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _fine import key, block_size, frames, split_piece +from _famref import legs, stride_diff + +HERE = os.path.dirname(os.path.abspath(__file__)) +SCRATCH = os.path.join(HERE, '_refcheck') +MODEL = os.environ.get('SPLAT_MODEL', 'google/gemini-3-pro-image') +# THE THRESHOLDS ARE CALIBRATED ON ART THAT ALREADY SHIPS, not chosen. Every accepted stride pair in +# the repo, measured by the two functions below: +# +# pair legs upper pair legs upper +# z_r2 / z_r3 51.1% 4.4% z_e1 / z_e2 72.1% 24.0% +# z_a1 / z_a2 39.2% 13.2% z_g1 / z_g2 33.6% 8.9% +# z_b1 / z_b2 58.3% 8.0% z_br2 / z_br3 40.9% 2.2% +# z_d1 / z_d2 53.1% 9.9% z_d3 / z_d4 34.6% 5.7% +# +# ...against the frozen pairs this file exists to replace: z_rw1/z_rw1b 15.1%, z_rw2/z_rw2b 14.7%. +# So a real stride starts at 33.6 and a frozen one tops out at 15.1 — 30 sits in the gap with room on +# both sides. Upper allows 25 because the ball boy's accepted pair spends 24 of it: a step moves the +# torso and the racket arm a little, and on a one-armed or headless body there is less silhouette for +# the same absolute shift to hide in. My first guesses (35 / 14) rejected both of those shipping pairs. +# The two-frame BOSS walk (16.3%) is deliberately outside this: it is a slow pace, not a stride pair. +LEGS_MIN = 30.0 # % the legs must differ +UPPER_MAX = 25.0 # % the body above the hips may differ — it is meant to be the same drawing +BED_MIN = 0.60 # the racket bed must keep at least this fraction of the base frame's bed area + + +def upper(im): + """Silhouette of the TOP 60%, normalised like legs() — the half that must NOT change.""" + a = np.array(im.convert('RGBA')) + op = a[..., 3] > 40 + if not op.any(): return np.zeros((32, 64), bool) + h, w = op.shape + y1 = int(h * 0.60) + ys, xs = np.nonzero(op[:y1]) + cx = xs.mean() if len(xs) else w / 2 + W, H, mpp = 64, 32, w / 140 + gx = np.clip((cx + (np.arange(W) - W / 2) * mpp).round().astype(int), 0, w - 1) + gy = np.clip((np.arange(H) * (y1 / H)).round().astype(int), 0, h - 1) + return op[np.ix_(gy, gx)] + + +def pair_ref(a, b, out, scale=4): + """IMAGE 2: two frames side by side on magenta — the edit, demonstrated.""" + ims = [Image.open(os.path.join(HERE, n + '.png')).convert('RGBA') for n in (a, b)] + ims = [i.resize((i.width * scale, i.height * scale), Image.NEAREST) for i in ims] + gap = round(ims[0].height * 0.10) + pad = gap + W = sum(i.width for i in ims) + gap + 2 * pad + H = max(i.height for i in ims) + 2 * pad + s = Image.new('RGB', (W, H), (255, 0, 255)) + x = pad + for i in ims: + s.paste(i, (x, H - pad - i.height), i); x += i.width + gap + s.save(out) + return out + + +def one_ref(n, out, scale=4): + im = Image.open(os.path.join(HERE, n + '.png')).convert('RGBA') + big = im.resize((im.width * scale, im.height * scale), Image.NEAREST) + pad = round(big.height * 0.10) + s = Image.new('RGB', (big.width + 2 * pad, big.height + 2 * pad), (255, 0, 255)) + s.paste(big, (pad, pad), big) + s.save(out) + return out + + +PROMPT = ( + 'IMAGE 1 is one figure from a zombie tennis game: a walking corpse holding a racket, drawn as ' + 'hard-edged pixel art on a flat magenta background.\n\n' + 'Redraw IMAGE 1 with ONE change: THE LEGS ARE IN THE OPPOSITE PHASE OF THE SAME WALKING STEP. ' + 'The leg that is forward becomes the back leg and the leg that is back swings forward, so the two ' + 'drawings together read as one body taking a step. The feet must clearly be in different places ' + 'than in IMAGE 1.\n\n' + 'IMAGE 2 shows exactly the change being asked for: it is the same character, undamaged, drawn ' + 'twice — one drawing, the legs in the two phases of one step. Copy THAT relationship. The ' + 'difference between your output and IMAGE 1 must be the difference between the two figures in ' + 'IMAGE 2, and nothing else.\n\n' + 'EVERYTHING FROM THE HIPS UP IS UNCHANGED AND IDENTICAL TO IMAGE 1, pixel for pixel where it can ' + 'be: the same head or the same headless neck stump, the same shoulders, the same arms, the same ' + 'racket in the same hand at the same angle, the same clothing with the same tears and the same ' + 'stains, the same wounds with the same blood, the same colours, the same facing, the same height, ' + 'the same build. Do NOT redraw the character. Do NOT heal any injury. Do NOT add a head or an arm ' + 'that IMAGE 1 does not have. Only the legs and the feet move.\n\n' + 'Match IMAGE 1\'s drawing technique exactly: the same square pixel grid at the same pixel size, the ' + 'same hard black outline, the same flat blocks of colour, the same stepped aliased edges, the same ' + 'dithering. No soft brushwork, no airbrushing, no blur, no gradients, no anti-aliasing.\n\n' + 'Output ONE figure only, standing on the same ground line, at the same size as IMAGE 1, on a ' + 'background of ONE flat solid magenta #FF00FF edge to edge. The inside of the racket head is that ' + 'same EXACT #FF00FF. No text, no shadows, no ground lines, no second figure.') + + +def api_key(): + if os.environ.get('OPENROUTER_API_KEY'): return os.environ['OPENROUTER_API_KEY'] + for line in open(os.path.join(HERE, '..', '.env')): + if line.startswith('OPENROUTER_API_KEY='): + return line.split('=', 1)[1].strip().strip('"').strip("'") + sys.exit('no OPENROUTER_API_KEY') + + +def u(p): + return 'data:image/png;base64,' + base64.b64encode(open(p, 'rb').read()).decode() + + +def pale_blob(im): + """Largest flat pale blob as a % of the body — the racket bed. A bed drawn as STRINGS breaks into + fragments and scores a third of what a flat one does, which is the whole use of the number.""" + a = np.array(im.convert('RGBA')).astype(int) if isinstance(im, Image.Image) else im.astype(int) + R, G, B, A = a[..., 0], a[..., 1], a[..., 2], a[..., 3] + lo, hi = np.minimum(np.minimum(R, G), B), np.maximum(np.maximum(R, G), B) + pale = (A > 40) & (lo > 140) & (hi - lo < 50) + lab = np.zeros(pale.shape, int); n = 0 + for y, x in zip(*np.nonzero(pale)): + if lab[y, x]: continue + n += 1; st = [(y, x)]; lab[y, x] = n + while st: + cy, cx = st.pop() + for dy in (-1, 0, 1): + for dx in (-1, 0, 1): + ny, nx = cy + dy, cx + dx + if 0 <= ny < pale.shape[0] and 0 <= nx < pale.shape[1] and pale[ny, nx] and not lab[ny, nx]: + lab[ny, nx] = n; st.append((ny, nx)) + if not n: return 0.0, None, None + sizes = [(int((lab == i).sum()), i) for i in range(1, n + 1)] + big, idx = max(sizes) + m = lab == idx + colour = a[m][:, :3].mean(0) if m.any() else None + return round(big / max(1, int((A > 40).sum())) * 100, 2), m, colour + + +def fill_bed(crop, ref): + """Repaint the racket's enclosed background in the BASE frame's bed colour. + + `_fine.key` keeps background trapped inside a figure as ink rather than punching it out, so the + hole is here to be filled; the colour comes off the base's own bed so the pair cannot disagree. + """ + _, _, colour = pale_blob(ref) + if colour is None: return crop + a = crop.copy() + op = a[..., 3] > 40 + h, w = op.shape + seen = np.zeros_like(op) + stack = [(y, x) for x in range(w) for y in (0, h - 1) if not op[y, x]] + stack += [(y, x) for y in range(h) for x in (0, w - 1) if not op[y, x]] + for y, x in stack: seen[y, x] = True + while stack: + cy, cx = stack.pop() + for dy, dx in ((1, 0), (-1, 0), (0, 1), (0, -1)): + ny, nx = cy + dy, cx + dx + if 0 <= ny < h and 0 <= nx < w and not op[ny, nx] and not seen[ny, nx]: + seen[ny, nx] = True; stack.append((ny, nx)) + inside = ~op & ~seen + if inside.any(): + a[inside, :3] = colour.astype(a.dtype) + a[inside, 3] = 255 + return a + + +def cut(sheet, like): + """Slice the roll down to ONE sprite at the SAME height as the frame it twins, on its palette. + + Same height is not a nicety: the game draws every frame at its own PNG height, so a twin that + came back 20 px taller would make the body rise once per step — the bounce this project has + already fixed twice. + """ + src = Image.open(sheet).convert('RGB') + a = key(src) + # THE RACKET BED IS FILLED HERE, NOT KEYED OUT — the opposite of what _zsheet.py does, and for the + # opposite reason. _zsheet cuts a whole family, where every frame's bed keys out together and the + # racket is an open hoop throughout. A stride twin is half of a PAIR whose other half is already + # on disk with a painted bed, so keying this one out makes the racket face flicker empty/filled + # once per step — measured: z_rw1 has 0 px of enclosed transparency and the first z_rw1b roll had + # 959. `_fine.key` already keeps enclosed background as ink; it is repainted in the base frame's + # own bed colour so the pair matches. + b = block_size(a) + spans = frames(a, [], 1) + if len(spans) != 1: + return None, f'{len(spans)} figures in the roll, expected 1' + x0, x1 = spans[0] + sub = a[:, x0:x1] + ys = np.nonzero((sub[..., 3] > 0).any(1))[0] + xs = np.nonzero((sub[..., 3] > 0).any(0))[0] + crop = sub[ys.min():ys.max() + 1, xs.min():xs.max() + 1] + if split_piece(crop): + return None, 'the roll holds two stacked bodies' + ref = Image.open(os.path.join(HERE, like + '.png')).convert('RGBA') + crop = fill_bed(crop, ref) + snap = Image.fromarray(crop, 'RGBA').resize( + (max(1, round(crop.shape[1] / b)), max(1, round(crop.shape[0] / b))), Image.BOX) + k = ref.height / snap.height + im = snap.resize((max(1, round(snap.width * k)), max(1, round(snap.height * k))), Image.BOX) + arr = np.array(im).astype(int) + solid = arr[..., 3] > 110 + px = np.array(ref)[np.array(ref)[..., 3] > 200][:, :3].astype(int) + pal = np.unique((px // 8) * 8, axis=0) + f = arr[..., :3].reshape(-1, 3) + arr[..., :3] = pal[((f[:, None, :] - pal[None, :, :]) ** 2).sum(2).argmin(1)].reshape(arr.shape[0], arr.shape[1], 3) + arr[..., 3] = np.where(solid, 255, 0) + return Image.fromarray(arr.astype('uint8'), 'RGBA'), None + + +def gate(new, base): + """Legs moved, body did not, racket still has a bed. All three numbers, always printed.""" + ref = Image.open(os.path.join(HERE, base + '.png')) + dl = stride_diff(legs(new), legs(ref)) + du = stride_diff(upper(new), upper(ref)) + bn, br = pale_blob(new)[0], pale_blob(ref)[0] + # the bed is drawn, not keyed out, so it must survive at close to the base's area. A string mesh + # fragments it — the first z_rw2b roll scored 2.25 against its base's 7.22. + bed = bn / br if br else 1.0 + ok = dl >= LEGS_MIN and du <= UPPER_MAX and bed >= BED_MIN + print(f' legs {dl:5.1f}% (>={LEGS_MIN}) upper {du:5.1f}% (<={UPPER_MAX}) ' + f'bed {bn:5.2f}% vs {br:5.2f}% = {bed:.2f}x (>={BED_MIN}) {"PASS" if ok else "REJECT"}') + return ok + + +def attempt(base, example, i): + """One roll. EVERY candidate is kept under its own name — a re-roll that comes back worse must + never be able to overwrite one that passed, which is exactly how the first good z_rw1b was lost.""" + twin = base + 'b' + i1 = one_ref(base, os.path.join(SCRATCH, f'stride_in_{base}.png')) + i2 = pair_ref(example[0], example[1], os.path.join(SCRATCH, f'stride_ex_{example[0]}.png')) + r = requests.post('https://openrouter.ai/api/v1/images', + headers={'Authorization': f'Bearer {api_key()}', 'Content-Type': 'application/json'}, + json={'model': MODEL, 'prompt': PROMPT, 'resolution': '2K', 'aspect_ratio': '1:1', + 'input_references': [{'type': 'image_url', 'image_url': {'url': u(i1)}}, + {'type': 'image_url', 'image_url': {'url': u(i2)}}]}, + timeout=1800) + if r.status_code != 200: + print(f' [{i}] HTTP {r.status_code} {r.text[:160]}'); return None + d = r.json().get('data') or [] + if not d: + print(f' [{i}] no image returned'); return None + b64 = d[0].get('b64_json') or d[0].get('image_url', {}).get('url', '').split(',')[-1] + raw = os.path.join(SCRATCH, f'stride_raw_{twin}_{i}.png') + open(raw, 'wb').write(base64.b64decode(b64)) + im, err = cut(raw, base) + if err: + print(f' [{i}] {err} — discarded'); return None + cand = os.path.join(SCRATCH, f'stride_cand_{twin}_{i}.png') + im.save(cand) + print(f' [{i}] cut {im.width}x{im.height} -> {cand}') + return im if gate(im, base) else None + + +def run(base, example=None, tries=3, install=True): + fam = base[2:base.index('w')] if 'w' in base[2:] else base[2:] + example = example or (f'z_{fam}2', f'z_{fam}3') + twin = base + 'b' + print(f'{base} -> {twin} stride example: {example[0]} / {example[1]}') + for i in range(1, tries + 1): + im = attempt(base, example, i) + if im: + if install: + im.save(os.path.join(HERE, twin + '.png')) + print(f' installed {twin}.png') + return True + print(f' {tries} rolls, none passed — {twin}.png left alone. Candidates are in _refcheck/.') + return False + + +if __name__ == '__main__': + # a flag's VALUE is not a frame name — skip the token after every option that takes one + argv, args, skip = sys.argv[1:], [], False + for i, a in enumerate(argv): + if skip: skip = False; continue + if a in ('--example', '--tries'): skip = True; continue + if not a.startswith('-'): args.append(a) + if not args: sys.exit('usage: _stride.py ... [--example z_rw1] [--tries N] [--check]') + # --example NAME uses NAME / NAMEb as the demonstration pair instead of the healthy shuffle. A + # wound stage is a closer picture of the edit than an undamaged body is — same chaining logic the + # wound ladder itself uses (see _master.py). + ex = None + if '--example' in sys.argv: + e = sys.argv[sys.argv.index('--example') + 1] + ex = (e, e + 'b') + tries = int(sys.argv[sys.argv.index('--tries') + 1]) if '--tries' in sys.argv else 3 + if '--check' in sys.argv: + for base in args: + print(base) + gate(Image.open(os.path.join(HERE, base + 'b.png')), base) + else: + for base in args: + run(base, ex, tries, install='--no-install' not in sys.argv) diff --git a/assets/z_bb1.png b/assets/z_bb1.png index 82ff0ce..35b1ab9 100644 Binary files a/assets/z_bb1.png and b/assets/z_bb1.png differ diff --git a/assets/z_bb2.png b/assets/z_bb2.png index 1e2f5f5..722f4b8 100644 Binary files a/assets/z_bb2.png and b/assets/z_bb2.png differ diff --git a/assets/z_bb3.png b/assets/z_bb3.png index 368090d..0cbbe67 100644 Binary files a/assets/z_bb3.png and b/assets/z_bb3.png differ diff --git a/assets/z_bb4.png b/assets/z_bb4.png index 82543ab..f819cf8 100644 Binary files a/assets/z_bb4.png and b/assets/z_bb4.png differ diff --git a/assets/z_bbbounce.png b/assets/z_bbbounce.png index 4794fcd..ed4c08d 100644 Binary files a/assets/z_bbbounce.png and b/assets/z_bbbounce.png differ diff --git a/assets/z_bbfall1.png b/assets/z_bbfall1.png index 6aa9911..208dab3 100644 Binary files a/assets/z_bbfall1.png and b/assets/z_bbfall1.png differ diff --git a/assets/z_bbfall2.png b/assets/z_bbfall2.png index f4d3c29..b70ccb8 100644 Binary files a/assets/z_bbfall2.png and b/assets/z_bbfall2.png differ diff --git a/assets/z_bbfall3.png b/assets/z_bbfall3.png index 52fb8c6..3278087 100644 Binary files a/assets/z_bbfall3.png and b/assets/z_bbfall3.png differ diff --git a/assets/z_bbrise.png b/assets/z_bbrise.png index 97a9c06..0163789 100644 Binary files a/assets/z_bbrise.png and b/assets/z_bbrise.png differ diff --git a/assets/z_bbserve.png b/assets/z_bbserve.png index 125f066..7f8bcea 100644 Binary files a/assets/z_bbserve.png and b/assets/z_bbserve.png differ diff --git a/assets/z_bbtoss.png b/assets/z_bbtoss.png index 0c6ae38..d811110 100644 Binary files a/assets/z_bbtoss.png and b/assets/z_bbtoss.png differ diff --git a/assets/z_bbw1.png b/assets/z_bbw1.png index a0b3dde..c52a9f1 100644 Binary files a/assets/z_bbw1.png and b/assets/z_bbw1.png differ diff --git a/assets/z_bbw1b.png b/assets/z_bbw1b.png index f45b2d4..9404cfb 100644 Binary files a/assets/z_bbw1b.png and b/assets/z_bbw1b.png differ diff --git a/assets/z_bbw1bounce.png b/assets/z_bbw1bounce.png index a829826..c87a0f2 100644 Binary files a/assets/z_bbw1bounce.png and b/assets/z_bbw1bounce.png differ diff --git a/assets/z_bbw1fall1.png b/assets/z_bbw1fall1.png index e284221..0ad640c 100644 Binary files a/assets/z_bbw1fall1.png and b/assets/z_bbw1fall1.png differ diff --git a/assets/z_bbw1fall2.png b/assets/z_bbw1fall2.png index 9c78b66..50fb3f0 100644 Binary files a/assets/z_bbw1fall2.png and b/assets/z_bbw1fall2.png differ diff --git a/assets/z_bbw1fall3.png b/assets/z_bbw1fall3.png index 5a612ef..8f179cf 100644 Binary files a/assets/z_bbw1fall3.png and b/assets/z_bbw1fall3.png differ diff --git a/assets/z_bbw1rise.png b/assets/z_bbw1rise.png index 58ac53d..5c49754 100644 Binary files a/assets/z_bbw1rise.png and b/assets/z_bbw1rise.png differ diff --git a/assets/z_bbw1s.png b/assets/z_bbw1s.png index 7326fef..c3b0778 100644 Binary files a/assets/z_bbw1s.png and b/assets/z_bbw1s.png differ diff --git a/assets/z_bbw1serve.png b/assets/z_bbw1serve.png index eba6782..383d8a5 100644 Binary files a/assets/z_bbw1serve.png and b/assets/z_bbw1serve.png differ diff --git a/assets/z_bbw1toss.png b/assets/z_bbw1toss.png index 1fbdec7..59200df 100644 Binary files a/assets/z_bbw1toss.png and b/assets/z_bbw1toss.png differ diff --git a/assets/z_bbw2.png b/assets/z_bbw2.png index df6551d..4b38cf6 100644 Binary files a/assets/z_bbw2.png and b/assets/z_bbw2.png differ diff --git a/assets/z_bbw2b.png b/assets/z_bbw2b.png index f4729a4..6f19490 100644 Binary files a/assets/z_bbw2b.png and b/assets/z_bbw2b.png differ diff --git a/assets/z_bbw2bounce.png b/assets/z_bbw2bounce.png index 12bc221..98673f3 100644 Binary files a/assets/z_bbw2bounce.png and b/assets/z_bbw2bounce.png differ diff --git a/assets/z_bbw2fall1.png b/assets/z_bbw2fall1.png index bd79bc7..a8839c7 100644 Binary files a/assets/z_bbw2fall1.png and b/assets/z_bbw2fall1.png differ diff --git a/assets/z_bbw2fall2.png b/assets/z_bbw2fall2.png index e879c25..716fb53 100644 Binary files a/assets/z_bbw2fall2.png and b/assets/z_bbw2fall2.png differ diff --git a/assets/z_bbw2fall3.png b/assets/z_bbw2fall3.png index 6a10871..2fc7be1 100644 Binary files a/assets/z_bbw2fall3.png and b/assets/z_bbw2fall3.png differ diff --git a/assets/z_bbw2rise.png b/assets/z_bbw2rise.png index 58e46f3..ce5a21d 100644 Binary files a/assets/z_bbw2rise.png and b/assets/z_bbw2rise.png differ diff --git a/assets/z_bbw2s.png b/assets/z_bbw2s.png index 12aa068..62bbec9 100644 Binary files a/assets/z_bbw2s.png and b/assets/z_bbw2s.png differ diff --git a/assets/z_bbw2serve.png b/assets/z_bbw2serve.png index 6860d11..9883796 100644 Binary files a/assets/z_bbw2serve.png and b/assets/z_bbw2serve.png differ diff --git a/assets/z_bbw2toss.png b/assets/z_bbw2toss.png index ac46a20..e972209 100644 Binary files a/assets/z_bbw2toss.png and b/assets/z_bbw2toss.png differ diff --git a/assets/z_br1.png b/assets/z_br1.png index 06f3b13..cf9aadb 100644 Binary files a/assets/z_br1.png and b/assets/z_br1.png differ diff --git a/assets/z_br2.png b/assets/z_br2.png index bb19d9d..b52c0f2 100644 Binary files a/assets/z_br2.png and b/assets/z_br2.png differ diff --git a/assets/z_br3.png b/assets/z_br3.png index f0f5b2c..2591252 100644 Binary files a/assets/z_br3.png and b/assets/z_br3.png differ diff --git a/assets/z_br4.png b/assets/z_br4.png index 973dd46..b2c657d 100644 Binary files a/assets/z_br4.png and b/assets/z_br4.png differ diff --git a/assets/z_brbounce.png b/assets/z_brbounce.png index 713d94b..f4de501 100644 Binary files a/assets/z_brbounce.png and b/assets/z_brbounce.png differ diff --git a/assets/z_brfall1.png b/assets/z_brfall1.png index 3a383a3..d312c88 100644 Binary files a/assets/z_brfall1.png and b/assets/z_brfall1.png differ diff --git a/assets/z_brfall2.png b/assets/z_brfall2.png index 99052c3..7dd9144 100644 Binary files a/assets/z_brfall2.png and b/assets/z_brfall2.png differ diff --git a/assets/z_brfall3.png b/assets/z_brfall3.png index 96be3b7..cbb0fc9 100644 Binary files a/assets/z_brfall3.png and b/assets/z_brfall3.png differ diff --git a/assets/z_brrise.png b/assets/z_brrise.png index ef37a4e..cef93e0 100644 Binary files a/assets/z_brrise.png and b/assets/z_brrise.png differ diff --git a/assets/z_brserve.png b/assets/z_brserve.png index 65ddad7..bcde6f6 100644 Binary files a/assets/z_brserve.png and b/assets/z_brserve.png differ diff --git a/assets/z_brtoss.png b/assets/z_brtoss.png index 49c7c45..6971472 100644 Binary files a/assets/z_brtoss.png and b/assets/z_brtoss.png differ diff --git a/assets/z_brw1.png b/assets/z_brw1.png index 9d9644e..e979697 100644 Binary files a/assets/z_brw1.png and b/assets/z_brw1.png differ diff --git a/assets/z_brw1b.png b/assets/z_brw1b.png index 320f1e3..8835714 100644 Binary files a/assets/z_brw1b.png and b/assets/z_brw1b.png differ diff --git a/assets/z_brw1bounce.png b/assets/z_brw1bounce.png index d874c1e..8679e4c 100644 Binary files a/assets/z_brw1bounce.png and b/assets/z_brw1bounce.png differ diff --git a/assets/z_brw1fall1.png b/assets/z_brw1fall1.png index 10ebd61..cd1ce14 100644 Binary files a/assets/z_brw1fall1.png and b/assets/z_brw1fall1.png differ diff --git a/assets/z_brw1fall2.png b/assets/z_brw1fall2.png index dea3241..f4b2332 100644 Binary files a/assets/z_brw1fall2.png and b/assets/z_brw1fall2.png differ diff --git a/assets/z_brw1fall3.png b/assets/z_brw1fall3.png index 3927889..3c97eb3 100644 Binary files a/assets/z_brw1fall3.png and b/assets/z_brw1fall3.png differ diff --git a/assets/z_brw1rise.png b/assets/z_brw1rise.png index 2fa0c22..94e93c9 100644 Binary files a/assets/z_brw1rise.png and b/assets/z_brw1rise.png differ diff --git a/assets/z_brw1s.png b/assets/z_brw1s.png index 10146cf..8b94436 100644 Binary files a/assets/z_brw1s.png and b/assets/z_brw1s.png differ diff --git a/assets/z_brw1serve.png b/assets/z_brw1serve.png index 050a432..1936222 100644 Binary files a/assets/z_brw1serve.png and b/assets/z_brw1serve.png differ diff --git a/assets/z_brw1toss.png b/assets/z_brw1toss.png index 62b924c..71c6158 100644 Binary files a/assets/z_brw1toss.png and b/assets/z_brw1toss.png differ diff --git a/assets/z_brw2.png b/assets/z_brw2.png index 83c94bb..4b5fd6e 100644 Binary files a/assets/z_brw2.png and b/assets/z_brw2.png differ diff --git a/assets/z_brw2b.png b/assets/z_brw2b.png index 602e87e..384d467 100644 Binary files a/assets/z_brw2b.png and b/assets/z_brw2b.png differ diff --git a/assets/z_brw2bounce.png b/assets/z_brw2bounce.png index fdaedec..c6e1fa6 100644 Binary files a/assets/z_brw2bounce.png and b/assets/z_brw2bounce.png differ diff --git a/assets/z_brw2fall1.png b/assets/z_brw2fall1.png index 8013db0..d226684 100644 Binary files a/assets/z_brw2fall1.png and b/assets/z_brw2fall1.png differ diff --git a/assets/z_brw2fall2.png b/assets/z_brw2fall2.png index f8efab1..c14069b 100644 Binary files a/assets/z_brw2fall2.png and b/assets/z_brw2fall2.png differ diff --git a/assets/z_brw2fall3.png b/assets/z_brw2fall3.png index 2148409..4d68abc 100644 Binary files a/assets/z_brw2fall3.png and b/assets/z_brw2fall3.png differ diff --git a/assets/z_brw2rise.png b/assets/z_brw2rise.png index 9c4b96e..383ae1d 100644 Binary files a/assets/z_brw2rise.png and b/assets/z_brw2rise.png differ diff --git a/assets/z_brw2s.png b/assets/z_brw2s.png index 002baed..6e47dd8 100644 Binary files a/assets/z_brw2s.png and b/assets/z_brw2s.png differ diff --git a/assets/z_brw2serve.png b/assets/z_brw2serve.png index f1a683b..2e5aa28 100644 Binary files a/assets/z_brw2serve.png and b/assets/z_brw2serve.png differ diff --git a/assets/z_brw2toss.png b/assets/z_brw2toss.png index ea6cd1c..8adecac 100644 Binary files a/assets/z_brw2toss.png and b/assets/z_brw2toss.png differ diff --git a/assets/z_gk1.png b/assets/z_gk1.png index 6325577..a7870f2 100644 Binary files a/assets/z_gk1.png and b/assets/z_gk1.png differ diff --git a/assets/z_gk2.png b/assets/z_gk2.png index db298cb..a035f68 100644 Binary files a/assets/z_gk2.png and b/assets/z_gk2.png differ diff --git a/assets/z_gk3.png b/assets/z_gk3.png index e181c46..5411d31 100644 Binary files a/assets/z_gk3.png and b/assets/z_gk3.png differ diff --git a/assets/z_gk4.png b/assets/z_gk4.png index 17a5ec4..21afd76 100644 Binary files a/assets/z_gk4.png and b/assets/z_gk4.png differ diff --git a/assets/z_gkbounce.png b/assets/z_gkbounce.png index 2bd1ca6..e30f80d 100644 Binary files a/assets/z_gkbounce.png and b/assets/z_gkbounce.png differ diff --git a/assets/z_gkfall1.png b/assets/z_gkfall1.png index 713e11b..df3fbba 100644 Binary files a/assets/z_gkfall1.png and b/assets/z_gkfall1.png differ diff --git a/assets/z_gkfall2.png b/assets/z_gkfall2.png index 27b3691..727d146 100644 Binary files a/assets/z_gkfall2.png and b/assets/z_gkfall2.png differ diff --git a/assets/z_gkfall3.png b/assets/z_gkfall3.png index c595eca..5bc9fa5 100644 Binary files a/assets/z_gkfall3.png and b/assets/z_gkfall3.png differ diff --git a/assets/z_gkrise.png b/assets/z_gkrise.png index 0adc587..d7e8c33 100644 Binary files a/assets/z_gkrise.png and b/assets/z_gkrise.png differ diff --git a/assets/z_gkserve.png b/assets/z_gkserve.png index 25646ff..d6e9c90 100644 Binary files a/assets/z_gkserve.png and b/assets/z_gkserve.png differ diff --git a/assets/z_gktoss.png b/assets/z_gktoss.png index f801a57..086b411 100644 Binary files a/assets/z_gktoss.png and b/assets/z_gktoss.png differ diff --git a/assets/z_gkw1.png b/assets/z_gkw1.png index 9351bce..40e1d55 100644 Binary files a/assets/z_gkw1.png and b/assets/z_gkw1.png differ diff --git a/assets/z_gkw1b.png b/assets/z_gkw1b.png index 6df94da..6ae60c4 100644 Binary files a/assets/z_gkw1b.png and b/assets/z_gkw1b.png differ diff --git a/assets/z_gkw1bounce.png b/assets/z_gkw1bounce.png index 92dab8e..1547c82 100644 Binary files a/assets/z_gkw1bounce.png and b/assets/z_gkw1bounce.png differ diff --git a/assets/z_gkw1fall1.png b/assets/z_gkw1fall1.png index 12d987c..eb28e62 100644 Binary files a/assets/z_gkw1fall1.png and b/assets/z_gkw1fall1.png differ diff --git a/assets/z_gkw1fall2.png b/assets/z_gkw1fall2.png index 0e07425..07d3cde 100644 Binary files a/assets/z_gkw1fall2.png and b/assets/z_gkw1fall2.png differ diff --git a/assets/z_gkw1fall3.png b/assets/z_gkw1fall3.png index 5ec56e3..d5be902 100644 Binary files a/assets/z_gkw1fall3.png and b/assets/z_gkw1fall3.png differ diff --git a/assets/z_gkw1rise.png b/assets/z_gkw1rise.png index 59fc6c4..de75a65 100644 Binary files a/assets/z_gkw1rise.png and b/assets/z_gkw1rise.png differ diff --git a/assets/z_gkw1s.png b/assets/z_gkw1s.png index 1cdd292..a0b2f4d 100644 Binary files a/assets/z_gkw1s.png and b/assets/z_gkw1s.png differ diff --git a/assets/z_gkw1serve.png b/assets/z_gkw1serve.png index 3a777b3..8710714 100644 Binary files a/assets/z_gkw1serve.png and b/assets/z_gkw1serve.png differ diff --git a/assets/z_gkw1toss.png b/assets/z_gkw1toss.png index 268273e..c482499 100644 Binary files a/assets/z_gkw1toss.png and b/assets/z_gkw1toss.png differ diff --git a/assets/z_gkw2.png b/assets/z_gkw2.png index 778e929..29265c8 100644 Binary files a/assets/z_gkw2.png and b/assets/z_gkw2.png differ diff --git a/assets/z_gkw2b.png b/assets/z_gkw2b.png index f30bb34..64d5531 100644 Binary files a/assets/z_gkw2b.png and b/assets/z_gkw2b.png differ diff --git a/assets/z_gkw2bounce.png b/assets/z_gkw2bounce.png index 44e2864..bf8fbf0 100644 Binary files a/assets/z_gkw2bounce.png and b/assets/z_gkw2bounce.png differ diff --git a/assets/z_gkw2fall1.png b/assets/z_gkw2fall1.png index 3711d14..2927509 100644 Binary files a/assets/z_gkw2fall1.png and b/assets/z_gkw2fall1.png differ diff --git a/assets/z_gkw2fall2.png b/assets/z_gkw2fall2.png index 9ffcf35..876d642 100644 Binary files a/assets/z_gkw2fall2.png and b/assets/z_gkw2fall2.png differ diff --git a/assets/z_gkw2fall3.png b/assets/z_gkw2fall3.png index fb2b47b..fa5c00b 100644 Binary files a/assets/z_gkw2fall3.png and b/assets/z_gkw2fall3.png differ diff --git a/assets/z_gkw2rise.png b/assets/z_gkw2rise.png index 461cb86..edc424d 100644 Binary files a/assets/z_gkw2rise.png and b/assets/z_gkw2rise.png differ diff --git a/assets/z_gkw2s.png b/assets/z_gkw2s.png index 21418f6..a1eb022 100644 Binary files a/assets/z_gkw2s.png and b/assets/z_gkw2s.png differ diff --git a/assets/z_gkw2serve.png b/assets/z_gkw2serve.png index 94006d2..515da10 100644 Binary files a/assets/z_gkw2serve.png and b/assets/z_gkw2serve.png differ diff --git a/assets/z_gkw2toss.png b/assets/z_gkw2toss.png index 27b8f50..9824a2a 100644 Binary files a/assets/z_gkw2toss.png and b/assets/z_gkw2toss.png differ diff --git a/assets/z_lj1.png b/assets/z_lj1.png index bb97e42..ef6dd31 100644 Binary files a/assets/z_lj1.png and b/assets/z_lj1.png differ diff --git a/assets/z_lj2.png b/assets/z_lj2.png index bfa659e..242652c 100644 Binary files a/assets/z_lj2.png and b/assets/z_lj2.png differ diff --git a/assets/z_lj3.png b/assets/z_lj3.png index 87fdf1c..29d88ad 100644 Binary files a/assets/z_lj3.png and b/assets/z_lj3.png differ diff --git a/assets/z_lj4.png b/assets/z_lj4.png index e95c31b..b8f5fd3 100644 Binary files a/assets/z_lj4.png and b/assets/z_lj4.png differ diff --git a/assets/z_ljbounce.png b/assets/z_ljbounce.png index 1a77460..5d4028a 100644 Binary files a/assets/z_ljbounce.png and b/assets/z_ljbounce.png differ diff --git a/assets/z_ljfall1.png b/assets/z_ljfall1.png index 1fd2c2d..13b3a52 100644 Binary files a/assets/z_ljfall1.png and b/assets/z_ljfall1.png differ diff --git a/assets/z_ljfall2.png b/assets/z_ljfall2.png index 8b9e8bf..49829a6 100644 Binary files a/assets/z_ljfall2.png and b/assets/z_ljfall2.png differ diff --git a/assets/z_ljfall3.png b/assets/z_ljfall3.png index 75a695c..31b71ae 100644 Binary files a/assets/z_ljfall3.png and b/assets/z_ljfall3.png differ diff --git a/assets/z_ljrise.png b/assets/z_ljrise.png index 05936e3..41d71e6 100644 Binary files a/assets/z_ljrise.png and b/assets/z_ljrise.png differ diff --git a/assets/z_ljserve.png b/assets/z_ljserve.png index c6c9931..96c4d1d 100644 Binary files a/assets/z_ljserve.png and b/assets/z_ljserve.png differ diff --git a/assets/z_ljtoss.png b/assets/z_ljtoss.png index 4552fee..61949f2 100644 Binary files a/assets/z_ljtoss.png and b/assets/z_ljtoss.png differ diff --git a/assets/z_ljw1.png b/assets/z_ljw1.png index 9435c4e..86e97d0 100644 Binary files a/assets/z_ljw1.png and b/assets/z_ljw1.png differ diff --git a/assets/z_ljw1b.png b/assets/z_ljw1b.png index 3941c32..ecd4c7d 100644 Binary files a/assets/z_ljw1b.png and b/assets/z_ljw1b.png differ diff --git a/assets/z_ljw1bounce.png b/assets/z_ljw1bounce.png index c11466f..29e61ec 100644 Binary files a/assets/z_ljw1bounce.png and b/assets/z_ljw1bounce.png differ diff --git a/assets/z_ljw1fall1.png b/assets/z_ljw1fall1.png index 0eb7738..e8ffe17 100644 Binary files a/assets/z_ljw1fall1.png and b/assets/z_ljw1fall1.png differ diff --git a/assets/z_ljw1fall2.png b/assets/z_ljw1fall2.png index 6186562..1d53f50 100644 Binary files a/assets/z_ljw1fall2.png and b/assets/z_ljw1fall2.png differ diff --git a/assets/z_ljw1fall3.png b/assets/z_ljw1fall3.png index 03a419e..1bee4c6 100644 Binary files a/assets/z_ljw1fall3.png and b/assets/z_ljw1fall3.png differ diff --git a/assets/z_ljw1rise.png b/assets/z_ljw1rise.png index 4539915..4d5f4d3 100644 Binary files a/assets/z_ljw1rise.png and b/assets/z_ljw1rise.png differ diff --git a/assets/z_ljw1s.png b/assets/z_ljw1s.png index 6d6d1c0..119fe9a 100644 Binary files a/assets/z_ljw1s.png and b/assets/z_ljw1s.png differ diff --git a/assets/z_ljw1serve.png b/assets/z_ljw1serve.png index dcaa5cf..5942711 100644 Binary files a/assets/z_ljw1serve.png and b/assets/z_ljw1serve.png differ diff --git a/assets/z_ljw1toss.png b/assets/z_ljw1toss.png index f241b49..7a83361 100644 Binary files a/assets/z_ljw1toss.png and b/assets/z_ljw1toss.png differ diff --git a/assets/z_ljw2.png b/assets/z_ljw2.png index 17b973c..34495a8 100644 Binary files a/assets/z_ljw2.png and b/assets/z_ljw2.png differ diff --git a/assets/z_ljw2b.png b/assets/z_ljw2b.png index 95c05dc..9bdeeb7 100644 Binary files a/assets/z_ljw2b.png and b/assets/z_ljw2b.png differ diff --git a/assets/z_ljw2bounce.png b/assets/z_ljw2bounce.png index cd30427..c876c97 100644 Binary files a/assets/z_ljw2bounce.png and b/assets/z_ljw2bounce.png differ diff --git a/assets/z_ljw2fall1.png b/assets/z_ljw2fall1.png index 1f79ab5..fb70028 100644 Binary files a/assets/z_ljw2fall1.png and b/assets/z_ljw2fall1.png differ diff --git a/assets/z_ljw2fall2.png b/assets/z_ljw2fall2.png index 9c13b9e..bfe2df0 100644 Binary files a/assets/z_ljw2fall2.png and b/assets/z_ljw2fall2.png differ diff --git a/assets/z_ljw2fall3.png b/assets/z_ljw2fall3.png index 68ae3fb..e90881c 100644 Binary files a/assets/z_ljw2fall3.png and b/assets/z_ljw2fall3.png differ diff --git a/assets/z_ljw2rise.png b/assets/z_ljw2rise.png index e6f3887..d511bc9 100644 Binary files a/assets/z_ljw2rise.png and b/assets/z_ljw2rise.png differ diff --git a/assets/z_ljw2s.png b/assets/z_ljw2s.png index b40ec0d..14e7bff 100644 Binary files a/assets/z_ljw2s.png and b/assets/z_ljw2s.png differ diff --git a/assets/z_ljw2serve.png b/assets/z_ljw2serve.png index 0651cdc..f448a36 100644 Binary files a/assets/z_ljw2serve.png and b/assets/z_ljw2serve.png differ diff --git a/assets/z_ljw2toss.png b/assets/z_ljw2toss.png index bee6187..ab12833 100644 Binary files a/assets/z_ljw2toss.png and b/assets/z_ljw2toss.png differ diff --git a/assets/z_ru1.png b/assets/z_ru1.png index 4fd8f78..88e744e 100644 Binary files a/assets/z_ru1.png and b/assets/z_ru1.png differ diff --git a/assets/z_ru2.png b/assets/z_ru2.png index 8d83a71..76f617d 100644 Binary files a/assets/z_ru2.png and b/assets/z_ru2.png differ diff --git a/assets/z_ru3.png b/assets/z_ru3.png index 8031678..d4471d9 100644 Binary files a/assets/z_ru3.png and b/assets/z_ru3.png differ diff --git a/assets/z_ru4.png b/assets/z_ru4.png index 4125d16..a627424 100644 Binary files a/assets/z_ru4.png and b/assets/z_ru4.png differ diff --git a/assets/z_rubounce.png b/assets/z_rubounce.png index 739c741..3fa2e8f 100644 Binary files a/assets/z_rubounce.png and b/assets/z_rubounce.png differ diff --git a/assets/z_rufall1.png b/assets/z_rufall1.png index 359f1b5..b2736ef 100644 Binary files a/assets/z_rufall1.png and b/assets/z_rufall1.png differ diff --git a/assets/z_rufall2.png b/assets/z_rufall2.png index 4a9e024..c5aaae7 100644 Binary files a/assets/z_rufall2.png and b/assets/z_rufall2.png differ diff --git a/assets/z_rufall3.png b/assets/z_rufall3.png index 46c8c05..30e9292 100644 Binary files a/assets/z_rufall3.png and b/assets/z_rufall3.png differ diff --git a/assets/z_rurise.png b/assets/z_rurise.png index e129a5a..86e2ddf 100644 Binary files a/assets/z_rurise.png and b/assets/z_rurise.png differ diff --git a/assets/z_ruserve.png b/assets/z_ruserve.png index 682d781..77998ee 100644 Binary files a/assets/z_ruserve.png and b/assets/z_ruserve.png differ diff --git a/assets/z_rutoss.png b/assets/z_rutoss.png index b91f424..4311287 100644 Binary files a/assets/z_rutoss.png and b/assets/z_rutoss.png differ diff --git a/assets/z_ruw1.png b/assets/z_ruw1.png index 686c44a..58caaf0 100644 Binary files a/assets/z_ruw1.png and b/assets/z_ruw1.png differ diff --git a/assets/z_ruw1b.png b/assets/z_ruw1b.png index fad770a..c977d5e 100644 Binary files a/assets/z_ruw1b.png and b/assets/z_ruw1b.png differ diff --git a/assets/z_ruw1bounce.png b/assets/z_ruw1bounce.png index 95ae0f0..19d18e2 100644 Binary files a/assets/z_ruw1bounce.png and b/assets/z_ruw1bounce.png differ diff --git a/assets/z_ruw1fall1.png b/assets/z_ruw1fall1.png index f107e73..c789350 100644 Binary files a/assets/z_ruw1fall1.png and b/assets/z_ruw1fall1.png differ diff --git a/assets/z_ruw1fall2.png b/assets/z_ruw1fall2.png index 0a1a01f..38c946f 100644 Binary files a/assets/z_ruw1fall2.png and b/assets/z_ruw1fall2.png differ diff --git a/assets/z_ruw1fall3.png b/assets/z_ruw1fall3.png index f0db581..0a90042 100644 Binary files a/assets/z_ruw1fall3.png and b/assets/z_ruw1fall3.png differ diff --git a/assets/z_ruw1rise.png b/assets/z_ruw1rise.png index 0106622..18bc85d 100644 Binary files a/assets/z_ruw1rise.png and b/assets/z_ruw1rise.png differ diff --git a/assets/z_ruw1s.png b/assets/z_ruw1s.png index a519a9f..c09bf69 100644 Binary files a/assets/z_ruw1s.png and b/assets/z_ruw1s.png differ diff --git a/assets/z_ruw1serve.png b/assets/z_ruw1serve.png index a62b6cf..e3cd0ee 100644 Binary files a/assets/z_ruw1serve.png and b/assets/z_ruw1serve.png differ diff --git a/assets/z_ruw1toss.png b/assets/z_ruw1toss.png index 04adf5b..1424410 100644 Binary files a/assets/z_ruw1toss.png and b/assets/z_ruw1toss.png differ diff --git a/assets/z_ruw2.png b/assets/z_ruw2.png index 27a2287..23c0b70 100644 Binary files a/assets/z_ruw2.png and b/assets/z_ruw2.png differ diff --git a/assets/z_ruw2b.png b/assets/z_ruw2b.png index db338bd..79d7845 100644 Binary files a/assets/z_ruw2b.png and b/assets/z_ruw2b.png differ diff --git a/assets/z_ruw2bounce.png b/assets/z_ruw2bounce.png index 0d8bf15..5abdd00 100644 Binary files a/assets/z_ruw2bounce.png and b/assets/z_ruw2bounce.png differ diff --git a/assets/z_ruw2fall1.png b/assets/z_ruw2fall1.png index b22d882..fc38678 100644 Binary files a/assets/z_ruw2fall1.png and b/assets/z_ruw2fall1.png differ diff --git a/assets/z_ruw2fall2.png b/assets/z_ruw2fall2.png index 22864d4..5feadc0 100644 Binary files a/assets/z_ruw2fall2.png and b/assets/z_ruw2fall2.png differ diff --git a/assets/z_ruw2fall3.png b/assets/z_ruw2fall3.png index 9b34085..34f6fff 100644 Binary files a/assets/z_ruw2fall3.png and b/assets/z_ruw2fall3.png differ diff --git a/assets/z_ruw2rise.png b/assets/z_ruw2rise.png index dec63f7..e41e3af 100644 Binary files a/assets/z_ruw2rise.png and b/assets/z_ruw2rise.png differ diff --git a/assets/z_ruw2s.png b/assets/z_ruw2s.png index 36c4ab3..4736124 100644 Binary files a/assets/z_ruw2s.png and b/assets/z_ruw2s.png differ diff --git a/assets/z_ruw2serve.png b/assets/z_ruw2serve.png index ed2edc0..6c2db1a 100644 Binary files a/assets/z_ruw2serve.png and b/assets/z_ruw2serve.png differ diff --git a/assets/z_ruw2toss.png b/assets/z_ruw2toss.png index ee456e8..80a099d 100644 Binary files a/assets/z_ruw2toss.png and b/assets/z_ruw2toss.png differ diff --git a/assets/z_rw1b.png b/assets/z_rw1b.png index abdbd5f..5faeb0d 100644 Binary files a/assets/z_rw1b.png and b/assets/z_rw1b.png differ diff --git a/assets/z_rw2b.png b/assets/z_rw2b.png index e268e8b..c4ed0df 100644 Binary files a/assets/z_rw2b.png and b/assets/z_rw2b.png differ diff --git a/assets/z_sh1.png b/assets/z_sh1.png index 711e5c2..9c686ef 100644 Binary files a/assets/z_sh1.png and b/assets/z_sh1.png differ diff --git a/assets/z_sh2.png b/assets/z_sh2.png index 6a93120..dbba7f6 100644 Binary files a/assets/z_sh2.png and b/assets/z_sh2.png differ diff --git a/assets/z_sh3.png b/assets/z_sh3.png index 221ab2e..496ef9d 100644 Binary files a/assets/z_sh3.png and b/assets/z_sh3.png differ diff --git a/assets/z_sh4.png b/assets/z_sh4.png index c4aadbb..0d5af4a 100644 Binary files a/assets/z_sh4.png and b/assets/z_sh4.png differ diff --git a/assets/z_shbounce.png b/assets/z_shbounce.png index fad0f2c..0980695 100644 Binary files a/assets/z_shbounce.png and b/assets/z_shbounce.png differ diff --git a/assets/z_shfall1.png b/assets/z_shfall1.png index f27ce42..7a3309d 100644 Binary files a/assets/z_shfall1.png and b/assets/z_shfall1.png differ diff --git a/assets/z_shfall2.png b/assets/z_shfall2.png index f5346a2..6e45f93 100644 Binary files a/assets/z_shfall2.png and b/assets/z_shfall2.png differ diff --git a/assets/z_shfall3.png b/assets/z_shfall3.png index 5fcaedd..3f0dd15 100644 Binary files a/assets/z_shfall3.png and b/assets/z_shfall3.png differ diff --git a/assets/z_shrise.png b/assets/z_shrise.png index d2a6a45..8b81a1c 100644 Binary files a/assets/z_shrise.png and b/assets/z_shrise.png differ diff --git a/assets/z_shserve.png b/assets/z_shserve.png index a5ce398..a57628e 100644 Binary files a/assets/z_shserve.png and b/assets/z_shserve.png differ diff --git a/assets/z_shtoss.png b/assets/z_shtoss.png index f9f266d..bdc9832 100644 Binary files a/assets/z_shtoss.png and b/assets/z_shtoss.png differ diff --git a/assets/z_shw1.png b/assets/z_shw1.png index 281ae8c..33a8c93 100644 Binary files a/assets/z_shw1.png and b/assets/z_shw1.png differ diff --git a/assets/z_shw1b.png b/assets/z_shw1b.png index b5583bc..917c5ff 100644 Binary files a/assets/z_shw1b.png and b/assets/z_shw1b.png differ diff --git a/assets/z_shw1bounce.png b/assets/z_shw1bounce.png index 645f5a3..fe54c7b 100644 Binary files a/assets/z_shw1bounce.png and b/assets/z_shw1bounce.png differ diff --git a/assets/z_shw1fall1.png b/assets/z_shw1fall1.png index 27f926c..1dbd9e0 100644 Binary files a/assets/z_shw1fall1.png and b/assets/z_shw1fall1.png differ diff --git a/assets/z_shw1fall2.png b/assets/z_shw1fall2.png index 8453a8f..226c820 100644 Binary files a/assets/z_shw1fall2.png and b/assets/z_shw1fall2.png differ diff --git a/assets/z_shw1fall3.png b/assets/z_shw1fall3.png index 48abda0..5b349f8 100644 Binary files a/assets/z_shw1fall3.png and b/assets/z_shw1fall3.png differ diff --git a/assets/z_shw1rise.png b/assets/z_shw1rise.png index b63fa36..5f9ab1a 100644 Binary files a/assets/z_shw1rise.png and b/assets/z_shw1rise.png differ diff --git a/assets/z_shw1s.png b/assets/z_shw1s.png index 3345dc9..589bfa5 100644 Binary files a/assets/z_shw1s.png and b/assets/z_shw1s.png differ diff --git a/assets/z_shw1serve.png b/assets/z_shw1serve.png index 7f3f95d..917e81b 100644 Binary files a/assets/z_shw1serve.png and b/assets/z_shw1serve.png differ diff --git a/assets/z_shw1toss.png b/assets/z_shw1toss.png index 31cd151..9d72155 100644 Binary files a/assets/z_shw1toss.png and b/assets/z_shw1toss.png differ diff --git a/assets/z_shw2.png b/assets/z_shw2.png index d1eafe7..d05f28b 100644 Binary files a/assets/z_shw2.png and b/assets/z_shw2.png differ diff --git a/assets/z_shw2b.png b/assets/z_shw2b.png index 302e1b6..1a4b57c 100644 Binary files a/assets/z_shw2b.png and b/assets/z_shw2b.png differ diff --git a/assets/z_shw2bounce.png b/assets/z_shw2bounce.png index 1a983ab..7e0f3be 100644 Binary files a/assets/z_shw2bounce.png and b/assets/z_shw2bounce.png differ diff --git a/assets/z_shw2fall1.png b/assets/z_shw2fall1.png index 35cfe49..1b5e400 100644 Binary files a/assets/z_shw2fall1.png and b/assets/z_shw2fall1.png differ diff --git a/assets/z_shw2fall2.png b/assets/z_shw2fall2.png index bdb56df..e9b644f 100644 Binary files a/assets/z_shw2fall2.png and b/assets/z_shw2fall2.png differ diff --git a/assets/z_shw2fall3.png b/assets/z_shw2fall3.png index f792acb..96c8fc6 100644 Binary files a/assets/z_shw2fall3.png and b/assets/z_shw2fall3.png differ diff --git a/assets/z_shw2rise.png b/assets/z_shw2rise.png index a4673b4..d4da566 100644 Binary files a/assets/z_shw2rise.png and b/assets/z_shw2rise.png differ diff --git a/assets/z_shw2s.png b/assets/z_shw2s.png index 5d63d0b..1342a34 100644 Binary files a/assets/z_shw2s.png and b/assets/z_shw2s.png differ diff --git a/assets/z_shw2serve.png b/assets/z_shw2serve.png index aa9bc87..94f3e70 100644 Binary files a/assets/z_shw2serve.png and b/assets/z_shw2serve.png differ diff --git a/assets/z_shw2toss.png b/assets/z_shw2toss.png index f0355c4..60d2bd2 100644 Binary files a/assets/z_shw2toss.png and b/assets/z_shw2toss.png differ diff --git a/assets/z_st1.png b/assets/z_st1.png index f65a0a9..ec56b83 100644 Binary files a/assets/z_st1.png and b/assets/z_st1.png differ diff --git a/assets/z_st2.png b/assets/z_st2.png index 58c56d6..3c01db3 100644 Binary files a/assets/z_st2.png and b/assets/z_st2.png differ diff --git a/assets/z_st3.png b/assets/z_st3.png index 10647bd..8a2f65f 100644 Binary files a/assets/z_st3.png and b/assets/z_st3.png differ diff --git a/assets/z_st4.png b/assets/z_st4.png index cd11d69..668e104 100644 Binary files a/assets/z_st4.png and b/assets/z_st4.png differ diff --git a/assets/z_stbounce.png b/assets/z_stbounce.png index 3398654..b32f827 100644 Binary files a/assets/z_stbounce.png and b/assets/z_stbounce.png differ diff --git a/assets/z_stfall1.png b/assets/z_stfall1.png index cefdc75..552f2d6 100644 Binary files a/assets/z_stfall1.png and b/assets/z_stfall1.png differ diff --git a/assets/z_stfall2.png b/assets/z_stfall2.png index f4ba98b..7100e76 100644 Binary files a/assets/z_stfall2.png and b/assets/z_stfall2.png differ diff --git a/assets/z_stfall3.png b/assets/z_stfall3.png index 1c670d2..81233e4 100644 Binary files a/assets/z_stfall3.png and b/assets/z_stfall3.png differ diff --git a/assets/z_strise.png b/assets/z_strise.png index e6e80f3..1b01d89 100644 Binary files a/assets/z_strise.png and b/assets/z_strise.png differ diff --git a/assets/z_stserve.png b/assets/z_stserve.png index 0b15554..4bc60aa 100644 Binary files a/assets/z_stserve.png and b/assets/z_stserve.png differ diff --git a/assets/z_sttoss.png b/assets/z_sttoss.png index 32c8079..043c73e 100644 Binary files a/assets/z_sttoss.png and b/assets/z_sttoss.png differ diff --git a/assets/z_stw1.png b/assets/z_stw1.png index 92ea01e..e02579d 100644 Binary files a/assets/z_stw1.png and b/assets/z_stw1.png differ diff --git a/assets/z_stw1b.png b/assets/z_stw1b.png index 42e489a..8097374 100644 Binary files a/assets/z_stw1b.png and b/assets/z_stw1b.png differ diff --git a/assets/z_stw1bounce.png b/assets/z_stw1bounce.png index 5c9ad01..edaefaa 100644 Binary files a/assets/z_stw1bounce.png and b/assets/z_stw1bounce.png differ diff --git a/assets/z_stw1fall1.png b/assets/z_stw1fall1.png index 966a4de..1b96823 100644 Binary files a/assets/z_stw1fall1.png and b/assets/z_stw1fall1.png differ diff --git a/assets/z_stw1fall2.png b/assets/z_stw1fall2.png index 859c6d6..ac8b34c 100644 Binary files a/assets/z_stw1fall2.png and b/assets/z_stw1fall2.png differ diff --git a/assets/z_stw1fall3.png b/assets/z_stw1fall3.png index 9b9b9c4..e949e2c 100644 Binary files a/assets/z_stw1fall3.png and b/assets/z_stw1fall3.png differ diff --git a/assets/z_stw1rise.png b/assets/z_stw1rise.png index ad484d4..9601e17 100644 Binary files a/assets/z_stw1rise.png and b/assets/z_stw1rise.png differ diff --git a/assets/z_stw1s.png b/assets/z_stw1s.png index 920b2c1..815917b 100644 Binary files a/assets/z_stw1s.png and b/assets/z_stw1s.png differ diff --git a/assets/z_stw1serve.png b/assets/z_stw1serve.png index e6ebb04..e1eeb51 100644 Binary files a/assets/z_stw1serve.png and b/assets/z_stw1serve.png differ diff --git a/assets/z_stw1toss.png b/assets/z_stw1toss.png index a107adb..6e1d286 100644 Binary files a/assets/z_stw1toss.png and b/assets/z_stw1toss.png differ diff --git a/assets/z_stw2.png b/assets/z_stw2.png index debf2b8..1f0dbb0 100644 Binary files a/assets/z_stw2.png and b/assets/z_stw2.png differ diff --git a/assets/z_stw2b.png b/assets/z_stw2b.png index b4f8f18..2110e4c 100644 Binary files a/assets/z_stw2b.png and b/assets/z_stw2b.png differ diff --git a/assets/z_stw2bounce.png b/assets/z_stw2bounce.png index c5de52b..85e6101 100644 Binary files a/assets/z_stw2bounce.png and b/assets/z_stw2bounce.png differ diff --git a/assets/z_stw2fall1.png b/assets/z_stw2fall1.png index 34dbb4c..7914df9 100644 Binary files a/assets/z_stw2fall1.png and b/assets/z_stw2fall1.png differ diff --git a/assets/z_stw2fall2.png b/assets/z_stw2fall2.png index 7e5163a..47b06a9 100644 Binary files a/assets/z_stw2fall2.png and b/assets/z_stw2fall2.png differ diff --git a/assets/z_stw2fall3.png b/assets/z_stw2fall3.png index c76e6fb..2de1c91 100644 Binary files a/assets/z_stw2fall3.png and b/assets/z_stw2fall3.png differ diff --git a/assets/z_stw2rise.png b/assets/z_stw2rise.png index cfae8db..03b29d7 100644 Binary files a/assets/z_stw2rise.png and b/assets/z_stw2rise.png differ diff --git a/assets/z_stw2s.png b/assets/z_stw2s.png index d7a6cf6..8d519e1 100644 Binary files a/assets/z_stw2s.png and b/assets/z_stw2s.png differ diff --git a/assets/z_stw2serve.png b/assets/z_stw2serve.png index ba3913e..e1e5e9d 100644 Binary files a/assets/z_stw2serve.png and b/assets/z_stw2serve.png differ diff --git a/assets/z_stw2toss.png b/assets/z_stw2toss.png index be98a7b..ce40827 100644 Binary files a/assets/z_stw2toss.png and b/assets/z_stw2toss.png differ diff --git a/index.html b/index.html index de89687..4e8fc37 100644 --- a/index.html +++ b/index.html @@ -431,6 +431,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/render.js b/src/render.js index 8982e83..9431940 100644 --- a/src/render.js +++ b/src/render.js @@ -195,7 +195,7 @@ export function proj(x, y, z) { // Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the // menu screens hang tags on the same files, and two copies of this number is one copy too // many: the one that does not get bumped is the one that serves a cached 404. -export const SPRV = 105; +export const SPRV = 107; // The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was // already requested at import — it just makes the wait visible and holds the menu back until the // last sheet has landed. See the onerror note below for the other half of "sometimes missing". @@ -707,8 +707,16 @@ function drawZombie(zb) { // _slice.py's common scaling is built on (see PROJECT.md: heights follow the PNG). if (SPR[name]) h *= SPR[name].height / SPRITE_H; // feeding: a fast bob over the body instead of a stride. Same sprites, different rhythm. + // ...and the SHAMBLE BOB is the same rule again, which it was breaking twice over. It was + // `Math.floor(zb.walkT * 5.2) % 2` — a 1 px lift on a WALL CLOCK, applied to every body every + // frame whether it was moving or not. So a held pose bobbed forever (the sprite bench shows a + // FOREHAND that will not sit still), a collapsing body bobbed while it lay on the ground, and at + // RES 4 that 1 logical px is 4 device px flicking at 2.6 Hz. Two fixes, both the house rule: + // it only happens while the body is actually WALKING, and it steps off GROUND COVERED. + const still = zb.pose === 'idle' || zb.pose === 'swing' || zb.pose === 'serve'; const bob = zb.feeding ? Math.abs(Math.sin(zb.walkT * 11)) * Math.max(1, p0(zb) * 0.05) - : Math.floor(zb.walkT * 5.2) % 2; + : still ? 0 + : Math.floor((zb.dist ?? zb.z) / 0.42) % 2; // Every side-on body is DRAWN facing left, so one walking right was moonwalking — obvious the // moment they close on you across your own half. The front-facing ones (the turned CPU, the // runner, the machine, a chapter boss) have no left or right to turn and are left alone.