The zombie finger iOS never buries: a drag now digs the stick out from under it
dropStale trusted e.touches — but a finger stolen for a system edge gesture can stay listed there forever, so the stick survived and the player stayed welded to the edge (field-proven: escape taps still swished, meaning dropStale ran and found the id). The zombie is indistinguishable from a parked thumb by data, so it's told apart by intent: in the absolute-steering modes, a new finger that drags past the deadzone while the stick has been silent over a second inherits the stick. Taps still swish and steal nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
482c7cdce4
commit
15cad26105
1 file changed
+24
-3
@@ -64,7 +64,7 @@ export const touchPX = () => steerPX;
|
|||||||
export function resetInput() {
|
export function resetInput() {
|
||||||
for (const k in keys) delete keys[k];
|
for (const k in keys) delete keys[k];
|
||||||
touchAxis.x = touchAxis.z = 0;
|
touchAxis.x = touchAxis.z = 0;
|
||||||
stick = flickAim = steerPX = null;
|
stick = flickAim = steerPX = spare = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A LOST TOUCHEND LOCKS THE GAME, part two. visibilitychange/blur (below) only catches the case
|
// A LOST TOUCHEND LOCKS THE GAME, part two. visibilitychange/blur (below) only catches the case
|
||||||
@@ -80,6 +80,22 @@ const dropStale = e => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A LOST TOUCHEND LOCKS THE GAME, part three. dropStale above trusts e.touches — but iOS can
|
||||||
|
// steal a finger for an edge gesture WITHOUT tearing the touch down, and then WebKit lists the
|
||||||
|
// dead touch in e.touches forever, so dropStale never fires. (Proven in the field: the player's
|
||||||
|
// escape taps still swished, so touchstart ran dropStale and still found the id listed.) By data
|
||||||
|
// the zombie is indistinguishable from a real thumb parked still at the edge; by INTENT it isn't:
|
||||||
|
// a welded player puts a new finger down and DRAGS. So remember the newest extra finger, and if
|
||||||
|
// it drags past the deadzone while the stick has been silent over a second in an auto-fire mode
|
||||||
|
// (the only modes that steer by absolute finger x), the drag takes the stick. A parked thumb
|
||||||
|
// plus a melee tap stays legit two-thumb play — a tap doesn't drag, so it steals nothing.
|
||||||
|
let spare = null; // {id, x0, y0} — the newest extra finger, candidate to inherit a zombie stick
|
||||||
|
const STALE_MS = 1000;
|
||||||
|
const stickStolen = t =>
|
||||||
|
stick && spare && t.identifier === spare.id && (G.horde || G.mgT > 0)
|
||||||
|
&& performance.now() - stick.seen > STALE_MS
|
||||||
|
&& Math.hypot(t.clientX - spare.x0, t.clientY - spare.y0) > DEAD;
|
||||||
|
|
||||||
cv.addEventListener('touchstart', e => {
|
cv.addEventListener('touchstart', e => {
|
||||||
e.preventDefault(); audio(); dropStale(e);
|
e.preventDefault(); audio(); dropStale(e);
|
||||||
const r = cv.getBoundingClientRect();
|
const r = cv.getBoundingClientRect();
|
||||||
@@ -93,12 +109,13 @@ cv.addEventListener('touchstart', e => {
|
|||||||
// `r` rides along: the canvas is position: fixed, so its rect only moves on viewport changes
|
// `r` rides along: the canvas is position: fixed, so its rect only moves on viewport changes
|
||||||
// — re-measuring it per touchmove (60-120 Hz on iOS) was layout work for a constant answer
|
// — re-measuring it per touchmove (60-120 Hz on iOS) was layout work for a constant answer
|
||||||
stick = { id: t.identifier, x0: t.clientX, y0: t.clientY, t0: performance.now(), r,
|
stick = { id: t.identifier, x0: t.clientX, y0: t.clientY, t0: performance.now(), r,
|
||||||
|
seen: performance.now(),
|
||||||
moved: false, aimable: canAimServe() }; // context locked at touch start — a movement drag never morphs into a serve
|
moved: false, aimable: canAimServe() }; // context locked at touch start — a movement drag never morphs into a serve
|
||||||
// the reticle appears the moment the thumb lands, but mag 0 aims at nothing — you have to pull
|
// the reticle appears the moment the thumb lands, but mag 0 aims at nothing — you have to pull
|
||||||
if (stick.aimable) G.aim = { fx: 0, fy: 0, mag: 0 };
|
if (stick.aimable) G.aim = { fx: 0, fy: 0, mag: 0 };
|
||||||
else steerPX = (t.clientX - r.left) / r.width * 2 - 1;
|
else steerPX = (t.clientX - r.left) / r.width * 2 - 1;
|
||||||
}
|
}
|
||||||
else keyPressed('Space');
|
else { spare = { id: t.identifier, x0: t.clientX, y0: t.clientY }; keyPressed('Space'); }
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
@@ -106,7 +123,11 @@ cv.addEventListener('touchmove', e => {
|
|||||||
e.preventDefault(); dropStale(e); // unpins the player as soon as any finger moves; the next touchstart takes the stick
|
e.preventDefault(); dropStale(e); // unpins the player as soon as any finger moves; the next touchstart takes the stick
|
||||||
if (!stick) return;
|
if (!stick) return;
|
||||||
for (const t of e.changedTouches) {
|
for (const t of e.changedTouches) {
|
||||||
|
if (stickStolen(t)) // the zombie stick loses to a finger that provably wants to steer
|
||||||
|
stick = { id: t.identifier, x0: t.clientX, y0: t.clientY, t0: performance.now(),
|
||||||
|
r: stick.r, seen: performance.now(), moved: true, adopted: true, aimable: false };
|
||||||
if (t.identifier !== stick.id) continue;
|
if (t.identifier !== stick.id) continue;
|
||||||
|
stick.seen = performance.now();
|
||||||
const dx = t.clientX - stick.x0, dy = t.clientY - stick.y0, dist = Math.hypot(dx, dy);
|
const dx = t.clientX - stick.x0, dy = t.clientY - stick.y0, dist = Math.hypot(dx, dy);
|
||||||
if (dist > DEAD) stick.moved = true;
|
if (dist > DEAD) stick.moved = true;
|
||||||
if (stick.aimable) { // slingshot: PULL BACK, launch forward — the shot is the inverse of the drag
|
if (stick.aimable) { // slingshot: PULL BACK, launch forward — the shot is the inverse of the drag
|
||||||
@@ -153,7 +174,7 @@ const endTouch = e => {
|
|||||||
if (aim && dist >= TUNE.pullMin) dragServe(aim);
|
if (aim && dist >= TUNE.pullMin) dragServe(aim);
|
||||||
}
|
}
|
||||||
else if (!stick.moved && dt < 250) keyPressed('Space'); // tap
|
else if (!stick.moved && dt < 250) keyPressed('Space'); // tap
|
||||||
else if (dt < 300 && dist > 28) { // flick = aimed swing
|
else if (!stick.adopted && dt < 300 && dist > 28) { // flick = aimed swing — a rescue drag is never a swing
|
||||||
flickAim = { ax: Math.abs(dx) > dist * 0.38 ? Math.sign(dx) : 0,
|
flickAim = { ax: Math.abs(dx) > dist * 0.38 ? Math.sign(dx) : 0,
|
||||||
az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 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
|
fx: dx / dist, fy: dy / dist, mag: dist, // raw vector: splatter free-aim
|
||||||
|
|||||||
Reference in new issue
Block a user