SERVE UP was dropping you into gore whatever you picked

THE MENU BUG, and it explains three reports at once. cStart's handler was
`addEventListener('click', start)` — and addEventListener hands a listener the
MouseEvent as its first argument, so `start(modeId = pickedMode)` never saw its
default: modeId was an EVENT OBJECT. `G.classic = modeId === 'classic'` was
therefore always false and the zombie branch never fired. Picking CLASSIC or
ZOMBIE TENNIS on the setup screen did nothing at all; SERVE UP always started
the gore mode. A default parameter only applies to `undefined`, and a click
handler always passes something. `() => start()`.

That is also why "in normal tennis i dont see the ball circles and the serve
hint bar": there was no normal tennis to be in. Both are drawn unconditionally
in render() and the tennis struggle hints are already mode-gated correctly.
Verified through the real menu after the fix: CLASSIC gives mode=tennis with
no bodies on court, ZOMBIE gives match=true against the SHAMBLER in its own
body. Every other bare handler in the codebase takes no parameters — checked.

THE MOP BOY had the same class of bug one layer down. sendBoy was called and
mopWorthIt() refused: it tests `d.z >= COURT.netZ - 2`, the NEAR half, because
in the gore mode everything dies on the near court. A match's blood is all on
the FAR side, where the body stands and bleeds through four wound stages, so
the boy could never qualify. The near-side bias stays; the match is the
exception.

DANIEL was being dropped, not missing. Two causes, both mine: the asides were
queued at 0.7-1.5 s and matchPoint fires the score call at 0.4 s with a clip
over a second long — voice() drops anything that would start while he is still
talking (one man, one microphone). And asideEvery is 55 s, a whole match game.
Delays moved past the score call; matchAsideEvery is 15 s, which fits a mode
whose events are a limb and your blood rather than a wave of bodies.

THE FALL: quicker down, longer on the floor (0.09/0.11/2.0/0.30). And it was
genuinely SHRUNKEN — the generator draws fall rows smaller than standing rows
(the brute's buckle came back 190 px against a 224 px stance) and _zsheet's one
global scale preserved that faithfully, so the body shrank the moment it
started to fall. A fall row is anchored on its own buckle now, which must draw
at standing height. All seven generated families re-cut: 224/153/98/181 against
the hand-made racket family's 224/154/85/179 — the convergence is the check.

THE FLICK: 0.42 made going out impossible, which fixed the 8-in-10 bug and
overshot it ("too easy. i must be able to play out"). 0.62 puts risk back where
it belongs — ~15% out at quality 0, 5% at 0.75, and never on a perfect flick.
The test now asserts that shape: bounded, monotonically falling, zero at 1.

59 tests green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 20:07:02 +02:00
1 parent 52b38e4a39
commit e1ce2486c0
93 files changed
+70 -23

No files matched your search

+19 -2
View File
@@ -96,11 +96,28 @@ def cut(sheet, fam, outdir):
px = big[big[..., 3] > 200][:, :3]
pal = np.unique((px // 8) * 8, axis=0)
# A FALL ROW IS ANCHORED ON ITS OWN BUCKLE, not on the sheet. The generator draws the fall rows
# noticeably smaller than the standing rows — the brute's buckle came back at 190 px against a
# 224 px stance — and one global scale preserves that faithfully, so the body SHRANK the moment
# it started to fall ("the fall animation seems shrunken"). The first frame of a fall is a body
# still on its feet, so it must draw at standing height; the other three are scaled by the same
# factor, which keeps the collapse's own proportions intact. A row that was already right (the
# racket family's, at 224/224) gets a factor of 1.0 and is untouched — which is the check that
# this rule is the right one rather than a fudge for one bad sheet.
mult = [1.0] * len(crops)
i = 0
for row in want:
if row[0].endswith('fall1') and crops[i].shape[0]:
k_row = crops[0].shape[0] / crops[i].shape[0]
for j in range(i, i + len(row)):
mult[j] = k_row
i += len(row)
os.makedirs(outdir, exist_ok=True)
for c, name in zip(crops, flat):
for c, name, m in zip(crops, flat, mult):
h, w = c.shape[:2]
snap = Image.fromarray(c, 'RGBA').resize((max(1, round(w / b)), max(1, round(h / b))), Image.BOX)
im = snap.resize((max(1, round(snap.width * k)), max(1, round(snap.height * k))), Image.BOX)
im = snap.resize((max(1, round(snap.width * k * m)), max(1, round(snap.height * k * m))), Image.BOX)
arr = np.array(im).astype(int)
solid = arr[..., 3] > 110
f = arr[..., :3].reshape(-1, 3)