From 3d7e66449a2b1f127388a85c065c4ce3b64b1a5a Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Fri, 21 Aug 2026 15:04:33 +0200 Subject: [PATCH] A new run rewinds the clock; the camera flashes did not rewind with it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oliver: "I start a new zombie tennis match. when playing and scoring no fotograph flashes appear, but at a random place in the crowd some frozen midflash circle is visible. after a few points the flashes unfreeze." Both halves are the same cause. resetState() puts G.idleT back to 0 for every new run, while the flash list and its "next at" live in render.js's own closure and survive it. So the new run inherited a flashNext from the old one — no photographs at all until the clock climbed back past it — and any flash still in flight was left with a t0 in its own future, which makes its fade 1 - (now - t0)/life permanently greater than 1: a shutter stuck at full brightness in the stand until something else cleared it. tickFlashes now notices the clock moving backwards and clears and re-arms. One guard, in the only place that owns the state, so it covers any future mode that rewinds the clock as well. Verified on the canvas: after a rewind there is no permanently-bright pixel in the stand, and the photographers resume within seconds instead of staying dark. Co-Authored-By: Claude Opus 5 --- PROJECT.md | 11 +++++++++++ index.html | 2 +- src/render.js | 12 +++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index 81f2801..3901042 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -3732,6 +3732,17 @@ scaled so the BODY matches the standing frame (98% of sprite height) and the rac this on any new pose with a raised prop. ## Gotchas learned the hard way + +- **MODULE-LEVEL STATE CLOCKED OFF `G.idleT` MUST SURVIVE THE CLOCK GOING BACK TO ZERO.** + `resetState()` rewinds `G.idleT` for every new run; anything living in a module's own closure does + not rewind with it. The camera flashes had both halves of that bug at once (2026-08-21): a + `flashNext` inherited from the previous run meant NO photographs until the new run's clock climbed + past it, and any flash still in flight was left with a `t0` in its own future, which makes + `1 - (now - t0)/life` permanently greater than 1 — a shutter frozen at full brightness in the + stand for the rest of the session. Oliver: "at a random place in the crowd some frozen midflash + circle is visible. after a few points the flashes unfreeze." `tickFlashes` now clears and re-arms + whenever the clock moves backwards. **If you add anything else with a remembered `t0` or a "next + at" in render.js, give it the same guard** — the reset path will not do it for you. - **Nothing outside the four slot accessors may know how many slots there are.** `endBurst` had `[0, 1, 2, 3, 4].find(i => slotId(i) === id)`, which was correct for exactly as long as the rack held five. `RACK_MAX` 4 → 9 and a burst weapon in slot 5..9 was suddenly unfindable, so it was diff --git a/index.html b/index.html index 165aa6c..56a8da3 100644 --- a/index.html +++ b/index.html @@ -664,6 +664,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat - + diff --git a/src/render.js b/src/render.js index 4204157..d8df765 100644 --- a/src/render.js +++ b/src/render.js @@ -388,9 +388,19 @@ function buildCrowd() { // while the crowd is on its feet, which is the "slightly increase during applause" — a stand that // went white during a point would be a different, much louder feature. const flashes = []; // {x, y, r, t0} -let flashNext = 0; +let flashNext = 0, flashLast = 0; function tickFlashes(standsBottom) { const now = G.idleT, top = Math.max(4, standsBottom - AD_H - 4); + // THE CLOCK CAN GO BACKWARDS AND THIS STATE CANNOT. `resetState` puts G.idleT back to 0 for every + // new run, while `flashes` and `flashNext` live in this module and survive it — so a fresh match + // inherited a `flashNext` from the last one (no photographs at all until the clock climbed back + // past it) and any flash still alive was left with a t0 in its future, which makes its fade + // `1 - (now - t0)/life` permanently greater than 1: a shutter frozen at full brightness in the + // stand, for ever. Oliver, 2026-08-21: "at a random place in the crowd some frozen midflash circle + // is visible. after a few points the flashes unfreeze". One guard, here, because this is the only + // place that owns the state — and it covers any future mode that rewinds the clock too. + if (now < flashLast) { flashes.length = 0; flashNext = now; } + flashLast = now; if (now >= flashNext) { // THE BIGGER THE MOMENT, THE MORE SHUTTERS. cheer() queues crowd.burst shutters proportional to // its own vol, and they fire back to back at a fifth of the normal gap — a match point empties