Fix the serve deadlock, give the mop boy a job, align net and umpire

The freeze first: G.mgT only counted down inside `G.phase === 'rally'`, so
anything that moved the phase mid-burst — a scheduled duel calling setupPoint —
stranded it above zero forever. mgT > 0 is one of keyPressed's early returns, so
no tap could serve again, and the yellow shape over the player's head was
drawMgRacket's muzzle flash still being drawn. Fixed at the root (the clock ticks
in every phase, only the pellets are gated on rally) and semantically: starting a
duel cancels any burst, because the duel is tennis.

The mop boy now does an actual round: walk to the biggest nearby stain, stand on
it and push the mop while the cycle animates in place, scrub it out, move to the
next, then leave. Gore fades ~3.5x faster while he is stopped and working than
when walking past, so the stain dies under him instead of fading as he drifts by.
boyMaxJobs caps the round so a kill streak can't employ him forever.

Applause: the clip is 4.16s and always plays in full, but every cheer() passed a
shorter beat, so the stand sat down while it was still audible. Floored at the
clip length. New, much bolder clap frame — the re-roll came back brighter and
bluer, which would have flashed the whole stand, so it is colour-matched to frame
one per channel on mean and sd before quantising.

Net and umpire were both wrong since forever: netW was the singles-stick width
(10.06) against a court drawn to the doubles lines, so the net stopped 0.46m
short each side; the chair's near legs stood 0.45m inside the sideline. Both now
sit exactly on the doubles line.

26-game soak clean, no stalls. Serve returns 134/150 = 89.3% +/-2.5% with the
wider net (the bar is inside one sigma), 30 human-tapped points = 50 hits /
0 whiffs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-27 18:09:18 +03:00
1 parent 32401366fe
commit 55ddf5932e
8 files changed
+108 -23

No files matched your search

+29
View File
@@ -182,6 +182,35 @@ Oliver rejected the procedural fakes on sight. The rule this round: **animation
- Editing a full-scene image (`x_crowd` → `x_crowd2`) preserved the layout far better than expected:
mean difference 3.2/255, railing and tier rows unmoved. Worth reusing for other 2-frame swaps.
## Third feedback round (2026-07-27)
- **THE FREEZE (serving dead, gun racket stuck over your head).** `G.mgT` only counted down inside
`if (G.mgT > 0 && G.phase === 'rally')`. Anything that moved the phase mid-burst — a scheduled
duel calling `setupPoint()` — stranded `mgT` above zero *forever*, and `mgT > 0` is one of
`keyPressed`'s early returns, so no tap could ever serve again. The yellow blob Oliver saw was
`drawMgRacket`'s muzzle flash still being drawn. Fixed at the root (the clock ticks in every
phase; only the pellets are gated on 'rally') **and** semantically (`startDuel` cancels any
burst — the duel is tennis). If another "can't serve" report ever comes in, check these flags
first: `G.mgT`, `G.horde`, `G.changeover`, `G.interlude.introT` — each one blocks `keyPressed`.
- **The mop boy has a job now** (`mopRound` in gameplay.js): walk to the biggest nearby stain,
stand on it for `TUNE.boyMopDwell` while the push cycle animates in place, scrub it out, pick the
next, then walk off. `TUNE.boyMaxJobs` caps the round so a kill streak can't employ him forever.
Gore fades ~3.5× faster while he is actually stopped and mopping than when merely walking past.
A straight-line constant-speed crossing was what read as moonwalking past the mess.
- **Applause is floored at the audio length.** `assets/audio/tennis-applaus.wav` is 4.16s and always
plays out in full, but every `cheer()` call passed a shorter beat, so the stand sat down while the
clap was still audible. `cheer()` now does `Math.max(crowd.cheer, seconds, APPLAUSE_T)`.
- **Regenerating a crowd frame: colour-match it or don't ship it.** The bolder re-roll came back
much brighter and bluer; swapping to it would have flashed the whole stand. Fixed by a per-channel
linear map onto frame one's mean and sd (means now agree to within half a level), then quantised
to 64 colours. Verify with a mean-difference number before trusting any regenerated frame.
- **Net and umpire were both wrong since forever.** `COURT.netW` was the singles-stick width 10.06
while the court is drawn to the doubles lines, so the net stopped 0.46 m short each side — now
10.97, flush. `UMP_X` 5.6 put the chair's near legs 0.45 m *inside* the doubles line (x_ump is
42×112 px at UMP_H 3.0 = 1.12 m wide as drawn); 6.05 stands them exactly on the line. Do not push
past ~6.3 or the chair clips the right edge of the portrait viewport.
The wider net is a real physics change (balls between |x| 5.03 and 5.485 now catch the tape);
serve returns measured 134/150 = 89.3% ±2.5% afterwards, bar still inside one sigma.
## Gotchas learned the hard way
- Browser-pane verification: python http.server sends no cache headers, so edited ES
modules often load STALE even after location.reload(). Reliable fix: preview_stop +