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

+4 -4
View File
@@ -865,9 +865,9 @@ function drawApproachRing() {
}
// THE APEX BAND (the serve tap): a thin guide beside the tossed ball with a gold band spanning the
// perfect window (serveApexY +- the same 0.12 m keyPressed scores as perfect) and a wider white
// zone for the playable one. A tick rides the live ball height. The ghost hand MIMES this moment;
// the band makes it a target you can see coming.
// perfect window (serveApexY ± TUNE.apexSnap — the SAME number keyPressed scores as perfect, so
// tightening the mechanic tightens the picture) and a wider white zone for the playable one. A
// tick rides the live ball height. The band makes the strike a target you can see coming.
function drawApexBand() {
if (!(G.match || G.mode === 'tennis') || G.phase !== 'toss' || score.server !== 'P') return;
const gx = ball.x + 0.55; // beside the toss, out of the ball's way
@@ -878,7 +878,7 @@ function drawApexBand() {
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(255,255,255,0.35)'; // the guide
ctx.beginPath(); ctx.moveTo(x, yTop.y); ctx.lineTo(x, yBot.y); ctx.stroke();
const bTop = proj(gx, apex + 0.12, ball.z).y, bBot = proj(gx, apex - 0.12, ball.z).y;
const bTop = proj(gx, apex + TUNE.apexSnap, ball.z).y, bBot = proj(gx, apex - TUNE.apexSnap, ball.z).y;
ctx.fillStyle = hexA(UI.warm, 0.8); // the perfect window, gold
ctx.fillRect(x - 3, Math.round(bTop), 6, Math.max(2, Math.round(bBot - bTop)));
const tick = proj(gx, ball.y, ball.z).y; // where the ball is NOW