Bring the swing log to every rally mode, and time the gold ring

Oliver wants to hand over a log of why he missed balls. It existed behind
a scoreboard tap but was gated on G.mode === 'tennis', so the Climb, the
cup and training (which run as 'splatter') logged nothing. It now writes
in every rally mode: hits with shot, tap age and any quality cap, misses
with the new let-it-bounce reason, and a WON/LOST line naming why each
point went. Tapping the open log copies all 200 entries with a header.

Its first catch: the gold ring went gold at a distance, which is too
early for the slow balls steps 1-3 brought in. It is now timed off the
ball's real path to his reach (against where his run is taking him):
gold means touch now. ringTime 0 restores the old ring. v365.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-14 11:41:05 +02:00
1 parent 180288ef8f
commit d110e12321
8 files changed
+225 -41

No files matched your search

+46 -6
View File
@@ -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 { predictLanding, solveShot, update, forceZombies, startGore, splatPoint, restartRun, leaguePace, duelSkill, tinLadder, deliverTin, peerK: _peerK, legLevel: _legLevel, winLevel: _winLevel, assistK: _assistK, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = await import('./src/gameplay.js');
const { ringCue, predictLanding, solveShot, update, forceZombies, startGore, splatPoint, restartRun, leaguePace, duelSkill, tinLadder, deliverTin, peerK: _peerK, legLevel: _legLevel, winLevel: _winLevel, assistK: _assistK, 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, netAgain } = await import('./src/net.js');
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
@@ -1160,8 +1160,7 @@ test('a TAP is timed from touch-down: a normal 120 ms tap on the gold ring still
if (G.phase === 'pretoss' && !(G.interlude && G.interlude.introT > 0)) { if (i % 40 === 0) keyPressed('Space'); }
else if (G.phase === 'toss') { if (ball.vy < 0.4 && ball.y > 2.4) keyPressed('Space'); }
else if (G.phase === 'rally' && ball.active && ball.lastHit === 'O' && ball.vz > 0 && !armed) {
const d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8);
if (Math.abs(d - TUNE.matchRingSweet) < TUNE.matchRingBand) { armed = true; incoming++; fire = i + LAG; }
if (ringCue()?.gold) { armed = true; incoming++; fire = i + LAG; } // the finger LANDS on gold and lifts LAG later
}
if (i === fire) keyPressed('Space', LAG / 60); // the lift: the touch's age rides along
const before = ball.lastHit;
@@ -1326,16 +1325,19 @@ test('a NET RUSHER serves and volleys: the serve is his approach and he sprints
score.server = 'O'; setupPoint();
Object.assign(zb, { x: 1.3, z: -0.5, serveHold: 0 });
// the serve's flight plus the return's: ~1.1 s — what a real serve-and-volleyer has to close in
let served = -1, z0 = null, appr = null, zMax = -99;
let served = -1, z0 = null, appr = null, zMax = -99, rushed = false, sprint = 0;
for (let i = 0; i < 4 * 60 && !(served >= 0 && i - served > 66); i++) {
G.modal = null; G.modalQ.length = 0;
const zPrev = zb.z;
update(DT);
if (served < 0 && ball.active && ball.lastHit === 'O' && G.phase === 'rally') { served = i; z0 = zb.z; appr = ball.approach; }
if (served >= 0) zMax = Math.max(zMax, zb.z);
if (served >= 0) { zMax = Math.max(zMax, zb.z); rushed ||= !!zb.rushing; sprint = Math.max(sprint, (zb.z - zPrev) / DT); }
}
assert.ok(served >= 0, 'he served');
assert.equal(appr, 'O', 'the serve is an approach');
assert.ok(zMax > z0 + 2, `he charged the net behind it: ${z0.toFixed(1)} -> ${zMax.toFixed(1)}`);
assert.ok(rushed && zMax > z0 + 1.2, `he charged the net behind it: ${z0.toFixed(1)} -> ${zMax.toFixed(1)}`);
// ...at a SPRINT, not the half-pace walk an ordinary recovery uses
assert.ok(sprint > 0.8 * cfg.TUNE.racketSpeed * 1.25, 'he sprints in: ' + sprint.toFixed(2) + ' m/s');
});
test('LOBBED at the net, the player backpedals too, and only blocks it back', () => {
@@ -1353,6 +1355,44 @@ test('LOBBED at the net, the player backpedals too, and only blocks it back', ()
} finally { TUNE.autoAssist = assist; }
});
test('THE SWING LOG writes in the match too: a whiff, a hit with its tap, and why the point went', () => {
// Oliver, 2026-10-14: "bitte bau nochmal die Funktion ein die protokolliert warum ich einen Ball nicht
// bekommen habe". It was gated on G.mode === 'tennis' and the match runs as 'splatter' — the Climb
// and the cup logged nothing at all.
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
try {
startMatch(); G.interlude.introT = 0;
for (let i = 0; i < 2.2 * 60; i++) update(DT);
G.missLog.length = 0;
const zb = oppOf(); zb.swingCool = 999;
const incoming = () => {
G.phase = 'rally'; G.wasServe = false; G.swung = false; G.rallyNear = null;
const v = solveShot(0, 1, 2, 0.5, 21, 20);
Object.assign(ball, { active: true, scored: false, lastHit: 'O', bounces: 0, rx: null, spin: 1, soft: false,
x: 0, y: 1, z: 2, vx: v.vx, vy: v.vy, vz: v.vz, g: TUNE.gravity });
};
incoming();
keyPressed('Space', 0.1); // far too early: the ball has barely left
for (let i = 0; i < 30; i++) update(DT);
const miss = G.missLog.find(e => e.result === 'MISS');
assert.ok(miss && /tap too early/.test(miss.reason) && /tap 100ms/.test(miss.reason), 'the whiff is explained: ' + (miss && miss.reason));
for (let i = 0; i < 4 * 60 && G.phase === 'rally'; i++) update(DT);
const lost = G.missLog.find(e => e.result === 'LOST');
assert.ok(lost, 'the point it cost is logged: ' + JSON.stringify(G.missLog.map(e => e.result)));
// a clean hit carries its shot and its tap
for (let i = 0; i < 4 * 60 && G.phase !== 'pretoss'; i++) update(DT);
incoming(); G.missLog.length = 0;
let lift = -1;
for (let i = 0; i < 3 * 60 && ball.lastHit === 'O'; i++) {
if (lift < 0 && ringCue()?.gold) lift = i + 6; // the finger lands on gold...
if (i === lift) keyPressed('Space', 0.1); // ...and lifts 100 ms later
update(DT);
}
const hit = G.missLog.find(e => e.result === 'HIT');
assert.ok(hit && /^q=/.test(hit.reason) && /tap 100ms/.test(hit.reason), 'the hit says how it was struck: ' + (hit && hit.reason));
} finally { TUNE.autoAssist = assist; }
});
test('rally depth: deep by default, mistiming lands it SHORTER, never longer', () => {
// the only out a rally ball may find is wide — depth must never put one past the baseline
startMatch(); G.interlude.introT = 0;