Time flicks from touch-down, log the opponent's shots, save early half-volleys

From Oliver's first swing log (cup vs the Brute): 13 of 22 hits were
perfect, 9 badly early or late — mostly forward-right flicks — and the
weak balls became seven "passed you". Every hit was a flick, and flicks
still swung only at the flickMin crossing while the timed ring says "touch
now": they are now timed from touch-down too (capped at flickLead 0.1 s).

The log gains its other half: a THEM line per opponent shot (kind, pace,
where it lands relative to you, coming in) and "never swung" judged per
incoming ball instead of once a point. And an early swing on a low ball
digs it out as a capped half-volley instead of whiffing. v366.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-14 11:48:51 +02:00
1 parent d110e12321
commit 5bbde720ad
7 files changed
+66 -9

No files matched your search

+27 -5
View File
@@ -366,6 +366,20 @@ function doHit(ch, quality, aim) {
ball.approach = rallyFeel() && ball.rx && ball.rx.short && !(ball.from < TUNE.netFrom) ? ch.side : null;
ball.from = Math.abs(ch.z - COURT.netZ);
ball.lastHit = ch.side; ball.bounces = 0; ball.rx = null; // a new shot: the receiver plans it afresh
// THE SWING LOG's other half: what came at you. A LOST "passed you" line is unreadable without it —
// was that ball reachable at all? And "never swung" is judged per incoming ball, not once a point.
if (ch.side !== 'P' && swingLog()) {
G.swung = false; G.rallyNear = null;
const land = predictLanding(), mark = !land ? '' : (() => {
const dx = land.x - P.x, inside = r2(COURT.L - land.z);
const out = Math.abs(land.x) > COURT.W / 2 || land.z > COURT.L || land.z < COURT.netZ;
return ` · lands ${r2(Math.abs(dx))}m ${dx < 0 ? 'left' : 'right'} of you, ${inside}m inside your line${out ? ' (OUT)' : ''}`;
})();
const kind = soft === null ? (aim && aim.az < 0 ? 'deep' : 'drive') + (ball.spin > 1 ? ' topspin' : ball.spin < 1 ? ' slice' : '')
: soft === 0 ? 'drop' : soft >= 1 ? 'lob' : 'moonball';
logSwing({ result: 'THEM', reason: `${kind} q=${r2(quality)} · ${Math.round(Math.hypot(ball.vx, ball.vz))}m/s${mark}`
+ (ball.approach === ch.side ? ' · coming in' : '') });
}
G.wasServe = false; // a serve volleyed back pre-bounce must not fault-check the return
ball.pow = quality; // carried pace: zombies splatter only above zSplatQ
ball.kills = 0; // fresh shot: multi-kill streak restarts
@@ -1603,7 +1617,7 @@ function logWhiff() {
else if (inWin.every(s => s.y < CONTACT.minY)) reason = `too low — ball at ${r2(best.y)}m, past you`;
else reason = 'unclassified — in reach, in window, in height band'; // should not happen
}
logSwing({ result: 'MISS', reason: reason + ` · tap ${P.tap || 0}ms`, dMin: r2(best.d), y: r2(best.y), tSwing: r2(best.t),
logSwing({ result: 'MISS', reason: reason + ` · ${P.flick ? 'flick' : 'tap'} ${P.tap || 0}ms`, dMin: r2(best.d), y: r2(best.y), tSwing: r2(best.t),
ballZ: r2(best.bz), bounces: best.bounces, px: r2(P.x), pz: r2(P.z) });
}
@@ -1629,7 +1643,14 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
const dx = ball.x - ch.x, dz = ball.z - ch.z;
if (Math.hypot(dx, dz * 0.8) > reach || ball.y > CONTACT.maxY) return;
if (ball.y < CONTACT.minY && ball.vy <= 0) return; // a DROPPING ball below the ankles is past you — but a rising one is a half-volley, always scoopable
if (letBounce()) return; // planned from the back: the swing waits for the bounce instead of a weak pick-up
// planned from the back, the swing WAITS for the bounce instead of a weak pick-up — but only while
// the bounce will still land inside this window. If it will not, this is the last chance and the
// low ball is dug out as a half-volley (the below-the-tape cap prices it): a swing a touch early
// used to WHIFF here, which read as the game eating a good swing (Oliver's log, 2026-10-14).
if (letBounce()) {
const gg = ball.g || TUNE.gravity, tb = (ball.vy + Math.sqrt(ball.vy * ball.vy + 2 * gg * ball.y)) / gg;
if (t + tb < TUNE.swingWindup + win) return;
}
// quality: how centered in the window the contact lands
const center = TUNE.swingWindup + win / 2;
const off = Math.abs(t - center) / (win / 2);
@@ -1684,7 +1705,7 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
const sway = swayOf(aim), lane = Math.abs(sway) > 0.15 ? (sway < 0 ? ' left' : ' right') : '';
const cap = q >= q0 ? '' : ball.rx && ball.rx.lob ? ' · capped: lobbed, on your heels'
: ball.bounces === 0 ? ' · capped: volley below the tape' : ' · capped: dug out a drop';
logSwing({ result: 'HIT', reason: `q=${r2(q)}${kind}${lane}${volley ? ' volley' : ''} · tap ${P.tap || 0}ms${cap}`,
logSwing({ result: 'HIT', reason: `q=${r2(q)}${kind}${lane}${volley ? ' volley' : ''} · ${P.flick ? 'flick' : 'tap'} ${P.tap || 0}ms${cap}`,
d: r2(Math.hypot(dx, dz * 0.8)), y: r2(ball.y) });
}
doHit(ch, q, aim);
@@ -1802,6 +1823,7 @@ function racketAI(rz, dt) {
+ (G.match && G.match.surprised ? TUNE.surpFlub * learnK(rz) : 0);
if (Math.random() < flub) {
recordEvent('opponent_whiff', { chance: flub, chaseSpeed: rzChaseSpeed(rz), reach: TUNE.aiReach * wnd.reach });
if (swingLog()) logSwing({ result: 'THEM', reason: 'whiffed it' });
rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
}
else {
@@ -1904,7 +1926,7 @@ function meleeCooldown() {
TUNE.meleeCool / (1 + Math.max(0, near - 1) * TUNE.meleeCrowd));
}
export function keyPressed(code, lead = 0) {
export function keyPressed(code, lead = 0, flick = false) {
// THE SPRITE BENCH takes the key before anything else reads it: it is a back-room art test with no
// ball and no scoring, so every other meaning a key has here is meaningless inside it.
if (G.bench) return benchKey(code);
@@ -2003,7 +2025,7 @@ export function keyPressed(code, lead = 0) {
// clock starts that far along: the timing is judged from touch-down, the ball still has to be
// there for the contact.
P.swingT = rallyFeel() ? lead : 0; P.pose = 'swing'; P.poseT = 0; G.swung = true;
P.tap = Math.round(lead * 1000); // ...and the log says how long the tap was (0 = a key or a flick)
P.tap = Math.round(lead * 1000); P.flick = flick; // ...and the log says which gesture, and how long it took to fire
}
}