A stolen finger no longer welds you to the edge, and the stack is yours

The right edge is where iOS keeps Control Center, and it is exactly where the
horde's absolute finger-steering parks your thumb. When the system takes that
gesture it delivers neither touchend nor touchcancel: `stick` survives forever,
so every later touchstart falls through to the extra-finger branch (swish, and
only swish) while touchmove ignores the new finger's id. steerPX stays frozen at
0.97 and the player is pinned to the screen edge, melee firing, for the rest of
the run. Any new touch event proves the old finger is gone if it is not in
e.touches — drop the stick then. Verified against the bug: pinned at x=6.11
before, steers to -5.20 after, and a genuine second finger still only swishes.

And wCap 9 -> 99, which is no cap in practice: game 70 wants a bigger stack, not
a rationed one. The barrel weapons move from "paid out only once all four tins
are full" (a branch an uncapped rack would never reach again) to rungs of the
same ladder, so a late duel still hands out a rocket instead of a fifth bomb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-09 15:35:53 +02:00
1 parent 5de42648ec
commit f4c6f408e7
3 files changed
+26 -15

No files matched your search

+4 -1
View File
@@ -259,7 +259,10 @@ export const TUNE = {
// How many a fresh tin holds now lives on its WEAPONS row (`tin`), because the rack is no longer
// four fixed types — bombMax/tinTriples/tinFlames/tinGuns lived here and nothing read them after
// the table landed, which is a value you can tune in the panel all day for no effect.
wCap: 9, // per-type cap on the courtside rack — tins accumulate, but only so many
wCap: 99, // per-type cap on the courtside rack. Effectively NO cap: at 9 a late run spent
// every duel win on a full tin, and the answer to game 70 is a bigger stack, not
// a rationed one. 99 rather than Infinity only because the count is drawn as
// text in a rack slot and '×999' does not fit.
rackH: 30, // px height of the bottom weapon rack (feel knob: how much court it covers).
// 22 logical px is ~30 CSS px on a phone — under half Apple's 44px target, which
// is why switching tins felt fiddly. rackHit also pads the touch band (render.js).
+7 -12
View File
@@ -658,21 +658,16 @@ export function deliverTin() {
// full, so most late-run duels paid out in literally nothing. Oliver: "later in the game a won
// duel does not give you weapons anymore."
const stock = id => { const s = slotOf(id); return s < 0 ? 0 : slotStock(s); }; // rack OR the temp slot
// The barrel weapons are RUNGS OF THE SAME LADDER, not a fallback. They used to be paid out only
// once all four tins hit wCap — and now that wCap is effectively no cap that branch would never
// fire again, so a late duel could only ever hand you a fifth bomb. Unlocked ones only, so this
// still cannot hand you a rocket in game three.
const ladder = [...TINS, ...Object.keys(BARREL_WEAPONS).filter(w => G.runGames >= BARREL_WEAPONS[w].from)];
let id = null;
for (let k = 0; k < TINS.length; k++) {
const cand = TINS[(G.deliveries + k) % TINS.length];
for (let k = 0; k < ladder.length; k++) {
const cand = ladder[(G.deliveries + k) % ladder.length];
if (stock(cand) < TUNE.wCap) { id = cand; G.deliveries += k + 1; break; }
}
// ...and when the whole four-rung ladder is capped, PAY OUT OF THE BARREL RACK instead. Past
// about game 20 all four tins sit at wCap and the duel — the one thing in the run you play for —
// handed you nothing at all: "the duel does not always give you new weapons". The barrel weapons
// are the natural next rung, they already exist, and you cannot otherwise win one without
// shooting a barrel. Unlocked ones only, so this can never hand you a rocket in game three.
if (!id) {
const late = Object.keys(BARREL_WEAPONS)
.filter(w => G.runGames >= BARREL_WEAPONS[w].from && stock(w) < TUNE.wCap);
if (late.length) id = late[G.deliveries++ % late.length];
}
// Every tin AND every barrel weapon full is a real state and not a bug, so say so plainly instead
// of miming a handover that cannot happen. No boy, no umpire line — nothing was delivered.
if (!id) { G.flash = { text: 'RACK FULL', t: 1.1 }; return; }
+15 -2
View File
@@ -67,8 +67,21 @@ export function resetInput() {
stick = flickAim = steerPX = null;
}
// A LOST TOUCHEND LOCKS THE GAME, part two. visibilitychange/blur (below) only catches the case
// where the app goes away. The other one: a system edge gesture (Control Center on the right edge —
// exactly where the horde's absolute finger-steering parks your thumb) steals the finger and iOS
// delivers neither touchend nor touchcancel. `stick` then survives forever, so every later
// touchstart falls through to the extra-finger branch (swish, and only swish) while touchmove
// ignores the new finger's id — leaving steerPX frozen and the player welded to the screen edge.
// 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;
}
};
cv.addEventListener('touchstart', e => {
e.preventDefault(); audio();
e.preventDefault(); audio(); dropStale(e);
const r = cv.getBoundingClientRect();
for (const t of e.changedTouches) {
if (dismissModal()) return; // the popup owns this gesture and nothing else sees it
@@ -90,7 +103,7 @@ cv.addEventListener('touchstart', e => {
}, { passive: false });
cv.addEventListener('touchmove', e => {
e.preventDefault();
e.preventDefault(); dropStale(e); // unpins the player as soon as any finger moves; the next touchstart takes the stick
if (!stick) return;
for (const t of e.changedTouches) {
if (t.identifier !== stick.id) continue;