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