The serve gets stakes: tight apex window, depth rides quality, the strike flick aims

Three mechanical causes of 'no serve agency', measured then fixed:
- the 0.12 m perfect snap was ~0.3 s of hang time (now TUNE.apexSnap 0.05,
  and the gold band reads the same knob);
- the fixed mid-box target depth capped every good serve at ~36 m/s in the
  loft loop — player serve depth now rides quality, flush aims 0.3 m inside
  the service line (ladder: 49 / 52 hard / 36 mistimed / 30 bad);
- the strike flick was splatter-only — in tennis and the duel it now sways
  placement (horizontal) and buys pace (vertical, serveHard + serveHardSpin,
  the extra dive IS the clearance), paid for in scatter against the real
  fault machinery. Tap stays the safe second serve.

Phone verdict pending — see the PROJECT.md session log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-13 17:23:07 +02:00
1 parent c084d53497
commit de77ccfea5
6 files changed
+112 -20

No files matched your search

+21 -1
View File
@@ -83,7 +83,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ----------------------------------------------
const { G, P, O, score, ball, setupPoint } = await import('./src/state.js');
const { update, forceZombies, splatPoint, restartRun, flickTarget, keyPressed, classicGame } = await import('./src/gameplay.js');
const { update, forceZombies, splatPoint, restartRun, flickTarget, keyPressed, classicGame, serveApexY } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN } = await import('./src/beats.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
@@ -938,6 +938,26 @@ test('classic tennis: a game is not a set, and six with a two-game lead is', ()
assert.equal(G.done, null, 'one set is not a match');
});
test('the toss strike has stakes: flush is fast, a miss is slow, the old free window is gone', () => {
// the serve-agency pass: apexSnap/apexFall are TUNE knobs and deliberately tight, so timing
// must show up in the ball's actual pace (the fault economy then prices the aimed serve)
G.mode = 'tennis'; G.classic = true; G.match = null; G.interlude = false; G.done = null;
const strike = dy => {
G.phase = 'pretoss'; ball.active = false; score.server = 'P'; P.pose = 'idle';
keyPressed('Space'); // toss
assert.equal(G.phase, 'toss', 'the tap tossed');
ball.y = serveApexY() - dy; ball.vy = 0.1; // park the ball dy below the apex
keyPressed('Space'); // strike
assert.equal(G.phase, 'rally', 'the strike fired');
return Math.hypot(ball.vx, ball.vy, ball.vz);
};
const flush = strike(0);
const oldFree = strike(0.1); // inside the old 0.12 snap, outside the new one
const missed = strike(0.4);
assert.ok(flush > missed * 1.2, `a flush strike is materially faster (${flush.toFixed(1)} vs ${missed.toFixed(1)})`);
assert.ok(oldFree < flush - 0.5, 'the 0.12 m free-perfect window no longer exists');
});
test('classic tennis: the second set ends the run, with no corpse', () => {
G.mode = 'tennis'; G.classic = true;
G.sets = [1, 0]; score.games = [6, 0]; G.runGames = 14;