The crowd is drawn again, in detail — and the stand now moves one spectator at a time

Oliver on the first pass: "the new lighter areas read as rather sloppy artwork.
please regenerate the crowd art and make it more detailed. also the animation
also looks pretty artificial. the flashes from the fotographs are too rare and
the whole thing somehow does not translate well to the other courts. also when
the opponent makes a point there is an aww-sample (which has an applaus trail)
so the crowd should still clap but more subtle."

The paint pass is deleted — recolouring blobs of an old drawing is not drawing,
and the reason it looked necessary was a wrong conclusion: the roll scripts read
OPENROUTER_API_KEY from the repo's .env, so the art could be rolled all along.

_crowdroll.py rolls the crowd itself: individually drawn spectators with hats,
sunglasses, drinks, cameras, flags and banners, empty seats, stairways, and
three zombies sitting in the middle of it behaving like furniture. ONE roll now
serves all three courts through a per-channel histogram match onto each court's
old palette, so anything the art gains shows up on clay and at night too.

_crowdpass.py keeps only what was empty stand in the seated roll and became a
person in the clap roll — the raised arms, 1.2% of the sheet — so the clap frame
cannot drift by construction, and writes a box around each pair of arms into
src/crowd.js. render.js raises ONE SPECTATOR AT A TIME out of that table, each
on their own period: the 16-column strip swap moved a seventeenth of the crowd
as a block, which is what read as machinery. crowd.pop is now a head count, so
the same code covers a dozen people and the whole stand, and past 0.75 the wave
travels person by person.

aww() sets a small crowd.pop of its own — the sample is a groan that settles
into applause, and the stand has to match what you can hear. flashEvery 5.0 ->
1.9, because at a five-second mean over a stand this wide nobody ever saw one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-21 13:30:34 +02:00
1 parent 74afd5988c
commit 5f53a686c8
14 files changed
+499 -398

No files matched your search

+73 -22
View File
@@ -20,9 +20,10 @@ anything, so if you add a `return` for an awkward deal, check the body still run
the state-machine contracts that have historically produced the show-stoppers. The Dockerfile runs
it as a build stage, so a red suite cannot deploy — but finding out from Coolify is the slow way.
Last updated 2026-08-21 (THE STAND: the clap frame rebuilt as a real pose of the seated one, which
is what the "wobble" was; an art pass for chroma and silhouette; the wave, the idle fidget, and
paparazzi that scale with the moment. See "THE STAND STOPS WOBBLING". Before that 2026-08-20 (THE SHELL: a Capacitor iOS project, haptics on the existing sfx call
Last updated 2026-08-21 (THE CROWD: rolled again, in far more detail, once for all three courts;
the clap frame is now the seated sheet plus the raised arms and nothing else, and the stand animates
ONE SPECTATOR AT A TIME off `src/crowd.js`. The wave, the idle fidget, paparazzi that scale with the
moment, and an aww that still claps. See "THE CROWD, ROLLED AGAIN". Before that 2026-08-20 (THE SHELL: a Capacitor iOS project, haptics on the existing sfx call
sites, portrait enforced in Info.plist, and swipe-left-to-go-back in the menus — the web build
unchanged and still primary. See "THE SHELL"). Before that, same day: the friend across the net gets his OWN sheets — the player's kit, drawn
from the front, so his pick dyes the same garment yours does; the setup screen shows that front too.
@@ -347,19 +348,25 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
`mopRound` walks him to a stain, holds him there for `TUNE.boyMopDwell` while the push cycle
animates in place, then moves him on. Three sheets: `x_boymop` (stopped, working),
`x_boycarry` (walking with the mop shouldered), `x_boyrun` (tennis-mode fetch).
- **THE CLAP FRAME IS THE SEATED FRAME PLUS FORTY PAIRS OF ARMS — nothing else may differ.**
`x_crowd2` is built FROM `x_crowd` by `assets/_crowdpass.py`: identical pixel for pixel except
inside blobs that are genuinely arms-up and that each sit **inside one of render's 16 columns**
(`COLS_CROWD`, and `COLS` in that script — change one and you must recut the sheets). That is
what makes a column swap seamless, and what lets the same machinery drive three behaviours
(applause / wave / idle fidget) off one sheet. It was NOT true before 2026-08-21: the clap sheet
was a re-roll that differed from the seated one in 13-16% of its pixels, everywhere, so a swapped
column re-hashed the crowd rather than raising arms — Oliver, "the crowd wobbles with a client
side animation right now.. that feels odd." **Never hand-edit one crowd frame.** Edit the seated
sheet and re-run the tool, which rebuilds its partner. Draws stay integer-aligned with **no
vertical offset**: a fractional `dy` on a scaled image re-snaps its pixel rows every frame under
nearest-neighbour — that boiling beside the HUD was the "scoreboard flicker".
`assets/_sheet_crowd1.png` is the pre-brightening original.
- **THE CLAP FRAME IS THE SEATED FRAME PLUS THE ARMS, and that is meant literally.** `x_crowd2` is
built FROM `x_crowd` by `assets/_crowdpass.py`, which copies only the pixels that were STAND in
the seated roll and became PERSON in the clap roll — 93 pairs of raised arms and hands, 1.2% of
the sheet. Not the raiser's face, not his shirt, not his seat: those are the seated sheet's own
pixels and cannot drift. Each pair of arms has a box in **`src/crowd.js`** (generated), and
render.js raises **one spectator at a time** out of that table. It was NOT true before 2026-08-21:
the clap sheet was a separate roll that differed from the seated one in 13-16% of its pixels,
everywhere, so the animation re-hashed the crowd rather than raising arms — Oliver, "the crowd
wobbles with a client side animation right now.. that feels odd." **Never hand-edit one crowd
frame, and never edit `src/crowd.js`.** Re-roll with `_crowdroll.py`, then re-run `_crowdpass.py`,
which rebuilds the pair, the three court grades and the table together. Draws stay integer-aligned
with **no vertical offset**: a fractional `dy` on a scaled image re-snaps its pixel rows every
frame under nearest-neighbour — that boiling beside the HUD was the "scoreboard flicker".
- **The three courts are ONE crowd, graded.** `x_crowd`, `x_crowd_c` and `x_crowd_h` are the same
roll under three per-channel histogram matches onto the courts' old palettes, so anything the
crowd art gains shows up on all three. They were three separate rolls until 2026-08-21, and the
cost of that was exactly what Oliver reported of the first pass: "the whole thing somehow does not
translate well to the other courts, there the difference is barely visible."
`assets/_sheet_crowd1.png` is the pre-brightening original of the FIRST crowd, kept for history.
## Settled feel decisions — do not regress silently
- **Far bodies draw off a flattened scale curve; the camera holds still.** `TUNE.farK` 0.25
@@ -2307,7 +2314,7 @@ it is the WRONG tool, superseded by the reference-sheet flow. Route by job:
| THE SAME CHARACTER IN SOMEONE ELSE'S CLOTHES, every pose at once | `_frontroll.py` — famroll's trick pointed at the tennis poses: image 1 is the ten `o_*` frames composed from disk, image 2 is the body to dress them in. Two calls made the whole netplay family. Ten `_regen` batches would have been ten chances for the man to drift. |
| Single-sheet remaster, boss roster, any non-family sprite | `_regen.py` + `_regen_batches.json` — the only jobs it is still for. |
| Standalone art (logo, panels) | `_key.py`; coarse legacy sheets, `_slice.py`. |
| The CROWD sheets — either frame, any court | `_crowdpass.py`. It owns the seated/clap pair as ONE artefact; there is no such thing as editing half of it. Not idempotent: `git checkout assets/x_crowd*.png` before every run. |
| The CROWD — the art itself | `_crowdroll.py` (two rolls: the seated master, then the same picture with arms up) -> `_crowdpass.py` (throws the drift away, keeps the arms, grades the three courts, writes `src/crowd.js`). Both frames of all three courts come out of one run; there is no such thing as editing half of it. |
**And when installed sprites look WORSE than the `_refcheck/fam_*.png` sheet they came from, the
suspect is the CUT, not the roll** — the sheets on disk are the evidence, and a re-cut is free (see
@@ -2548,6 +2555,46 @@ The gates, and the numbers they were calibrated on:
**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 CROWD, ROLLED AGAIN — AND ONE SPECTATOR AT A TIME (2026-08-21, same day)
Oliver on the pass below: *"the still look a little rough.. and now the new lighter areas read as
rather sloppy artwork. please regenerate the crowd art and make it more detailed. also the animation
also looks pretty artificial. the flashes from the fotographs are too rare and the the whole thing
somehow does not translate well to the other courts. also when the opponent makes a point there is
an aww-sample (which has an applaus trail) so the crowd should still clap but more subtle."*
All five, and the first one retracts a claim the section below made: **there IS a key** — the roll
scripts read `OPENROUTER_API_KEY` from the repo's own `.env`, not from the environment, so "no API
key in the environment" was a wrong conclusion, and painting detail onto an old sheet with numpy was
never the right tool. It is deleted. The art is rolled.
- **THE ART IS A NEW ROLL** (`assets/_crowdroll.py`, two calls). Individually drawn spectators —
hair, hats, sunglasses, drinks, cameras, small flags, plain banners, empty seats, stairways, a few
already on their feet, and three zombies sitting in the middle of the crowd behaving like
furniture. The prompt states the art resolution in FIGURE HEIGHTS ("about 90 pixels of height
instead of the reference's 40"), which is the one lesson the sprite rolls paid for: the generator
picks its own canvas, so a pixel count means nothing to it and a proportion does. The old sheet
goes in as image 1 for LAYOUT ONLY — same camera, same tiers, same railing.
- **ONE ROLL, THREE COURTS.** Each court is a per-channel histogram match onto its own old sheet, so
clay is still terracotta and the night court is still night, but the crowd underneath is one
drawing. The same LUT grades both frames of a pair, which is what keeps them a pair.
- **THE ANIMATION IS PER SPECTATOR.** The clap roll drifts by 29% of its pixels; `_crowdpass.py`
keeps only the blobs that were empty stand and became a person — the raised arms, 1.2% of the
sheet — and writes a box around each into `src/crowd.js`. render.js draws ONE BOX AT A TIME, so a
single spectator's arms go up. The 16-column strip swap it replaced moved a seventeenth of the
crowd as a block, which is what read as machinery ("the animation also looks pretty artificial").
Boxes that overlap are merged into one entry: two neighbours who always rise together are one
person as far as the animation is concerned.
- **THE SIZE OF THE MOMENT IS NOW A HEAD COUNT.** `crowd.pop` decides HOW MANY of the 93 join in
(each on their own period, staggered by a stable per-person draw), so the same code does the whole
range: a groan is a dozen people, a game is half the stand, and past 0.75 it becomes the wave —
which is now a front travelling person by person rather than column by column.
- **THE AWW CLAPS.** `tennis-aww` is a groan that settles into scattered applause, so a stand that
sits through it contradicts the sound. `aww()` now sets a small `crowd.pop` itself — in audio.js,
because it is the SAMPLE that makes this true and every current and future caller should get it.
- **MORE PHOTOGRAPHERS.** `TUNE.flashEvery` 5.0 -> 1.9. At a five-second mean, jittered, spread over
a stand this wide, Oliver never saw one.
## THE STAND STOPS WOBBLING, AND STARTS BEING A CROWD (2026-08-21)
*"the crowd spritesheet could use some love (resolution and movement wise)... also the crowd wobbles
@@ -2606,10 +2653,13 @@ held at the loudest while an applause is live** — a won rally landing inside a
must not demote the stand to a polite clap, and `cheer()` is called from a dozen places that overlap.
That is the one new state-machine rule here, and it has a test.
**What this did NOT do:** the crowd is still the same drawing. A genuinely richer stand — more
distinct silhouettes, people doing different things — needs a re-roll of the sheet, which this
session could not do (no API key in the environment) and which would cost the clap animation
outright: the raises are lifted from the OLD roll, and a new composition has nothing to lift from.
**SUPERSEDED THE SAME DAY — see the section above.** Two things in this one were wrong. The paint
pass ("55 shirts and 44 heads of hair...") is gone: Oliver, *"the new lighter areas read as rather
sloppy artwork"*, and he is right — recolouring blobs of somebody else's drawing is not drawing. And
"no API key in the environment" was a wrong conclusion that made the paint pass look necessary: the
key is in the repo's `.env`, which is where the roll scripts have always read it from. What survives
from this session is the diagnosis (the frame pair, not the animation), `crowd.pop`, the paparazzi
scaling and the fidget.
## THE SHELL — CAPACITOR, HAPTICS, PORTRAIT, SWIPE-BACK (2026-08-20)
@@ -3712,7 +3762,8 @@ Nothing here is broken; these are known gaps and unverified risks.
are (a) author the sheet at the aspect portrait actually uses, or (b) tile it at its own aspect the
way `render()` already tiles the ad strip. **Deliberately not done 2026-08-21**: at ~8 logical px
per person nobody sees a 25% narrow spectator, and the same effort spent on chroma and silhouette
was worth more. Revisit only if the crowd is ever re-rolled at a chosen size.
was worth more, and the 2026-08-21 re-roll went out at the same 1408x768 rather than spending
the session on a fit nobody can see. Revisit only alongside a re-roll at a chosen size.
4. **`p_idle1`/`p_idle2` and most `o_*` frames are 2x-upscaled, not re-sliced.** They are on the right
scale but gained no detail in the remaster, and they are the two most-seen poses in the game.
Fix by hand-slicing `_sheet_player2/3` and `_sheet_opp2/3` at `IDLE_TARGET_H` 224 rather than going