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 @@
-
+