diff --git a/dev/nebula.test.mjs b/dev/nebula.test.mjs index 4ede6c9..e69cf16 100644 --- a/dev/nebula.test.mjs +++ b/dev/nebula.test.mjs @@ -477,6 +477,52 @@ check('lightning: a strike frame draws the bolt (both stroked); a quiet frame cl return drewOnStrike && cleared(lScene.gfx[0]) && cleared(lScene.gfx[1]); })()); +// Thunder (the VOICE of the bolt) — a fresh instance + a sound-recording +// fake scene (the Sfx seam: scene.sound.play + the audio-cache guard), so +// the assertions are independent of the update history above. +const tScene = makeFakeScene(); +const thunderPlays = []; +tScene.sound = { play: (key, o) => thunderPlays.push({ key, volume: o?.volume }) }; +tScene.cache = { audio: { has: (key) => String(key).startsWith('sfx_thunder_') } }; +const THUNDER_KEYS = ['sfx_thunder_01', 'sfx_thunder_02', 'sfx_thunder_03', 'sfx_thunder_04']; +const tL = new NebulaLightning(tScene, '#f0884a', '#e9f36d', lcfg, lFx); +tL.create(); +const tEdge = (tL.phase % tL.interval) * 1000; // this cycle's strike boundary +tL.update(tEdge, 16); // first frame: arms the detector (entered mid-strike → silent) +check('thunder: the arm frame (a system entered mid-strike) stays silent', thunderPlays.length === 0); +// A dark frame between strikes (the real cadence has a long one — the +// window is 1.35s, the interval 8–15s) resets the 0→>0 edge. +const DARK = (t) => tL.update(t, 16); +DARK(tEdge + 1500); // just past the 1.35s strobe window → dark sky +tL.update(tEdge + tL.interval * 1000, 16); // the NEXT strike: a real 0→>0 edge +check('thunder: one clap on the strike edge — a far, soft thunder voice (0.35)', (() => { + return ( + thunderPlays.length === 1 && + THUNDER_KEYS.includes(thunderPlays[0].key) && + Math.abs(thunderPlays[0].volume - 0.35) < 1e-9 + ); +})()); +tL.update(tEdge + tL.interval * 1000 + 100, 16); // mid-strobe: the bolt is still up +DARK(tEdge + tL.interval * 1000 + 1500); // …and then the sky darkens +tL.update(tEdge + tL.interval * 2000, 16); // the strike AFTER next: a new edge +tL.update(tEdge + tL.interval * 2000 + 100, 16); // …and its mid-strobe +check('thunder: one clap per strike (mid-strobe frames stay silent)', thunderPlays.length === 2); + +// The `thunder: false` config silences the voice (the bolt still strikes). +const qScene = makeFakeScene(); +const quietPlays = []; +qScene.sound = { play: () => quietPlays.push(1) }; +qScene.cache = { audio: { has: () => true } }; +const qL = new NebulaLightning(qScene, '#f0884a', '#e9f36d', { ...lcfg, thunder: false }, lFx); +qL.create(); +const qEdge = (qL.phase % qL.interval) * 1000; +qL.update(qEdge, 16); +qL.update(qEdge + qL.interval * 1000, 16); +check('thunder: `thunder: false` silences the voice (the bolt still strikes)', (() => { + const drew = (g) => g.calls.filter((c) => c === 'line').length > 0; + return quietPlays.length === 0 && drew(qScene.gfx[0]) && drew(qScene.gfx[1]); +})()); + check('lightning: camera-anchored (sky model); destroy is idempotent', (() => { lScene.cameras.main.scrollX = 2600; lScene.cameras.main.scrollY = 1500; diff --git a/js/visuals/NebulaLightning.js b/js/visuals/NebulaLightning.js index 2a177c8..fb2b586 100644 --- a/js/visuals/NebulaLightning.js +++ b/js/visuals/NebulaLightning.js @@ -164,15 +164,15 @@ export class NebulaLightning { this.wasOn = false; return; } - this.wasOn = true; - // THE STRIKE (data/sfx.json → thunder_01…04, soft): the bolt going - // off is the strobe's 0 → >0 edge (this.wasOn is reset on the dark - // frames above). The first frame only arms (a system entered - // mid-strike stays silent), and the throttle makes near-simultaneous - // frames one clap. + // off is the strobe's 0 → >0 edge — this.wasOn is still the + // PREVIOUS frame's state here (it was reset on the dark frames + // above, and latched AFTER this check). The first frame only arms + // (a system entered mid-strike stays silent), and the throttle + // makes near-simultaneous frames one clap. if (this.armed && !this.wasOn) this.thunderStrike(nowMs); this.armed = true; + this.wasOn = true; // THE BOLT: shape from (strike ordinal, system seed) — stable through // this strobe, different every strike, identical every run.