diff --git a/assets/_regen.py b/assets/_regen.py new file mode 100644 index 0000000..b2d1725 --- /dev/null +++ b/assets/_regen.py @@ -0,0 +1,126 @@ +# _regen.py — redraw a batch of sprites at real pixel-art resolution, end to end. +# +# python3 assets/_regen.py shambler_a # one batch +# python3 assets/_regen.py --list # what batches exist +# python3 assets/_regen.py --all # everything not already done +# +# Per batch it: builds a reference sheet from the sprites in the repo, asks the generator to redraw +# them at HIGH art resolution (the style block below is the whole point — the previous prompts asked +# for "big chunky pixel blocks", which is a specification for coarse art), slices with _fine.py, +# colour-matches each frame onto what it replaces, and installs — but only if the sheet passes its +# checks: right number of frames, nothing touching a canvas edge (that is what clipped z_f1), and a +# real gain in inner-edge density (the number that tracks "is the drawing actually finer"). +# +# Failures leave the sprites alone and print why, so a batch can just be re-run. +import json, os, subprocess, sys +import numpy as np +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +SCRATCH = os.environ.get('SPLAT_SCRATCH', '/tmp/splatter-regen') +GEN = os.path.expanduser('~/.claude/skills/generate-image/scripts/generate_image.py') +PY = os.environ.get('SPLAT_PY', sys.executable) + +STYLE = ( + "Redraw them as HIGH-RESOLUTION pixel art. The art pixels must be SMALL: build each figure out " + "of roughly 90 to 110 pixels of height instead of the reference's 40, so there is room for real " + "detail, and spend it — a face with teeth, sunken eyes and a jawline; individual fingers; " + "clothing with visible seams, hems, folds and frayed torn edges; laced shoes; four or five " + "distinct shades per material with fine hatched shading where the light falls away. Keep every " + "pose, every body proportion, every height, every facing and every wound EXACTLY as in the " + "reference — only the drawing resolution and the amount of fine detail change. Keep the same " + "colours. Hard black outline around each figure. The background must be ONE FLAT solid magenta " + "#FF00FF across the entire canvas — no scenery, no gradient, no vignette. Keep at least 200 " + "pixels of empty magenta between figures and at least 150 pixels of empty magenta at every edge " + "of the canvas: no figure or limb may touch or run off an edge. No shadow, no ground line, no text." +) + +def batches(): + with open(os.path.join(HERE, '_regen_batches.json')) as f: + return json.load(f) + +# ---------------------------------------------------------------- helpers ---- +def ref_sheet(names, out, scale=2.0, gap=220, pad=150): + ims = [Image.open(os.path.join(HERE, n + '.png')).convert('RGBA') for n in names] + ims = [i.resize((max(1, int(i.width * scale)), max(1, int(i.height * scale))), Image.NEAREST) for i in ims] + w = sum(i.width for i in ims) + gap * (len(ims) - 1) + pad * 2 + h = max(i.height for i in ims) + pad * 2 + s = Image.new('RGBA', (w, h), (255, 0, 255, 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 density(path): + """Inner-edge density: how much detail lives INSIDE the silhouette. Coarse art ~0.15, the + redrawn runner ~0.45. This is the number that matched Oliver's eye.""" + a = np.array(Image.open(path).convert('RGBA')) + solid = a[..., 3] > 128 + d = np.abs(np.diff(a[..., :3].astype(int), axis=1)).sum(2) + return round(float(((d > 40) & solid[:, :-1]).sum() / max(1, solid.sum())), 3) + +def touches_edge(sheet): + a = np.array(Image.open(sheet).convert('RGB')).astype(int) + bg = a[0, 0] + fg = (((a - bg) ** 2).sum(2) > 3600) & ~(np.minimum(a[..., 0], a[..., 2]) > a[..., 1] + 40) + return bool(fg[0].any() or fg[-1].any() or fg[:, 0].any() or fg[:, -1].any()) + +def colour_match(src, dst_name): + """Fit the new frame onto the palette statistics of the sprite it replaces, so a redrawn body + does not change colour halfway through a walk cycle.""" + op = lambda x: x[..., 3] > 200 + old = np.array(Image.open(os.path.join(HERE, dst_name + '.png')).convert('RGBA'), dtype=float) + new = np.array(Image.open(src).convert('RGBA'), dtype=float) + po, pn = old[op(old)][:, :3], new[op(new)][:, :3] + m = op(new) + px = (new[m][:, :3] - pn.mean(0)) / np.maximum(pn.std(0), 1e-6) * po.std(0) + po.mean(0) + new[m] = np.concatenate([np.clip(px, 0, 255), new[m][:, 3:]], axis=1) + Image.fromarray(new.astype('uint8'), 'RGBA').save(src) + +# ------------------------------------------------------------------- run ---- +def run(name, spec, dry=False): + os.makedirs(SCRATCH, exist_ok=True) + sprites = spec['sprites'] + ref = ref_sheet(sprites, f'{SCRATCH}/{name}_ref.png', spec.get('scale', 2.0)) + sheet = f'{SCRATCH}/{name}_hi.png' + prompt = f"{spec['subject']} {STYLE}" + if dry: + print(f'[{name}] would generate {len(sprites)} frames: {", ".join(sprites)}'); return True + print(f'[{name}] generating {len(sprites)} frames...') + r = subprocess.run([PY, GEN, prompt, '--input', ref, '--output', sheet], + capture_output=True, text=True, cwd=os.path.dirname(HERE)) + if not os.path.exists(sheet): + print(f'[{name}] FAILED: generator returned no image\n{r.stdout[-400:]}'); return False + if touches_edge(sheet): + print(f'[{name}] REJECTED: a figure runs off the canvas edge — re-run'); return False + outs = [f'{SCRATCH}/{name}_{i}.png' for i in range(len(sprites))] + cut = ['--cut', spec['cut']] if spec.get('cut') else [] + r = subprocess.run([PY, os.path.join(HERE, '_fine.py'), sheet, *outs, + '--anchor', str(spec.get('anchor', 0)), '--h', str(spec.get('h', 224)), *cut], + capture_output=True, text=True) + print(r.stdout.strip() or r.stderr.strip()) + if r.returncode or not all(os.path.exists(o) for o in outs): + print(f'[{name}] REJECTED: slicing failed (frames merged?) — re-run or add "cut"'); return False + before = [density(os.path.join(HERE, s + '.png')) for s in sprites] + after = [density(o) for o in outs] + # 1.25x, not 1.5x: a sheet that already has blood on it starts with a high edge density, so a + # genuinely finer redraw of a wounded body scores a smaller gain than a clean one. + if sum(after) / len(after) < sum(before) / len(before) * 1.25: + print(f'[{name}] REJECTED: not finer than what it replaces {before} -> {after}'); return False + for o, s in zip(outs, sprites): + colour_match(o, s) + Image.open(o).save(os.path.join(HERE, s + '.png')) + print(f'[{name}] installed. detail {before} -> {after}') + return True + +if __name__ == '__main__': + args = sys.argv[1:] + B = batches() + if not args or '--list' in args: + for k, v in B.items(): print(f'{k:16s} {len(v["sprites"]):2d} frames {", ".join(v["sprites"])}') + sys.exit() + dry = '--dry' in args + names = list(B) if '--all' in args else [a for a in args if not a.startswith('--')] + bad = [n for n in names if not run(n, B[n], dry)] + print('\nFAILED BATCHES (re-run):', ' '.join(bad) if bad else 'none') diff --git a/assets/_regen_batches.json b/assets/_regen_batches.json new file mode 100644 index 0000000..4e723d9 --- /dev/null +++ b/assets/_regen_batches.json @@ -0,0 +1,107 @@ +{ + "shambler_a": { + "sprites": ["z_a1", "z_a2", "z_a3", "z_a4"], + "subject": "Four frames of the same walking zombie in a grey shirt and dark trousers, a four-frame shamble cycle, drawn as coarse low-resolution pixel art with big blocky pixels and almost no internal detail.", + "anchor": 0 + }, + "shambler_b": { + "sprites": ["z_b1", "z_b2", "z_b3", "z_b4"], + "subject": "Four frames of the same walking zombie in a brown coat over a shirt, a four-frame shamble cycle, drawn as coarse low-resolution pixel art with big blocky pixels and almost no internal detail.", + "anchor": 0 + }, + "shambler_wounds": { + "sprites": ["z_aw", "z_awb", "z_bw", "z_bwb"], + "subject": "Four frames of two WOUNDED walking zombies, drawn as coarse low-resolution pixel art. The first two are the same grey-shirted zombie whose right arm ends in a ragged bloody stump, in two stages of one stride. The last two are the same brown-coated zombie with an open red gash across his skull, in two stages of one stride. Keep each wound exactly where it is.", + "anchor": 0 + }, + "brute_walk": { + "sprites": ["z_g1", "z_g2", "z_g3", "z_g4"], + "subject": "Four frames of the same enormous fat zombie brute in a stained grey-green shirt and brown trousers, dragging a rusty pole in one hand, a four-frame heavy lumber, drawn as coarse low-resolution pixel art.", + "anchor": 0 + }, + "brute_wounds": { + "sprites": ["z_gw", "z_gw2", "z_gw3", "z_gw4"], + "subject": "Four damage stages of the same enormous fat zombie brute, drawn as coarse low-resolution pixel art: (1) one arm torn off at the shoulder leaving a ragged stump, still dragging his pole; (2) both arms gone; (3) chest and belly torn wide open with ribs showing; (4) headless, ribs out, still standing. Keep each stage's injuries exactly as they are.", + "anchor": 0 + }, + "brute_wounds_b": { + "sprites": ["z_gwb", "z_gw2b", "z_gw3b", "z_gw4b"], + "subject": "Four damage stages of the same enormous fat zombie brute, mid-stride on the opposite foot, drawn as coarse low-resolution pixel art: (1) one arm torn off at the shoulder, dragging his pole; (2) both arms gone; (3) chest and belly torn open with ribs showing; (4) headless, ribs out. Keep each stage's injuries exactly as they are.", + "anchor": 0 + }, + "steward": { + "sprites": ["z_d1", "z_d2", "z_d3", "z_d4"], + "subject": "Four frames of the same big heavy zombie stadium steward in a high-visibility ORANGE safety vest over a dark shirt, dark trousers and heavy boots, a four-frame walk cycle, drawn as coarse low-resolution pixel art. Keep the vest's reflective stripes.", + "anchor": 0 + }, + "steward_wounds": { + "sprites": ["z_dw", "z_dwb"], + "subject": "Two frames of the same badly wounded zombie stadium steward in a torn high-visibility ORANGE vest — one arm mangled, a deep bloody gash across the chest, head lolling — walking, two stages of one stride, drawn as coarse low-resolution pixel art.", + "anchor": 0 + }, + "cpu_walk": { + "sprites": ["z_c1", "z_c2", "z_c3", "z_c4"], + "subject": "Four frames of the same zombie tennis player facing the viewer — yellow hair, red headband, red shirt, grey shorts, a wooden racket hanging from one hand — a four-frame shuffle towards the camera, drawn as coarse low-resolution pixel art.", + "anchor": 0 + }, + "cpu_wounds": { + "sprites": ["z_cw1", "z_cw1b", "z_cw2", "z_cw2b"], + "subject": "Four frames of the same wounded zombie tennis player in a red shirt holding a wooden racket, drawn as coarse low-resolution pixel art. The first two are him wounded and bleeding, in two stages of one stride. The last two are the same character HEADLESS, a ragged red stump where the neck was, still holding the racket, in two stages of one stride.", + "anchor": 0 + }, + "shield": { + "sprites": ["z_s1", "z_s2", "z_s3", "z_s4", "z_sdown"], + "subject": "Five frames of the same zombie spectator in a grey-brown coat, drawn as coarse low-resolution pixel art. In the first four he holds a wooden folding chair up in front of himself like a shield, shuffling forward in a four-frame walk. In the fifth the chair has been knocked DOWN out of his hands and he is staggering with his arms up, exactly as shown.", + "anchor": 0 + }, + "player_stance": { + "sprites": ["p_idle1", "p_idle2", "p_bounce", "p_bounce2"], + "subject": "Four frames of the same tennis player seen from BEHIND in a white shirt, blue shorts and a white headband, holding a racket, drawn as coarse low-resolution pixel art: two waiting stances and two frames of him bouncing a ball before serving.", + "anchor": 0 + }, + "player_serve": { + "sprites": ["p_toss", "p_smash", "p_swing1", "p_swing2"], + "subject": "Four frames of the same tennis player in a white shirt, blue shorts and a white headband, drawn as coarse low-resolution pixel art: tossing the ball up for a serve; the smash at full stretch with the racket overhead; and two frames of a forehand swing.", + "anchor": 0 + }, + "player_run": { + "sprites": ["p_run1", "p_run2", "p_run3", "p_run4"], + "subject": "Four frames of the same tennis player seen from BEHIND in a white shirt and blue shorts, running away from the camera, a four-frame run cycle with no racket, drawn as coarse low-resolution pixel art.", + "anchor": 0 + }, + "player_death": { + "sprites": ["p_die1", "p_die2", "p_die3", "p_die4", "p_die5"], + "subject": "Five frames of the same tennis player in a white shirt and blue shorts collapsing, each one lower than the last, drawn as coarse low-resolution pixel art: arched backwards with both arms flung up; dropped to his knees; pitching forward onto one hand; face down and flat with both arms out in a dark pool; and a low flattened heap of torn cloth and dark red with the stained headband on top. Each figure must stay clearly shorter than the one before it.", + "anchor": 0 + }, + "opponent_stance": { + "sprites": ["o_idle1", "o_idle2", "o_bounce", "o_bounce2"], + "subject": "Four frames of the same tennis opponent in a RED shirt and dark shorts, seen from the front across the net, holding a racket, drawn as coarse low-resolution pixel art: two waiting stances and two frames of him bouncing a ball before serving.", + "anchor": 0 + }, + "opponent_play": { + "sprites": ["o_toss", "o_smash", "o_swing1", "o_swing2"], + "subject": "Four frames of the same tennis opponent in a RED shirt and dark shorts, drawn as coarse low-resolution pixel art: tossing the ball up for a serve; the smash at full stretch with the racket overhead; and two frames of a forehand swing.", + "anchor": 0 + }, + "opponent_run": { + "sprites": ["o_run1", "o_run2"], + "subject": "Two frames of the same tennis opponent in a RED shirt and dark shorts running across the court towards the camera, two stages of one stride, drawn as coarse low-resolution pixel art.", + "anchor": 0 + }, + "ump_boy": { + "sprites": ["x_ump", "x_boyrun1", "x_boyrun2"], + "subject": "Three frames drawn as coarse low-resolution pixel art: a tennis UMPIRE in a white sun hat, blazer and tie, seated upright with a clipboard; and two frames of a young BALL BOY in a white polo shirt and shorts sprinting, two stages of one stride. Keep the umpire seated and the ball boy running.", + "anchor": 0 + }, + "boy_carry": { + "sprites": ["x_boycarry1", "x_boycarry2", "x_boycarry3", "x_boycarry4"], + "subject": "Four frames of the same young ball boy in a white polo shirt and shorts walking with a mop shouldered, a four-frame walk cycle, drawn as coarse low-resolution pixel art.", + "anchor": 0 + }, + "boy_mop": { + "sprites": ["x_boymop1", "x_boymop2", "x_boymop3", "x_boymop4"], + "subject": "Four frames of the same young ball boy in a white polo shirt and shorts pushing a mop across the ground, a four-frame mopping cycle, bent over the mop handle, drawn as coarse low-resolution pixel art.", + "anchor": 0 + } +} diff --git a/assets/p_bounce.png b/assets/p_bounce.png index fbaf783..baea86f 100644 Binary files a/assets/p_bounce.png and b/assets/p_bounce.png differ diff --git a/assets/p_bounce2.png b/assets/p_bounce2.png index 6a1389e..1982542 100644 Binary files a/assets/p_bounce2.png and b/assets/p_bounce2.png differ diff --git a/assets/p_idle1.png b/assets/p_idle1.png index a797c6b..3537ad2 100644 Binary files a/assets/p_idle1.png and b/assets/p_idle1.png differ diff --git a/assets/p_idle2.png b/assets/p_idle2.png index 65f2efa..b3b59c6 100644 Binary files a/assets/p_idle2.png and b/assets/p_idle2.png differ diff --git a/assets/z_a1.png b/assets/z_a1.png index d4d59f2..1f340f8 100644 Binary files a/assets/z_a1.png and b/assets/z_a1.png differ diff --git a/assets/z_a2.png b/assets/z_a2.png index a09930a..500ca67 100644 Binary files a/assets/z_a2.png and b/assets/z_a2.png differ diff --git a/assets/z_a3.png b/assets/z_a3.png index 9896ca5..9e464c5 100644 Binary files a/assets/z_a3.png and b/assets/z_a3.png differ diff --git a/assets/z_a4.png b/assets/z_a4.png index 8877041..c835436 100644 Binary files a/assets/z_a4.png and b/assets/z_a4.png differ diff --git a/assets/z_aw.png b/assets/z_aw.png index 3cfc998..fc479bf 100644 Binary files a/assets/z_aw.png and b/assets/z_aw.png differ diff --git a/assets/z_awb.png b/assets/z_awb.png index 43ea7bc..a86431c 100644 Binary files a/assets/z_awb.png and b/assets/z_awb.png differ diff --git a/assets/z_b1.png b/assets/z_b1.png index 953317c..a67d473 100644 Binary files a/assets/z_b1.png and b/assets/z_b1.png differ diff --git a/assets/z_b2.png b/assets/z_b2.png index 3646823..4e6fe38 100644 Binary files a/assets/z_b2.png and b/assets/z_b2.png differ diff --git a/assets/z_b3.png b/assets/z_b3.png index 801fdfb..c42c0cb 100644 Binary files a/assets/z_b3.png and b/assets/z_b3.png differ diff --git a/assets/z_b4.png b/assets/z_b4.png index ce937fe..bfcb5c0 100644 Binary files a/assets/z_b4.png and b/assets/z_b4.png differ diff --git a/assets/z_bw.png b/assets/z_bw.png index a304854..70c566f 100644 Binary files a/assets/z_bw.png and b/assets/z_bw.png differ diff --git a/assets/z_bwb.png b/assets/z_bwb.png index 1c32651..482c653 100644 Binary files a/assets/z_bwb.png and b/assets/z_bwb.png differ diff --git a/assets/z_c1.png b/assets/z_c1.png index 6b7b853..900728b 100644 Binary files a/assets/z_c1.png and b/assets/z_c1.png differ diff --git a/assets/z_c2.png b/assets/z_c2.png index 7a26b51..bf1b915 100644 Binary files a/assets/z_c2.png and b/assets/z_c2.png differ diff --git a/assets/z_c3.png b/assets/z_c3.png index aae9a40..9b213d6 100644 Binary files a/assets/z_c3.png and b/assets/z_c3.png differ diff --git a/assets/z_c4.png b/assets/z_c4.png index 77b54cd..2e13445 100644 Binary files a/assets/z_c4.png and b/assets/z_c4.png differ diff --git a/assets/z_d1.png b/assets/z_d1.png index a7cc9f9..43b8f2e 100644 Binary files a/assets/z_d1.png and b/assets/z_d1.png differ diff --git a/assets/z_d2.png b/assets/z_d2.png index 8e81df8..10d5841 100644 Binary files a/assets/z_d2.png and b/assets/z_d2.png differ diff --git a/assets/z_d3.png b/assets/z_d3.png index e5975dc..2ebedc3 100644 Binary files a/assets/z_d3.png and b/assets/z_d3.png differ diff --git a/assets/z_d4.png b/assets/z_d4.png index 33988f5..b4c0fdf 100644 Binary files a/assets/z_d4.png and b/assets/z_d4.png differ diff --git a/assets/z_dw.png b/assets/z_dw.png index 2c7cf5a..37e3f15 100644 Binary files a/assets/z_dw.png and b/assets/z_dw.png differ diff --git a/assets/z_dwb.png b/assets/z_dwb.png index 4319dac..6cd8f47 100644 Binary files a/assets/z_dwb.png and b/assets/z_dwb.png differ diff --git a/assets/z_g1.png b/assets/z_g1.png index 6f854e2..780add7 100644 Binary files a/assets/z_g1.png and b/assets/z_g1.png differ diff --git a/assets/z_g2.png b/assets/z_g2.png index f7436ff..574ae33 100644 Binary files a/assets/z_g2.png and b/assets/z_g2.png differ diff --git a/assets/z_g3.png b/assets/z_g3.png index 7365321..625fdaf 100644 Binary files a/assets/z_g3.png and b/assets/z_g3.png differ diff --git a/assets/z_g4.png b/assets/z_g4.png index caf95c1..b9318f4 100644 Binary files a/assets/z_g4.png and b/assets/z_g4.png differ diff --git a/assets/z_gw2b.png b/assets/z_gw2b.png index 7bde6c9..21a0c99 100644 Binary files a/assets/z_gw2b.png and b/assets/z_gw2b.png differ diff --git a/assets/z_gw3b.png b/assets/z_gw3b.png index 993c710..d476815 100644 Binary files a/assets/z_gw3b.png and b/assets/z_gw3b.png differ diff --git a/assets/z_gw4b.png b/assets/z_gw4b.png index b1898e5..4be8405 100644 Binary files a/assets/z_gw4b.png and b/assets/z_gw4b.png differ diff --git a/assets/z_gwb.png b/assets/z_gwb.png index df12dbc..d13f253 100644 Binary files a/assets/z_gwb.png and b/assets/z_gwb.png differ