Give the friend's racket a sound and the duel a rematch
Two netplay-only finds from Oliver's two-phone session. THE FRIEND'S STRIKE was silent: every other opponent is simulated locally and cracks through gameplay's shared return path, but the friend's swing runs on their phone alone and arrives as a 'shot' message that wrote the ball and nothing else. netApplyShot now plays the same sample at the same volume, pitched off the pace that was actually sent (strikeRate, read after the ball is written). Both netplay modes, not just the death match. THE REMATCH: the summary's only netplay door was THE CLUBHOUSE, so playing the same friend again meant two trips through the lobby and a fresh code. The relay cannot re-pair, so the rematch is the socket that is still open — one 'again' frame and both phones reset the run under the decisions the room already carries (same side, same mode, same court, host serves). netAgain keeps G.net across restartRun(true) and replays netFresh, netStart's own tail split out so pairing and rematch cannot drift into two courts. Applied, never echoed; a 2 s window swallows the second frame when both tap at once; a dead line refuses and falls through to the clubhouse. Also: the dev server runs with SPLAT_AUTH= (server.py always said local dev should), which the shared password had been 401-ing. audio.sfxTest is a new test seam in netTest's shape. 146 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b53fc07dd1
commit
78946015b4
9 files changed
+189
-17
No files matched your search
+16
-8
@@ -3088,12 +3088,18 @@ export function render() {
|
||||
: G.classic ? 'LONGEST MATCH ' + G.best.games + ' GAMES'
|
||||
: 'BEST ' + G.best.games + ' GAMES · ' + G.best.sets + ' SETS')
|
||||
: null;
|
||||
// THE FRIEND IS STILL ON THE LINE — so the duel has a door back into itself (the REMATCH,
|
||||
// 2026-09-11). Netplay used to end on THE CLUBHOUSE alone, which meant a fresh room code and
|
||||
// a second trip through the lobby to play the same person again.
|
||||
const online = !promo && !!(G.net && G.net.ready && !G.net.dead);
|
||||
const fxLines = ready ? wrapText(promo ? 'THE CLUB WANTS A WORD'
|
||||
: online ? 'YOUR FRIEND IS STILL ON THE LINE'
|
||||
: G.net ? 'THE CLUB IS WAITING' : fx.line, F(8, 0), inner) : [];
|
||||
let h = pad + headLines.length * 24 + lineLines.length * 13 + 6;
|
||||
if (xd.xp > 0) h += MODAL_GAP + 32;
|
||||
if (G.newBest || mark) h += MODAL_GAP + (G.newBest ? 20 : 13);
|
||||
const twoUp = ready && !promo && !G.net; // is there a second door to draw?
|
||||
const solo = !promo && !G.net;
|
||||
const twoUp = ready && (solo || online); // is there a second door to draw?
|
||||
if (ready) h += MODAL_GAP + 16 + fxLines.length * 12 + MODAL_FOOT + MODAL_BTN
|
||||
+ (stack && twoUp ? MODAL_BTN + 8 : 0);
|
||||
h += pad;
|
||||
@@ -3151,7 +3157,8 @@ export function render() {
|
||||
ctx.font = F(7, 0);
|
||||
shadowText('NEXT', inX, y + 6, UI.dim);
|
||||
ctx.font = F(9, 700);
|
||||
shadowText(promo ? 'THE PROMOTION' : G.net ? 'BACK TO THE CLUBHOUSE' : fx.title,
|
||||
shadowText(promo ? 'THE PROMOTION' : online ? 'ANOTHER DUEL'
|
||||
: G.net ? 'BACK TO THE CLUBHOUSE' : fx.title,
|
||||
inX + 34, y + 6, UI.warm);
|
||||
y += 16;
|
||||
ctx.font = F(8, 0);
|
||||
@@ -3159,9 +3166,8 @@ export function render() {
|
||||
y += fxLines.length * 12 + MODAL_FOOT;
|
||||
// one button when the ceremony owns the tap, two when there is a real choice: the fixture
|
||||
// (the canonical path) and the way back to what you were just doing
|
||||
// NETPLAY ends on one door only: the other phone's socket is still being torn down, and
|
||||
// "enter the draw" would deal a solo cup into a live session.
|
||||
const solo = !promo && !G.net;
|
||||
// NETPLAY never offers the cup — "enter the draw" would deal a solo tournament into a live
|
||||
// session. Its own two doors are the rematch and the way out (see `online` above).
|
||||
// THE SECOND DOOR NAMES ITS MODE (2026-08-31, Oliver: "auf dem gleichen screen steht dann
|
||||
// PLAY AGAIN — wo ist der unterschied?"). Beside ENTER THE CUP an unqualified "again" is
|
||||
// an instruction without a subject: both read as "start something", and which something
|
||||
@@ -3171,13 +3177,15 @@ export function render() {
|
||||
: G.match && G.match.train ? 'TRAIN AGAIN'
|
||||
: G.match ? 'CLIMB AGAIN' : 'GORE AGAIN';
|
||||
const second = solo ? (cup || (G.done && G.done.cup) ? ['club', 'THE CLUBHOUSE']
|
||||
: ['again', againLabel]) : null;
|
||||
: ['again', againLabel])
|
||||
: online ? ['club', 'THE CLUBHOUSE'] : null;
|
||||
const bw = second && !stack ? Math.round((inner - 8) / 2) : inner;
|
||||
// "THE DRAW" was tennis for the field of entrants — and nothing is drawn any more since the
|
||||
// cup went deterministic (2026-08-29). The button names what it opens instead.
|
||||
const label = promo ? 'TAKE THE FLOOR' : solo ? 'ENTER THE CUP' : 'THE CLUBHOUSE';
|
||||
const label = promo ? 'TAKE THE FLOOR' : solo ? 'ENTER THE CUP'
|
||||
: online ? 'REMATCH' : 'THE CLUBHOUSE';
|
||||
drawButton(inX, y, bw, MODAL_BTN, label, UI.pick, true);
|
||||
endBtns.push({ id: solo ? 'next' : 'club', x: inX, y, w: bw, h: MODAL_BTN });
|
||||
endBtns.push({ id: solo ? 'next' : online ? 'rematch' : 'club', x: inX, y, w: bw, h: MODAL_BTN });
|
||||
if (second) {
|
||||
const sx = stack ? inX : inX + bw + 8, sy = stack ? y + MODAL_BTN + 8 : y;
|
||||
drawButton(sx, sy, bw, MODAL_BTN, second[1], UI.warm, false);
|
||||
|
||||
Reference in new issue
Block a user