Pay out every won duel, and stop the popup eating the announcement

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 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-04 06:49:41 +02:00
1 parent 5a4c6e4359
commit c75bd5b9ba
3 files changed
+32 -3

No files matched your search

+11
View File
@@ -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` /
+20 -2
View File
@@ -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())
+1 -1
View File
@@ -184,6 +184,6 @@
</div>
</div>
</div>
<script type="module" src="main.js?v=118"></script>
<script type="module" src="main.js?v=119"></script>
</body>
</html>