The receiver waits behind his line, and a missed serve buys a second one
Oliver, three notes; two of them are here. "when the opponent serves the player does not stand at the baseline - but that's how it should be." setupPoint's receiving marks sat over a metre INSIDE the court on both sides (22.5 near, 1.2 far) — where you stand to attack a second serve, not where you wait for a first one. They mirror the server's depth now, in the one line every serving mode shares. "when serving results in an out there should be a second service... the umpire should announce 2nd service and the player must get another chance." True in tennis mode since forever and absent everywhere else: the duel, the match and the death match had no service rule at all, so a serve long or into the net was judged as an ordinary ground stroke — a point, and in the match a point costs blood. The out branch of onBounce's interlude block routes a serve to fault() instead, so the second serve, the HUD line, the umpire and the double fault all come for free. Three things it dragged out with it: - fault()'s double-fault branch called pointTo — TENNIS scoring — which in an interlude banks a number and skips the mode that owns the court. It goes through resolveInterlude there now. Nothing had ever reached that line from a duel or a match, because those modes could not fault. - startDuel never reset G.serveNum: harmless while a duel could not fault, the next duel's death loop the moment it can. - the match puts the body back on its receiving mark for the second serve (placeReceiver, split out of matchReady, which resets the serve number). "Fault. Second service." is one voiceSeq — a second voice() call would be dropped, one man one microphone. New recording via the usual say/afconvert. The service BOX is still not enforced in the match, deliberately: this is strictly more forgiving than what shipped. The lob gesture is OPEN 14. 4 new tests, 110 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d7e66449a
commit
6e07dc74bb
7 files changed
+182
-19
No files matched your search
@@ -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, cheer } = await import('./src/gameplay.js');
|
||||
const { update, forceZombies, startGore, splatPoint, restartRun, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = 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
|
||||
@@ -668,6 +668,73 @@ test('THE DEATH LOOP: a fault before a lost point must not arm second-serve fore
|
||||
assert.equal(G.serveNum, 1, '...whoever won it');
|
||||
});
|
||||
|
||||
test('SECOND SERVICE: a missed serve in the match faults instead of losing the point', () => {
|
||||
// Oliver, 2026-08-22: "when serving results in an out there should be a second service... the
|
||||
// umpire should announce 2nd service and the player must get another chance." The duel and the
|
||||
// match had no service rule at all — the first serve was judged as an ordinary ground stroke, so
|
||||
// a serve long or into the net was a point, and in the match a point costs blood.
|
||||
startMatch();
|
||||
for (let i = 0; i < 2.6 * 60; i++) update(DT); // the walk-on beat, then his serve
|
||||
const zb = oppOf();
|
||||
const pts = score.pts.slice(), hp = G.hp;
|
||||
zb.x = 3.9; zb.z = 5.5; // it chased the fault out to the wing
|
||||
const serveOut = () => { // a serve that lands past the far baseline
|
||||
G.phase = 'rally'; G.wasServe = true;
|
||||
ball.active = true; ball.scored = false; ball.lastHit = 'P'; ball.bounces = 0;
|
||||
ball.x = 0.4; ball.z = 4; ball.y = 0.5; ball.vx = 0; ball.vy = -1; ball.vz = -20; ball.g = TUNE.gravity;
|
||||
for (let i = 0; i < 60 && ball.active; i++) update(DT);
|
||||
};
|
||||
serveOut();
|
||||
assert.equal(G.serveNum, 2, 'the first miss buys a second serve');
|
||||
assert.deepEqual(score.pts, pts, '...and costs no point');
|
||||
assert.equal(G.hp, hp, '...and no blood');
|
||||
assert.equal(G.matchFall, null, 'nobody falls over a fault');
|
||||
assert.equal(zb.z, 1.0, 'the body is back on its receiving mark for the second serve');
|
||||
assert.equal(zb.x, O.x, '...on the same mark the invisible tennis body serves against');
|
||||
serveOut(); // the double fault
|
||||
assert.equal(score.pts[1], pts[1] + 1, 'two misses is the point, theirs');
|
||||
assert.equal(G.hp, hp - 1, '...and the match scores it in blood, like every other lost point');
|
||||
assert.equal(G.serveNum, 1, 'the next point starts on a first serve');
|
||||
});
|
||||
|
||||
test('SECOND SERVICE: a serve that lands IN is still played, fault or not', () => {
|
||||
// The guard is on the OUT branch only — the mode's other rules (the early-decided winner, the
|
||||
// second bounce) must not learn about serves.
|
||||
startMatch();
|
||||
for (let i = 0; i < 2.6 * 60; i++) update(DT);
|
||||
const pts = score.pts.slice();
|
||||
G.phase = 'rally'; G.wasServe = true;
|
||||
ball.active = true; ball.scored = false; ball.lastHit = 'P'; ball.bounces = 0;
|
||||
ball.x = 0.4; ball.z = 8; ball.y = 0.5; ball.vx = 0; ball.vy = -1; ball.vz = -8; ball.g = TUNE.gravity;
|
||||
for (let i = 0; i < 30 && ball.bounces < 1; i++) update(DT);
|
||||
assert.equal(ball.bounces, 1, 'setup: it bounced');
|
||||
assert.ok(inCourt(ball.x, ball.z), '...in court');
|
||||
assert.equal(G.serveNum, 1, 'a legal serve is no fault');
|
||||
assert.deepEqual(score.pts, pts, 'and no point either — it is a rally now');
|
||||
});
|
||||
|
||||
test('a duel opens on a FIRST serve, whatever the last one ended on', () => {
|
||||
// Same shape as the match's death loop, one mode over: nothing in startDuel reset G.serveNum, and
|
||||
// now that a duel can fault, a fault in one duel would have armed second-serve in the next.
|
||||
restartRun();
|
||||
G.mode = 'splatter';
|
||||
G.serveNum = 2;
|
||||
startDuel();
|
||||
assert.equal(G.serveNum, 1, 'the duel is served for the first time');
|
||||
});
|
||||
|
||||
test('a receiver stands BEHIND his baseline, on both sides of the net', () => {
|
||||
// Oliver: "when the opponent serves the player does not stand at the baseline — but that's how it
|
||||
// should be." Both marks used to sit over a metre inside the court.
|
||||
restartRun();
|
||||
score.server = 'O'; setupPoint();
|
||||
assert.ok(P.z > COURT.L, 'receiving, the player waits behind his own baseline: z=' + P.z);
|
||||
assert.ok(O.z < -0.2, '...and the server is behind his: z=' + O.z);
|
||||
score.server = 'P'; setupPoint();
|
||||
assert.ok(O.z < 0, 'and mirrored, the far receiver is behind the far baseline: z=' + O.z);
|
||||
assert.ok(P.z > COURT.L, '...with the server behind his own line');
|
||||
});
|
||||
|
||||
test('the match brings its own legs: auto-run works even where autoAssist is 0', () => {
|
||||
// The desktop pane (pointer: fine) has autoAssist 0, so the match played as a statue with a
|
||||
// racket — the flight recorder showed a swing in every lost point and an x that never moved.
|
||||
|
||||
Reference in new issue
Block a user