From 1798fc67b843bcff13c467c2dc9cdf323bbf9562 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Sun, 2 Aug 2026 14:22:55 +0200 Subject: [PATCH] Point at them, then wind up: the lane comes from where the thumb lands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lane used to come from the pull's own direction, so aiming up-court was always a downward drag. Every shot ended lower than it began, the next touch starts where the last one ended, and over a horde the thumb walks itself into the iOS bottom edge — where a pull down is Reachability and the system takes the gesture out from under you. A web page cannot refuse that. So touch-down picks the lane now (a ray from his chest through the thumb) and the pull after it is power and nothing else, in whatever direction there is room for. Aiming is what drags the thumb UP the screen, because that is where the shamblers are. The cost, and the thing to feel: the lane locks when the thumb lands. You can no longer sweep the aim while drawn. Co-Authored-By: Claude Opus 5 --- gameplay.js | 2 +- input.js | 41 ++++++++++++++++++++++++++++++++--------- main.js | 2 +- render.js | 17 +++++++++++------ 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/gameplay.js b/gameplay.js index ac0c129..b1752fd 100644 --- a/gameplay.js +++ b/gameplay.js @@ -588,7 +588,7 @@ function transition(dt) { // the mechanic changed under you — the one popup that gets an illustration, because it has // to TEACH the gesture, not just announce it showModal({ title: 'THE MATCH HAS CHANGED', color: UI.hot, icon: 'x_howto', iconH: 120, - body: ['PULL BACK TO AIM — THEN LET GO', 'THE FURTHER YOU PULL, THE HARDER IT HITS', + body: ['POINT AT THEM, THEN PULL BACK', 'THE FURTHER YOU PULL, THE HARDER IT HITS', 'A WEAK SHOT ONLY STAGGERS THEM'] }); }); } diff --git a/input.js b/input.js index 077d91b..158897e 100644 --- a/input.js +++ b/input.js @@ -5,9 +5,9 @@ import { cv } from './dom.js'; import { audio, pock } from './audio.js'; import { TUNE } from './config.js'; -import { G, score } from './state.js'; +import { G, P, score } from './state.js'; import { keyPressed, dragServe, selectWeapon, serveToss, pullQuality, dismissModal } from './gameplay.js'; -import { rackHit } from './render.js'; +import { rackHit, proj, W, H } from './render.js'; import { togglePanel } from './panel.js'; const keys = {}; @@ -41,8 +41,18 @@ export function takeFlickAim(maxAgeMs = 450) { return null; } +// POINT, THEN WIND UP. Where the thumb LANDS picks the lane — a ray from your man through the +// touch — and the pull-back after that is power and nothing else, in whatever direction there is +// room for. The lane used to come from the pull's own direction, which meant aiming up-court was +// always a downward drag: every shot ended lower than it began, the next touch started where the +// last one ended, and over a horde the thumb walked itself into the iOS bottom edge, where a pull +// down is Reachability and the system takes the gesture. Now touch-down is dictated by where the +// shamblers are, which is up-court, so the thumb is pulled UP the screen by the game itself. +// The cost, and the thing to feel: the lane locks when the thumb lands. You can no longer sweep +// the aim while drawn. +// // the two-stage serve: a drag that STARTS at a serveable pretoss aims a visible reticle -// (G.aim, drawn by render); release fires the serve there, dragging back home cancels. +// (G.aim, drawn by render); release fires the serve there, easing back home cancels. // EVERY phase of splatter play, so the drag never silently turns into something else. The old // "drag walks the player" mode between shots was invisible state and read as a bug. const canAimServe = () => @@ -54,6 +64,18 @@ const canAimServe = () => let steerPX = null; // live absolute finger x (-1..1 across the canvas) — machine-gun steering reads it export const touchPX = () => steerPX; +// The lane: a unit vector in CANVAS space from your man's chest to the thumb. gameplay reads it as +// the shot direction straight off (screen up = -z), which is the same contract the flick-swing has +// always used — so nothing downstream of G.aim changes. +function laneFrom(t, r) { + const p = proj(P.x, 1.2, P.z); // chest height: where the shot actually leaves him + const dx = (t.clientX - r.left) / r.width * W - p.x; + const dy = (t.clientY - r.top) / r.height * H - p.y; + const d = Math.hypot(dx, dy); + if (d < 6) return { fx: 0, fy: -1 }; // thumb on top of him: no lane to read, so fire up-court + return { fx: dx / d, fy: dy / d }; +} + // Restarting a run no longer reloads the page (see state.resetState), so this module's own state has // to be put back by hand: a key still held, or a half-finished drag, would otherwise be inherited by // the next run's very first frame. @@ -75,8 +97,9 @@ cv.addEventListener('touchstart', e => { if (!stick) { stick = { id: t.identifier, x0: t.clientX, y0: t.clientY, t0: performance.now(), 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 }; + // the LANE is locked here too, from where the thumb landed. mag 0 aims at nothing — + // pointing costs nothing, only the pull spends anything. + if (stick.aimable) G.aim = { ...(stick.dir = laneFrom(t, r)), mag: 0 }; else steerPX = (t.clientX - r.left) / r.width * 2 - 1; } else keyPressed('Space'); @@ -90,15 +113,15 @@ cv.addEventListener('touchmove', e => { if (t.identifier !== stick.id) continue; const dx = t.clientX - stick.x0, dy = t.clientY - stick.y0, dist = Math.hypot(dx, dy); if (dist > DEAD) stick.moved = true; - if (stick.aimable) { // slingshot: PULL BACK, launch forward — the shot is the inverse of the drag - if (dist > DEAD) { + if (stick.aimable) { // the lane is already locked (see laneFrom) — the drag is PURE POWER now, + if (dist > DEAD) { // so pull back, sideways, wherever there is screen. Only the length counts. if (!stick.tossed) { serveToss(); stick.tossed = true; } // the pull kicks off the toss animation - G.aim = { fx: -dx / dist, fy: -dy / dist, mag: dist }; + G.aim = { ...stick.dir, mag: dist }; // the moment the pull is strong enough to actually splatter, it clicks — you can feel // where the threshold is without watching the reticle const hot = pullQuality(dist) >= TUNE.zSplatQ; if (hot !== !!stick.hot) { stick.hot = hot; if (hot) pock(0.45, 1250); } - } else G.aim = { fx: 0, fy: 0, mag: 0 }; + } else G.aim = { ...stick.dir, mag: 0 }; continue; } const r = cv.getBoundingClientRect(); diff --git a/main.js b/main.js index 3327dd2..84bf737 100644 --- a/main.js +++ b/main.js @@ -89,7 +89,7 @@ const TUT = [ "stops being the other fellow.", "Nobody stops the match. Nobody ever does."] }, { pose: 2, shot: 'tut_aim', say: ["Now you do not rally. You AIM.", - "PULL BACK from your man and let go.", + "POINT at them, then PULL BACK and let go.", "The further you pull, the harder it lands.", "A soft one only makes them cross."] }, { pose: 2, shot: 'tut_rack', say: ["I bring tins out to the rack down there.", diff --git a/render.js b/render.js index 9d437a9..b302407 100644 --- a/render.js +++ b/render.js @@ -646,18 +646,23 @@ function drawCard(title, sub1, sub2, color) { if (sub2) ctx.fillText(sub2, W / 2, H / 2 + 12); } -// Sit in the serve stance doing nothing and the gesture demonstrates itself: a ghost thumb slides -// BACK from the player while chevrons grow FORWARD up the court, then it releases and repeats. -// Pulling is the whole game in splatter mode, and a line of text was carrying that alone. +// Sit in the serve stance doing nothing and the gesture demonstrates itself: a ghost thumb lands +// OUT ON THE COURT — that is the aiming half, and it is why the anchor sits well up-court of him +// rather than at his feet — then slides BACK while chevrons grow FORWARD up the court, and +// releases. Both halves of the gesture in one loop; a line of text was carrying that alone. function drawPullHint() { const cyc = 1.9, t = (G.idleHintT - TUNE.pullHintAfter) % cyc; const draw = Math.min(1, t / 1.15); // the pull itself, then a beat held at full const gone = t > 1.45; // released — the thumb lifts off - const anchor = proj(P.x, 0, P.z + 0.6); - const ax = Math.round(anchor.x), ay = Math.round(Math.min(anchor.y + 6, METER_Y() - 26)); + const anchor = proj(P.x, 0, P.z - 12); + const ax = Math.round(anchor.x), ay = Math.round(Math.min(anchor.y + 6, METER_Y() - 40)); const len = 30 * draw; ctx.save(); ctx.globalAlpha = gone ? 0 : 0.85; + // the ring is where the thumb LANDED — the aiming half, and the half nobody would guess from a + // moving dot alone. It stays put while the thumb slides away from it. + ctx.strokeStyle = UI.dim; ctx.lineWidth = 1; + ctx.beginPath(); ctx.arc(ax, ay, 4, 0, 7); ctx.stroke(); // the dotted track the thumb travels, straight down-screen = "pull back" ctx.fillStyle = UI.dim; for (let d = 6; d < 34; d += 6) ctx.fillRect(ax - 1, ay + d, 2, 2); @@ -1096,7 +1101,7 @@ export function render() { ? (G.weapon === 4 ? (touch ? 'TAP TO UNLOAD THE GUN' : 'SPACE UNLOADS THE GUN') : G.aim ? 'PULL BACK: FURTHER = HARDER' : G.weapon > 0 ? `SERVE FIRES: ${sel} · ${switchHint}` - : touch ? 'PULL BACK TO AIM · RELEASE TO FIRE' : 'DRAG BACK TO AIM · SPACE SERVES') + : touch ? 'POINT AT THEM · PULL BACK TO FIRE' : 'POINT AND DRAG BACK · SPACE SERVES') : G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE') : (touch ? 'TAP TO TOSS, TAP TO SERVE' : 'SPACE TO TOSS, SPACE TO SERVE'); ctx.fillText(txt, W / 2, METER_Y()); // same slot the draw meter uses