Draw the whole serve right-handed, and let the pull wind it up

The racket kept changing hands because the art disagreed with itself: p_idle,
p_bounce and p_toss held it in his LEFT hand while p_smash and p_swing used his
right, so every new frame copied whichever reference it was handed. The idle, the
bounce pair, the toss and the four wind-up frames are all regenerated with the
side named — drawn from behind, his right hand is the right-hand side of the
image, and only the racket HEAD crosses behind his back.

What separates the four wind-up stages is how far the racket has fallen, and that
needed the racket head pinned to a body landmark per frame — above his head,
beside his ear, between his shoulder blades, at the back of his thighs. Described
as "further into it each time" it drew four near-identical poses twice running.
They are wired late in the pull, Oliver's call: frame 1 owns the first half and
the three coiled frames are packed into the top half.

assets/_derive.py is deleted. Fixing generated art by mangling it afterwards is
always worse than regenerating; three guards in _regen.py turn the bad rolls that
used to install silently into automatic re-rolls — a portrait reference canvas
(the generator answered with a 2x2 grid), a non-magenta background (a green sheet
keyed away four racket heads), and a mirrored frame.

The bounce pair is one drawing from one sheet now, differing only in the free
hand, so BOUNCE_IDLE moved to his left side and was re-measured off the sprite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-01 00:49:28 +02:00
1 parent 231cd8650a
commit bcff17812f
17 files changed
+181 -170

No files matched your search

+66 -34
View File
@@ -409,41 +409,66 @@ Known generator quirks, all still true:
- **Colour-match any regenerated frame** of an existing pair before shipping: fit a per-channel
linear map onto the original's mean and sd, then verify with a mean-difference number.
## Session log: the serve is the gesture (2026-07-31, after the art pass)
## Session log: the serve is the gesture (2026-08-01)
Oliver's notes on the art pass: the zombies are good, the runner "flickers very hard", and the
player has three problems. All four fixed, all four measured first.
player has three problems. The serve is now a drawn wind-up driven by the pull.
- **The pull IS the wind-up.** Four new frames (`p_pull1..4`, `assets/_regen_batches.json` batch
`player_pull`) — racket above the head, behind the head, behind the back, fully coiled — chosen in
`drawChar` off `pullFrac(G.aim.mag)`, the same 0..1 that sets the shot's power. Drag further, coil
further; release fires `p_smash`. Verified per stage with drawImage instrumentation:
mag 10/50/90/130 -> pull1/pull2/pull3/pull4.
- **The racket must stay in ONE hand across a whole sheet, and the generator will not do it unasked.**
Oliver, straight off the file browser: *"the racket changes the hand in 3 and 4"*. It did — frames
1-2 came back right-handed and 3-4 left-handed. Both mirrored into line (a back-view figure mirrors
for free) and the batch prompt now names the side explicitly. The convention the rest of the art
uses: **racket on the LEFT-HAND SIDE OF THE IMAGE, free arm on the RIGHT** — which is also the side
the game drops the bounced ball (`BOUNCE_IDLE.dx` > 0) and tosses the served one (`P.x + 0.35`).
`p_smash` is the deliberate exception: the racket comes over to the right to meet the ball it is
actually hitting.
- **The bounce pair is now ONE drawing.** `p_bounce2` is `p_bounce` with the free arm sheared up 12
px (0.10 m) — everything else is pixel-identical, so the swap cannot flicker. The swap follows the
BALL now (`|sin| > 0.5`) instead of its rising phase, and `BOUNCE_IDLE` was re-measured off the
sprite (`apex` 0.55 -> 0.6, `dx` 0.5 -> 0.43) so the ball tops out under the raised palm instead of
passing beside it.
- **`p_toss` had the tossed ball drawn into it.** Cut, without re-cropping the canvas: the game
scales him off `img.height`, so trimming the dead space would have shrunk him. Still 100x308.
- **The runner.** Two measured defects, both in the PNGs, no code change. Its four frames were
bbox-cropped and drawn centred on the body's x, so hip centres sat -2.0/+7.1/-6.0/-0.1 px off
centre — a 13 px lateral jump at 12 frames a second, which is the flicker. And Oliver's own
diagnosis was right: all four frames leaned the same way and lifted the SAME leg, so the cycle
never used his other side. Frames 3 and 4 are mirrored (he is front-on, so a mirror costs nothing)
and all four are padded to one width with the hip on the centre line.
- **The pull IS the wind-up.** Four new frames (`p_pull1..4`, batch `player_windup`), chosen in
`drawChar` off `pullFrac(G.aim.mag)`. What separates the four is HOW FAR THE RACKET HAS FALLEN, and
that only came out once the prompt pinned the racket head to a different BODY LANDMARK per frame —
above his head, beside his ear, between his shoulder blades, down at the back of his thighs.
Described as "further into the wind-up each time" it drew four near-identical poses twice running
(Oliver: *"pull3 and pull4 are now basically the same image"*), which is the generator quirk the
pipeline notes already warned about, in a place I did not expect it. The stages are NOT spread evenly: **frame 1 owns the whole
first half of the pull and the three coiled frames are packed into the top half**, Oliver's call,
so the part of the gesture that costs power is the part that visibly costs effort. Release fires
`p_smash`. Verified: 30/74/95/125 px of drag -> pull1/2/3/4.
- **The whole player is RIGHT-HANDED now, and that was the real bug.** `p_idle`, `p_bounce` and
`p_toss` had been drawn holding the racket in his LEFT hand while `p_smash` and `p_swing` used his
right, so any new frame copied whichever reference it was given and the racket changed hands
mid-serve. Everything from the idle to the wind-up was regenerated with the hand named explicitly:
**drawn from behind, his right hand is the RIGHT-HAND SIDE OF THE IMAGE**. Only the racket HEAD
crosses behind his back in the deep frames — the arm never does.
- **The bounce pair is one drawing from one sheet**, differing only in the free hand, so the swap
cannot flicker; it follows the BALL (`|sin| > 0.5`) rather than its rising phase. He pats the ball
with his LEFT hand now, so `BOUNCE_IDLE` moved to the other side and was re-measured off the sprite
(`dx` 0.36, `apex` 0.68). The served toss moved to `+0.12` — nearly over his head, because the
tossing hand is on the left and the racket meets it on the right, and a big offset either way is
wrong for one of those two frames.
- **The runner** still carries a hand-mirrored fix from the previous commit (see OPEN) — its frames
need regenerating, not mangling.
Not done, and the honest reason: the runner's cadence is still time-driven (12 fps) rather than
stepped off distance covered like every other body. It did not read as wrong once the frames stopped
jumping, and it is one line if a playtest says otherwise.
**THE RULE, from Oliver, that this session existed to learn:** *"fixing generated art by mangling
with it afterwards is always worse than regenerating. it's just a fact."* Mirroring a frame,
shearing a limb, cutting a prop out — a wrong frame is wrong in more ways than the one you spotted
(perspective, foreshortening, where the weight sits) and none of those survive a flip. There was an
`assets/_derive.py` doing exactly that; it is deleted. Fix the PROMPT and re-run the batch.
**Three pipeline guards were added to `_regen.py`, each after a bad sheet installed silently:**
1. **The reference's ASPECT is the answer's aspect.** One tall reference figure gave a portrait
reference canvas and the generator answered with a 2x2 GRID, which `_fine.py` cannot cut (it
segments columns). `ref_sheet` now widens to a landscape strip with one slot per frame asked for.
2. **A non-magenta background is a rejected roll.** "It sometimes ignores the magenta" was written
off as harmless because the key uses the corner colour — until it returned a GREEN sheet and keyed
away four grey-green racket heads with it, installing four men holding a grip and nothing else.
3. **`arm_left` rejects a MIRRORED frame.** The generator flips the later frames of a serve about
half the time, which puts the racket in the other hand halfway through the animation. Oliver
caught it twice by eye before the check existed. **It is a net, not a proof**: it asks whether
anything reaches the top of the LEFT half, and a mirrored frame whose racket head hangs over there
passes. A tighter discriminator was tried (ink mass in the top third, left vs right) and does NOT
separate the two — measured 0.61/0.57/0.41 on a mirrored sheet against 0.54/0.56/0.55 on a good
one. Every wind-up sheet still has to be looked at.
Still imperfect, and worth a look on the phone: the empty tossing arm WANDERS between the four
wind-up frames (up, out, bent, up) instead of staying thrown up at the ball. The roll that held the
arm still put the racket back in the wrong hand, and the racket is the load-bearing half of the
gesture, so this is the set that shipped. One more roll may fix both.
Also worth knowing: a batch of raised-arm poses has no honest scale to anchor on, so frame 0 is
always a throwaway STANDING frame (`p_pull0`, deleted after installing). And a photo reference of a
real serve is what finally got the poses right — Oliver supplied a 10-frame sequence, and the four
stages are its frames 3, 4, 5 and 6, the last being maximum tension before release.
## Session log: the art-resolution pass (2026-07-31)
@@ -582,12 +607,19 @@ Nothing here is broken; these are known gaps and unverified risks.
8. **A chapter can be skipped.** `G.bossDue` takes the max, so going a long stretch without ever
clearing the court means you face the current chapter, not a backlog. Deliberate; noted in case it
ever reads as a bug.
9. **Step 6 stays PARKED** by Oliver's decision: the crowd turning into zombies (spawning from the
9. **The runner's four frames were fixed BY HAND and must be regenerated.** `z_f1..4` are currently
hip-registered (padded to one width with the hip on the centre line) and frames 3 and 4 are
mirrored copies, because all four were drawn leaning the same way and lifting the same leg. That
is exactly the post-processing Oliver ruled out — it shipped before he stated the rule. The batch
to write asks for a four-frame front-on sprint whose stride and lean ALTERNATE, and the numbers to
check afterwards are the per-frame hip centroid (it was 13 px of lateral jump at 12 frames a
second) and which leg leads.
10. **Step 6 stays PARKED** by Oliver's decision: the crowd turning into zombies (spawning from the
stands — attacks from behind, high spectacle, `x_crowd` sheets already exist) and the ball-boy zombie
that catches your shots. Recorded so the ideas are not lost, not as a to-do.
10. **`ump_no_deal` does not exist.** Oliver asked for its removal; `ump_no_appeal` was removed as the
11. **`ump_no_deal` does not exist.** Oliver asked for its removal; `ump_no_appeal` was removed as the
closest match. Worth one confirmation that no other line is still unwanted.
11. **Four sounds are missing and need RESEARCH first** (find/choose the source before implementing —
12. **Four sounds are missing and need RESEARCH first** (find/choose the source before implementing —
every existing sfx is either synthesised in `audio.js` or a wav in `assets/audio/`):
- the **ball machine** while it moves — a squeaking wheel/castor, so the furniture announces
itself. It already telegraphs its shot (`machWind` 0.9 s); this is the *approach* cue.