The dead club doubles its staff, and the draw is made where you can see it

Six new opponent families (head coach, stringer, strawberry seller, chair
umpire, spectator, roller man) through the famroll/zsheet route, placed by
hierarchy into an 18-rung ladder; the champion steps up behind them.

THE DRAW: picking the tournament now rolls its own bracket in front of you —
only the final was ever settled — and what lands is what walks on.

Netplay, off Oliver's cellular report: the mid-flight early-decide was missing
its !G.net gate, so the striker judged its own outbound flight and the two
scoreboards drifted a point apart into a court nobody could serve on. The
host's court now rides the room, its scoreboard rides the pretoss stream, and
a called ball rolls to its own halt instead of freezing where the call caught it.

Plus the night court, and somebody in the stand is taking pictures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-15 11:49:19 +02:00
1 parent 4c47295a73
commit 9cc3ac54b3
207 files changed
+632 -81

No files matched your search

+97 -9
View File
@@ -82,10 +82,11 @@ globalThis.ResizeObserver = class { observe() {} disconnect() {} };
globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ----------------------------------------------
const { G, P, O, score, ball, setupPoint } = await import('./src/state.js');
const state = await import('./src/state.js');
const { G, P, O, score, ball, setupPoint } = state;
const { update, forceZombies, splatPoint, restartRun, flickTarget, keyPressed, classicGame, serveApexY, assistK, rallyFeel } = 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 } = await import('./src/net.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
// player would, so the tests step a live court
const startDrill = (...a) => { const r = _startDrill(...a); G.modal = null; G.modalQ.length = 0; return r; };
@@ -695,9 +696,11 @@ test('the opponent card is a RAMP: it starts easy and only gets harder', () => {
// "the players should start easy and get harder over time." That is a property of the table, not
// of any code, so the table is what gets asserted — a row edited into the wrong slot is otherwise
// invisible until someone plays ten opponents deep.
const { matchFoe, TUNE } = cfg;
const { matchFoe, TUNE, LADDER } = cfg;
let lastSpeed = 0, lastWhiff = 1e9;
for (let n = 1; n <= 12; n++) { // twelve rungs since 2026-08-13 (champion caps 12)
// read off the table rather than pinned: the ladder went 12 -> 18 on 2026-08-15 and a hardcoded
// count here would simply have stopped testing the six new rungs without failing
for (let n = 1; n <= LADDER; n++) {
const f = matchFoe(n);
const speed = (TUNE.matchSpeedBase + (n - 1) * TUNE.matchSpeedPer) * (f.speed ?? 1);
const whiff = TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, n - 1) * (f.flub ?? 1);
@@ -713,11 +716,22 @@ test('the opponent card is a RAMP: it starts easy and only gets harder', () => {
assert.ok(f.short.length <= 8, `${f.short} fits the board's 8-char slot`);
}
// ...and the opening must actually be gentle: opponent one is the softest thing on the card.
assert.ok((matchFoe(1).speed ?? 1) < (matchFoe(12).speed ?? 1) * 0.6, 'the shambler is no champion');
// the four 2026-08-13 families wear their own bodies, and the big two carry real stature
for (const [n, fam] of [[5, 'ma'], [8, 'ph'], [11, 'dr'], [12, 'ch']])
assert.ok((matchFoe(1).speed ?? 1) < (matchFoe(LADDER).speed ?? 1) * 0.6, 'the shambler is no champion');
// every rung that has its own family wears it — the 2026-08-15 six included, whose whole point is
// that the rung and the thing that hunts you in the gore mode are the same person
for (const [n, fam] of [[3, 'sp'], [5, 'rm'], [7, 'ma'], [9, 'sb'], [10, 'sg'], [12, 'cu'],
[13, 'ph'], [14, 'hc'], [17, 'dr'], [18, 'ch']])
assert.equal(matchFoe(n).fam, fam, `rung ${n} wears its own family`);
assert.ok(matchFoe(12).h > 1.85, 'the champion stands taller than a stock body');
// ...and every named family is one the preloader actually loads
for (let n = 1; n <= LADDER; n++)
assert.ok(cfg.ZFAMS.includes(matchFoe(n).fam), `rung ${n}'s family is registered in ZFAMS`);
assert.ok(matchFoe(LADDER).h > 1.85, 'the champion stands taller than a stock body');
// the cup's final draws the top rung and nothing else: a band pointing past the ladder would
// wrap through matchFoe's modulo and quietly deal the shambler as THE CHAMPION
const fin = cfg.CUP_ROUNDS[cfg.CUP_ROUNDS.length - 1];
assert.deepEqual(fin.band, [LADDER], 'the final is the top of the ladder');
for (const r of cfg.CUP_ROUNDS) for (const rung of r.band)
assert.ok(rung >= 1 && rung <= LADDER, `${r.name} draws a rung that exists (${rung})`);
});
test('the first opponent cannot paint lines: its returns are width-clamped', () => {
@@ -863,6 +877,28 @@ test('THE TOURNAMENT: bands draw, bodies deepen, rounds hold their body, the cha
assert.equal(G.match.felled, CUP_ROUNDS.length, 'four bodies felled, banked as a match run');
});
test('THE DRAW: a bracket settled on screen is the bracket that is played', () => {
// The screen would be worse than nothing if it dealt a different tournament from the one it just
// showed you settling (main.buildBracket hands its rungs straight to startMatch). The guard is
// the other half: a rung that is not in the round's band falls back to an honest roll rather
// than dealing THE CHAMPION in round one.
const { CUP_ROUNDS, LADDER } = cfg;
const drawn = CUP_ROUNDS.map(r => r.band[r.band.length - 1]);
_startMatch(true, drawn); G.modal = null; G.modalQ.length = 0;
assert.equal(G.match.opp, drawn[0], 'round one walks on the body the screen landed on');
assert.deepEqual(G.match.cup.draw, drawn, 'and the rest of the draw rides with the run');
endMatch();
// a nonsense draw is refused per round, not trusted
_startMatch(true, [LADDER, LADDER, LADDER, LADDER]); G.modal = null; G.modalQ.length = 0;
assert.ok(CUP_ROUNDS[0].band.includes(G.match.opp),
'a rung round one cannot contain is rolled instead of dealt');
endMatch();
// ...and no draw at all is the old behaviour, which the #cup back door still uses
_startMatch(true); G.modal = null; G.modalQ.length = 0;
assert.ok(CUP_ROUNDS[0].band.includes(G.match.opp), 'no draw: the round rolls its own');
endMatch();
});
test('cup rallies never strand the latch: no-crossing points, the fall, and the mid-round game', () => {
// Oliver's second lock: in the quarter-final, matchFallWillPlay still divided by 4 while
// matchPoint divided by 4*need — it promised a fall on a point that crossed nothing, the
@@ -1268,8 +1304,60 @@ test('netplay: a serve fault is the receiver\'s call, and re-arms the server\'s
G.serveNum = 1; G.phase = 'rally'; ball.active = true; ball.lastHit = 'P'; G.wasServe = true;
netMsg({ t: 'fault' });
assert.equal(G.serveNum, 2, 'their call re-armed the second serve');
assert.ok(!ball.active, 'the dead serve is off the court');
// THE CALLED BALL PLAYS OUT (2026-08-15). It used to be switched off where it stood — which on
// the striker's phone is a whole court and a latency away from the call, and read as the game
// loop stopping. It is `scored` (nothing it does can move the scoreboard again) and still LIVE.
assert.ok(ball.scored, 'the call is made: this flight can never score again');
assert.ok(ball.active, '...but the ball keeps rolling instead of freezing where the call caught it');
assert.ok(!netTest.sent.some(m => m.t === 'fault'), 'applying a fault does not echo one back');
const pts = score.pts.slice(), sn = G.serveNum;
for (let i = 0; i < 600 && ball.active; i++) update(DT);
assert.ok(!ball.active, 'and it comes to a natural halt by itself');
assert.deepEqual(score.pts, pts, 'the roll scored nothing on the way');
assert.equal(G.serveNum, sn, '...and faulted nothing either');
});
test('netplay: the host\'s court rides the room, so both phones play the same surface', () => {
const { court, selectCourt } = state;
selectCourt('grass');
freshNet('B'); // ...and the start message says otherwise
netMsg({ t: 'start', side: 'B', court: 'clay' });
assert.equal(state.court.id, 'clay', 'the guest is moved onto the host\'s court');
assert.equal(TUNE.bounceXZ, cfg.COURTS.find(c => c.id === 'clay').bounceXZ,
'...and onto its physics, not just its picture');
netMsg({ t: 'start', side: 'B' }); // an old build sends no court: keep what we have
assert.equal(state.court.id, 'clay', 'a start without a court changes nothing');
selectCourt('grass');
});
test('netplay: the scoreboards reconcile, so one lost verdict cannot lock the court', () => {
// THE BUG (Oliver, over cellular): "both players stand on the same side and nobody can serve."
// setupPoint reads score.pts for the deuce/ad side and score.server for who tosses, so two sims
// a point apart place both players on the same half and each waits for the other. The host's
// pretoss stream carries its scoreboard; the guest adopts it, mirrored.
freshNet('B'); // we are the guest: the host is the authority
score.pts = [2, 1]; score.server = 'P'; // ...and we have drifted a point away from it
const myX = P.x;
netMsg({ t: 'pos', x: 0, z: 1, m: 0, srv: 'P', p: [2, 3] }); // their P serves, their 2 to our 3
assert.equal(score.server, 'O', 'the server is taken from the host, mirrored');
assert.deepEqual(score.pts, [3, 2], '...and so are the points');
assert.notEqual(P.x, myX, 'the point is re-armed, so both bodies stand where the new score puts them');
// an ordinary position frame carries no scoreboard and must never touch one
score.pts = [1, 1];
netMsg({ t: 'pos', x: 0, z: 1, m: 0 });
assert.deepEqual(score.pts, [1, 1], 'a plain pos frame is picture only');
// ...and a live rally is never reconciled underneath itself
G.phase = 'rally';
netMsg({ t: 'pos', x: 0, z: 1, m: 0, srv: 'P', p: [3, 3] });
assert.deepEqual(score.pts, [1, 1], 'mid-rally the scoreboard is left alone');
// only the HOST publishes it: two authorities is the drift, not the cure
freshNet('B'); netTest.sent = []; G.phase = 'pretoss';
netTick(0.2);
assert.ok(netTest.sent.some(m => m.t === 'pos'), 'the guest still streams its position');
assert.ok(!netTest.sent.some(m => m.srv), '...but never a scoreboard');
freshNet('A'); netTest.sent = []; G.phase = 'pretoss';
netTick(0.2);
assert.ok(netTest.sent.some(m => m.t === 'pos' && m.srv), 'the host publishes its scoreboard');
});
// ---- NETPLAY DEATH MATCH: the host's pick rides the room's start message, and the match