From 1c793f59a4a5118590e7b509ead315034265af55 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Mon, 10 Aug 2026 21:28:37 +0200 Subject: [PATCH] A miss at the rack is a miss, not a shot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping a tile and landing a thumb-width off it returned -1 — free court — and the tap became Space, which instantly armed whatever burst weapon was selected. Reaching for the flamer and missing by 10px fired the gun: 22px tiles, a second row the thumb hasn't learned, and a selection that moves under you (a won duel re-equips the tin it tops up). rackHit now reports a dead margin around the band (-2), and only the TAP dies there, decided at touchend — a drag or hold starting in the same spot still steers and still serves, so burst steering parked low keeps working. Co-Authored-By: Claude Fable 5 --- input.js | 13 ++++++++++--- render.js | 12 ++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/input.js b/input.js index ba32e7f..1643ace 100644 --- a/input.js +++ b/input.js @@ -101,21 +101,28 @@ cv.addEventListener('touchstart', e => { const r = cv.getBoundingClientRect(); for (const t of e.changedTouches) { if (dismissModal()) return; // the popup owns this gesture and nothing else sees it + // A TAP that lands NEAR the rack but on no tile is a miss at a tile, not a shot — Space here + // instantly armed whatever burst weapon was selected ("selecting the flamer activated the + // gun"). Only the TAP dies, decided at touchend: a drag or hold starting in the same spot is + // a deliberate gesture (burst steering parks the thumb low, serve pulls start anywhere) and + // must keep working, so the stick is still created. + let nearRack = false; if (!G.horde) { // rack tap selects a tin — a burst can be aborted out of, a horde cannot (selectWeapon) const slot = rackHit((t.clientX - r.left) / r.width, (t.clientY - r.top) / r.height); if (slot >= 0) { selectWeapon(slot); continue; } + nearRack = slot === -2; } if (!stick) { // `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 stick = { id: t.identifier, x0: t.clientX, y0: t.clientY, t0: performance.now(), r, - seen: performance.now(), + seen: performance.now(), nearRack, 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 if (stick.aimable) G.aim = { fx: 0, fy: 0, mag: 0 }; else steerPX = (t.clientX - r.left) / r.width * 2 - 1; } - else { spare = { id: t.identifier, x0: t.clientX, y0: t.clientY }; keyPressed('Space'); } + else { spare = { id: t.identifier, x0: t.clientX, y0: t.clientY }; if (!nearRack) keyPressed('Space'); } } }, { passive: false }); @@ -173,7 +180,7 @@ const endTouch = e => { // (a bare hold used to fire at a free auto-target; that was the whole exploit) if (aim && dist >= TUNE.pullMin) dragServe(aim); } - else if (!stick.moved && dt < 250) keyPressed('Space'); // tap + 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 flickAim = { ax: Math.abs(dx) > dist * 0.38 ? Math.sign(dx) : 0, az: Math.abs(dy) > dist * 0.38 ? Math.sign(dy) : 0, diff --git a/render.js b/render.js index 71e91ae..0e12688 100644 --- a/render.js +++ b/render.js @@ -1183,13 +1183,21 @@ function rackGeom() { const rows = rackRows(); return { x0: Math.round((W - rw) / 2), y0: rackTop(), tw, h, rows, n: rows * RACK_ROW, rw }; } -export function rackHit(nx, ny) { // normalized [0,1] canvas coords -> slot index, or -1 +// A NEAR-MISS AT THE RACK IS DEAD, NOT A SHOT. A tap that lands a thumb-width off a tile used to +// return -1 — free court — and input turned it into Space, which INSTANTLY armed whatever burst +// weapon was selected. So reaching for the flamer and missing by 10px fired the gun: the exact +// "selecting X activated Y" report, made likely by 22px tiles, a second row the thumb hasn't +// learned yet, and a selection that can move under you (a won duel re-equips the tin it tops up). +const RACK_MISS = 18; // px of dead zone around the band: no selection, no swing, no burst +export function rackHit(nx, ny) { // normalized [0,1] canvas coords -> slot index; -1 free court, -2 dead near-miss if (G.mode !== 'splatter' || G.interlude) return -1; // no rack during the tennis-rules duel const g = rackGeom(), cx = nx * W, cy = ny * H; // The touch band is deliberately BIGGER than the art: up by RACK_GRAB and all the way down to // the screen edge, reclaiming the dead margin below. At 22px tall the tiles were ~30 CSS px on a // phone — well under the 44px everyone's thumb expects — which is why switching tins felt fiddly. - if (cy < g.y0 - RACK_GRAB || cx < g.x0 || cx >= g.x0 + g.rw) return -1; + if (cy < g.y0 - RACK_GRAB || cx < g.x0 || cx >= g.x0 + g.rw) + return cy >= g.y0 - RACK_GRAB - RACK_MISS && cx >= g.x0 - RACK_MISS && cx < g.x0 + g.rw + RACK_MISS + ? -2 : -1; // The FIRST five never move: they stay welded to the bottom edge and the second five stack above, // so growing the rack doesn't slide the ball out from under a thumb that already knows where it is. // Clamped rather than rejected — the RACK_GRAB overhang sits above the top row.