The stand stops wobbling: a real clap frame, a wave, and three spectators who have already turned

The wobble was never the animation. x_crowd2 was a RE-ROLL of x_crowd, not a
pose of it — 13-16% of its pixels differed, over faces, torsos, seat backs and
the railing — so swapping 16 columns at 4.5 Hz re-hashed a strip of crowd twice
a second. assets/_crowdpass.py rebuilds the pair: the clap frame is now the
seated frame plus 36 genuine raises, each contained in ONE of render's columns,
so any column swap is seamless by construction and an empty column is invisible
instead of being a wobble. The three courts are one composition in three
palettes, so a raise found in one court's roll is transplanted into the other
two through a colour map learnt from the aligned seated pair.

The same tool paints the crowd, because at ~8 logical px a spectator detail can
only mean chroma and silhouette: 55 shirts and 44 heads of hair in colours the
court does not own, 28 caps, 4 flags above the heads, 8 emptied seats, 8 people
permanently standing — and 3 who are already turned, grey-green, mid-row, who
are also the only three who never applaud.

Movement, one draw path and three behaviours: applause scatters per column, a
WAVE crosses the stand for a moment big enough to deserve one (crowd.pop >=
0.75), and with nobody cheering ONE column stands for a third of every
TUNE.fidgetEvery, so the stand between points is no longer a photograph.

Paparazzi now scale with the moment: cheer() records the size of it (vol was
already exactly that at every call site) and queues shutters proportional to it,
drained at a fifth of the normal gap. pop is held at the loudest while an
applause is live, so a won rally landing inside a match-point ovation cannot
demote the stand to a polite clap — that rule has a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-21 12:58:26 +02:00
1 parent 76ea1d0d79
commit 74afd5988c
14 files changed
+585 -42

No files matched your search

+23 -1
View File
@@ -89,7 +89,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ----------------------------------------------
const state = await import('./src/state.js');
const { G, P, O, score, ball, setupPoint } = state;
const { update, forceZombies, startGore, splatPoint, restartRun, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe } = await import('./src/gameplay.js');
const { update, forceZombies, startGore, splatPoint, restartRun, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs } = await import('./src/beats.js');
const { mirror, netMsg, netTest, netTick } = await import('./src/net.js');
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
@@ -1899,5 +1899,27 @@ test('haptics: a blast that kills a dozen bodies is felt twice, not thirteen tim
assert.equal(buzz.length, 2, 'one heavy for the blast, one medium for the kills — not one per body');
});
test('the stand reads the SIZE of the moment, and a small cheer inside a big one cannot demote it', () => {
// crowd.pop is what decides whether the wave rolls and how many cameras go off (render.js), and
// every one of its inputs is a vol somebody passed to cheer(). The trap it is written against:
// a won rally landing in the middle of a match-point ovation must not drop the stand back to a
// polite clap, because cheer() is called from a dozen places and they overlap constantly.
const { crowd } = state;
crowd.cheer = crowd.pop = crowd.burst = 0;
cheer(0.8, 0.25); // a rally
assert.equal(crowd.pop, 0.25);
assert.ok(crowd.burst >= 1, 'even a small moment gets a shutter or two');
cheer(4, 1); // ...and then the match
assert.equal(crowd.pop, 1, 'the bigger moment owns the stand');
const queued = crowd.burst;
cheer(0.8, 0.25); // a rally INSIDE the ovation
assert.equal(crowd.pop, 1, 'a small cheer inside a live one may not demote it');
assert.ok(crowd.burst > queued, 'it still adds shutters, at the big moment\'s rate');
crowd.cheer = 0; // the ovation ends
cheer(0.8, 0.25);
assert.equal(crowd.pop, 0.25, 'once the stand has sat down, the next moment sets its own size');
crowd.cheer = crowd.pop = crowd.burst = 0;
});
console.log(`\n${passed} tests passed`);
process.exit(0); // render.js schedules viewport timers at load; don't let them outlive the report