The flick stops losing the point, and the corpse falls and serves
THE FLICK. "when returning a ball with a spin it seems too sensitive, 8/10 times the ball lands out." Arithmetic, not feel. The singles line is 4.115 m; an aimed shot targeted a flat ax*3.3 and THEN added err() on top — up to +-2.4 m at quality 0 — so a flicked return landed as wide as 5.7 m, and the only clamp in the path sat at +-5, outside the court. An UNAIMED return targets 0 + the same scatter and is therefore always in: aiming was the thing losing the point, which is backwards for the mode's one act of skill. TUNE.flickReach scales the aim width by the same quality the scatter shrinks with, so the two can never sum past the line — worst case 1.49+2.4 = 3.89, and a perfectly timed flick still gets the full 3.3 with nothing left to scatter. The test asserts that sum across the whole quality range rather than sampling rallies. Not named `aimReach`: that key already exists further down TUNE (the slingshot's 22 m aim ray) and the duplicate silently won until a test caught it. The placement ring carries `q` now and is the flick's receipt — tight and hot when your timing earned the full width, wide and dim when you barely got a racket on it. Without it the rule is invisible: the shot just goes less wide. THE FALL (handoff item 4). A point that takes a limb knocks the body down and it drags itself up short of it. The three collapse frames wear the stage it STILL WAS and the rise wears the stage it has BECOME — that order is the whole beat, and it is what turns a popping wound stage into something the point did. Own clock beside the horde's warning, for the reason this file keeps relearning (a beat that moves the phase under a latch strands it); the 'over' beat it sets outlives the fall so it cannot expire into setupPoint() mid-collapse. Only fires when the stage actually moved — a quarter off a full pool is 0.75, so point one has no limb to explain and a second of collapse for nothing is a beat the player waits out. THE OPPONENT SERVES (handoff item 7), and it was mostly switched off rather than missing. setupPoint already placed a non-player server behind its own baseline and armed aiServeT; update() already tossed and struck for O. Both were gated on `G.mode === 'tennis'` and the match runs in splatter under a permanent interlude — one condition. Genuinely new: service alternates at each game boundary (the match calls advancePoint directly and the flip lives in tennis's pointTo, which it bypasses — the same shape as the G.serveNum bug), the zombie takes O's mark so picture and ballistics agree on where it stands, and the serve poses are named onto it because O is invisible here. racketStage() moves to config: drawZombie and the fall must not answer the same question twice, which is how the boss's skins and its blood drifted apart. Docs: PROJECT.md gains THE ZOMBIE FAMILY PIPELINE — the measured API contract (4K is a parameter and the default is 1K; n is 1 so "return both" is not a thing; aspect is an enum so pad to it), why magenta stays despite green being the documented key, and the two things _zsheet.py must do globally. Verified in the browser: brute in the match, z_brfall2 -> z_brw1rise on a wounding point, and bounce -> toss -> serve off the opponent's own baseline. 58 tests green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cba2fb8a8b
commit
c1f7268a04
9 files changed
+412
-37
No files matched your search
+116
@@ -1751,6 +1751,122 @@ The gates, and the numbers they were calibrated on:
|
||||
wound half needs the reference discipline applied harder than prose currently applies it.
|
||||
- The brute is PARKED, not deleted: `config.ZFAMS` is one entry away from making it live, its 31
|
||||
frames are on disk, and its nine batches are in `_regen_batches.json`.
|
||||
**SUPERSEDED 2026-08-12 — see the next section.** Those 31 frames were not art: `--verify` found
|
||||
27 of them dead. The brute is now live (`ZFAMS` has `'br'`, the KEEPER wears it) off ONE sheet.
|
||||
|
||||
## THE ZOMBIE FAMILY PIPELINE — one sheet, one character, 31 sprites (2026-08-12)
|
||||
|
||||
**This replaces the nine-batch flow above for a whole opponent family.** `_regen.py` still exists and
|
||||
is still right for single sheets, remasters and the boss roster; what it could not do is make a
|
||||
FAMILY that is one character, because nine separate rolls is nine chances to drift and it took every
|
||||
one of them — three different brutes, and a corpse that came back as a living man.
|
||||
|
||||
### The rule the whole thing rests on
|
||||
|
||||
**ASK FOR THE WHOLE FAMILY IN ONE IMAGE.** One roll, one light, one palette, 31 figures. Consistency
|
||||
stops being something you police with gates and becomes a property of the request. This is also what
|
||||
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 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 <sheet.png> <famkey> --out assets/_refcheck/sprites # review first
|
||||
python3 assets/_zsheet.py <sheet.png> <famkey> --install # then install
|
||||
```
|
||||
|
||||
Then: add the key to `config.ZFAMS`, put `fam: '<key>'` on an `OPPONENTS` row, bump `SPRV` and
|
||||
`main.js?v=`, `node test.js`, and look at it in `#bench` (arrow keys walk families and wound stages).
|
||||
|
||||
### The API facts, measured, not read off a blog
|
||||
|
||||
Queried from `/api/v1/images/models` — the endpoint's own capability descriptor:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `resolution` | `1K` / `2K` / `4K`, **default 1K**, uppercase K required |
|
||||
| `aspect_ratio` | a 10-value enum (1:1 … 21:9). No arbitrary ratios — a sheet gets SNAPPED |
|
||||
| `n` | **min 1, max 1** |
|
||||
| `input_references` | 0…14 |
|
||||
|
||||
- **Use `/api/v1/images`, not `/chat/completions`.** Chat completions has no resolution control, so
|
||||
every call silently returns 1K. The same sheet came back 1616x656 through chat and **6336x2688**
|
||||
at `"resolution": "4K"` — 15x the pixels, for one JSON field.
|
||||
- **`n` is 1. Always.** "Return both halves" is not a thing the model can do; asked for two, it
|
||||
merges them into one picture and returns that same image twice. Two byte-identical files is the
|
||||
tell.
|
||||
- **Pad the input to the aspect you ask for.** The ratio is an enum, so a 1:3.2 sheet handed to a
|
||||
9:16 request gets rearranged to fit. Pad with magenta and the figures stay where they are.
|
||||
- **Multiple references compose ONE output.** They are not a batch. Google's sub-limits: <=10 object
|
||||
images, <=5 character images, <=3 style references.
|
||||
|
||||
### What the model will and will not do
|
||||
|
||||
- **It cannot output alpha.** No Google image model can. "Transparent background" returns white,
|
||||
black, or a *painted* checkerboard. The chroma key is not a workaround, it is the method — and
|
||||
green is the documented default, which is wrong here because our zombies are green. Magenta stays.
|
||||
- **Its magenta is NOISY**: 8,000–22,000 distinct tones per sheet, corners anywhere from (250,1,250)
|
||||
to (254,3,253). Key a RANGE. An exact `#FF00FF` match leaves a magenta fringe on every figure.
|
||||
- **It draws pixel-art-STYLE illustration, not pixel art** — uneven pixel sizes, too many colours,
|
||||
and it adds anti-aliasing and gradients unless forbidden. Prohibitions help and do not solve it.
|
||||
`_fine.py`'s block-snap + box-filter + palette quantise is what makes it read as sprite art; the
|
||||
softness does NOT wash out in the downscale alone (measured against `z_g1`, it stays visibly
|
||||
smoother).
|
||||
- **Name each image's JOB in the prompt.** "IMAGE 1 is the layout and poses only; IMAGE 2 is the
|
||||
ONLY source for who the character is." Without that the majority wins: `racket_play`'s reference
|
||||
is one zombie and three living humans, and a re-roll came back as a **living man** despite the
|
||||
prompt carrying the CORPSE rule verbatim. A picture beats a paragraph — including when you did not
|
||||
mean it to.
|
||||
- **Frame it as an EDIT, not a composition**, and tell it to match image 1's *drawing style*. Every
|
||||
"draw a new sheet" roll came back painterly; the edits kept the technique.
|
||||
|
||||
### `_zsheet.py`, and the two things that must be GLOBAL
|
||||
|
||||
It reuses `_fine.py`'s keyer, art-block detection and column cutter, adds row-band splitting, and
|
||||
fixes the two things that break a family if they are done per-row:
|
||||
|
||||
- **SCALE** — taken once, from the play row's standing frame. `_fine.py` anchors one scale per sheet,
|
||||
so nine bands would each normalise to their own tallest figure and the serve strike would come out
|
||||
the same height as the idle stance. The game sizes every sprite off its own PNG height, so that is
|
||||
a body that pops. Measured after: serve **319**, prone fall **83** — the racket family's own are
|
||||
307 and 85, arrived at independently.
|
||||
- **PALETTE** — taken once, from the largest figure anywhere, so a wound row cannot quantise onto a
|
||||
slightly different set of colours than the row above it.
|
||||
|
||||
**And it INVERTS `_fine.py`'s enclosed-magenta rule, deliberately.** `_fine.py` repaints magenta
|
||||
trapped inside a figure as pale grey, because the generator used to fill racket string beds with the
|
||||
backdrop colour BY ACCIDENT. Here the prompt asks for exactly that and gets it, so the bed cuts to
|
||||
transparency and the racket is an open hoop. That is only safe because nothing else on the figure is
|
||||
magenta — check a new sheet.
|
||||
|
||||
### Three fixes this found in `_regen.py`, which still apply to it
|
||||
|
||||
- **A margin is a PROPORTION, never a pixel count.** "At least 150 pixels of empty magenta at every
|
||||
edge" means nothing to a model that picks its own output size; it composed at 92% of a canvas it
|
||||
chose and clipped 3 rolls in 4. Stated as a fraction, edge rejections went to 1 in 3.
|
||||
- **A reference sheet must not show a racket as a magenta hole.** Older sprites were cut before
|
||||
`_fine.py` learned to keep enclosed background, so their string beds are transparent — `o_smash`
|
||||
has 3,425 such pixels — and pasted on the magenta canvas they came back as magenta. Four of the
|
||||
racket family's nine references were showing the generator exactly what the prompt forbade.
|
||||
`ref_sheet` fills them with a woven pale bed now.
|
||||
- **`--verify` asks the question no gate here ever asked**: is the art already on disk what it claims
|
||||
to be? Every other check fires on a NEW roll and says nothing about installed frames — and a
|
||||
chained batch draws its reference from installed frames. It found 27 of the brute's 31 dead:
|
||||
`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.
|
||||
|
||||
### Still open
|
||||
|
||||
- The style is close but not identical to the hand-made sprites: softer outline, blended shading
|
||||
rather than dithering. Oliver accepted it ("I like the style") on the grounds that a RELIABLE way
|
||||
to generate art is worth more right now than an exact match.
|
||||
- The 9:16 padding spends most of the 4K budget on empty magenta — the figures came back at 243x288
|
||||
before downscaling. A layout that fills the canvas would give each figure far more real pixels.
|
||||
|
||||
## Session log: Oliver's playtest of the barrel weapons (2026-08-03, same day)
|
||||
|
||||
|
||||
Reference in new issue
Block a user