From c75bd5b9bac51fcded4235b5cc7ad533cd23b96e Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Tue, 4 Aug 2026 06:49:41 +0200 Subject: [PATCH] Pay out every won duel, and stop the popup eating the announcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two separate faults, both of which read as "the duel gave me nothing". The first is mine, from making weaponModal first-sight-only. G.flash ticked ABOVE the `if (G.modal) return` in update(), so a flash raised in the same beat as a popup spent its whole 1.1 s counting down invisibly behind the dim. That was harmless while every tin opened its own modal — but once the modal went quiet the label flash WAS the announcement, and the trainer's WOUNDS TREATED popup ate it every single time you won a duel while wounded, which late in a run is every time. Measured: flash null after 1.5 s of reading, nothing left on dismiss. A modal freezes the world and a transient HUD message is part of the world, so the tick moved below the return. It now holds at 1.1 for as long as you read, and starts counting when you tap. The second is older and Oliver felt it directly. deliverTin CYCLED the ladder — TINS[G.deliveries++ % 4] — handing you whatever rung came next even when that type was already at wCap, where grantWeapon's Math.min clamp makes the delivery a silent no-op. Bomb and fire fill in three duels each, triple in one, so past about the thirteenth duel three rungs of four are full and most late duels paid out in literally nothing. It walks to the first rung with ROOM now. Simulated twenty wins with nothing ever spent: every one delivers through #16, where before #10, #13, #14 and #15 were already empty handovers. Past that it says RACK FULL, which is a real state and not a bug, so it says so plainly instead of miming a delivery — no boy, no umpire line. Co-Authored-By: Claude Opus 5 --- HANDOFF.md | 11 +++++++++++ gameplay.js | 22 ++++++++++++++++++++-- index.html | 2 +- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 40b1a41..ce9824b 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1180,6 +1180,17 @@ this on any new pose with a raised prop. index, never a search — which is why the laser looked fine while the other four did not. Note also that spending re-indexes the rack (`spendSlot` splices), so a slot number goes stale the moment any tin empties. Look weapons up by ID, not by a slot you cached a frame ago. +- **A modal freezes the WORLD, and `G.flash` is part of the world.** Its tick used to sit above the + `if (G.modal) return` in `update()`, so any flash raised in the same beat as a popup spent its + whole 1.1 s counting down invisibly behind the dim. Harmless while every tin opened a modal of its + own; the moment `weaponModal` became first-sight-only, the label flash WAS the announcement and + the trainer's popup ate it on every wounded duel win — *"later in the game a won duel does not + give you weapons anymore"*. Anything transient and drawn belongs below that return, not above it. +- **`deliverTin` walks the ladder, it does not cycle it.** `TINS[G.deliveries++ % 4]` handed you + whatever rung came next even when that type was already at `TUNE.wCap`, where `grantWeapon`'s + `Math.min` clamp made the whole delivery a no-op. Past roughly the thirteenth duel three rungs of + four are full, so most late duels paid out in nothing at all. It now advances to the first rung + with room, and only says `RACK FULL` when every one of them genuinely is. - **Every sound in audio.js swallows its own errors** (`try { ... } catch (e) {}`), so a clean console proves NOTHING about whether one played. To verify a sound really built its graph, count nodes — patch `AudioContext.prototype.createBufferSource` / `createOscillator` / diff --git a/gameplay.js b/gameplay.js index 0d5412f..b44f870 100644 --- a/gameplay.js +++ b/gameplay.js @@ -562,7 +562,20 @@ export function spendSlot(i) { // instead of spending it now. G.weapon picks which tin the next serve throws. const TINS = ['bomb', 'triple', 'fire', 'gun']; // the duel ladder, in the order it hands them out export function deliverTin() { - const id = TINS[G.deliveries++ % TINS.length]; + // WALK the ladder to the next rung you have room for, rather than cycling blindly onto one that + // is already at wCap. Blind cycling meant `grantWeapon` clamped the top-up to a no-op and a won + // duel silently handed you nothing — and past about the thirteenth duel three rungs of four are + // 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 + let id = null; + for (let k = 0; k < TINS.length; k++) { + const cand = TINS[(G.deliveries + k) % TINS.length]; + if (stock(cand) < TUNE.wCap) { id = cand; G.deliveries += k + 1; break; } + } + // Every tin full is a real state and it is 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; } const wasPlain = G.weapon === 0 && !G.temp; // never yank you off a deliberate PLAIN (banking) grantWeapon(id); if (wasPlain) G.weapon = 0; @@ -1206,7 +1219,6 @@ export function update(dt) { updateZombies(dt); return; } - if (G.flash) { G.flash.t -= dt; if (G.flash.t <= 0) G.flash = null; } // A card can wait before it shows. The chapter card used to land on the same frame as the boss // died, so the biggest gore burst in the game happened underneath it and nobody ever saw it. if (G.card) { @@ -1225,6 +1237,12 @@ export function update(dt) { if (m.in < 1 && (m.in = Math.min(1, m.in + dt / TUNE.modalIn)) >= 1) pock(0.4, 620); // it lands with a thud return; } + // The flash is FROZEN behind a popup too, and it has to be: it used to tick above the return, so + // any flash raised in the same beat as a modal spent its whole 1.1 s invisible behind the dim and + // was gone before you had tapped. That is exactly what made a won duel look like it paid nothing + // late in a run — the weapon modal is first-sight-only, so once you have seen a tin the label + // flash IS the announcement, and the trainer's popup was eating it every time you were wounded. + if (G.flash) { G.flash.t -= dt; if (G.flash.t <= 0) G.flash = null; } crowd.cheer = Math.max(0, crowd.cheer - dt); G.idleT += dt; if (G.asideT > 0) G.asideT -= dt; // the asides' shared cooldown (see aside()) diff --git a/index.html b/index.html index 2d48429..ff61ffe 100644 --- a/index.html +++ b/index.html @@ -184,6 +184,6 @@ - +