Add thunder audio on lightning strike edges
- Play a soft thunder SFX (sfx_thunder_01..04 at volume 0.35) on the strobe's 0→>0 edge, once per strike; mid-strobe frames stay silent. - Move `wasOn` latch after the thunder check so it reflects the previous frame's state (fixes arm-frame and edge detection). - Add `thunder: false` config to silence the voice while keeping the bolt. - Cover with tests: arm frame silent, one clap per strike, mute path.
This commit is contained in:
parent
8e035d0891
commit
32215fa737
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue