diff --git a/assets/__pycache__/_regen.cpython-314.pyc b/assets/__pycache__/_regen.cpython-314.pyc index a1cb4a1..ec3f066 100644 Binary files a/assets/__pycache__/_regen.cpython-314.pyc and b/assets/__pycache__/_regen.cpython-314.pyc differ diff --git a/assets/_master.py b/assets/_master.py index 9570443..e65a0b0 100644 --- a/assets/_master.py +++ b/assets/_master.py @@ -65,6 +65,7 @@ def build(name, frames, scale=2, gap=90, pad=70): # master: the reference specifies eleven poses that already work, and the prompt only says who. import json, shutil, collections +# THE FIRST SHEET describes the character in WORDS, because at that point no picture of it exists. RESTYLE = ( "The reference image is a {n}-FRAME SPRITE SHEET of one zombie tennis player. Redraw THE WHOLE " "SHEET as a COMPLETELY DIFFERENT ZOMBIE: {who} " @@ -75,6 +76,28 @@ RESTYLE = ( "There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and " "the air are empty and the poses are the same as if a ball were there. " "Output exactly {n} figures side by side in ONE horizontal row, all on the same ground line. ") + +# ...AND EVERY SHEET AFTER IT DESCRIBES THE CHARACTER WITH A PICTURE OF IT. This is the whole fix. +# The brute's three sheets each restyled from the master independently and each re-derived "pale +# grey-green work shirt" from the same paragraph, so the game got three different brutes and the +# palette gate — correctly — refused two of them. A paragraph is a lossy specification of a +# character; the character is not. +# +# It is also the pattern that already worked: the racket zombie's own play sheet was generated from +# [z_c1, o_idle1, o_run1, o_swing1] — the character as an IMAGE, the poses as IMAGES — in one call, +# first time. I had it, and then did not use it for variants. +RESTYLE_KNOWN = ( + "The FIRST reference image is THE CHARACTER — the exact zombie to draw, and the ONLY source for " + "who it is: its build, its bulk, its height, its clothing and every colour of it. Copy that " + "character exactly. " + "The REMAINING reference images are {n} POSES, and are there ONLY to show what the body is doing " + "— never who it is. " + "Draw THE CHARACTER FROM THE FIRST IMAGE in each of those {n} poses, in the same order, at the " + "same size and the same distance from the camera, all facing the same way. The character never " + "changes; only the pose does. " + "There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and " + "the air are empty and the poses are the same as if a ball were there. " + "Output exactly {n} figures side by side in ONE horizontal row, all on the same ground line. ") STRIDE_NOTE = ("In particular frames 2 and 3 must stay what they are in the reference: ONE DRAWING " "SHOWN TWICE with only the legs different — everything above the hips identical " "between them, the legs clearly swapped, and the two frames NOT identical copies. ") @@ -117,12 +140,16 @@ def variant(key, who): seed = lambda src, dst: shutil.copy(os.path.join(HERE, src + '.png'), os.path.join(HERE, dst + '.png')) made = [] - def batch(name, sprites, refs, subject, stride=None, changed=None, newart=False): + def batch(name, sprites, refs, subject, stride=None, changed=None, newart=False, lead=False): + # `lead` means the first reference is THE CHARACTER and not a pose — it is shown to the + # generator and is not one of the frames asked for, so it is skipped when pairing references + # to outputs (both for seeding and for the run-time baseline in _regen). + pose_refs = refs[1:] if lead else refs # Only the sheets drawn off the MASTER are seeded here. A chained wound stage is seeded by # _regen at run time, from whatever its reference has become by then — seeding it now would # score it against a copy of the master, two steps better than the stage it follows. if newart: - for src, dst in zip(refs, sprites): + for src, dst in zip(pose_refs, sprites): seed(src, dst) spec = dict(sprites=sprites, ref_only=list(refs), check='loose', anchor=0, subject=subject) spec['newart' if newart else 'state'] = True @@ -139,11 +166,16 @@ def variant(key, who): print(f'{name:16s} <- {refs[0]}… {", ".join(sprites)}') K = 'z_' + key - # --- the whole body, restyled off the master + # --- the whole body. The PLAY sheet invents the character from the words; serve and fall are then + # drawn from the character it produced (K+'1' leads their reference list) plus the master's poses. + # That ordering is why the three must be run in the order printed at the end. for part, frames in SHEETS.items(): - batch(f'{key}_{part}', [K + f[3:] for f in frames], frames, - RESTYLE.format(n=len(frames), who=who) + (STRIDE_NOTE if part == 'play' else ''), - stride=[[1, 2]] if part == 'play' else None, newart=True) + first = part == 'play' + refs = frames if first else [K + '1'] + frames + batch(f'{key}_{part}', [K + f[3:] for f in frames], refs, + (RESTYLE.format(n=len(frames), who=who) + STRIDE_NOTE) if first + else RESTYLE_KNOWN.format(n=len(frames)), + stride=[[1, 2]] if first else None, newart=True, lead=not first) # --- the two wound stages, each chained off the one above it WALK = ['2', '3', '4'] # shuffle A, shuffle B, forehand — the wound rows' three frames diff --git a/assets/_regen.py b/assets/_regen.py index 38d2523..1ce8b81 100644 --- a/assets/_regen.py +++ b/assets/_regen.py @@ -277,6 +277,41 @@ def palette_off(outs, anchor, want): f'{d:.1f} away (want <= {want}). Something the size of a garment or the skin changed.') return None +# ---- COLOUR IS ARITHMETIC, NOT LUCK. Every call re-renders, and re-rendering re-lights: the brute's +# serve sheet came back with his build, his garments and his dark tones PIXEL-IDENTICAL to the anchor +# and his big pale shirt two shades brighter. That is not a specification failure — the model was +# looking straight at the character — and no prompt fixes it, which is why six gates spent a day +# trying to make a generator do arithmetic. +# +# So do the arithmetic here. Pair each of the frame's dominant colours with its nearest anchor +# colour, fit one gain+offset per channel across those pairs, and apply it to every pixel. Pairs +# further apart than NEW_COLOUR are dropped from the FIT — those are colours the anchor genuinely +# does not have (blood on a fresh wound) — but they still receive the transform, so a wound stays +# the right red relative to the body it is on. +NEW_COLOUR = 60 +def match_to_anchor(src, anchor_name): + ap = os.path.join(HERE, anchor_name + '.png') + if not os.path.exists(ap): return + ua, _ = dom_colours(ap) + uo, wo = dom_colours(src) + if not len(ua) or not len(uo): return + d = np.sqrt(((uo[:, None, :] - ua[None, :, :]) ** 2).sum(2)) + near = d.argmin(1) + keep = d.min(1) < NEW_COLOUR + if keep.sum() < 2: return # nothing recognisable to fit against + src_c, dst_c, w = uo[keep].astype(float), ua[near[keep]].astype(float), wo[keep] + a = np.array(Image.open(src).convert('RGBA')).astype(float) + m = a[..., 3] > 0 + for ch in range(3): + x, y = src_c[:, ch], dst_c[:, ch] + # weighted least squares y = gain*x + offset, with the gain kept sane so one odd pair + # cannot posterise the frame + W = w.sum(); mx, my = (w * x).sum() / W, (w * y).sum() / W + var = (w * (x - mx) ** 2).sum() + gain = float(np.clip(((w * (x - mx) * (y - my)).sum() / var) if var > 1e-6 else 1, 0.7, 1.4)) + a[m, ch] = np.clip(gain * (a[m, ch] - mx) + my, 0, 255) + Image.fromarray(a.astype('uint8'), 'RGBA').save(src) + def not_magenta(sheet): """The generator sometimes ignores the magenta and paints its own background. That was written off as harmless — the key uses the corner colour — right up until it returned a GREEN sheet and @@ -374,7 +409,10 @@ def run(name, spec, dry=False): bar = {'finer': 1.25, 'same': 0.85, 'loose': 0.7, 'any': 0.0}[spec.get('check', 'finer')] if sum(after) / len(after) < sum(before) / len(before) * bar: print(f'[{name}] REJECTED: not finer than what it replaces {before} -> {after}'); return False - # the palette gate supersedes the feature-picked kit gate wherever a batch declares an anchor + # CORRECT, THEN CHECK. The frames are pulled onto the anchor's colours first, so the gate below is + # a safety net for what arithmetic could not fix rather than a tripwire for what it can. + if spec.get('palette_anchor'): + for o in outs: match_to_anchor(o, spec['palette_anchor']) if spec.get('palette_anchor'): bad = palette_off(outs, spec['palette_anchor'], spec.get('palette_max', 10)) if bad: print(f'[{name}] REJECTED: {bad}'); return False diff --git a/assets/_regen_batches.json b/assets/_regen_batches.json index 05f96d8..f3583d4 100644 --- a/assets/_regen_batches.json +++ b/assets/_regen_batches.json @@ -1465,13 +1465,14 @@ "z_brserve" ], "ref_only": [ + "z_br1", "z_rbounce", "z_rtoss", "z_rserve" ], "check": "loose", "anchor": 0, - "subject": "The reference image is a 3-FRAME SPRITE SHEET of one zombie tennis player. Redraw THE WHOLE SHEET as a COMPLETELY DIFFERENT ZOMBIE: an ENORMOUS FAT ZOMBIE BRUTE — the same creature as this game's brute, now playing tennis: a HUGE SAGGING BELLY hanging out over the belt, a filthy PALE GREY-GREEN button-up work shirt straining over it, dark stains and a torn ragged hem, heavy OLIVE-BROWN work trousers, thick tree-trunk legs, scuffed brown boots, a SMALL BALD HEAD sunk low into massive rounded shoulders, heavy jowls, sunken eyes, green-grey dead skin, and a wooden tennis racket looking tiny in one huge fist. He is FAR bulkier, heavier and wider than the lean man in the reference — twice his width — and much shorter in the neck. KEEP ALL 3 POSES EXACTLY AS THEY ARE IN THE REFERENCE — the same stance, the same steps with the same leg forward in each, the same swing, the same facing, the same arm and racket positions, the same order left to right, the same total height and the same distance from the camera. Only the CHARACTER changes; the POSES do not. There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and the air are empty and the poses are the same as if a ball were there. Output exactly 3 figures side by side in ONE horizontal row, all on the same ground line. ", + "subject": "The FIRST reference image is THE CHARACTER — the exact zombie to draw, and the ONLY source for who it is: its build, its bulk, its height, its clothing and every colour of it. Copy that character exactly. The REMAINING reference images are 3 POSES, and are there ONLY to show what the body is doing — never who it is. Draw THE CHARACTER FROM THE FIRST IMAGE in each of those 3 poses, in the same order, at the same size and the same distance from the camera, all facing the same way. The character never changes; only the pose does. There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and the air are empty and the poses are the same as if a ball were there. Output exactly 3 figures side by side in ONE horizontal row, all on the same ground line. ", "newart": true, "palette_anchor": "z_br1" }, @@ -1483,6 +1484,7 @@ "z_brrise" ], "ref_only": [ + "z_br1", "z_rfall1", "z_rfall2", "z_rfall3", @@ -1490,7 +1492,7 @@ ], "check": "loose", "anchor": 0, - "subject": "The reference image is a 4-FRAME SPRITE SHEET of one zombie tennis player. Redraw THE WHOLE SHEET as a COMPLETELY DIFFERENT ZOMBIE: an ENORMOUS FAT ZOMBIE BRUTE — the same creature as this game's brute, now playing tennis: a HUGE SAGGING BELLY hanging out over the belt, a filthy PALE GREY-GREEN button-up work shirt straining over it, dark stains and a torn ragged hem, heavy OLIVE-BROWN work trousers, thick tree-trunk legs, scuffed brown boots, a SMALL BALD HEAD sunk low into massive rounded shoulders, heavy jowls, sunken eyes, green-grey dead skin, and a wooden tennis racket looking tiny in one huge fist. He is FAR bulkier, heavier and wider than the lean man in the reference — twice his width — and much shorter in the neck. KEEP ALL 4 POSES EXACTLY AS THEY ARE IN THE REFERENCE — the same stance, the same steps with the same leg forward in each, the same swing, the same facing, the same arm and racket positions, the same order left to right, the same total height and the same distance from the camera. Only the CHARACTER changes; the POSES do not. There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and the air are empty and the poses are the same as if a ball were there. Output exactly 4 figures side by side in ONE horizontal row, all on the same ground line. ", + "subject": "The FIRST reference image is THE CHARACTER — the exact zombie to draw, and the ONLY source for who it is: its build, its bulk, its height, its clothing and every colour of it. Copy that character exactly. The REMAINING reference images are 4 POSES, and are there ONLY to show what the body is doing — never who it is. Draw THE CHARACTER FROM THE FIRST IMAGE in each of those 4 poses, in the same order, at the same size and the same distance from the camera, all facing the same way. The character never changes; only the pose does. There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and the air are empty and the poses are the same as if a ball were there. Output exactly 4 figures side by side in ONE horizontal row, all on the same ground line. ", "newart": true, "palette_anchor": "z_br1" }, diff --git a/assets/z_brbounce.png b/assets/z_brbounce.png index 80b845f..ab5fcc0 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 34b6e0c..b9ea4cd 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 ed004a5..b127853 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 17dd8e2..95b0b0f 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 5366766..b78fea6 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 0101e34..752bd50 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 b4bd171..f734a56 100644 Binary files a/assets/z_brtoss.png and b/assets/z_brtoss.png differ