Two-stage serve aim: hold + drag shows the reticle, release fires

Flick-to-aim was aim-by-faith — you learned where you aimed after the ball
was gone. Now, at a serveable pretoss, touching and dragging shows a LIVE
reticle (same mapping: direction + length = landing point); release serves
there. The magnet is finally visible: within zSnap the ring turns red and
rides the locked zombie (plus a marker over its head) — you see the lock
BEFORE you commit the tin.

- Context locked at touchstart: a movement drag started mid-rally never
  morphs into a serve when the ball dies (sub-300ms releases still count
  as the rapid flick re-serve — the Phase 1 cadence survives)
- Drag back home = cancel: no serve, the tin stays in stock
- Aimed (drag) serves commit specials at fixed 0.62 quality; the tap-toss
  ritual keeps the PERFECT-timing power ceiling and the stock — placement
  vs power is now a real choice
- Rally / horde / machine gun untouched: drag steers, flick swings
- Serve prompt: 'TAP TO SERVE - HOLD + DRAG TO AIM' / 'AIMED SERVES FIRE
  THE BOMB - TAP KEEPS IT'

Verified with synthetic touch sequences: drag arms the reticle without
serving and it tracks the thumb; release serves at the marker with the bomb
committed; cancel keeps the tin; rally-started drags never morph; locked
reticle renders red on the zombie (screenshot); tennis 12/12; no console
errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-07-12 13:00:15 +02:00
1 parent 2f1419b1f2
commit 8cfe9fdbcd
4 files changed
+46 -9

No files matched your search

+11 -1
View File
@@ -24,7 +24,7 @@ export function predictLanding() { // first-bounce point, closed form — exact,
// splatter FREE-AIM: the flick vector maps to a landing point from where you stand — a short
// flick drops it close, a long one reaches the far baseline. A zombie within zSnap of that
// point magnets the shot (with walk-lead); otherwise the ball lands exactly where you flicked.
function flickTarget(ch, aim) {
export function flickTarget(ch, aim) { // exported: render draws the live reticle with it
const R = Math.min(1, Math.max(0.12, aim.mag / 160)) * 22; // px flick length -> meters of reach
const tx = Math.max(-6.5, Math.min(6.5, ch.x + aim.fx * R));
const tz = Math.max(1, Math.min(24.5, ch.z + aim.fy * R)); // screen up = up-court (-z)
@@ -513,6 +513,16 @@ function racketAI(rz, dt) {
}
}
// release of the two-stage aim: an instant serve to the reticle (quality fixed — placement
// is this serve's power; the toss ritual keeps the PERFECT-timing power ceiling)
export function dragServe(aim) {
if (G.mode !== 'splatter' || G.phase !== 'pretoss' || score.server !== 'P') return;
if (G.horde || G.mgT > 0 || G.changeover) return;
ball.active = true; ball.collected = false; ball.bounces = 0; ball.firstLand = null;
ball.x = P.x + 0.35; ball.z = P.z; ball.y = 2.2; ball.g = TUNE.gravity;
hitServe(P, 0.62, aim);
}
export function keyPressed(code) {
if (code !== 'Space') return;
audio(); // unlock on first gesture