@adatechnology/game-weapons-three 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,6 +35,7 @@ var GameWeaponsThree = (() => {
35
35
  MOUNTED_M134_ITEM_ID: () => MOUNTED_M134_ITEM_ID,
36
36
  WEAPON_ITEM_PREFIX: () => WEAPON_ITEM_PREFIX,
37
37
  WEAPON_THUMBNAILS: () => WEAPON_THUMBNAILS,
38
+ WORLD_EFFECTS: () => WORLD_EFFECTS,
38
39
  WORLD_EXPLOSIVE_IDS: () => WORLD_EXPLOSIVE_IDS,
39
40
  WORLD_EXPLOSIVE_PREFIX: () => WORLD_EXPLOSIVE_PREFIX,
40
41
  applyCondition: () => applyCondition,
@@ -50,6 +51,7 @@ var GameWeaponsThree = (() => {
50
51
  createWeaponAudio: () => createWeaponAudio,
51
52
  createWeaponFx: () => createWeaponFx,
52
53
  createWeaponsTestModule: () => createWeaponsTestModule,
54
+ createWorldEffects: () => createWorldEffects,
53
55
  deriveFrames: () => deriveFrames,
54
56
  fitAttachmentModels: () => fitAttachmentModels,
55
57
  hasWeaponModel: () => hasWeaponModel,
@@ -12935,10 +12937,10 @@ var GameWeaponsThree = (() => {
12935
12937
  __publicField(this, "lastRoundAt", Number.NEGATIVE_INFINITY);
12936
12938
  }
12937
12939
  /** When the barrels of a rotary gun are at speed: `upSeconds` after the pull began, and at once while they still spin from the last round. */
12938
- spunUpAt(spin, start, end) {
12940
+ spunUpAt(spin, start, end3) {
12939
12941
  if (!spin || start - this.lastRoundAt <= spin.holdSeconds + EPS3) return 0;
12940
12942
  if (this.spinSince === void 0 || start - this.spinFrameEnd > SPIN_PULL_GAP) this.spinSince = start;
12941
- this.spinFrameEnd = end;
12943
+ this.spinFrameEnd = end3;
12942
12944
  return this.spinSince + spin.upSeconds;
12943
12945
  }
12944
12946
  oncePerPull(held) {
@@ -12947,7 +12949,7 @@ var GameWeaponsThree = (() => {
12947
12949
  this.latched = true;
12948
12950
  return true;
12949
12951
  }
12950
- fireFirearm(a, origin, dir, start, end, held, source) {
12952
+ fireFirearm(a, origin, dir, start, end3, held, source) {
12951
12953
  if (this._reloading) {
12952
12954
  if (this.loadStep !== null && this._ammo > 0) this.stopReload(start);
12953
12955
  else return result("reloading");
@@ -12957,10 +12959,10 @@ var GameWeaponsThree = (() => {
12957
12959
  this.emit({ type: "empty", at: start });
12958
12960
  return result("empty");
12959
12961
  }
12960
- const t0 = Math.max(start, this.readyAt, this.spunUpAt(a.spin, start, end));
12961
- if (t0 > end + EPS3) return result("cooldown");
12962
+ const t0 = Math.max(start, this.readyAt, this.spunUpAt(a.spin, start, end3));
12963
+ if (t0 > end3 + EPS3) return result("cooldown");
12962
12964
  const res = result("ok");
12963
- const times = planRounds(a, this.mode, this._ammo, t0, end);
12965
+ const times = planRounds(a, this.mode, this._ammo, t0, end3);
12964
12966
  let lastAt = t0;
12965
12967
  for (const t of times) {
12966
12968
  const fault = rollFault({ def: this.def, caliber: this.caliber, condition: this._condition, ammoQuality: this._ammoQuality, heat: this._heat, rules: this.faultRules(), random: this.faultRng });
@@ -13076,7 +13078,7 @@ var GameWeaponsThree = (() => {
13076
13078
  this.pend = { src: "trigger", amt: d };
13077
13079
  }
13078
13080
  const start = this.clock - overlap;
13079
- const end = start + d;
13081
+ const end3 = start + d;
13080
13082
  this.settle(start);
13081
13083
  const dir = normalize(direction);
13082
13084
  let res;
@@ -13087,10 +13089,10 @@ var GameWeaponsThree = (() => {
13087
13089
  else if (launcher) res = launcher.trigger(origin, dir, d, held, source);
13088
13090
  else switch (a.kind) {
13089
13091
  case "firearm":
13090
- res = this.fireFirearm(a, origin, dir, start, end, held, source);
13092
+ res = this.fireFirearm(a, origin, dir, start, end3, held, source);
13091
13093
  break;
13092
13094
  case "melee":
13093
- res = this.fireMelee(a, origin, dir, start, end);
13095
+ res = this.fireMelee(a, origin, dir, start, end3);
13094
13096
  break;
13095
13097
  case "flame":
13096
13098
  case "heat":
@@ -13100,7 +13102,7 @@ var GameWeaponsThree = (() => {
13100
13102
  res = this.fireExtinguisher(a, origin, dir, d);
13101
13103
  break;
13102
13104
  case "throwable":
13103
- res = this.fireThrowable(a, origin, dir, held, start, end);
13105
+ res = this.fireThrowable(a, origin, dir, held, start, end3);
13104
13106
  break;
13105
13107
  case "launcher":
13106
13108
  res = this.fireLauncher(a, origin, dir, start, held);
@@ -13112,7 +13114,7 @@ var GameWeaponsThree = (() => {
13112
13114
  res = this.oncePerPull(held) ? result("ok", { uses: [{ kind: "hand", reach: a.reach, origin, direction: dir }] }) : result("cooldown");
13113
13115
  break;
13114
13116
  }
13115
- this.advance(Math.max(0, end - this.clock));
13117
+ this.advance(Math.max(0, end3 - this.clock));
13116
13118
  this.settle(this.clock);
13117
13119
  return res;
13118
13120
  }
@@ -13144,9 +13146,9 @@ var GameWeaponsThree = (() => {
13144
13146
  this.emit({ type: "shot", shots: 0, rounds: 1, mode: this.mode, at: t });
13145
13147
  return result("ok", { uses: launcherUses(a, this.explosive, origin, dir), recoil: [a.recoil[0], a.recoil[1]], noise: a.noise ?? 0, rounds: 1, roundTimes: [t - start] });
13146
13148
  }
13147
- fireMelee(a, origin, dir, start, end) {
13149
+ fireMelee(a, origin, dir, start, end3) {
13148
13150
  const t0 = Math.max(start, this.readyAt);
13149
- if (t0 > end + EPS3) return result("cooldown");
13151
+ if (t0 > end3 + EPS3) return result("cooldown");
13150
13152
  this.readyAt = t0 + 60 / a.rpm;
13151
13153
  const use = { kind: "melee", damage: a.damage, energy: a.energy, radius: a.radius, reach: a.reach, origin, direction: dir };
13152
13154
  return result("ok", { uses: [use], noise: a.noise });
@@ -13180,8 +13182,8 @@ var GameWeaponsThree = (() => {
13180
13182
  return result("ok", { uses: [throwableUse({ action: a, explosive: this.explosive, origin, direction: dir, fuse, rules: this.rules.explosives, ...extra })], noise: a.noise });
13181
13183
  }
13182
13184
  /** A timer throwable is cooked from the pull; impact, remote and wire ones (and a long fuse that sets down) act at once. */
13183
- fireThrowable(a, origin, dir, held, start, end) {
13184
- if (this.cook) return this.cookStep(a, origin, dir, end);
13185
+ fireThrowable(a, origin, dir, held, start, end3) {
13186
+ if (this.cook) return this.cookStep(a, origin, dir, end3);
13185
13187
  if (!this.oncePerPull(held)) return result("cooldown");
13186
13188
  if (this._ammo <= 0) return result("empty");
13187
13189
  const fuse = this.sampleThrowFuse(a);
@@ -13191,7 +13193,7 @@ var GameWeaponsThree = (() => {
13191
13193
  }
13192
13194
  this.cook = { start, fuse, origin, direction: dir };
13193
13195
  this.emit({ type: "cook", phase: "start", fuse, at: start });
13194
- return this.cookStep(a, origin, dir, end);
13196
+ return this.cookStep(a, origin, dir, end3);
13195
13197
  }
13196
13198
  /** The trigger stays down on a cooked throwable: nothing happens until the fuse runs out in the hand. */
13197
13199
  cookStep(a, origin, dir, now) {
@@ -13619,14 +13621,1434 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
13619
13621
  };
13620
13622
  var accessoryTextOf = (locale) => ACCESSORY_TEXT[locale] ?? ACCESSORY_TEXT.en;
13621
13623
 
13624
+ // ../game-audio/dist/index.js
13625
+ var FNV_OFFSET = 2166136261;
13626
+ var FNV_PRIME = 16777619;
13627
+ var MIX = 2654435761;
13628
+ function createRng2(seed) {
13629
+ let state = seed >>> 0;
13630
+ return () => {
13631
+ state = state + 1831565813 >>> 0;
13632
+ let mixed = Math.imul(state ^ state >>> 15, state | 1);
13633
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
13634
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
13635
+ };
13636
+ }
13637
+ function streamSeed(seed, name) {
13638
+ const label = String(name);
13639
+ let hash = FNV_OFFSET;
13640
+ for (let index = 0; index < label.length; index++) hash = Math.imul(hash ^ label.charCodeAt(index), FNV_PRIME) >>> 0;
13641
+ return Math.imul(seed >>> 0 ^ hash, MIX) >>> 0;
13642
+ }
13643
+ function streamFor(seed, name) {
13644
+ return createRng2(streamSeed(seed, name));
13645
+ }
13646
+ var DISTANCE_CURVES = {
13647
+ /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
13648
+ game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
13649
+ /** The game's `grito`: `1 / (1 + d² × 0.006)`, silent below 0.02 (jogo L62129–62130). */
13650
+ game_scream: { model: "inverse_square", k: 6e-3, minGain: 0.02 },
13651
+ /** The game's `voz`: `1 / (1 + d² × 0.01)`, silent below 0.02 (jogo L62146–62147). */
13652
+ game_voice: { model: "inverse_square", k: 0.01, minGain: 0.02 },
13653
+ /** The game's `boom`: `1.4 / (1 + d² × 0.002)`, never silent (jogo L68280). */
13654
+ game_boom: { model: "inverse_square", k: 2e-3, scale: 1.4 },
13655
+ /** The art scene's `outAt`: `vol / (1 + d² × 0.008)` (cena L1320). */
13656
+ art_scene: { model: "inverse_square", k: 8e-3 },
13657
+ /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
13658
+ animals: { model: "linear", range: 70, scale: 0.25, minGain: 5e-3 }
13659
+ };
13660
+ function falloffGain(falloff, volume, distance2) {
13661
+ if (!falloff || falloff.model === "none") return volume;
13662
+ if (falloff.model === "linear") {
13663
+ const level2 = Math.max(0, 1 - distance2 / falloff.range) * volume;
13664
+ return falloff.scale === void 0 ? level2 : level2 * falloff.scale;
13665
+ }
13666
+ const level = volume / (1 + distance2 * distance2 * falloff.k);
13667
+ return falloff.scale === void 0 ? level : falloff.scale * level;
13668
+ }
13669
+ function isSilent(falloff, gain) {
13670
+ if (!falloff || falloff.model === "none" || falloff.minGain === void 0) return false;
13671
+ return falloff.model === "linear" ? gain <= falloff.minGain : gain < falloff.minGain;
13672
+ }
13673
+ function cutoffDivisor(falloff, distance2) {
13674
+ if (!falloff || falloff.model !== "inverse_square" || falloff.cutoffK === void 0) return 1;
13675
+ return 1 + distance2 * falloff.cutoffK;
13676
+ }
13677
+ var DEFAULT_LISTENER = { position: [0, 0, 0], yaw: 0 };
13678
+ var PAN_LIMIT = 0.85;
13679
+ var PAN_MIN_DISTANCE = 2;
13680
+ function listenerDistance(listener, position, mode = "xz") {
13681
+ const dx = position[0] - listener.position[0];
13682
+ const dz = position[2] - listener.position[2];
13683
+ if (mode === "xz") return Math.hypot(dx, dz);
13684
+ return Math.hypot(dx, position[1] - listener.position[1], dz);
13685
+ }
13686
+ function panFor(listener, position) {
13687
+ const rightX = Math.cos(listener.yaw);
13688
+ const rightZ = -Math.sin(listener.yaw);
13689
+ const dx = position[0] - listener.position[0];
13690
+ const dz = position[2] - listener.position[2];
13691
+ const distance2 = Math.hypot(dx, dz) || 1;
13692
+ const pan = (dx * rightX + dz * rightZ) / Math.max(distance2, PAN_MIN_DISTANCE);
13693
+ return pan < -PAN_LIMIT ? -PAN_LIMIT : pan > PAN_LIMIT ? PAN_LIMIT : pan;
13694
+ }
13695
+ var voice_groups_default = {
13696
+ undead_voice: {
13697
+ maxConcurrent: 5,
13698
+ cooldown: 0.25,
13699
+ maxDistance: 40,
13700
+ sources: [
13701
+ "cena:L1342 `AU.groans > 4` refuses, so five play at once",
13702
+ "jogo:L66986 one groan every 0.25 s (`groanBudget`), only nearer than 40 m",
13703
+ "jogo:L70472 the budget counts down by the frame time"
13704
+ ]
13705
+ }
13706
+ };
13707
+ var VOICE_GROUPS = voice_groups_default;
13708
+ var BUS_PRIORITY = { ui: 5, weapons: 4, voices: 3, world: 2, weather: 1, ambience: 1 };
13709
+ var DEFAULT_MAX_VOICES = 32;
13710
+ function busPriority(bus) {
13711
+ return BUS_PRIORITY[bus];
13712
+ }
13713
+ function createVoiceManager(options = {}) {
13714
+ const groups = new Map(Object.entries(options.groups ?? VOICE_GROUPS));
13715
+ const maxVoices = options.maxVoices ?? DEFAULT_MAX_VOICES;
13716
+ const budgets = /* @__PURE__ */ new Map();
13717
+ let voices = [];
13718
+ let nextId = 1;
13719
+ let peak = 0;
13720
+ let dropped = 0;
13721
+ const refuse = (reason) => {
13722
+ dropped += 1;
13723
+ return { accepted: false, reason };
13724
+ };
13725
+ const groupCheck = (request, evicted) => {
13726
+ const group = request.group === void 0 ? void 0 : groups.get(request.group);
13727
+ if (!group) return void 0;
13728
+ if (group.maxDistance !== void 0 && !((request.distance ?? 0) < group.maxDistance)) return "distance";
13729
+ if (group.cooldown !== void 0 && (budgets.get(request.group) ?? 0) > 0) return "cooldown";
13730
+ const members = voices.filter((voice) => voice.group === request.group);
13731
+ if (group.maxConcurrent === void 0 || members.length < group.maxConcurrent) return void 0;
13732
+ if (!group.steal) return "group_full";
13733
+ evicted.push(members[0].id);
13734
+ return void 0;
13735
+ };
13736
+ return {
13737
+ request(request) {
13738
+ const evicted = [];
13739
+ const refusal = groupCheck(request, evicted);
13740
+ if (refusal) return refuse(refusal);
13741
+ const score = request.priority * request.gain;
13742
+ const remaining = voices.filter((voice) => !evicted.includes(voice.id));
13743
+ if (remaining.length >= maxVoices) {
13744
+ const lowest = remaining.reduce((low, voice) => voice.score < low.score ? voice : low);
13745
+ if (score <= lowest.score) return refuse("global_full");
13746
+ evicted.push(lowest.id);
13747
+ dropped += 1;
13748
+ }
13749
+ const group = request.group === void 0 ? void 0 : groups.get(request.group);
13750
+ if (group?.cooldown !== void 0) budgets.set(request.group, group.cooldown);
13751
+ const id = nextId++;
13752
+ voices = [...voices.filter((voice) => !evicted.includes(voice.id)), { id, ...request.group === void 0 ? {} : { group: request.group }, score, remaining: request.length }];
13753
+ peak = Math.max(peak, voices.length);
13754
+ return { accepted: true, id, evicted };
13755
+ },
13756
+ release(id) {
13757
+ voices = voices.filter((voice) => voice.id !== id);
13758
+ },
13759
+ setLength(id, seconds) {
13760
+ const voice = voices.find((entry) => entry.id === id);
13761
+ if (voice) voice.remaining = seconds;
13762
+ },
13763
+ advance(dt) {
13764
+ for (const [group, budget] of budgets) budgets.set(group, budget - dt);
13765
+ const ended = [];
13766
+ for (const voice of voices) {
13767
+ voice.remaining -= dt;
13768
+ if (voice.remaining <= 0) ended.push(voice.id);
13769
+ }
13770
+ if (ended.length > 0) voices = voices.filter((voice) => voice.remaining > 0);
13771
+ return ended;
13772
+ },
13773
+ setGroup(id, group) {
13774
+ if (group) groups.set(id, group);
13775
+ else groups.delete(id);
13776
+ },
13777
+ stats: () => ({ active: voices.length, peak, dropped, maxVoices })
13778
+ };
13779
+ }
13780
+ var DEFAULT_TAIL = 0.05;
13781
+ var DEFAULT_NOISE_SECONDS = 1.5;
13782
+ var OCCLUSION_GAIN = 0.5;
13783
+ var OCCLUSION_CUTOFF = 0.6;
13784
+ function isParamRef(range) {
13785
+ return typeof range === "object" && !Array.isArray(range);
13786
+ }
13787
+ function draw(range, scope2) {
13788
+ if (typeof range === "number") return range;
13789
+ if (isParamRef(range)) {
13790
+ const value = scope2.params[range.param];
13791
+ if (value === void 0) throw new Error(`game-audio: the recipe reads the param "${range.param}", which was not given`);
13792
+ return value * (range.scale ?? 1) + (range.add ?? 0);
13793
+ }
13794
+ const [min, max] = range;
13795
+ return min + scope2.random() * (max - min);
13796
+ }
13797
+ function drawTime(time, scope2) {
13798
+ if (time === "duration" || typeof time === "string" && time.endsWith("d")) return time;
13799
+ return draw(time, scope2);
13800
+ }
13801
+ function timeOf(time, duration) {
13802
+ if (typeof time === "number") return time;
13803
+ if (time === "duration") return duration;
13804
+ return Number(time.slice(0, -1)) * duration;
13805
+ }
13806
+ function drawAutomation(automation, scope2) {
13807
+ const pending = {};
13808
+ for (const key of Object.keys(automation)) {
13809
+ if (key === "set" && "set" in automation) pending.value = draw(automation.set, scope2);
13810
+ else if (key === "from" && "from" in automation) pending.from = draw(automation.from, scope2);
13811
+ else if (key === "to" && automation.to !== void 0) pending.to = automation.to === "from" ? pending.from ?? pending.value ?? 0 : draw(automation.to, scope2);
13812
+ else if (key === "curve") pending.curve = automation.curve;
13813
+ else if (key === "over" && automation.over !== void 0) pending.over = drawTime(automation.over, scope2);
13814
+ }
13815
+ return pending;
13816
+ }
13817
+ function finishAutomation(pending, duration) {
13818
+ if (pending.from !== void 0 && pending.to === void 0) return { from: pending.from };
13819
+ if (pending.from !== void 0) return { from: pending.from, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over ?? "duration", duration) };
13820
+ if (pending.to === void 0 || pending.over === void 0) return { value: pending.value };
13821
+ return { value: pending.value, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over, duration) };
13822
+ }
13823
+ var startFrequency = (automation) => automation.from ?? automation.value ?? 0;
13824
+ function drawFilter(spec, scope2) {
13825
+ const pending = { spec, frequency: {} };
13826
+ for (const key of Object.keys(spec)) {
13827
+ if (key === "frequency") pending.frequency = drawAutomation(spec.frequency, scope2);
13828
+ else if (key === "q" && spec.q !== void 0) pending.q = draw(spec.q, scope2);
13829
+ else if (key === "gain" && spec.gain !== void 0) pending.gain = draw(spec.gain, scope2);
13830
+ else if (key === "level" && spec.level !== void 0) pending.level = draw(spec.level, scope2);
13831
+ }
13832
+ return pending;
13833
+ }
13834
+ function drawModulator(modulator, scope2) {
13835
+ const pending = { modulator, rate: 0 };
13836
+ for (const key of Object.keys(modulator)) {
13837
+ if (key === "rate") pending.rate = draw(modulator.rate, scope2);
13838
+ else if (key === "depth" && !(typeof modulator.depth === "object" && "scaleOf" in modulator.depth)) pending.depth = draw(modulator.depth, scope2);
13839
+ }
13840
+ return pending;
13841
+ }
13842
+ function drawVoice(source, scope2) {
13843
+ const pending = { frequency: {}, vibratoRate: 0, formants: [], breath: { frequency: 0, q: source.breath.q, gain: 0 } };
13844
+ for (const key of Object.keys(source)) {
13845
+ if (key === "frequency") pending.frequency = drawAutomation(source.frequency, scope2);
13846
+ else if (key === "vibrato") pending.vibratoRate = draw(source.vibrato.rate, scope2);
13847
+ else if (key === "formants") pending.formants = source.formants.map((formant) => ({ frequency: draw(formant.frequency, scope2), q: formant.q }));
13848
+ else if (key === "breath") pending.breath = { frequency: draw(source.breath.frequency, scope2), q: source.breath.q, gain: draw(source.breath.gain, scope2) };
13849
+ }
13850
+ return pending;
13851
+ }
13852
+ function drawLayer(layer, scope2) {
13853
+ const pending = { at: 0, gain: 1, filters: [], modulators: [] };
13854
+ for (const key of Object.keys(layer)) {
13855
+ if (key === "at" && layer.at !== void 0) pending.at = draw(layer.at, scope2);
13856
+ else if (key === "duration" && layer.duration !== void 0) pending.duration = draw(layer.duration, scope2);
13857
+ else if (key === "gain" && layer.gain !== void 0) pending.gain = draw(layer.gain, scope2);
13858
+ else if (key === "source" && layer.source.kind === "osc") pending.automation = drawAutomation(layer.source.frequency, scope2);
13859
+ else if (key === "source" && layer.source.kind === "formant_voice") pending.voice = drawVoice(layer.source, scope2);
13860
+ else if (key === "filters" && layer.filters) pending.filters = layer.filters.bank.map((spec) => drawFilter(spec, scope2));
13861
+ else if (key === "modulators" && layer.modulators) pending.modulators = layer.modulators.map((modulator) => drawModulator(modulator, scope2));
13862
+ }
13863
+ return pending;
13864
+ }
13865
+ function drawOffset(source, duration, scope2, noiseSeconds) {
13866
+ if (source.kind === "formant_voice") return scope2.random();
13867
+ if (source.kind !== "noise" || source.offset === void 0) return 0;
13868
+ if (typeof source.offset === "number") return source.offset;
13869
+ if (source.offset === "fit") return scope2.random() * Math.max(0, noiseSeconds - duration);
13870
+ return scope2.random() * (source.offsetScale ?? 1);
13871
+ }
13872
+ function finishSource(layer, pending, duration, offset) {
13873
+ const source = layer.source;
13874
+ if (source.kind === "noise") return { kind: "noise", offset, loop: source.loop === true };
13875
+ if (source.kind === "osc") return { kind: "osc", wave: source.wave, frequency: finishAutomation(pending.automation ?? {}, duration) };
13876
+ const voice = pending.voice;
13877
+ return {
13878
+ kind: "formant_voice",
13879
+ wave: source.wave,
13880
+ frequency: finishAutomation(voice.frequency, duration),
13881
+ vibrato: { rate: voice.vibratoRate, depth: startFrequency(voice.frequency) * source.vibrato.depth },
13882
+ formants: voice.formants,
13883
+ breath: { ...voice.breath, offset },
13884
+ mix: source.mix
13885
+ };
13886
+ }
13887
+ function finishModulator(pending, sourceFrequency) {
13888
+ const { modulator } = pending;
13889
+ const depth = typeof modulator.depth === "object" && "scaleOf" in modulator.depth ? sourceFrequency * modulator.depth.factor : pending.depth ?? 0;
13890
+ return { target: modulator.target, ...modulator.wave ? { wave: modulator.wave } : {}, rate: pending.rate, depth, ...modulator.base === void 0 ? {} : { base: modulator.base } };
13891
+ }
13892
+ function finishEnvelope(envelope, duration, level) {
13893
+ return envelope.map(([time, value, curve, fixed2]) => ({ time: timeOf(time, duration), value: fixed2 === "fixed" ? value : value * level, curve }));
13894
+ }
13895
+ function planLayer(layer, scope2, defaults, levelInEnvelope, soundLevel) {
13896
+ const pending = drawLayer(layer, scope2);
13897
+ const duration = pending.duration ?? defaults.duration;
13898
+ if (duration === void 0) throw new Error("game-audio: a layer needs a duration (its own or the recipe\u2019s)");
13899
+ const offset = drawOffset(layer.source, duration, scope2, defaults.noiseSeconds);
13900
+ const level = pending.gain * soundLevel;
13901
+ const sourceFrequency = layer.source.kind === "noise" ? 0 : startFrequency(pending.automation ?? pending.voice?.frequency ?? {});
13902
+ const planned = {
13903
+ at: pending.at,
13904
+ duration,
13905
+ tail: layer.tail ?? defaults.tail ?? DEFAULT_TAIL,
13906
+ level,
13907
+ ...layer.wet === void 0 ? {} : { wet: layer.wet },
13908
+ source: finishSource(layer, pending, duration, offset),
13909
+ ...layer.filters ? {
13910
+ filters: {
13911
+ mode: layer.filters.mode,
13912
+ bank: pending.filters.map(({ spec, frequency, q, gain, level: filterLevel }) => ({
13913
+ type: spec.type,
13914
+ frequency: finishAutomation(frequency, duration),
13915
+ ...q === void 0 ? {} : { q },
13916
+ ...gain === void 0 ? {} : { gain },
13917
+ ...filterLevel === void 0 ? {} : { level: filterLevel }
13918
+ }))
13919
+ }
13920
+ } : {},
13921
+ modulators: pending.modulators.map((modulator) => finishModulator(modulator, sourceFrequency)),
13922
+ envelope: finishEnvelope(layer.envelope, duration, levelInEnvelope ? level : 1)
13923
+ };
13924
+ if (!layer.repeat) return [planned];
13925
+ return Array.from({ length: layer.repeat.count }, (_, index) => ({ ...planned, at: planned.at + index * (layer.repeat?.spacing ?? 0) }));
13926
+ }
13927
+ function scaleAutomation(automation, divisor) {
13928
+ const scale = (value) => value === void 0 ? void 0 : value / divisor;
13929
+ const scaled = { ...automation };
13930
+ for (const key of ["value", "from", "to"]) if (automation[key] !== void 0) scaled[key] = scale(automation[key]);
13931
+ return scaled;
13932
+ }
13933
+ function muffle(layer, divisor) {
13934
+ if (divisor === 1 || !layer.filters) return layer;
13935
+ const bank = layer.filters.bank.map((filter) => filter.type === "lowpass" ? { ...filter, frequency: scaleAutomation(filter.frequency, divisor) } : filter);
13936
+ return { ...layer, filters: { ...layer.filters, bank } };
13937
+ }
13938
+ var layerEnd = (layer) => layer.at + layer.duration + layer.tail;
13939
+ function planSound(recipe, input = {}) {
13940
+ const listener = input.listener ?? DEFAULT_LISTENER;
13941
+ const distance2 = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0);
13942
+ const occlusion = Math.min(1, Math.max(0, input.occlusion ?? 0));
13943
+ const order = recipe.order ?? "layered";
13944
+ const falloffLevel = falloffGain(recipe.falloff, input.volume ?? 1, distance2);
13945
+ const level = occlusion > 0 ? falloffLevel * (1 - OCCLUSION_GAIN * occlusion) : falloffLevel;
13946
+ const silent = isSilent(recipe.falloff, falloffLevel);
13947
+ const base = {
13948
+ id: recipe.id,
13949
+ bus: recipe.bus,
13950
+ order,
13951
+ ...recipe.group === void 0 ? {} : { group: recipe.group },
13952
+ priority: recipe.priority ?? busPriority(recipe.bus),
13953
+ level,
13954
+ distance: distance2,
13955
+ ...recipe.pan ? { pan: input.position ? panFor(listener, input.position) : 0 } : {}
13956
+ };
13957
+ if (silent) return recipe.keepSilent ? { ...base, silent: true, length: 0, layers: [] } : null;
13958
+ const scope2 = { random: input.random ?? createRng2(1), params: { ...input.params } };
13959
+ for (const [name, range] of Object.entries(recipe.vars ?? {})) if (scope2.params[name] === void 0) scope2.params[name] = draw(range, scope2);
13960
+ const defaults = {
13961
+ ...recipe.duration === void 0 ? {} : { duration: draw(recipe.duration, scope2) },
13962
+ ...recipe.tail === void 0 ? {} : { tail: recipe.tail },
13963
+ noiseSeconds: input.noiseSeconds ?? DEFAULT_NOISE_SECONDS
13964
+ };
13965
+ const divisor = cutoffDivisor(recipe.falloff, distance2) / (1 - OCCLUSION_CUTOFF * occlusion);
13966
+ const layers = recipe.layers.flatMap((layer) => planLayer(layer, scope2, defaults, order !== "layered", level)).map((layer) => muffle(layer, divisor));
13967
+ return { ...base, silent: false, length: Math.max(0, ...layers.map(layerEnd)), layers };
13968
+ }
13969
+ var LOOP_START_ABOVE = 2e-3;
13970
+ var LOOP_SMOOTHING = { volume: 0.15, frequency: 0.08 };
13971
+ function createLoopState(recipe, open = false) {
13972
+ const startAbove = recipe.startAbove ?? LOOP_START_ABOVE;
13973
+ let created = open;
13974
+ let volume = recipe.volume ?? 0;
13975
+ return {
13976
+ set(state) {
13977
+ volume = state.volume ?? volume;
13978
+ const create = !created;
13979
+ if (create && !(volume >= startAbove)) return null;
13980
+ created = true;
13981
+ const frequency = recipe.source.kind === "osc" && state.frequency ? state.frequency : void 0;
13982
+ return { create, volume, ...frequency === void 0 ? {} : { frequency } };
13983
+ },
13984
+ get created() {
13985
+ return created;
13986
+ },
13987
+ reset() {
13988
+ created = false;
13989
+ }
13990
+ };
13991
+ }
13992
+ var mix_profiles_default = [
13993
+ {
13994
+ id: "game",
13995
+ master: 0.5,
13996
+ noise: { seconds: 1.5, create: "lazy" },
13997
+ sources: ["jogo:L62077-62081 master 0.5 to the output", "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"]
13998
+ },
13999
+ {
14000
+ id: "art_scene",
14001
+ master: 0.85,
14002
+ compressor: { threshold: -12, ratio: 5 },
14003
+ reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
14004
+ noise: { seconds: 2, create: "eager" },
14005
+ sources: [
14006
+ "cena:L1308 compressor -12 dB, ratio 5",
14007
+ "cena:L1309 master 0.85",
14008
+ "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
14009
+ "cena:L1311 wet 0.4",
14010
+ "cena:L1312 noise 2 s made with the chain"
14011
+ ]
14012
+ }
14013
+ ];
14014
+ var MIX_PROFILES = Object.fromEntries(
14015
+ mix_profiles_default.map((profile) => [profile.id, profile])
14016
+ );
14017
+ var DEFAULT_MIX = "art_scene";
14018
+ function resolveMix(mix2, profiles = MIX_PROFILES) {
14019
+ if (typeof mix2 !== "string") return mix2;
14020
+ const profile = profiles[mix2];
14021
+ if (!profile) throw new Error(`game-audio: unknown mix profile "${mix2}" (known: ${Object.keys(profiles).join(", ")})`);
14022
+ return profile;
14023
+ }
14024
+ function busGain(setting) {
14025
+ if (!setting) return 1;
14026
+ return setting.muted ? 0 : setting.gain ?? 1;
14027
+ }
14028
+ function isPlainObject2(value) {
14029
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14030
+ }
14031
+ function mergeRecords(base, patch) {
14032
+ const merged = { ...base };
14033
+ for (const [key, value] of Object.entries(patch)) {
14034
+ if (value === void 0) continue;
14035
+ const current = merged[key];
14036
+ merged[key] = isPlainObject2(current) && isPlainObject2(value) ? mergeRecords(current, value) : value;
14037
+ }
14038
+ return merged;
14039
+ }
14040
+ function createSoundRegistry(banks = []) {
14041
+ const sounds = /* @__PURE__ */ new Map();
14042
+ const loops = /* @__PURE__ */ new Map();
14043
+ const owners = /* @__PURE__ */ new Map();
14044
+ const registry = {
14045
+ register(bank) {
14046
+ for (const sound2 of bank.sounds ?? []) {
14047
+ sounds.set(sound2.id, sound2);
14048
+ owners.set(sound2.id, bank.id);
14049
+ }
14050
+ for (const loop of bank.loops ?? []) {
14051
+ loops.set(loop.id, loop);
14052
+ owners.set(loop.id, bank.id);
14053
+ }
14054
+ },
14055
+ patch(id, patch) {
14056
+ const sound2 = sounds.get(id);
14057
+ if (sound2) sounds.set(id, mergeRecords(sound2, patch));
14058
+ const loop = loops.get(id);
14059
+ if (loop) loops.set(id, mergeRecords(loop, patch));
14060
+ },
14061
+ remove(id) {
14062
+ sounds.delete(id);
14063
+ loops.delete(id);
14064
+ owners.delete(id);
14065
+ },
14066
+ sound: (id) => sounds.get(id),
14067
+ loop: (id) => loops.get(id),
14068
+ soundIds: () => [...sounds.keys()],
14069
+ loopIds: () => [...loops.keys()],
14070
+ bankOf: (id) => owners.get(id)
14071
+ };
14072
+ for (const bank of banks) registry.register(bank);
14073
+ return registry;
14074
+ }
14075
+ var game_sfx_default = {
14076
+ sources: ["jogo:L62089-62107"],
14077
+ note: "The game's sfx table P: [lowpass cutoff Hz, peak gain, seconds]. Played as a noise burst with the game's distance curve (falloff game_sfx) and a 0.05 s tail. Team names in `team`.",
14078
+ classes: [
14079
+ { id: "pistol", team: "pistola", bus: "weapons", cutoff: 1800, gain: 0.9, seconds: 0.18 },
14080
+ { id: "shotgun", team: "escopeta", bus: "weapons", cutoff: 900, gain: 1.2, seconds: 0.35 },
14081
+ { id: "rifle", team: "fuzil", bus: "weapons", cutoff: 1400, gain: 1.1, seconds: 0.28 },
14082
+ { id: "long_rifle", team: "rifle", bus: "weapons", cutoff: 1100, gain: 1.3, seconds: 0.45 },
14083
+ { id: "impact", team: "impacto", bus: "world", cutoff: 2500, gain: 0.25, seconds: 0.05 },
14084
+ { id: "flesh", team: "carne", bus: "world", cutoff: 600, gain: 0.35, seconds: 0.08 },
14085
+ { id: "step", team: "passo", bus: "world", cutoff: 400, gain: 0.08, seconds: 0.05 },
14086
+ { id: "glass", team: "vidro", bus: "world", cutoff: 5e3, gain: 0.5, seconds: 0.3 },
14087
+ { id: "wood", team: "madeira", bus: "world", cutoff: 700, gain: 0.5, seconds: 0.2 },
14088
+ { id: "reload", team: "recarga", bus: "weapons", cutoff: 3e3, gain: 0.15, seconds: 0.06 },
14089
+ { id: "empty", team: "vazio", bus: "weapons", cutoff: 4e3, gain: 0.12, seconds: 0.03 },
14090
+ { id: "explosion", team: "explosao", bus: "world", cutoff: 200, gain: 1.6, seconds: 1.4 }
14091
+ ],
14092
+ fallback: { id: "unknown", team: "(any other name)", bus: "world", cutoff: 1e3, gain: 0.5, seconds: 0.1 }
14093
+ };
14094
+ var GAME_SFX = game_sfx_default;
14095
+ var GAME_SFX_CLASSES = GAME_SFX.classes;
14096
+ var GAME_SFX_FALLBACK = GAME_SFX.fallback;
14097
+ function noiseClassRecipe(row, id = `sfx_${row.id}`) {
14098
+ return {
14099
+ id,
14100
+ bus: row.bus,
14101
+ order: "burst",
14102
+ duration: row.seconds,
14103
+ tail: 0.05,
14104
+ layers: [
14105
+ {
14106
+ source: { kind: "noise", offset: "random" },
14107
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: row.cutoff } }] },
14108
+ envelope: [
14109
+ [0, row.gain, "set"],
14110
+ ["1d", 1e-3, "exp", "fixed"]
14111
+ ]
14112
+ }
14113
+ ],
14114
+ falloff: DISTANCE_CURVES.game_sfx,
14115
+ sources: [...GAME_SFX.sources.map((source) => `${source} ${row.team}`)]
14116
+ };
14117
+ }
14118
+ var GAME_SFX_BANK = { id: "game_sfx", sounds: [...GAME_SFX_CLASSES, GAME_SFX_FALLBACK].map((row) => noiseClassRecipe(row)) };
14119
+ function fillNoise(random, length2) {
14120
+ const samples = new Float32Array(length2);
14121
+ for (let index = 0; index < length2; index++) samples[index] = random() * 2 - 1;
14122
+ return samples;
14123
+ }
14124
+ function fillImpulse(random, options) {
14125
+ const size = Math.floor(options.length);
14126
+ return Array.from({ length: options.channels }, () => {
14127
+ const samples = new Float32Array(size);
14128
+ for (let index = 0; index < options.length; index++) {
14129
+ const value = (random() * 2 - 1) * Math.exp(options.decay * Math.log(1 - index / options.length));
14130
+ if (index < size) samples[index] = value;
14131
+ }
14132
+ return samples;
14133
+ });
14134
+ }
14135
+ function bufferOf(context, channels, requestedLength) {
14136
+ const buffer = context.createBuffer(channels.length, requestedLength, context.sampleRate);
14137
+ channels.forEach((samples, channel) => buffer.getChannelData(channel).set(samples));
14138
+ return buffer;
14139
+ }
14140
+ function buildChain(context, profile, random) {
14141
+ let compressor;
14142
+ if (profile.compressor) {
14143
+ compressor = context.createDynamicsCompressor();
14144
+ compressor.threshold.value = profile.compressor.threshold;
14145
+ compressor.ratio.value = profile.compressor.ratio;
14146
+ if (profile.compressor.knee !== void 0) compressor.knee.value = profile.compressor.knee;
14147
+ if (profile.compressor.attack !== void 0) compressor.attack.value = profile.compressor.attack;
14148
+ if (profile.compressor.release !== void 0) compressor.release.value = profile.compressor.release;
14149
+ compressor.connect(context.destination);
14150
+ }
14151
+ const master = context.createGain();
14152
+ master.gain.value = profile.master;
14153
+ master.connect(compressor ?? context.destination);
14154
+ if (!profile.reverb) {
14155
+ return {
14156
+ master,
14157
+ disconnect: () => {
14158
+ master.disconnect();
14159
+ compressor?.disconnect();
14160
+ }
14161
+ };
14162
+ }
14163
+ const convolver = context.createConvolver();
14164
+ const length2 = context.sampleRate * profile.reverb.seconds;
14165
+ convolver.buffer = bufferOf(context, fillImpulse(random, { channels: 2, length: length2, decay: profile.reverb.decay }), length2);
14166
+ const wet = context.createGain();
14167
+ wet.gain.value = profile.reverb.wet;
14168
+ wet.connect(convolver);
14169
+ convolver.connect(master);
14170
+ return {
14171
+ master,
14172
+ wet,
14173
+ disconnect: () => {
14174
+ wet.disconnect();
14175
+ convolver.disconnect();
14176
+ master.disconnect();
14177
+ compressor?.disconnect();
14178
+ }
14179
+ };
14180
+ }
14181
+ function globalContext() {
14182
+ const scope2 = globalThis;
14183
+ const Constructor = scope2.AudioContext ?? scope2.webkitAudioContext;
14184
+ return Constructor ? new Constructor() : null;
14185
+ }
14186
+ function unlockContext(context) {
14187
+ if (context.state === "suspended") void context.resume();
14188
+ }
14189
+ function assign(param, automation, time) {
14190
+ if (automation.value !== void 0) param.value = automation.value;
14191
+ if (automation.from !== void 0) param.setValueAtTime(automation.from, time);
14192
+ }
14193
+ function ramp(param, curve, value, at) {
14194
+ if (curve === "set") param.setValueAtTime(value, at);
14195
+ else if (curve === "exp") param.exponentialRampToValueAtTime(value, at);
14196
+ else param.linearRampToValueAtTime(value, at);
14197
+ }
14198
+ function rampTo(param, automation, time) {
14199
+ if (automation.to !== void 0 && automation.at !== void 0) ramp(param, automation.curve ?? "linear", automation.to, time + automation.at);
14200
+ }
14201
+ function applyEnvelope(param, points, time) {
14202
+ for (const point of points) ramp(param, point.curve, point.value, time + point.time);
14203
+ }
14204
+ function noiseSource(target, loop) {
14205
+ const source = target.context.createBufferSource();
14206
+ source.buffer = target.noise();
14207
+ if (loop) source.loop = true;
14208
+ return source;
14209
+ }
14210
+ function filterNode(context, filter, time) {
14211
+ const node = context.createBiquadFilter();
14212
+ node.type = filter.type;
14213
+ assign(node.frequency, filter.frequency, time);
14214
+ if (filter.q !== void 0) node.Q.value = filter.q;
14215
+ if (filter.gain !== void 0) node.gain.value = filter.gain;
14216
+ return node;
14217
+ }
14218
+ function gainNode(context, value) {
14219
+ const node = context.createGain();
14220
+ node.gain.value = value;
14221
+ return node;
14222
+ }
14223
+ function voiceOf(sources) {
14224
+ return {
14225
+ sources,
14226
+ stop() {
14227
+ for (const source of sources) {
14228
+ try {
14229
+ source.stop();
14230
+ } catch {
14231
+ }
14232
+ }
14233
+ }
14234
+ };
14235
+ }
14236
+ var fixed = (range) => typeof range === "number" ? range : 0;
14237
+ var fixedFrequency = (automation) => fixed("set" in automation ? automation.set : automation.from);
14238
+ function sourceOf(recipe, target) {
14239
+ if (recipe.source.kind === "noise") return { source: noiseSource(target, true) };
14240
+ const oscillator = target.context.createOscillator();
14241
+ oscillator.type = recipe.source.wave;
14242
+ if (recipe.source.frequency) oscillator.frequency.value = fixedFrequency(recipe.source.frequency);
14243
+ return { source: oscillator, frequency: oscillator.frequency };
14244
+ }
14245
+ function stopAll(sources) {
14246
+ return () => {
14247
+ for (const source of sources) {
14248
+ try {
14249
+ source.stop();
14250
+ } catch {
14251
+ }
14252
+ }
14253
+ };
14254
+ }
14255
+ function realizeGameLoop(recipe, target) {
14256
+ const { context } = target;
14257
+ const gain = context.createGain();
14258
+ const filters = (recipe.filters?.bank ?? []).map(() => context.createBiquadFilter());
14259
+ gain.gain.value = 0;
14260
+ const last = filters.reduce((previous, filter) => previous ? previous.connect(filter) : filter, void 0);
14261
+ last?.connect(gain);
14262
+ gain.connect(target.output);
14263
+ const { source, frequency } = sourceOf(recipe, target);
14264
+ recipe.filters?.bank.forEach((spec, index) => {
14265
+ const filter = filters[index];
14266
+ filter.type = spec.type;
14267
+ filter.frequency.value = fixedFrequency(spec.frequency);
14268
+ if (spec.q !== void 0) filter.Q.value = fixed(spec.q);
14269
+ });
14270
+ const sources = [source];
14271
+ for (const modulator of recipe.modulators ?? []) {
14272
+ const lfo = context.createOscillator();
14273
+ const depth = context.createGain();
14274
+ const stage2 = context.createGain();
14275
+ if (modulator.wave) lfo.type = modulator.wave;
14276
+ lfo.frequency.value = fixed(modulator.rate);
14277
+ depth.gain.value = fixed(modulator.depth);
14278
+ stage2.gain.value = modulator.base ?? 0;
14279
+ lfo.connect(depth);
14280
+ depth.connect(stage2.gain);
14281
+ last?.disconnect();
14282
+ last?.connect(stage2);
14283
+ stage2.connect(gain);
14284
+ lfo.start();
14285
+ sources.push(lfo);
14286
+ }
14287
+ source.connect(filters[0] ?? gain);
14288
+ source.start();
14289
+ return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll(sources) };
14290
+ }
14291
+ function realizeChainLoop(recipe, target, volume) {
14292
+ const { context } = target;
14293
+ const { source, frequency } = sourceOf(recipe, target);
14294
+ const filters = (recipe.filters?.bank ?? []).map(
14295
+ (spec) => filterNode(context, { type: spec.type, frequency: { value: fixedFrequency(spec.frequency) }, ...spec.q === void 0 ? {} : { q: fixed(spec.q) } }, target.time)
14296
+ );
14297
+ const gain = gainNode(context, volume);
14298
+ const lfos = (recipe.modulators ?? []).map((modulator) => {
14299
+ const lfo = context.createOscillator();
14300
+ if (modulator.wave) lfo.type = modulator.wave;
14301
+ lfo.frequency.value = fixed(modulator.rate);
14302
+ const depth = gainNode(context, fixed(modulator.depth));
14303
+ lfo.connect(depth);
14304
+ depth.connect(gain.gain);
14305
+ return lfo;
14306
+ });
14307
+ let tail = source;
14308
+ for (const filter of filters) tail = tail.connect(filter);
14309
+ tail.connect(gain);
14310
+ gain.connect(target.output);
14311
+ source.start();
14312
+ for (const lfo of lfos) lfo.start();
14313
+ return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll([source, ...lfos]) };
14314
+ }
14315
+ var INERT = { set() {
14316
+ }, stop() {
14317
+ } };
14318
+ function createLoopHandle(recipe, host, volume) {
14319
+ if (!recipe) return INERT;
14320
+ const loopRecipe = recipe;
14321
+ const isChain = loopRecipe.order === "chain_loop";
14322
+ const opening = volume ?? loopRecipe.volume ?? 0;
14323
+ const smoothing = loopRecipe.smoothing ?? LOOP_SMOOTHING;
14324
+ let state = createLoopState(loopRecipe);
14325
+ let nodes;
14326
+ const entry = {
14327
+ get created() {
14328
+ return nodes !== void 0;
14329
+ },
14330
+ rewire() {
14331
+ nodes?.gain.disconnect();
14332
+ nodes?.gain.connect(host.output(loopRecipe.bus));
14333
+ },
14334
+ stop() {
14335
+ nodes?.stop();
14336
+ nodes?.gain.disconnect();
14337
+ nodes = void 0;
14338
+ state = createLoopState(loopRecipe);
14339
+ host.unregister(entry);
14340
+ }
14341
+ };
14342
+ const create = (context, startVolume) => {
14343
+ if (loopRecipe.source.kind === "noise") host.prepareNoise();
14344
+ const target = { context, output: host.output(loopRecipe.bus), noise: host.noise, time: context.currentTime };
14345
+ const made = isChain ? realizeChainLoop(loopRecipe, target, startVolume) : realizeGameLoop(loopRecipe, target);
14346
+ host.register(entry);
14347
+ return made;
14348
+ };
14349
+ const open = () => {
14350
+ const context = host.ready();
14351
+ if (!context) return;
14352
+ nodes = create(context, opening);
14353
+ state = createLoopState({ ...loopRecipe, volume: opening }, true);
14354
+ };
14355
+ if (isChain) open();
14356
+ return {
14357
+ set(next) {
14358
+ if (isChain && !nodes) open();
14359
+ const context = host.ready();
14360
+ if (!context) return;
14361
+ const action = state.set(next);
14362
+ if (!action) return;
14363
+ nodes ?? (nodes = create(context, 0));
14364
+ nodes.gain.gain.setTargetAtTime(action.volume, context.currentTime, smoothing.volume);
14365
+ if (action.frequency !== void 0 && nodes.frequency) nodes.frequency.setTargetAtTime(action.frequency, context.currentTime, smoothing.frequency);
14366
+ },
14367
+ stop: entry.stop
14368
+ };
14369
+ }
14370
+ function createNoiseStore(random) {
14371
+ const store = {
14372
+ random,
14373
+ reset() {
14374
+ store.samples = void 0;
14375
+ store.buffer = void 0;
14376
+ }
14377
+ };
14378
+ return store;
14379
+ }
14380
+ function prepareNoise(store, context, seconds) {
14381
+ store.samples ?? (store.samples = fillNoise(store.random, Math.floor(context.sampleRate * seconds)));
14382
+ return store.samples;
14383
+ }
14384
+ function noiseBuffer(store, context, seconds) {
14385
+ const samples = prepareNoise(store, context, seconds);
14386
+ store.buffer ?? (store.buffer = bufferOf(context, [samples], context.sampleRate * seconds));
14387
+ return store.buffer;
14388
+ }
14389
+ var end = (layer, start) => start + layer.duration + layer.tail;
14390
+ var firstFilter = (layer) => layer.filters?.bank[0];
14391
+ var offsetOf = (layer) => layer.source.kind === "noise" ? layer.source.offset : 0;
14392
+ function setFilter(node, filter, time) {
14393
+ if (!filter) return;
14394
+ node.type = filter.type;
14395
+ assign(node.frequency, filter.frequency, time);
14396
+ if (filter.q !== void 0) node.Q.value = filter.q;
14397
+ }
14398
+ function realizeBurst(plan, target) {
14399
+ const { context } = target;
14400
+ const layer = plan.layers[0];
14401
+ const time = target.time + layer.at;
14402
+ const gain = context.createGain();
14403
+ gain.connect(target.output);
14404
+ const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
14405
+ const filter = context.createBiquadFilter();
14406
+ source.connect(filter);
14407
+ filter.connect(gain);
14408
+ setFilter(filter, firstFilter(layer), time);
14409
+ applyEnvelope(gain.gain, layer.envelope, time);
14410
+ source.start(time, offsetOf(layer));
14411
+ source.stop(end(layer, time));
14412
+ return voiceOf([source]);
14413
+ }
14414
+ function realizeScream(plan, target) {
14415
+ const { context } = target;
14416
+ const layer = plan.layers[0];
14417
+ const time = target.time + layer.at;
14418
+ const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
14419
+ const gain = context.createGain();
14420
+ gain.connect(target.output);
14421
+ for (const band of layer.filters?.bank ?? []) {
14422
+ const filter = context.createBiquadFilter();
14423
+ filter.type = band.type;
14424
+ if (band.q !== void 0) filter.Q.value = band.q;
14425
+ assign(filter.frequency, band.frequency, time);
14426
+ rampTo(filter.frequency, band.frequency, time);
14427
+ const level = context.createGain();
14428
+ level.gain.value = band.level ?? 1;
14429
+ source.connect(filter);
14430
+ filter.connect(level);
14431
+ level.connect(gain);
14432
+ }
14433
+ const sources = [source];
14434
+ const lfos = layer.modulators.map((modulator) => {
14435
+ const lfo = context.createOscillator();
14436
+ const depth = context.createGain();
14437
+ if (modulator.wave) lfo.type = modulator.wave;
14438
+ lfo.frequency.value = modulator.rate;
14439
+ depth.gain.value = modulator.depth;
14440
+ lfo.connect(depth);
14441
+ depth.connect(gain.gain);
14442
+ return lfo;
14443
+ });
14444
+ applyEnvelope(gain.gain, layer.envelope, time);
14445
+ source.start(time, offsetOf(layer));
14446
+ source.stop(end(layer, time));
14447
+ for (const lfo of lfos) {
14448
+ lfo.start(time);
14449
+ lfo.stop(end(layer, time));
14450
+ sources.push(lfo);
14451
+ }
14452
+ return voiceOf(sources);
14453
+ }
14454
+ function realizeGroan(plan, target) {
14455
+ const { context } = target;
14456
+ const layer = plan.layers[0];
14457
+ const time = target.time + layer.at;
14458
+ const oscillator = context.createOscillator();
14459
+ const gain = context.createGain();
14460
+ const filter = context.createBiquadFilter();
14461
+ const band = firstFilter(layer);
14462
+ if (layer.source.kind === "osc") oscillator.type = layer.source.wave;
14463
+ if (band) {
14464
+ filter.type = band.type;
14465
+ if (band.q !== void 0) filter.Q.value = band.q;
14466
+ }
14467
+ if (layer.source.kind === "osc") {
14468
+ assign(oscillator.frequency, layer.source.frequency, time);
14469
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14470
+ }
14471
+ if (band) assign(filter.frequency, band.frequency, time);
14472
+ applyEnvelope(gain.gain, layer.envelope, time);
14473
+ oscillator.connect(filter);
14474
+ filter.connect(gain);
14475
+ gain.connect(target.output);
14476
+ oscillator.start(time);
14477
+ oscillator.stop(end(layer, time));
14478
+ return voiceOf([oscillator]);
14479
+ }
14480
+ function realizeBoom(plan, target) {
14481
+ const { context } = target;
14482
+ const layer = plan.layers[0];
14483
+ const time = target.time + layer.at;
14484
+ const oscillator = context.createOscillator();
14485
+ const gain = context.createGain();
14486
+ if (layer.source.kind === "osc") {
14487
+ oscillator.type = layer.source.wave;
14488
+ assign(oscillator.frequency, layer.source.frequency, time);
14489
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14490
+ }
14491
+ applyEnvelope(gain.gain, layer.envelope, time);
14492
+ oscillator.connect(gain);
14493
+ gain.connect(target.output);
14494
+ oscillator.start(time);
14495
+ oscillator.stop(end(layer, time));
14496
+ return voiceOf([oscillator]);
14497
+ }
14498
+ function realizeStatic(plan, target) {
14499
+ const { context } = target;
14500
+ const layer = plan.layers[0];
14501
+ const time = target.time + layer.at;
14502
+ const gain = context.createGain();
14503
+ const source = context.createBufferSource();
14504
+ const filter = context.createBiquadFilter();
14505
+ source.buffer = target.noise();
14506
+ setFilter(filter, firstFilter(layer), time);
14507
+ applyEnvelope(gain.gain, layer.envelope, time);
14508
+ source.connect(filter);
14509
+ filter.connect(gain);
14510
+ gain.connect(target.output);
14511
+ source.start(time, offsetOf(layer));
14512
+ source.stop(end(layer, time));
14513
+ return voiceOf([source]);
14514
+ }
14515
+ function realizeChord(plan, target) {
14516
+ const { context } = target;
14517
+ const layers = plan.layers;
14518
+ const oscillators = layers.map(() => context.createOscillator());
14519
+ const gain = context.createGain();
14520
+ for (let index = layers.length - 1; index >= 0; index--) {
14521
+ const source = layers[index].source;
14522
+ if (source.kind === "osc") oscillators[index].type = source.wave;
14523
+ }
14524
+ layers.forEach((layer, index) => {
14525
+ if (layer.source.kind === "osc") assign(oscillators[index].frequency, layer.source.frequency, target.time + layer.at);
14526
+ });
14527
+ applyEnvelope(gain.gain, layers[0].envelope, target.time + layers[0].at);
14528
+ for (const oscillator of oscillators) oscillator.connect(gain);
14529
+ gain.connect(target.output);
14530
+ layers.forEach((layer, index) => oscillators[index].start(target.time + layer.at));
14531
+ layers.forEach((layer, index) => oscillators[index].stop(end(layer, target.time + layer.at)));
14532
+ return voiceOf(oscillators);
14533
+ }
14534
+ function realizeCalls(plan, target) {
14535
+ const { context } = target;
14536
+ const gain = context.createGain();
14537
+ const filter = context.createBiquadFilter();
14538
+ if (plan.silent) return voiceOf([]);
14539
+ const band = firstFilter(plan.layers[0]);
14540
+ if (band) filter.type = band.type;
14541
+ filter.connect(gain);
14542
+ gain.connect(target.output);
14543
+ const oscillators = plan.layers.map((layer) => {
14544
+ const time = target.time + layer.at;
14545
+ const oscillator = context.createOscillator();
14546
+ if (layer.source.kind === "osc") {
14547
+ oscillator.type = layer.source.wave;
14548
+ assign(oscillator.frequency, layer.source.frequency, time);
14549
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14550
+ }
14551
+ if (band) {
14552
+ assign(filter.frequency, band.frequency, time);
14553
+ if (band.q !== void 0) filter.Q.value = band.q;
14554
+ }
14555
+ const envelope = context.createGain();
14556
+ applyEnvelope(envelope.gain, layer.envelope, time);
14557
+ oscillator.connect(envelope);
14558
+ envelope.connect(filter);
14559
+ oscillator.start(time);
14560
+ oscillator.stop(end(layer, time));
14561
+ return oscillator;
14562
+ });
14563
+ return voiceOf(oscillators);
14564
+ }
14565
+ var end2 = (layer, start) => start + layer.duration + layer.tail;
14566
+ function outputStage(plan, layer, target) {
14567
+ const { context } = target;
14568
+ const level = gainNode(context, layer.level);
14569
+ let node = level;
14570
+ if (plan.pan !== void 0 && typeof context.createStereoPanner === "function") {
14571
+ const panner = context.createStereoPanner();
14572
+ panner.pan.value = plan.pan;
14573
+ level.connect(panner);
14574
+ node = panner;
14575
+ }
14576
+ node.connect(target.output);
14577
+ if (layer.wet && target.wet) {
14578
+ const send = gainNode(context, layer.wet);
14579
+ node.connect(send);
14580
+ send.connect(target.wet);
14581
+ }
14582
+ return level;
14583
+ }
14584
+ function realizeNoiseLayer(plan, layer, target) {
14585
+ const { context } = target;
14586
+ const time = target.time + layer.at;
14587
+ const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
14588
+ const filters = (layer.filters?.bank ?? []).map((filter) => {
14589
+ const node = filterNode(context, filter, time);
14590
+ rampTo(node.frequency, filter.frequency, time);
14591
+ return node;
14592
+ });
14593
+ const envelope = context.createGain();
14594
+ applyEnvelope(envelope.gain, layer.envelope, time);
14595
+ let tail = source;
14596
+ for (const filter of filters) tail = tail.connect(filter);
14597
+ tail.connect(envelope);
14598
+ envelope.connect(outputStage(plan, layer, target));
14599
+ source.start(time, layer.source.kind === "noise" ? layer.source.offset : 0);
14600
+ source.stop(end2(layer, time));
14601
+ return [source];
14602
+ }
14603
+ function realizeToneLayer(plan, layer, target) {
14604
+ const { context } = target;
14605
+ const time = target.time + layer.at;
14606
+ const oscillator = context.createOscillator();
14607
+ if (layer.source.kind === "osc") {
14608
+ oscillator.type = layer.source.wave;
14609
+ assign(oscillator.frequency, layer.source.frequency, time);
14610
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14611
+ }
14612
+ const envelope = context.createGain();
14613
+ applyEnvelope(envelope.gain, layer.envelope, time);
14614
+ oscillator.connect(envelope);
14615
+ envelope.connect(outputStage(plan, layer, target));
14616
+ oscillator.start(time);
14617
+ oscillator.stop(end2(layer, time));
14618
+ return [oscillator];
14619
+ }
14620
+ function realizeVoiceLayer(plan, layer, target) {
14621
+ const source = layer.source;
14622
+ if (source.kind !== "formant_voice") return [];
14623
+ const { context } = target;
14624
+ const time = target.time + layer.at;
14625
+ const oscillator = context.createOscillator();
14626
+ oscillator.type = source.wave;
14627
+ assign(oscillator.frequency, source.frequency, time);
14628
+ rampTo(oscillator.frequency, source.frequency, time);
14629
+ const vibrato = context.createOscillator();
14630
+ vibrato.frequency.value = source.vibrato.rate;
14631
+ const vibratoDepth = gainNode(context, source.vibrato.depth);
14632
+ vibrato.connect(vibratoDepth);
14633
+ vibratoDepth.connect(oscillator.frequency);
14634
+ const formants = source.formants.map((formant) => filterNode(context, { type: "bandpass", frequency: { value: formant.frequency }, q: formant.q }, time));
14635
+ const breath = noiseSource(target, true);
14636
+ const breathBand = filterNode(context, { type: "bandpass", frequency: { value: source.breath.frequency }, q: source.breath.q }, time);
14637
+ const breathLevel = gainNode(context, source.breath.gain);
14638
+ const mix2 = gainNode(context, source.mix);
14639
+ for (const formant of formants) oscillator.connect(formant);
14640
+ breath.connect(breathBand);
14641
+ breathBand.connect(breathLevel);
14642
+ for (const formant of formants) formant.connect(mix2);
14643
+ breathLevel.connect(mix2);
14644
+ const envelope = context.createGain();
14645
+ applyEnvelope(envelope.gain, layer.envelope, time);
14646
+ mix2.connect(envelope);
14647
+ envelope.connect(outputStage(plan, layer, target));
14648
+ const stop = end2(layer, time);
14649
+ oscillator.start(time);
14650
+ vibrato.start(time);
14651
+ breath.start(time, source.breath.offset);
14652
+ oscillator.stop(stop);
14653
+ vibrato.stop(stop);
14654
+ breath.stop(stop);
14655
+ return [oscillator, vibrato, breath];
14656
+ }
14657
+ function realizeLayered(plan, target) {
14658
+ const sources = plan.layers.flatMap((layer) => {
14659
+ if (layer.source.kind === "noise") return realizeNoiseLayer(plan, layer, target);
14660
+ if (layer.source.kind === "osc") return realizeToneLayer(plan, layer, target);
14661
+ return realizeVoiceLayer(plan, layer, target);
14662
+ });
14663
+ return voiceOf(sources);
14664
+ }
14665
+ function realizeSiren(plan, target) {
14666
+ const { context } = target;
14667
+ const layer = plan.layers[0];
14668
+ const time = target.time + layer.at;
14669
+ const oscillator = context.createOscillator();
14670
+ if (layer.source.kind === "osc") {
14671
+ oscillator.type = layer.source.wave;
14672
+ assign(oscillator.frequency, layer.source.frequency, time);
14673
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14674
+ }
14675
+ const depths = layer.modulators.map((modulator) => {
14676
+ const lfo = context.createOscillator();
14677
+ if (modulator.wave) lfo.type = modulator.wave;
14678
+ lfo.frequency.value = modulator.rate;
14679
+ const depth = gainNode(context, modulator.depth);
14680
+ lfo.connect(depth);
14681
+ if (modulator.target === "frequency") depth.connect(oscillator.frequency);
14682
+ return { lfo, depth, target: modulator.target };
14683
+ });
14684
+ const lfos = depths.map(({ lfo }) => lfo);
14685
+ const envelope = context.createGain();
14686
+ applyEnvelope(envelope.gain, layer.envelope, time);
14687
+ for (const { depth, target: modulated } of depths) if (modulated === "gain") depth.connect(envelope.gain);
14688
+ const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
14689
+ let tail = oscillator;
14690
+ for (const filter of filters) tail = tail.connect(filter);
14691
+ tail.connect(envelope);
14692
+ envelope.connect(target.output);
14693
+ if (layer.wet && target.wet) {
14694
+ const send = gainNode(context, layer.wet);
14695
+ envelope.connect(send);
14696
+ send.connect(target.wet);
14697
+ }
14698
+ const stop = end2(layer, time);
14699
+ oscillator.start(time);
14700
+ for (const lfo of lfos) lfo.start(time);
14701
+ oscillator.stop(stop);
14702
+ for (const lfo of lfos) lfo.stop(stop);
14703
+ return voiceOf([oscillator, ...lfos]);
14704
+ }
14705
+ function noiseLayer(layer, target, time) {
14706
+ const { context } = target;
14707
+ const source = noiseSource(target, layer.source.kind === "noise" && layer.source.loop);
14708
+ const filters = (layer.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
14709
+ const envelope = context.createGain();
14710
+ applyEnvelope(envelope.gain, layer.envelope, time);
14711
+ let tail = source;
14712
+ for (const filter of filters) tail = tail.connect(filter);
14713
+ tail.connect(envelope).connect(target.output);
14714
+ source.start(time, layer.source.kind === "noise" ? layer.source.offset : 0);
14715
+ source.stop(time + layer.duration + layer.tail);
14716
+ return source;
14717
+ }
14718
+ function toneLayer(layer, target, time) {
14719
+ const { context } = target;
14720
+ const oscillator = context.createOscillator();
14721
+ if (layer.source.kind === "osc") {
14722
+ oscillator.type = layer.source.wave;
14723
+ assign(oscillator.frequency, layer.source.frequency, time);
14724
+ rampTo(oscillator.frequency, layer.source.frequency, time);
14725
+ }
14726
+ const envelope = context.createGain();
14727
+ applyEnvelope(envelope.gain, layer.envelope, time);
14728
+ oscillator.connect(envelope).connect(target.output);
14729
+ oscillator.start(time);
14730
+ oscillator.stop(time + layer.duration + layer.tail);
14731
+ return oscillator;
14732
+ }
14733
+ function realizeWeapon(plan, target) {
14734
+ const sources = plan.layers.map((layer) => {
14735
+ const time = target.time + layer.at;
14736
+ return layer.source.kind === "noise" ? noiseLayer(layer, target, time) : toneLayer(layer, target, time);
14737
+ });
14738
+ return voiceOf(sources);
14739
+ }
14740
+ var BUILDERS = {
14741
+ layered: realizeLayered,
14742
+ burst: realizeBurst,
14743
+ scream: realizeScream,
14744
+ groan: realizeGroan,
14745
+ boom: realizeBoom,
14746
+ static: realizeStatic,
14747
+ chord: realizeChord,
14748
+ calls: realizeCalls,
14749
+ siren: realizeSiren,
14750
+ weapon: realizeWeapon
14751
+ };
14752
+ function realize(plan, target) {
14753
+ return BUILDERS[plan.order](plan, target);
14754
+ }
14755
+ var usesNoise = (recipe) => recipe.layers.some((layer) => layer.source.kind !== "osc");
14756
+ var planUsesNoise = (plan) => plan.layers.some((layer) => layer.source.kind !== "osc");
14757
+ function createAudioEngine(options = {}) {
14758
+ const seed = options.seed ?? 1;
14759
+ const registry = createSoundRegistry(options.banks);
14760
+ const voices = createVoiceManager({ ...options.groups ? { groups: options.groups } : {}, ...options.maxVoices === void 0 ? {} : { maxVoices: options.maxVoices } });
14761
+ const engineRandom = createRng2(seed);
14762
+ const shared = createNoiseStore(engineRandom);
14763
+ const busSettings = /* @__PURE__ */ new Map();
14764
+ const busNodes = /* @__PURE__ */ new Map();
14765
+ const meters = /* @__PURE__ */ new Map();
14766
+ const active = /* @__PURE__ */ new Map();
14767
+ const loops = /* @__PURE__ */ new Set();
14768
+ const named = /* @__PURE__ */ new Map();
14769
+ let mix2 = resolveMix(options.mix ?? DEFAULT_MIX, options.profiles);
14770
+ let context = null;
14771
+ let chain = null;
14772
+ let listener = DEFAULT_LISTENER;
14773
+ let lastTime = 0;
14774
+ const outputFor = (bus) => busNodes.get(bus) ?? chain?.master;
14775
+ const watched = /* @__PURE__ */ new Set();
14776
+ const meterOf = (bus) => {
14777
+ const existing = meters.get(bus);
14778
+ if (existing) return existing;
14779
+ const node = busNodes.get(bus);
14780
+ if (!context || !node || typeof context.createAnalyser !== "function") return void 0;
14781
+ const analyser = context.createAnalyser();
14782
+ analyser.fftSize = 1024;
14783
+ node.connect(analyser);
14784
+ const made = { analyser, data: new Float32Array(analyser.fftSize) };
14785
+ meters.set(bus, made);
14786
+ return made;
14787
+ };
14788
+ const ensureBus = (bus) => {
14789
+ if (!context || !chain || busNodes.has(bus)) return;
14790
+ const node = context.createGain();
14791
+ node.connect(chain.master);
14792
+ busNodes.set(bus, node);
14793
+ if (watched.has(bus)) meterOf(bus);
14794
+ };
14795
+ const ready = () => {
14796
+ if (context) return context;
14797
+ const created = (options.createContext ?? globalContext)();
14798
+ if (!created) return null;
14799
+ context = created;
14800
+ chain = buildChain(created, mix2, engineRandom);
14801
+ if (mix2.noise.create === "eager") noiseBuffer(shared, created, mix2.noise.seconds);
14802
+ for (const [bus, setting] of Object.entries(mix2.buses ?? {})) {
14803
+ busSettings.set(bus, { ...setting, ...busSettings.get(bus) });
14804
+ }
14805
+ for (const [bus, setting] of busSettings) {
14806
+ ensureBus(bus);
14807
+ busNodes.get(bus).gain.value = busGain(setting);
14808
+ }
14809
+ lastTime = created.currentTime;
14810
+ return created;
14811
+ };
14812
+ const advance = () => {
14813
+ if (!context) return;
14814
+ const elapsed = context.currentTime - lastTime;
14815
+ lastTime = context.currentTime;
14816
+ if (elapsed > 0) for (const id of voices.advance(elapsed)) active.delete(id);
14817
+ };
14818
+ const inputFor = (recipe, playOptions, random) => ({
14819
+ random,
14820
+ listener,
14821
+ noiseSeconds: mix2.noise.seconds,
14822
+ ...options.distance ? { distanceMode: options.distance } : {},
14823
+ ...playOptions.volume === void 0 ? {} : { volume: playOptions.volume },
14824
+ ...playOptions.distance === void 0 ? {} : { distance: playOptions.distance },
14825
+ ...playOptions.position ? { position: playOptions.position } : {},
14826
+ ...playOptions.params ? { params: playOptions.params } : {},
14827
+ ...options.occlusion && playOptions.position && recipe.falloff?.model !== "none" ? { occlusion: options.occlusion(playOptions.position) } : {}
14828
+ });
14829
+ const start = (plan, store, voiceId, delay = 0) => {
14830
+ const audio = context;
14831
+ const realized = realize(plan, {
14832
+ context: audio,
14833
+ output: outputFor(plan.bus),
14834
+ ...chain?.wet ? { wet: chain.wet } : {},
14835
+ noise: () => noiseBuffer(store, audio, mix2.noise.seconds),
14836
+ time: audio.currentTime + delay
14837
+ });
14838
+ if (voiceId === void 0) return null;
14839
+ voices.setLength(voiceId, plan.length + delay);
14840
+ active.set(voiceId, realized);
14841
+ return {
14842
+ id: voiceId,
14843
+ plan,
14844
+ stop() {
14845
+ realized.stop();
14846
+ voices.release(voiceId);
14847
+ active.delete(voiceId);
14848
+ }
14849
+ };
14850
+ };
14851
+ const admit = (recipe, gain, distance2) => {
14852
+ const decision = voices.request({ ...recipe.group ? { group: recipe.group } : {}, priority: recipe.priority ?? busPriority(recipe.bus), gain, distance: distance2, length: 0 });
14853
+ if (!decision.accepted) return null;
14854
+ for (const id of decision.evicted) {
14855
+ active.get(id)?.stop();
14856
+ active.delete(id);
14857
+ }
14858
+ return decision.id;
14859
+ };
14860
+ const playWith = (id, playOptions, random, store) => {
14861
+ const recipe = registry.sound(id);
14862
+ if (!recipe || busSettings.get(recipe.bus)?.muted) return null;
14863
+ const distance2 = playOptions.distance ?? (playOptions.position ? listenerDistance(listener, playOptions.position, options.distance) : 0);
14864
+ const level = falloffGain(recipe.falloff, playOptions.volume ?? 1, distance2);
14865
+ const silent = isSilent(recipe.falloff, level);
14866
+ if (silent && !recipe.keepSilent) return null;
14867
+ const audio = ready();
14868
+ if (!audio) return null;
14869
+ unlockContext(audio);
14870
+ advance();
14871
+ const voiceId = silent ? void 0 : admit(recipe, level, distance2);
14872
+ if (voiceId === null) return null;
14873
+ if (!silent && usesNoise(recipe)) prepareNoise(store, audio, mix2.noise.seconds);
14874
+ const plan = planSound(recipe, inputFor(recipe, playOptions, playOptions.seed === void 0 ? random : createRng2(playOptions.seed)));
14875
+ return plan ? start(plan, store, voiceId, playOptions.delay) : null;
14876
+ };
14877
+ const randomFor = (playOptions) => {
14878
+ if (playOptions.channel === void 0) return engineRandom;
14879
+ const existing = named.get(playOptions.channel);
14880
+ if (existing) return existing;
14881
+ const stream = streamFor(seed, playOptions.channel);
14882
+ named.set(playOptions.channel, stream);
14883
+ return stream;
14884
+ };
14885
+ const planWith = (id, playOptions, random) => {
14886
+ const recipe = registry.sound(id);
14887
+ return recipe ? planSound(recipe, inputFor(recipe, playOptions, random)) : null;
14888
+ };
14889
+ const loopHost = {
14890
+ ready,
14891
+ output: outputFor,
14892
+ prepareNoise: () => context && prepareNoise(shared, context, mix2.noise.seconds),
14893
+ noise: () => noiseBuffer(shared, context, mix2.noise.seconds),
14894
+ register: (loop) => void loops.add(loop),
14895
+ unregister: (loop) => void loops.delete(loop)
14896
+ };
14897
+ return {
14898
+ registry,
14899
+ register: (bank) => registry.register(bank),
14900
+ plan: (id, playOptions = {}) => planWith(id, playOptions, createRng2(playOptions.seed ?? seed)),
14901
+ play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared),
14902
+ playPlan(plan, playOptions = {}) {
14903
+ const audio = ready();
14904
+ if (!audio) return null;
14905
+ advance();
14906
+ const recipe = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...plan.group ? { group: plan.group } : {} };
14907
+ const voiceId = plan.silent ? void 0 : admit(recipe, plan.level, plan.distance);
14908
+ if (voiceId === null) return null;
14909
+ if (planUsesNoise(plan)) prepareNoise(shared, audio, mix2.noise.seconds);
14910
+ return start(plan, shared, voiceId, playOptions.delay);
14911
+ },
14912
+ loop: (id, loopOptions = {}) => createLoopHandle(registry.loop(id), loopHost, loopOptions.volume),
14913
+ channel(channelOptions) {
14914
+ const random = channelOptions.random ?? (channelOptions.seed === void 0 ? engineRandom : createRng2(channelOptions.seed));
14915
+ const store = channelOptions.ownNoise ? createNoiseStore(random) : shared;
14916
+ return {
14917
+ random,
14918
+ play: (id, playOptions = {}) => playWith(id, playOptions, random, store),
14919
+ plan: (id, playOptions = {}) => planWith(id, playOptions, createRng2(playOptions.seed ?? seed))
14920
+ };
14921
+ },
14922
+ setListener(next) {
14923
+ listener = next;
14924
+ },
14925
+ setBus(id, setting) {
14926
+ busSettings.set(id, { ...busSettings.get(id), ...setting });
14927
+ if (!context) return;
14928
+ ensureBus(id);
14929
+ busNodes.get(id).gain.value = busGain(busSettings.get(id));
14930
+ },
14931
+ setMix(next) {
14932
+ const previous = mix2;
14933
+ mix2 = resolveMix(next, options.profiles);
14934
+ if (!context || !chain) return;
14935
+ const old = chain;
14936
+ chain = buildChain(context, mix2, engineRandom);
14937
+ if (mix2.noise.seconds !== previous.noise.seconds) shared.reset();
14938
+ for (const node of busNodes.values()) {
14939
+ node.disconnect();
14940
+ node.connect(chain.master);
14941
+ }
14942
+ for (const loop of loops) loop.rewire();
14943
+ old.disconnect();
14944
+ },
14945
+ setMaster(gain) {
14946
+ mix2 = { ...mix2, master: gain };
14947
+ if (chain) chain.master.gain.value = gain;
14948
+ },
14949
+ unlock() {
14950
+ const audio = ready();
14951
+ if (audio) unlockContext(audio);
14952
+ },
14953
+ level(bus) {
14954
+ watched.add(bus);
14955
+ ensureBus(bus);
14956
+ const meter = meterOf(bus);
14957
+ if (!meter) return 0;
14958
+ meter.analyser.getFloatTimeDomainData(meter.data);
14959
+ let sum2 = 0;
14960
+ for (const sample of meter.data) sum2 += sample * sample;
14961
+ return Math.sqrt(sum2 / meter.data.length);
14962
+ },
14963
+ stats() {
14964
+ advance();
14965
+ const voiceStats = voices.stats();
14966
+ return {
14967
+ context: context ? context.state : "none",
14968
+ voices: voiceStats.active,
14969
+ peak: voiceStats.peak,
14970
+ dropped: voiceStats.dropped,
14971
+ maxVoices: voiceStats.maxVoices,
14972
+ loops: [...loops].filter((loop) => loop.created).length
14973
+ };
14974
+ },
14975
+ dispose() {
14976
+ for (const id of active.keys()) voices.release(id);
14977
+ for (const loop of [...loops]) loop.stop();
14978
+ active.clear();
14979
+ void context?.close();
14980
+ context = null;
14981
+ chain = null;
14982
+ busNodes.clear();
14983
+ meters.clear();
14984
+ shared.reset();
14985
+ }
14986
+ };
14987
+ }
14988
+ var BENCH_BANK = {
14989
+ id: "audio_bench",
14990
+ sounds: [
14991
+ {
14992
+ id: "bench_ping",
14993
+ bus: "ui",
14994
+ pan: true,
14995
+ duration: 0.25,
14996
+ layers: [
14997
+ {
14998
+ gain: 0.5,
14999
+ wet: 0.3,
15000
+ source: { kind: "osc", wave: "sine", frequency: { from: 880 } },
15001
+ envelope: [
15002
+ [0, 1e-4, "set", "fixed"],
15003
+ [2e-3, 1, "exp"],
15004
+ ["1d", 1e-4, "exp", "fixed"]
15005
+ ]
15006
+ }
15007
+ ],
15008
+ falloff: DISTANCE_CURVES.art_scene,
15009
+ sources: ["sdk: bench probe, a 880 Hz ping through the art scene output stage"]
15010
+ },
15011
+ {
15012
+ id: "bench_hiss",
15013
+ bus: "ui",
15014
+ pan: true,
15015
+ duration: [0.3, 0.9],
15016
+ layers: [
15017
+ {
15018
+ gain: 0.4,
15019
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
15020
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3e3 }, q: 0.7 }] },
15021
+ envelope: [
15022
+ [0, 1e-4, "set", "fixed"],
15023
+ [2e-3, 1, "exp"],
15024
+ ["1d", 1e-4, "exp", "fixed"]
15025
+ ]
15026
+ }
15027
+ ],
15028
+ falloff: DISTANCE_CURVES.art_scene,
15029
+ sources: ["sdk: bench probe, a highpass hiss of random length"]
15030
+ }
15031
+ ]
15032
+ };
15033
+
13622
15034
  // src/audio/mechanical-sounds.ts
13623
15035
  var metal = (at, frequency, gain) => ({ layer: "metal", at, frequency, gain });
13624
15036
  var sound = (at, soundClass, volume) => ({ layer: "class", at, soundClass, volume });
13625
15037
  var thud = (at, gain, frequency) => ({ layer: "thud", at, gain, frequency });
15038
+ var sceneClick = (at, f, vol) => [
15039
+ { layer: "scene_tone", at, wave: "square", f, f2: f * 0.7, d: 0.02, vol },
15040
+ { layer: "scene_noise", at, type: "highpass", f: 3e3, q: 0.7, d: 0.03, vol: vol * 0.8 }
15041
+ ];
15042
+ var sceneNoise = (at, type, f, q, d, vol) => ({ layer: "scene_noise", at, type, f, q, d, vol });
15043
+ var sceneEmpty = sceneClick(0, 3200, 0.35);
15044
+ var sceneMagOut = [...sceneClick(0, 1600, 0.4), sceneNoise(0.03, "bandpass", 900, 2, 0.08, 0.3)];
15045
+ var sceneMagIn = [...sceneClick(0, 1100, 0.5), { layer: "scene_tone", at: 0, wave: "sine", f: 320, f2: 200, d: 0.05, vol: 0.35 }];
15046
+ var sceneBolt = [...sceneClick(0, 2200, 0.4), ...sceneClick(0.12, 2700, 0.4)];
15047
+ var sceneShell = [...sceneClick(0, 1400, 0.35), sceneNoise(0.02, "bandpass", 1200, 3, 0.05, 0.25)];
13626
15048
  var slide = [metal(0, 900, 0.35), metal(0.14, 1300, 0.35)];
13627
15049
  var seat = [metal(0, 1100, 0.45), sound(0, "reload", 0.8)];
13628
15050
  var MECHANICAL_SOUNDS = {
13629
- dry_fire: [sound(0, "empty", 1), metal(0, 2600, 0.25)],
15051
+ dry_fire: [sound(0, "empty", 1), ...sceneEmpty],
13630
15052
  misfire: [sound(0, "empty", 0.8), metal(0, 2200, 0.2)],
13631
15053
  feed: [metal(0, 1900, 0.2)],
13632
15054
  cycle: [metal(0.22, 900, 0.35), metal(0.36, 1300, 0.35)],
@@ -13639,11 +15061,11 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
13639
15061
  stock_fold: [metal(0, 800, 0.3), metal(0.18, 1200, 0.3), thud(0.3, 0.2, 600)],
13640
15062
  light_click: [metal(0, 2400, 0.2), metal(0.03, 1800, 0.12)],
13641
15063
  attach: [metal(0, 1400, 0.35), thud(0.04, 0.2, 700), metal(0.12, 1900, 0.25)],
13642
- magazine_out: [metal(0, 1500, 0.35), sound(0, "reload", 0.6)],
13643
- magazine_in: seat,
13644
- charge: slide,
15064
+ magazine_out: [...sceneMagOut, sound(0, "reload", 0.6)],
15065
+ magazine_in: [...sceneMagIn, sound(0, "reload", 0.8)],
15066
+ charge: sceneBolt,
13645
15067
  bolt_open: [metal(0, 900, 0.35)],
13646
- round_in: [metal(0, 2e3, 0.2), sound(0, "reload", 0.6)],
15068
+ round_in: [...sceneShell, sound(0, "reload", 0.6)],
13647
15069
  bolt_close: [metal(0, 1300, 0.35)],
13648
15070
  pump_cycle: slide,
13649
15071
  lever_cycle: [metal(0, 1e3, 0.35), metal(0.12, 1500, 0.35)],
@@ -13783,100 +15205,185 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
13783
15205
  };
13784
15206
  };
13785
15207
 
15208
+ // src/audio/recipes.ts
15209
+ var CLASS_BURST_ID = "weapon_class_burst";
15210
+ var BURST_IDS = { lowpass: "weapon_burst_lowpass", highpass: "weapon_burst_highpass" };
15211
+ var SCENE_NOISE_IDS = { lowpass: "scene_noise_lowpass", highpass: "scene_noise_highpass", bandpass: "scene_noise_bandpass" };
15212
+ var SCENE_TONE_IDS = { sine: "scene_tone_sine", square: "scene_tone_square" };
15213
+ var TONE_IDS = { sine: "weapon_tone_sine", sine_sweep: "weapon_tone_sine_sweep", square: "weapon_tone_square" };
15214
+ var SOURCE_CLASS_BURST = "jogo:L62097-62110 sfx: lowpass noise, gain set at once, exponential fall to 0.001, stopped 0.05 s later";
15215
+ var SOURCE_SDK = "sdk: the weapon synth of weapons-three (burst and tone), moved onto game-audio with the same nodes";
15216
+ var FIT = { kind: "noise", offset: "fit" };
15217
+ function classBurstId(curve) {
15218
+ const same = curve.gainFalloff === GAME_DISTANCE_CURVE.gainFalloff && curve.cutoffFalloff === GAME_DISTANCE_CURVE.cutoffFalloff && curve.minGain === GAME_DISTANCE_CURVE.minGain;
15219
+ return same ? CLASS_BURST_ID : `${CLASS_BURST_ID}@${curve.gainFalloff},${curve.cutoffFalloff},${curve.minGain}`;
15220
+ }
15221
+ function classBurstRecipe(curve) {
15222
+ return {
15223
+ id: classBurstId(curve),
15224
+ bus: "weapons",
15225
+ order: "weapon",
15226
+ duration: { param: "seconds" },
15227
+ layers: [
15228
+ {
15229
+ gain: { param: "gain" },
15230
+ source: FIT,
15231
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: { param: "cutoff" } } }] },
15232
+ envelope: [
15233
+ [0, 1, "set"],
15234
+ ["1d", 1e-3, "exp", "fixed"]
15235
+ ]
15236
+ }
15237
+ ],
15238
+ falloff: { model: "inverse_square", k: curve.gainFalloff, cutoffK: curve.cutoffFalloff, minGain: curve.minGain },
15239
+ sources: [SOURCE_CLASS_BURST]
15240
+ };
15241
+ }
15242
+ var burstLayer = (type) => ({
15243
+ gain: { param: "gain" },
15244
+ source: FIT,
15245
+ filters: { mode: "serial", bank: [{ type, frequency: { set: { param: "frequency" } }, q: { param: "q" } }] },
15246
+ envelope: [
15247
+ [0, 1e-4, "set", "fixed"],
15248
+ [4e-3, 1, "exp"],
15249
+ ["1d", 1e-4, "exp", "fixed"]
15250
+ ]
15251
+ });
15252
+ var burstRecipe = (type) => ({
15253
+ id: BURST_IDS[type],
15254
+ bus: "weapons",
15255
+ order: "weapon",
15256
+ duration: { param: "seconds" },
15257
+ layers: [burstLayer(type)],
15258
+ falloff: { model: "none" },
15259
+ sources: [SOURCE_SDK]
15260
+ });
15261
+ var toneLayer2 = (wave, sweep) => ({
15262
+ gain: { param: "gain" },
15263
+ source: { kind: "osc", wave, frequency: sweep ? { from: { param: "frequency" }, to: { param: "to" }, curve: "exp", over: "duration" } : { from: { param: "frequency" } } },
15264
+ envelope: [
15265
+ [0, 1e-4, "set", "fixed"],
15266
+ [3e-3, 1, "exp"],
15267
+ ["1d", 1e-4, "exp", "fixed"]
15268
+ ]
15269
+ });
15270
+ var toneRecipe = (id, wave, sweep) => ({
15271
+ id,
15272
+ bus: "weapons",
15273
+ order: "weapon",
15274
+ duration: { param: "seconds" },
15275
+ layers: [toneLayer2(wave, sweep)],
15276
+ falloff: { model: "none" },
15277
+ sources: [SOURCE_SDK]
15278
+ });
15279
+ var SOURCE_SCENE = "cena:L1323-L1324 sNoise and sTone, played at the listener (distance 0), through the art scene output stage";
15280
+ var SCENE_ATTACK = 2e-3;
15281
+ var sceneNoiseRecipe = (type, slide2) => ({
15282
+ id: slide2 ? `${SCENE_NOISE_IDS[type]}_slide` : SCENE_NOISE_IDS[type],
15283
+ bus: "weapons",
15284
+ order: "layered",
15285
+ duration: { param: "d", add: SCENE_ATTACK },
15286
+ pan: true,
15287
+ layers: [
15288
+ {
15289
+ gain: { param: "vol" },
15290
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
15291
+ filters: { mode: "serial", bank: [{ type, frequency: slide2 ? { set: { param: "f" }, to: { param: "f2" }, curve: "exp", over: { param: "d" } } : { set: { param: "f" } }, q: { param: "q" } }] },
15292
+ envelope: [
15293
+ [0, 1e-4, "set", "fixed"],
15294
+ [SCENE_ATTACK, 1, "exp"],
15295
+ ["1d", 1e-4, "exp", "fixed"]
15296
+ ]
15297
+ }
15298
+ ],
15299
+ falloff: DISTANCE_CURVES.art_scene,
15300
+ sources: [SOURCE_SCENE]
15301
+ });
15302
+ var sceneToneRecipe = (wave) => ({
15303
+ id: SCENE_TONE_IDS[wave],
15304
+ bus: "weapons",
15305
+ order: "layered",
15306
+ duration: { param: "d", add: SCENE_ATTACK },
15307
+ pan: true,
15308
+ layers: [
15309
+ {
15310
+ gain: { param: "vol" },
15311
+ source: { kind: "osc", wave, frequency: { from: { param: "f" }, to: { param: "f2" }, curve: "exp", over: "duration" } },
15312
+ envelope: [
15313
+ [0, 1e-4, "set", "fixed"],
15314
+ [SCENE_ATTACK, 1, "exp"],
15315
+ ["1d", 1e-4, "exp", "fixed"]
15316
+ ]
15317
+ }
15318
+ ],
15319
+ falloff: DISTANCE_CURVES.art_scene,
15320
+ sources: [SOURCE_SCENE]
15321
+ });
15322
+ function weaponRecipes(curve) {
15323
+ return [
15324
+ classBurstRecipe(curve),
15325
+ burstRecipe("lowpass"),
15326
+ burstRecipe("highpass"),
15327
+ toneRecipe(TONE_IDS.sine, "sine", false),
15328
+ toneRecipe(TONE_IDS.sine_sweep, "sine", true),
15329
+ toneRecipe(TONE_IDS.square, "square", false),
15330
+ ...["lowpass", "highpass", "bandpass"].flatMap((type) => [sceneNoiseRecipe(type, false), sceneNoiseRecipe(type, true)]),
15331
+ sceneToneRecipe("sine"),
15332
+ sceneToneRecipe("square")
15333
+ ];
15334
+ }
15335
+
13786
15336
  // src/audio.ts
13787
15337
  var NOISE_SECONDS = 1.5;
13788
- var globalContext = () => {
13789
- const scope2 = globalThis;
13790
- const Ctor = scope2.AudioContext ?? scope2.webkitAudioContext;
13791
- return Ctor ? new Ctor() : null;
13792
- };
15338
+ var DEFAULT_SEED = 1;
13793
15339
  function createWeaponAudio(volume = 0.6, options = {}) {
13794
- const makeContext = options.createContext ?? globalContext;
13795
- const rng = createRng(options.seed ?? 1);
15340
+ const seed = options.seed ?? DEFAULT_SEED;
13796
15341
  const classes = { ...GAME_SOUND_CLASSES, ...options.classes };
13797
15342
  const distanceCurve = { ...GAME_DISTANCE_CURVE, ...options.distance };
13798
15343
  const mechanicalTable = { ...MECHANICAL_SOUNDS, ...options.mechanical };
13799
- let ctx = null;
13800
- let master = null;
13801
- let noise = null;
13802
- let level = volume;
13803
- const ready = () => {
13804
- if (!ctx) {
13805
- const created = makeContext();
13806
- if (!created) return null;
13807
- ctx = created;
13808
- master = ctx.createGain();
13809
- master.gain.value = level;
13810
- master.connect(ctx.destination);
13811
- noise = ctx.createBuffer(1, Math.floor(ctx.sampleRate * NOISE_SECONDS), ctx.sampleRate);
13812
- const data = noise.getChannelData(0);
13813
- for (let i = 0; i < data.length; i++) data[i] = rng() * 2 - 1;
13814
- }
13815
- if (ctx.state === "suspended") void ctx.resume();
13816
- return ctx;
13817
- };
15344
+ const shared = options.engine;
15345
+ const engine = shared ?? createAudioEngine({
15346
+ seed,
15347
+ mix: { ...MIX_PROFILES.game, master: volume, noise: { seconds: NOISE_SECONDS, create: "eager" } },
15348
+ maxVoices: Number.POSITIVE_INFINITY,
15349
+ ...options.createContext ? { createContext: options.createContext } : {}
15350
+ });
15351
+ const channel = shared && options.seed !== void 0 ? shared.channel({ seed }) : engine.channel({});
15352
+ engine.register({ id: `weapons_three@${classBurstId(distanceCurve)}`, sounds: weaponRecipes(distanceCurve) });
15353
+ const classBurst = classBurstId(distanceCurve);
13818
15354
  const burst = (config) => {
13819
- const audio = ready();
13820
- if (!audio || !noise || !master) return;
13821
- const start = audio.currentTime + (config.at ?? 0);
13822
- const source = audio.createBufferSource();
13823
- source.buffer = noise;
13824
- const filter = audio.createBiquadFilter();
13825
- filter.type = config.filter;
13826
- filter.frequency.value = config.frequency;
13827
- filter.Q.value = config.q ?? 0.8;
13828
- const gain = audio.createGain();
13829
- gain.gain.setValueAtTime(1e-4, start);
13830
- gain.gain.exponentialRampToValueAtTime(Math.max(config.gain, 2e-4), start + 4e-3);
13831
- gain.gain.exponentialRampToValueAtTime(1e-4, start + config.seconds);
13832
- source.connect(filter).connect(gain).connect(master);
13833
- source.start(start, rng() * Math.max(0, NOISE_SECONDS - config.seconds));
13834
- source.stop(start + config.seconds + 0.05);
15355
+ const id = config.filter === "highpass" ? BURST_IDS.highpass : BURST_IDS.lowpass;
15356
+ const params = { frequency: config.frequency, q: config.q ?? 0.8, gain: Math.max(config.gain, 2e-4), seconds: config.seconds };
15357
+ channel.play(id, { params, ...config.at ? { delay: config.at } : {} });
13835
15358
  };
13836
- const classBurst = (at, cutoff, gain, seconds) => {
13837
- const audio = ready();
13838
- if (!audio || !noise || !master) return;
13839
- const start = audio.currentTime + at;
13840
- const source = audio.createBufferSource();
13841
- source.buffer = noise;
13842
- const filter = audio.createBiquadFilter();
13843
- filter.type = "lowpass";
13844
- filter.frequency.value = cutoff;
13845
- const envelope = audio.createGain();
13846
- envelope.gain.setValueAtTime(gain, start);
13847
- envelope.gain.exponentialRampToValueAtTime(1e-3, start + seconds);
13848
- source.connect(filter).connect(envelope).connect(master);
13849
- source.start(start, rng() * Math.max(0, NOISE_SECONDS - seconds));
13850
- source.stop(start + seconds + 0.05);
15359
+ const classBurstAt = (at, cutoff, gain, seconds) => {
15360
+ channel.play(classBurst, { params: { cutoff, gain, seconds }, ...at ? { delay: at } : {} });
13851
15361
  };
13852
15362
  const tone = (config) => {
13853
- const audio = ready();
13854
- if (!audio || !master) return;
13855
- const start = audio.currentTime + (config.at ?? 0);
13856
- const osc = audio.createOscillator();
13857
- osc.type = config.type ?? "sine";
13858
- osc.frequency.setValueAtTime(config.frequency, start);
13859
- if (config.to) osc.frequency.exponentialRampToValueAtTime(config.to, start + config.seconds);
13860
- const gain = audio.createGain();
13861
- gain.gain.setValueAtTime(1e-4, start);
13862
- gain.gain.exponentialRampToValueAtTime(Math.max(config.gain, 2e-4), start + 3e-3);
13863
- gain.gain.exponentialRampToValueAtTime(1e-4, start + config.seconds);
13864
- osc.connect(gain).connect(master);
13865
- osc.start(start);
13866
- osc.stop(start + config.seconds + 0.05);
15363
+ const id = config.to ? TONE_IDS.sine_sweep : config.type === "square" ? TONE_IDS.square : TONE_IDS.sine;
15364
+ const params = { frequency: config.frequency, gain: Math.max(config.gain, 2e-4), seconds: config.seconds, ...config.to ? { to: config.to } : {} };
15365
+ channel.play(id, { params, ...config.at ? { delay: config.at } : {} });
13867
15366
  };
13868
15367
  const metal2 = (at, frequency, gain) => {
13869
15368
  burst({ at, seconds: 0.05, gain, filter: "highpass", frequency, q: 2 });
13870
15369
  tone({ at, seconds: 0.04, gain: gain * 0.6, frequency: frequency * 0.6, type: "square" });
13871
15370
  };
13872
15371
  const play = (soundClass, classVolume = 1, distance2 = 0, at = 0) => {
13873
- const attenuation = classVolume / (1 + distance2 * distance2 * distanceCurve.gainFalloff);
13874
- if (attenuation < distanceCurve.minGain) return;
13875
15372
  const [cutoff, gain, seconds] = classes[soundClass] ?? FALLBACK_SOUND_CLASS;
13876
- classBurst(at, cutoff / (1 + distance2 * distanceCurve.cutoffFalloff), gain * attenuation, seconds);
15373
+ channel.play(classBurst, { volume: classVolume, distance: distance2, params: { cutoff, gain, seconds }, ...at ? { delay: at } : {} });
15374
+ };
15375
+ const sceneTone = (entry) => {
15376
+ channel.play(SCENE_TONE_IDS[entry.wave], { params: { f: entry.f, f2: entry.f2 ?? entry.f, d: entry.d, vol: entry.vol }, ...entry.at ? { delay: entry.at } : {} });
15377
+ };
15378
+ const sceneNoise2 = (entry) => {
15379
+ const slide2 = entry.f2 !== void 0;
15380
+ const id = slide2 ? `${SCENE_NOISE_IDS[entry.type]}_slide` : SCENE_NOISE_IDS[entry.type];
15381
+ channel.play(id, { params: { f: entry.f, ...slide2 ? { f2: entry.f2 } : {}, q: entry.q, d: entry.d, vol: entry.vol }, ...entry.at ? { delay: entry.at } : {} });
13877
15382
  };
13878
15383
  const step = (entry) => {
13879
15384
  if (entry.layer === "metal") metal2(entry.at, entry.frequency, entry.gain);
15385
+ else if (entry.layer === "scene_tone") sceneTone(entry);
15386
+ else if (entry.layer === "scene_noise") sceneNoise2(entry);
13880
15387
  else if (entry.layer === "class") play(entry.soundClass, entry.volume, 0, entry.at);
13881
15388
  else burst({ at: entry.at, seconds: 0.09, gain: entry.gain, filter: "lowpass", frequency: entry.frequency });
13882
15389
  };
@@ -13888,7 +15395,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
13888
15395
  shot(energyJ, category, extras = {}) {
13889
15396
  const sound2 = shotSound({ ...extras, energyJ, category });
13890
15397
  if (!sound2.audible) return;
13891
- classBurst(0, sound2.cutoff, sound2.gain, sound2.duration);
15398
+ classBurstAt(0, sound2.cutoff, sound2.gain, sound2.duration);
13892
15399
  if (sound2.crack.gain > 0) burst({ seconds: sound2.crack.duration, gain: sound2.crack.gain, filter: "highpass", frequency: sound2.crack.cutoff });
13893
15400
  tone({ seconds: sound2.body.duration, gain: sound2.body.gain, frequency: sound2.body.frequency, to: sound2.body.to });
13894
15401
  },
@@ -13916,22 +15423,20 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
13916
15423
  mechanical("cycle");
13917
15424
  },
13918
15425
  clink() {
13919
- const f = 3200 + rng() * 2e3;
13920
- tone({ seconds: 0.09, gain: 0.12, frequency: f });
13921
- tone({ seconds: 0.06, gain: 0.06, frequency: f * 2.7 });
15426
+ const f = 4200 + channel.random() * 1500;
15427
+ const at = 0.25 + channel.random() * 0.2;
15428
+ sceneTone({ layer: "scene_tone", at, wave: "sine", f, f2: 3800, d: 0.06, vol: 0.06 });
13922
15429
  },
13923
15430
  thud() {
13924
15431
  burst({ seconds: 0.09, gain: 0.35, filter: "lowpass", frequency: 380 });
13925
15432
  },
13926
15433
  setVolume(next) {
13927
- level = Math.max(0, Math.min(1, next));
13928
- if (master) master.gain.value = level;
15434
+ const level = Math.max(0, Math.min(1, next));
15435
+ if (shared) shared.setBus("weapons", { gain: level });
15436
+ else engine.setMaster(level);
13929
15437
  },
13930
15438
  dispose() {
13931
- void ctx?.close();
13932
- ctx = null;
13933
- master = null;
13934
- noise = null;
15439
+ if (!shared) engine.dispose();
13935
15440
  }
13936
15441
  };
13937
15442
  }
@@ -16685,13 +18190,13 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
16685
18190
  }
16686
18191
 
16687
18192
  // src/models/capture.ts
16688
- function captured(parent, draw) {
18193
+ function captured(parent, draw2) {
16689
18194
  const before = parent.children.length;
16690
- draw();
18195
+ draw2();
16691
18196
  return parent.children.slice(before);
16692
18197
  }
16693
- function recordFitting(parts, id, parent, draw) {
16694
- parts.fittings = { ...parts.fittings, [id]: captured(parent, draw) };
18198
+ function recordFitting(parts, id, parent, draw2) {
18199
+ parts.fittings = { ...parts.fittings, [id]: captured(parent, draw2) };
16695
18200
  }
16696
18201
 
16697
18202
  // src/models/attachments/bipod.ts
@@ -17114,7 +18619,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
17114
18619
  }
17115
18620
  return chosen;
17116
18621
  })();
17117
- var BUILDERS = {
18622
+ var BUILDERS2 = {
17118
18623
  pistol: buildPistol,
17119
18624
  revolver: buildRevolver,
17120
18625
  rifle: buildRifle,
@@ -17144,7 +18649,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
17144
18649
  const kit = modelKit(THREE);
17145
18650
  const options = { L: entry.length, B: entry.barrel, cal: entry.caliber, ...entry.options };
17146
18651
  if (id in ARCHETYPE_MODELS) delete options.chambers;
17147
- const built = BUILDERS[entry.builder](kit, options);
18652
+ const built = BUILDERS2[entry.builder](kit, options);
17148
18653
  nameParts(built.parts);
17149
18654
  let root;
17150
18655
  if (entry.builder === "launcher") {
@@ -17538,12 +19043,12 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
17538
19043
  var toggleClip = () => composeClip("toggle", [turn(ROOT, "z", 0, TOGGLE_SECONDS / 2, 0, TOGGLE_TILT), turn(ROOT, "z", TOGGLE_SECONDS / 2, TOGGLE_SECONDS / 2, TOGGLE_TILT, 0)]);
17539
19044
  function launcherClips(context) {
17540
19045
  if (!context.parts.ugl) return [];
17541
- const { open, loaded, close, end, slide: slide2, back } = LAUNCHER_RELOAD;
19046
+ const { open, loaded, close, end: end3, slide: slide2, back } = LAUNCHER_RELOAD;
17542
19047
  const reload = composeClip(
17543
19048
  "reload_ugl",
17544
19049
  [move("ugl", "x", open, slide2, 0, LAUNCHER_SLIDE), move("ugl", "x", close, back, LAUNCHER_SLIDE, 0)],
17545
19050
  [stage("breech_open", open), stage("round_in", loaded), stage("breech_close", close)],
17546
- end
19051
+ end3
17547
19052
  );
17548
19053
  const shot = composeClip("shot_ugl", [move(ROOT, "x", 0, 0.04, 0, -LAUNCHER_KICK), move(ROOT, "x", 0.04, 0.2, -LAUNCHER_KICK, 0)], [cue(0, "muzzle_flash", { at: "ugl" })]);
17549
19054
  return [reload, shot];
@@ -18200,8 +19705,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
18200
19705
  if (!look || !slide2) return null;
18201
19706
  const { M, cylinderX } = kit;
18202
19707
  const length2 = attachment.dims?.length ?? 0.35;
18203
- const fixed = kit.group();
18204
- drawFixed(kit, fixed, look, length2);
19708
+ const fixed2 = kit.group();
19709
+ drawFixed(kit, fixed2, look, length2);
18205
19710
  const tubeLength = length2 * look.tubeFraction;
18206
19711
  const start = -0.02;
18207
19712
  cylinderX(slide2, look.tubeRadius, tubeLength, M.metal, start + tubeLength / 2, -look.tubeRadius, 0, 18);
@@ -18210,7 +19715,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
18210
19715
  const muzzle = kit.mark(slide2, start + tubeLength + 0.01, -look.tubeRadius, 0);
18211
19716
  muzzle.name = "ugl_muzzle";
18212
19717
  return {
18213
- object: fixed,
19718
+ object: fixed2,
18214
19719
  muzzle,
18215
19720
  dispose() {
18216
19721
  slide2.remove(...slide2.children);
@@ -18837,11 +20342,11 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
18837
20342
  if (!session.cartridge) return;
18838
20343
  const { group } = buildRound(kit, session.cartridge.dims, session.cartridge.shape);
18839
20344
  const start = root.worldToLocal(from.clone());
18840
- const end = root.worldToLocal(to.clone());
20345
+ const end3 = root.worldToLocal(to.clone());
18841
20346
  group.rotation.z = tilt;
18842
20347
  group.position.copy(start);
18843
20348
  root.add(group);
18844
- world.schedule(0, 0.24, (eased) => group.position.lerpVectors(start, end, eased), () => {
20349
+ world.schedule(0, 0.24, (eased) => group.position.lerpVectors(start, end3, eased), () => {
18845
20350
  group.removeFromParent();
18846
20351
  group.traverse((child) => {
18847
20352
  const mesh = child;
@@ -19984,7 +21489,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
19984
21489
  shape: shape.shape,
19985
21490
  position: shape.position,
19986
21491
  ...SHELL,
19987
- explodes: { energy: def.energy, radius: def.injuryRadius, ...def.chain ? { chain: { ...def.chain } } : {} }
21492
+ explodes: { energy: def.energy, radius: def.injuryRadius, effect: def.id, ...def.chain ? { chain: { ...def.chain } } : {} }
19988
21493
  });
19989
21494
  var meshOf = (THREE, shape) => {
19990
21495
  const geometry = shape.shape.kind === "cylinder" ? new THREE.CylinderGeometry(shape.shape.radius, shape.shape.radius, shape.shape.height, 16) : shape.shape.kind === "box" ? new THREE.BoxGeometry(...shape.shape.size) : new THREE.SphereGeometry(0.2);
@@ -20012,6 +21517,51 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
20012
21517
  };
20013
21518
  };
20014
21519
 
21520
+ // src/data/world-effects.json
21521
+ var world_effects_default = {
21522
+ pistol: { muzzle: "muzzle_glock", casing: "casing_brass" },
21523
+ tiny: { muzzle: "muzzle_glock", casing: "casing_brass" },
21524
+ rifle: { muzzle: "muzzle_ak", casing: "casing_brass" },
21525
+ shell: { muzzle: "muzzle_r870", casing: "casing_shell" },
21526
+ big: { muzzle: "muzzle_barrett", casing: "casing_50" }
21527
+ };
21528
+
21529
+ // src/world-effects.ts
21530
+ var WORLD_EFFECTS = world_effects_default;
21531
+ var PORT_BACK = 0.35;
21532
+ var PORT_RIGHT = 0.04;
21533
+ function createWorldEffects(options) {
21534
+ const ids = options.ids ?? WORLD_EFFECTS;
21535
+ const caseTypeOf = options.caseTypeOf ?? ((weapon) => {
21536
+ const def = options.arsenal.weapons.get(weapon);
21537
+ return def && def.action.kind === "firearm" ? options.arsenal.calibers.get(def.action.caliber)?.caseType : void 0;
21538
+ });
21539
+ let counter = 0;
21540
+ return {
21541
+ /** Plays what the event asks for and answers how many effects started. */
21542
+ handle(event, sink) {
21543
+ if (!sink || event.type !== "shot" || !event.origin) return 0;
21544
+ const caseType = caseTypeOf(event.weapon) ?? "rifle";
21545
+ const entry = ids[caseType];
21546
+ if (!entry) return 0;
21547
+ const direction = event.direction ?? [0, 0, -1];
21548
+ const seed = ++counter;
21549
+ let started = 0;
21550
+ for (let shot = 0; shot < Math.max(1, event.rounds ?? 1); shot++) {
21551
+ if (sink.play({ effect: entry.muzzle, point: event.origin, direction, seed: seed * 8 + shot }) !== null) started++;
21552
+ }
21553
+ const side = [direction[2], 0, -direction[0]];
21554
+ const port = [
21555
+ event.origin[0] - direction[0] * PORT_BACK + side[0] * PORT_RIGHT,
21556
+ event.origin[1] - direction[1] * PORT_BACK,
21557
+ event.origin[2] - direction[2] * PORT_BACK + side[2] * PORT_RIGHT
21558
+ ];
21559
+ if (sink.play({ effect: entry.casing, point: port, direction, seed: seed * 8 + 7 }) !== null) started++;
21560
+ return started;
21561
+ }
21562
+ };
21563
+ }
21564
+
20015
21565
  // src/area.ts
20016
21566
  var WEAPON_ITEM_PREFIX = "weapon_";
20017
21567
  var COUNTER_ITEM_ID = "counter_weapons";
@@ -20180,7 +21730,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
20180
21730
  const arsenal = options.arsenal ?? createArsenal({ locale });
20181
21731
  const text = textOf(locale);
20182
21732
  const accessoryText = accessoryTextOf(locale);
20183
- const audio = options.sound === false ? null : createWeaponAudio(void 0, options.seed === void 0 ? {} : { seed: options.seed });
21733
+ const seedOption = options.seed === void 0 ? {} : { seed: options.seed };
21734
+ let audio = options.sound === false ? null : createWeaponAudio(void 0, seedOption);
20184
21735
  const weaponDefs = [...arsenal.weapons.values()].filter((def) => !BODY_ACTIONS.has(def.id));
20185
21736
  const thumbnailBase = options.thumbnailBase ?? DEFAULT_THUMBNAIL_BASE;
20186
21737
  const withThumbnail = (item, key) => {
@@ -20207,7 +21758,9 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
20207
21758
  id: "weapons_area",
20208
21759
  name: text.name,
20209
21760
  group: text.group,
20210
- audio,
21761
+ get audio() {
21762
+ return audio;
21763
+ },
20211
21764
  get fx() {
20212
21765
  return fx;
20213
21766
  },
@@ -20255,6 +21808,11 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
20255
21808
  ],
20256
21809
  attach(lent) {
20257
21810
  host = lent;
21811
+ const lentEngine = lent.audio?.engine;
21812
+ if (audio && lentEngine) {
21813
+ audio.dispose();
21814
+ audio = createWeaponAudio(void 0, { ...seedOption, engine: lentEngine });
21815
+ }
20258
21816
  const created = createWeaponFx({
20259
21817
  THREE: lent.THREE,
20260
21818
  scene: lent.scene,
@@ -20288,7 +21846,9 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
20288
21846
  } else if (change.kind === "control" && change.id === "sound_distance" && typeof change.value === "number") soundDistance = change.value;
20289
21847
  else if (change.kind === "control" && change.id === "fuse_length" && typeof change.value === "number") lent.configureTool?.({ fuse_length: String(change.value) });
20290
21848
  });
21849
+ const worldEffects = createWorldEffects({ arsenal });
20291
21850
  const offEvents = lent.on((event) => {
21851
+ worldEffects.handle(event, lent.effects);
20292
21852
  if (event.type === "explosion") {
20293
21853
  const away = Math.hypot(event.point[0] - camera.position.x, event.point[1] - camera.position.y, event.point[2] - camera.position.z);
20294
21854
  audio?.play("explosion", Math.min(1, EXPLOSION_VOLUME_BASE + event.radius * EXPLOSION_VOLUME_PER_METRE), away);