The flick fires the moment it is a flick: the swing launches at the crossing, and the aim rides the thumb to contact
Oliver's service-line report: flicking the incoming ball LEFT was near impossible mid-court at the service line while RIGHT worked, easing toward the baseline. The lane math was symmetric to the centimetre — the defect was latency. The swing waited for the RELEASE, so the whole gesture ran as dead time on top of the windup: ~120 ms of lateness drops contact quality 1.0 -> 0.08, where flickReach halves the width and aimError drowns the rest. A left swipe is a right thumb's slowest direction, the service line leaves ~700-870 ms of budget (measured), and any swipe over FLICK_MS was silently dead. Left + service line was the slowest gesture meeting the shortest budget. Now, in rally modes: crossing flickMin fires the swing (once per gesture, rally phase only) and the vector stays LIVE — trySwing reads G.swipe at contact, with the released flickAim carrying the aim only when the ball arrives after the finger lifts. The FLICK_MS deadline goes with it where the crossing can fire; it survives where it cannot (gore free-aim, the serve strike). Serve gesture, duel, gore and keys untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
532dd3b83e
commit
6f9dea4ffc
5 files changed
+82
-15
No files matched your search
+4
-1
@@ -1283,7 +1283,10 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
let q = Math.max(0.05, 1 - off);
|
||||
if (Math.abs(t - center) * 1000 < TUNE.perfectMs) q = 1;
|
||||
ch.facing = dx >= 0 ? 1 : -1; // forehand right (righty), backhand left → mirrored sprite
|
||||
const flick = isAI ? null : takeFlickAim();
|
||||
// the LIVE swipe outranks the released one: since the crossing fires the swing (input.js), the
|
||||
// finger is usually still down at contact, and the vector under it NOW is the aim the player
|
||||
// means — the released flickAim only carries it when the ball arrives after the finger lifted.
|
||||
const flick = isAI ? null : (G.swipe || takeFlickAim());
|
||||
// SMART AIM (G.diff.smart, classic difficulty): with this probability the CPU plays the OPEN
|
||||
// COURT — the corner away from where you stand — instead of rolling a random lane. Random aim
|
||||
// sends a third of everything straight back to your racket; hunting the open corner is what
|
||||
|
||||
+25
-10
@@ -191,14 +191,23 @@ cv.addEventListener('touchmove', e => {
|
||||
const legs = !rallyFeel();
|
||||
touchAxis.x = legs && Math.abs(dx) > DEAD ? Math.sign(dx) : 0;
|
||||
touchAxis.z = legs && Math.abs(dy) > DEAD ? Math.sign(dy) : 0;
|
||||
// THE FLICK, SHOWN WHILE IT IS STILL A FLICK. The placement ring is a receipt — it arrives with
|
||||
// the ball already gone. This is the same aim BEFORE the release, which is the half the player
|
||||
// could not see: how much width the length of the swipe is buying, and whether that is already
|
||||
// past the line. Only inside the window where a release would still BE a swing (the same
|
||||
// FLICK_MS/flickMin gate endTouch uses) — past that the gesture is a run and a target hanging
|
||||
// off it would be a lie. Rally modes only: gore's flick free-aims at a body, not at a lane.
|
||||
G.swipe = rallyFeel() && dist > TUNE.flickMin && performance.now() - stick.t0 < FLICK_MS
|
||||
? { fx: dx / dist, fy: dy / dist, mag: dist } : null;
|
||||
// THE FLICK FIRES THE MOMENT IT IS A FLICK (match + classic). The swing used to wait for the
|
||||
// release, so the whole gesture ran as dead time on top of the 60 ms windup — and ~120 ms of
|
||||
// lateness is all it takes to turn a perfectly aimed return into low-quality scatter. A LEFT
|
||||
// flick is the slowest direction a right thumb has, and at the service line the opponent's
|
||||
// return leaves only ~700-870 ms of budget, so "flick left" arrived at the window's tail or
|
||||
// not at all (Oliver: "almost impossible"). Crossing flickMin already commits the gesture:
|
||||
// the swing launches HERE, and the vector stays LIVE — trySwing reads G.swipe at contact, so
|
||||
// the aim keeps following the thumb after the swing is airborne. The release deadline
|
||||
// (FLICK_MS) is gone with it: a committed swipe that took 320 ms is a swing, not a rescue
|
||||
// drag — that exception belongs to the finger-steering modes, which never reach this line.
|
||||
// ax/az ride along so the live vector is drop-in for endTouch's flickAim shape.
|
||||
// Rally modes only: gore's flick free-aims at a body, not at a lane.
|
||||
G.swipe = rallyFeel() && dist > TUNE.flickMin
|
||||
? { ax: Math.abs(dx) > dist * 0.38 ? Math.sign(dx) : 0,
|
||||
az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 0,
|
||||
fx: dx / dist, fy: dy / dist, mag: dist } : null;
|
||||
if (G.swipe && !stick.swung && G.phase === 'rally') { stick.swung = true; keyPressed('Space'); }
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
@@ -215,12 +224,18 @@ const endTouch = e => {
|
||||
if (aim && dist >= TUNE.pullMin) dragServe(aim);
|
||||
}
|
||||
else if (!stick.moved && dt < 250) { if (!stick.nearRack) keyPressed('Space'); } // tap — dead if it was a near-miss at the rack
|
||||
else if (!stick.adopted && dt < FLICK_MS && dist > TUNE.flickMin) { // flick = aimed swing — a rescue drag is never a swing
|
||||
else if (!stick.adopted && dist > TUNE.flickMin
|
||||
&& (stick.swung || dt < FLICK_MS || (rallyFeel() && G.phase === 'rally'))) {
|
||||
// flick = aimed swing. The FLICK_MS deadline only still applies where the swing could NOT
|
||||
// fire at the crossing (gore's free-aim flick, the serve-phase strike) — there a late
|
||||
// release stays a rescue drag, never a swing. The aim is recorded even when the crossing
|
||||
// already swung: the ball may arrive AFTER the finger lifts, and takeFlickAim at contact
|
||||
// is what carries the released vector to it.
|
||||
flickAim = { ax: Math.abs(dx) > dist * 0.38 ? Math.sign(dx) : 0,
|
||||
az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 0,
|
||||
fx: dx / dist, fy: dy / dist, mag: dist, // raw vector: splatter free-aim
|
||||
t: performance.now() };
|
||||
keyPressed('Space');
|
||||
if (!stick.swung) keyPressed('Space'); // the crossing already swung — never twice per gesture
|
||||
}
|
||||
stick = null; touchAxis.x = 0; touchAxis.z = 0; G.swipe = null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user