The length of the flick is the strength of the aim, and you can see it leave

The rally flick was read as three lanes: sideways past 0.38 of its own length
took the whole corner, a hair under it took dead centre, and nothing lived in
between — "sometimes applied very strong, sometimes it barely does anything",
with no way to tell which one you had asked for. swayOf reads the swipe's LENGTH
as a continuous strength, exactly as the serve already did, and doHit spends
that instead of the lane vote. Keys keep the full lane; the opponent's aim has
no length and is untouched.

That is also what hands the OUT back: going out was never a choice, because
every aimed shot took the full width and then the scatter on top. Below about
0.7 of a full swipe — the line minus aimError, over the width timing can earn —
the worst mistiming in the game cannot sail it now, and above it the old risk
curve is exactly as tested.

And the aim is drawn while the thumb is still moving: the placement ring is a
receipt that arrives with the ball already gone, so the same ring is shown live
with a ray from the player's feet, cream while the ask is safe and hot once the
sway crosses that derived threshold. The receipt goes red when the shot really
is out, so the two rings together are the lesson.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-18 06:56:44 +02:00
1 parent 99b2dec3bf
commit f3d6f71144
8 files changed
+144 -14

No files matched your search

+14 -4
View File
@@ -7,7 +7,7 @@ import { audio, pock } from './audio.js';
import { TUNE, WEAPONS } from './config.js';
import { benchButtons, W, H } from './render.js'; // W/H are live bindings: fitViewport rewrites them
import { G, score } from './state.js';
import { keyPressed, dragServe, selectWeapon, serveToss, pullQuality, pullPerfect, dismissModal, slotId, dodge } from './gameplay.js';
import { keyPressed, dragServe, selectWeapon, serveToss, pullQuality, pullPerfect, dismissModal, slotId, dodge, rallyFeel } from './gameplay.js';
import { rackHit } from './render.js';
import { togglePanel } from './panel.js';
@@ -31,6 +31,7 @@ export const axisZ = () => touchAxis.z || (keys.ArrowUp || keys.KeyW ? -1 : 0) +
let stick = null; // {id, x0, y0, t0, moved}
let flickAim = null; // {ax, az, t} aim override for the swing just triggered
const DEAD = 14;
const FLICK_MS = 300; // a release later than this is a rescue drag, never a swing (see endTouch)
// gameplay consumes the flick at contact; returns null if none or expired
export function takeFlickAim(maxAgeMs = 450) {
@@ -66,6 +67,7 @@ export function resetInput() {
for (const k in keys) delete keys[k];
touchAxis.x = touchAxis.z = 0;
stick = flickAim = steerPX = spare = null;
G.swipe = null;
}
// A LOST TOUCHEND LOCKS THE GAME, part two. visibilitychange/blur (below) only catches the case
@@ -77,7 +79,7 @@ export function resetInput() {
// Any new touch event proves the old finger is gone if it isn't in e.touches. Drop it then.
const dropStale = e => {
if (stick && ![...e.touches].some(t => t.identifier === stick.id)) {
stick = null; touchAxis.x = touchAxis.z = 0; steerPX = null; G.aim = null;
stick = null; touchAxis.x = touchAxis.z = 0; steerPX = null; G.aim = null; G.swipe = null;
}
};
@@ -181,6 +183,14 @@ cv.addEventListener('touchmove', e => {
steerPX = (t.clientX - r.left) / r.width * 2 - 1;
touchAxis.x = Math.abs(dx) > DEAD ? Math.sign(dx) : 0;
touchAxis.z = 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;
}
}, { passive: false });
@@ -197,14 +207,14 @@ 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 < 300 && dist > 28) { // flick = aimed swing — a rescue drag is never a swing
else if (!stick.adopted && dt < FLICK_MS && dist > TUNE.flickMin) { // flick = aimed swing — a rescue drag is never a swing
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');
}
stick = null; touchAxis.x = 0; touchAxis.z = 0;
stick = null; touchAxis.x = 0; touchAxis.z = 0; G.swipe = null;
}
};
cv.addEventListener('touchend', endTouch, { passive: false });