@adatechnology/game-test-room 0.7.0 → 0.8.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.
@@ -341,6 +341,37 @@ var GameTestRoom = (() => {
341
341
  }
342
342
  return { ok: problems.length === 0, problems };
343
343
  }
344
+ var DEFAULT_ENVIRONMENT = {
345
+ rain: 0,
346
+ windX: 0,
347
+ windZ: 0,
348
+ windSpeed: 0,
349
+ gust: 0,
350
+ fogFar: 220,
351
+ visibility: 220,
352
+ nightFactor: 0,
353
+ temperatureC: 20,
354
+ wetness: 0
355
+ };
356
+ function createEnvironmentBus(initial = {}) {
357
+ let state = { ...DEFAULT_ENVIRONMENT, ...initial };
358
+ const listeners = /* @__PURE__ */ new Set();
359
+ return {
360
+ get: () => state,
361
+ set(patch) {
362
+ const changed = Object.keys(patch).filter((key) => patch[key] !== void 0 && patch[key] !== state[key]);
363
+ if (changed.length === 0) return;
364
+ const next = { ...state };
365
+ for (const key of changed) next[key] = patch[key];
366
+ state = next;
367
+ for (const listener of [...listeners]) listener(state, changed);
368
+ },
369
+ on(listener) {
370
+ listeners.add(listener);
371
+ return () => void listeners.delete(listener);
372
+ }
373
+ };
374
+ }
344
375
 
345
376
  // ../game-destruction/dist/index.js
346
377
  var FIRE_PRIME_C = 300;
@@ -836,9 +867,9 @@ var GameTestRoom = (() => {
836
867
  const step = count > 1 && room / (count - 1) < spacing ? room / (count - 1) : spacing;
837
868
  return Array.from({ length: count }, (_, i) => (i - (count - 1) / 2) * step);
838
869
  };
839
- var bar = (axis2, along, fixed) => {
870
+ var bar = (axis2, along, fixed2) => {
840
871
  const a = [0, 0, 0], b = [0, 0, 0];
841
- for (const [index, value] of fixed) {
872
+ for (const [index, value] of fixed2) {
842
873
  a[index] = value;
843
874
  b[index] = value;
844
875
  }
@@ -3147,15 +3178,15 @@ var GameTestRoom = (() => {
3147
3178
  return best;
3148
3179
  };
3149
3180
  const detonatePrimed = (until) => {
3150
- const end = time;
3181
+ const end3 = time;
3151
3182
  for (let next = nextPrimed(until); next; next = nextPrimed(until)) {
3152
3183
  primed.delete(next.p);
3153
- time = Math.max(0, Math.min(end, next.p.rt.chainDue));
3184
+ time = Math.max(0, Math.min(end3, next.p.rt.chainDue));
3154
3185
  next.p.rt.chainDue = void 0;
3155
3186
  failPart(next.o, next.p, null, null, 0, 40);
3156
3187
  drainBlasts();
3157
3188
  }
3158
- time = end;
3189
+ time = end3;
3159
3190
  };
3160
3191
  const releaseFront = () => {
3161
3192
  if (frontQueue.length === 0) return;
@@ -3767,6 +3798,1447 @@ var GameTestRoom = (() => {
3767
3798
  };
3768
3799
  var resolveStrings = (locale) => locales[locale ?? "en"] ?? locales.en;
3769
3800
 
3801
+ // ../game-audio/dist/index.js
3802
+ var FNV_OFFSET = 2166136261;
3803
+ var FNV_PRIME = 16777619;
3804
+ var MIX = 2654435761;
3805
+ function createRng2(seed) {
3806
+ let state = seed >>> 0;
3807
+ return () => {
3808
+ state = state + 1831565813 >>> 0;
3809
+ let mixed = Math.imul(state ^ state >>> 15, state | 1);
3810
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
3811
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
3812
+ };
3813
+ }
3814
+ function streamSeed(seed, name) {
3815
+ const label = String(name);
3816
+ let hash = FNV_OFFSET;
3817
+ for (let index = 0; index < label.length; index++) hash = Math.imul(hash ^ label.charCodeAt(index), FNV_PRIME) >>> 0;
3818
+ return Math.imul(seed >>> 0 ^ hash, MIX) >>> 0;
3819
+ }
3820
+ function streamFor(seed, name) {
3821
+ return createRng2(streamSeed(seed, name));
3822
+ }
3823
+ var DISTANCE_CURVES = {
3824
+ /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
3825
+ game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
3826
+ /** The game's `grito`: `1 / (1 + d² × 0.006)`, silent below 0.02 (jogo L62129–62130). */
3827
+ game_scream: { model: "inverse_square", k: 6e-3, minGain: 0.02 },
3828
+ /** The game's `voz`: `1 / (1 + d² × 0.01)`, silent below 0.02 (jogo L62146–62147). */
3829
+ game_voice: { model: "inverse_square", k: 0.01, minGain: 0.02 },
3830
+ /** The game's `boom`: `1.4 / (1 + d² × 0.002)`, never silent (jogo L68280). */
3831
+ game_boom: { model: "inverse_square", k: 2e-3, scale: 1.4 },
3832
+ /** The art scene's `outAt`: `vol / (1 + d² × 0.008)` (cena L1320). */
3833
+ art_scene: { model: "inverse_square", k: 8e-3 },
3834
+ /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
3835
+ animals: { model: "linear", range: 70, scale: 0.25, minGain: 5e-3 }
3836
+ };
3837
+ function falloffGain(falloff, volume, distance4) {
3838
+ if (!falloff || falloff.model === "none") return volume;
3839
+ if (falloff.model === "linear") {
3840
+ const level2 = Math.max(0, 1 - distance4 / falloff.range) * volume;
3841
+ return falloff.scale === void 0 ? level2 : level2 * falloff.scale;
3842
+ }
3843
+ const level = volume / (1 + distance4 * distance4 * falloff.k);
3844
+ return falloff.scale === void 0 ? level : falloff.scale * level;
3845
+ }
3846
+ function isSilent(falloff, gain) {
3847
+ if (!falloff || falloff.model === "none" || falloff.minGain === void 0) return false;
3848
+ return falloff.model === "linear" ? gain <= falloff.minGain : gain < falloff.minGain;
3849
+ }
3850
+ function cutoffDivisor(falloff, distance4) {
3851
+ if (!falloff || falloff.model !== "inverse_square" || falloff.cutoffK === void 0) return 1;
3852
+ return 1 + distance4 * falloff.cutoffK;
3853
+ }
3854
+ var DEFAULT_LISTENER = { position: [0, 0, 0], yaw: 0 };
3855
+ var PAN_LIMIT = 0.85;
3856
+ var PAN_MIN_DISTANCE = 2;
3857
+ function listenerDistance(listener, position, mode = "xz") {
3858
+ const dx = position[0] - listener.position[0];
3859
+ const dz = position[2] - listener.position[2];
3860
+ if (mode === "xz") return Math.hypot(dx, dz);
3861
+ return Math.hypot(dx, position[1] - listener.position[1], dz);
3862
+ }
3863
+ function panFor(listener, position) {
3864
+ const rightX = Math.cos(listener.yaw);
3865
+ const rightZ = -Math.sin(listener.yaw);
3866
+ const dx = position[0] - listener.position[0];
3867
+ const dz = position[2] - listener.position[2];
3868
+ const distance4 = Math.hypot(dx, dz) || 1;
3869
+ const pan = (dx * rightX + dz * rightZ) / Math.max(distance4, PAN_MIN_DISTANCE);
3870
+ return pan < -PAN_LIMIT ? -PAN_LIMIT : pan > PAN_LIMIT ? PAN_LIMIT : pan;
3871
+ }
3872
+ var voice_groups_default = {
3873
+ undead_voice: {
3874
+ maxConcurrent: 5,
3875
+ cooldown: 0.25,
3876
+ maxDistance: 40,
3877
+ sources: [
3878
+ "cena:L1342 `AU.groans > 4` refuses, so five play at once",
3879
+ "jogo:L66986 one groan every 0.25 s (`groanBudget`), only nearer than 40 m",
3880
+ "jogo:L70472 the budget counts down by the frame time"
3881
+ ]
3882
+ }
3883
+ };
3884
+ var VOICE_GROUPS = voice_groups_default;
3885
+ var BUS_PRIORITY = { ui: 5, weapons: 4, voices: 3, world: 2, weather: 1, ambience: 1 };
3886
+ var DEFAULT_MAX_VOICES = 32;
3887
+ function busPriority(bus) {
3888
+ return BUS_PRIORITY[bus];
3889
+ }
3890
+ function createVoiceManager(options = {}) {
3891
+ const groups = new Map(Object.entries(options.groups ?? VOICE_GROUPS));
3892
+ const maxVoices = options.maxVoices ?? DEFAULT_MAX_VOICES;
3893
+ const budgets = /* @__PURE__ */ new Map();
3894
+ let voices = [];
3895
+ let nextId = 1;
3896
+ let peak = 0;
3897
+ let dropped = 0;
3898
+ const refuse = (reason) => {
3899
+ dropped += 1;
3900
+ return { accepted: false, reason };
3901
+ };
3902
+ const groupCheck = (request, evicted) => {
3903
+ const group = request.group === void 0 ? void 0 : groups.get(request.group);
3904
+ if (!group) return void 0;
3905
+ if (group.maxDistance !== void 0 && !((request.distance ?? 0) < group.maxDistance)) return "distance";
3906
+ if (group.cooldown !== void 0 && (budgets.get(request.group) ?? 0) > 0) return "cooldown";
3907
+ const members = voices.filter((voice) => voice.group === request.group);
3908
+ if (group.maxConcurrent === void 0 || members.length < group.maxConcurrent) return void 0;
3909
+ if (!group.steal) return "group_full";
3910
+ evicted.push(members[0].id);
3911
+ return void 0;
3912
+ };
3913
+ return {
3914
+ request(request) {
3915
+ const evicted = [];
3916
+ const refusal = groupCheck(request, evicted);
3917
+ if (refusal) return refuse(refusal);
3918
+ const score = request.priority * request.gain;
3919
+ const remaining = voices.filter((voice) => !evicted.includes(voice.id));
3920
+ if (remaining.length >= maxVoices) {
3921
+ const lowest = remaining.reduce((low, voice) => voice.score < low.score ? voice : low);
3922
+ if (score <= lowest.score) return refuse("global_full");
3923
+ evicted.push(lowest.id);
3924
+ dropped += 1;
3925
+ }
3926
+ const group = request.group === void 0 ? void 0 : groups.get(request.group);
3927
+ if (group?.cooldown !== void 0) budgets.set(request.group, group.cooldown);
3928
+ const id = nextId++;
3929
+ voices = [...voices.filter((voice) => !evicted.includes(voice.id)), { id, ...request.group === void 0 ? {} : { group: request.group }, score, remaining: request.length }];
3930
+ peak = Math.max(peak, voices.length);
3931
+ return { accepted: true, id, evicted };
3932
+ },
3933
+ release(id) {
3934
+ voices = voices.filter((voice) => voice.id !== id);
3935
+ },
3936
+ setLength(id, seconds) {
3937
+ const voice = voices.find((entry) => entry.id === id);
3938
+ if (voice) voice.remaining = seconds;
3939
+ },
3940
+ advance(dt) {
3941
+ for (const [group, budget] of budgets) budgets.set(group, budget - dt);
3942
+ const ended = [];
3943
+ for (const voice of voices) {
3944
+ voice.remaining -= dt;
3945
+ if (voice.remaining <= 0) ended.push(voice.id);
3946
+ }
3947
+ if (ended.length > 0) voices = voices.filter((voice) => voice.remaining > 0);
3948
+ return ended;
3949
+ },
3950
+ setGroup(id, group) {
3951
+ if (group) groups.set(id, group);
3952
+ else groups.delete(id);
3953
+ },
3954
+ stats: () => ({ active: voices.length, peak, dropped, maxVoices })
3955
+ };
3956
+ }
3957
+ var DEFAULT_TAIL = 0.05;
3958
+ var DEFAULT_NOISE_SECONDS = 1.5;
3959
+ var OCCLUSION_GAIN = 0.5;
3960
+ var OCCLUSION_CUTOFF = 0.6;
3961
+ function isParamRef(range2) {
3962
+ return typeof range2 === "object" && !Array.isArray(range2);
3963
+ }
3964
+ function draw(range2, scope) {
3965
+ if (typeof range2 === "number") return range2;
3966
+ if (isParamRef(range2)) {
3967
+ const value = scope.params[range2.param];
3968
+ if (value === void 0) throw new Error(`game-audio: the recipe reads the param "${range2.param}", which was not given`);
3969
+ return value * (range2.scale ?? 1) + (range2.add ?? 0);
3970
+ }
3971
+ const [min2, max2] = range2;
3972
+ return min2 + scope.random() * (max2 - min2);
3973
+ }
3974
+ function drawTime(time, scope) {
3975
+ if (time === "duration" || typeof time === "string" && time.endsWith("d")) return time;
3976
+ return draw(time, scope);
3977
+ }
3978
+ function timeOf(time, duration) {
3979
+ if (typeof time === "number") return time;
3980
+ if (time === "duration") return duration;
3981
+ return Number(time.slice(0, -1)) * duration;
3982
+ }
3983
+ function drawAutomation(automation, scope) {
3984
+ const pending = {};
3985
+ for (const key of Object.keys(automation)) {
3986
+ if (key === "set" && "set" in automation) pending.value = draw(automation.set, scope);
3987
+ else if (key === "from" && "from" in automation) pending.from = draw(automation.from, scope);
3988
+ else if (key === "to" && automation.to !== void 0) pending.to = automation.to === "from" ? pending.from ?? pending.value ?? 0 : draw(automation.to, scope);
3989
+ else if (key === "curve") pending.curve = automation.curve;
3990
+ else if (key === "over" && automation.over !== void 0) pending.over = drawTime(automation.over, scope);
3991
+ }
3992
+ return pending;
3993
+ }
3994
+ function finishAutomation(pending, duration) {
3995
+ if (pending.from !== void 0 && pending.to === void 0) return { from: pending.from };
3996
+ if (pending.from !== void 0) return { from: pending.from, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over ?? "duration", duration) };
3997
+ if (pending.to === void 0 || pending.over === void 0) return { value: pending.value };
3998
+ return { value: pending.value, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over, duration) };
3999
+ }
4000
+ var startFrequency = (automation) => automation.from ?? automation.value ?? 0;
4001
+ function drawFilter(spec, scope) {
4002
+ const pending = { spec, frequency: {} };
4003
+ for (const key of Object.keys(spec)) {
4004
+ if (key === "frequency") pending.frequency = drawAutomation(spec.frequency, scope);
4005
+ else if (key === "q" && spec.q !== void 0) pending.q = draw(spec.q, scope);
4006
+ else if (key === "gain" && spec.gain !== void 0) pending.gain = draw(spec.gain, scope);
4007
+ else if (key === "level" && spec.level !== void 0) pending.level = draw(spec.level, scope);
4008
+ }
4009
+ return pending;
4010
+ }
4011
+ function drawModulator(modulator, scope) {
4012
+ const pending = { modulator, rate: 0 };
4013
+ for (const key of Object.keys(modulator)) {
4014
+ if (key === "rate") pending.rate = draw(modulator.rate, scope);
4015
+ else if (key === "depth" && !(typeof modulator.depth === "object" && "scaleOf" in modulator.depth)) pending.depth = draw(modulator.depth, scope);
4016
+ }
4017
+ return pending;
4018
+ }
4019
+ function drawVoice(source, scope) {
4020
+ const pending = { frequency: {}, vibratoRate: 0, formants: [], breath: { frequency: 0, q: source.breath.q, gain: 0 } };
4021
+ for (const key of Object.keys(source)) {
4022
+ if (key === "frequency") pending.frequency = drawAutomation(source.frequency, scope);
4023
+ else if (key === "vibrato") pending.vibratoRate = draw(source.vibrato.rate, scope);
4024
+ else if (key === "formants") pending.formants = source.formants.map((formant) => ({ frequency: draw(formant.frequency, scope), q: formant.q }));
4025
+ else if (key === "breath") pending.breath = { frequency: draw(source.breath.frequency, scope), q: source.breath.q, gain: draw(source.breath.gain, scope) };
4026
+ }
4027
+ return pending;
4028
+ }
4029
+ function drawLayer(layer2, scope) {
4030
+ const pending = { at: 0, gain: 1, filters: [], modulators: [] };
4031
+ for (const key of Object.keys(layer2)) {
4032
+ if (key === "at" && layer2.at !== void 0) pending.at = draw(layer2.at, scope);
4033
+ else if (key === "duration" && layer2.duration !== void 0) pending.duration = draw(layer2.duration, scope);
4034
+ else if (key === "gain" && layer2.gain !== void 0) pending.gain = draw(layer2.gain, scope);
4035
+ else if (key === "source" && layer2.source.kind === "osc") pending.automation = drawAutomation(layer2.source.frequency, scope);
4036
+ else if (key === "source" && layer2.source.kind === "formant_voice") pending.voice = drawVoice(layer2.source, scope);
4037
+ else if (key === "filters" && layer2.filters) pending.filters = layer2.filters.bank.map((spec) => drawFilter(spec, scope));
4038
+ else if (key === "modulators" && layer2.modulators) pending.modulators = layer2.modulators.map((modulator) => drawModulator(modulator, scope));
4039
+ }
4040
+ return pending;
4041
+ }
4042
+ function drawOffset(source, duration, scope, noiseSeconds) {
4043
+ if (source.kind === "formant_voice") return scope.random();
4044
+ if (source.kind !== "noise" || source.offset === void 0) return 0;
4045
+ if (typeof source.offset === "number") return source.offset;
4046
+ if (source.offset === "fit") return scope.random() * Math.max(0, noiseSeconds - duration);
4047
+ return scope.random() * (source.offsetScale ?? 1);
4048
+ }
4049
+ function finishSource(layer2, pending, duration, offset) {
4050
+ const source = layer2.source;
4051
+ if (source.kind === "noise") return { kind: "noise", offset, loop: source.loop === true };
4052
+ if (source.kind === "osc") return { kind: "osc", wave: source.wave, frequency: finishAutomation(pending.automation ?? {}, duration) };
4053
+ const voice = pending.voice;
4054
+ return {
4055
+ kind: "formant_voice",
4056
+ wave: source.wave,
4057
+ frequency: finishAutomation(voice.frequency, duration),
4058
+ vibrato: { rate: voice.vibratoRate, depth: startFrequency(voice.frequency) * source.vibrato.depth },
4059
+ formants: voice.formants,
4060
+ breath: { ...voice.breath, offset },
4061
+ mix: source.mix
4062
+ };
4063
+ }
4064
+ function finishModulator(pending, sourceFrequency) {
4065
+ const { modulator } = pending;
4066
+ const depth = typeof modulator.depth === "object" && "scaleOf" in modulator.depth ? sourceFrequency * modulator.depth.factor : pending.depth ?? 0;
4067
+ return { target: modulator.target, ...modulator.wave ? { wave: modulator.wave } : {}, rate: pending.rate, depth, ...modulator.base === void 0 ? {} : { base: modulator.base } };
4068
+ }
4069
+ function finishEnvelope(envelope2, duration, level) {
4070
+ return envelope2.map(([time, value, curve, fixed2]) => ({ time: timeOf(time, duration), value: fixed2 === "fixed" ? value : value * level, curve }));
4071
+ }
4072
+ function planLayer(layer2, scope, defaults, levelInEnvelope, soundLevel) {
4073
+ const pending = drawLayer(layer2, scope);
4074
+ const duration = pending.duration ?? defaults.duration;
4075
+ if (duration === void 0) throw new Error("game-audio: a layer needs a duration (its own or the recipe\u2019s)");
4076
+ const offset = drawOffset(layer2.source, duration, scope, defaults.noiseSeconds);
4077
+ const level = pending.gain * soundLevel;
4078
+ const sourceFrequency = layer2.source.kind === "noise" ? 0 : startFrequency(pending.automation ?? pending.voice?.frequency ?? {});
4079
+ const planned = {
4080
+ at: pending.at,
4081
+ duration,
4082
+ tail: layer2.tail ?? defaults.tail ?? DEFAULT_TAIL,
4083
+ level,
4084
+ ...layer2.wet === void 0 ? {} : { wet: layer2.wet },
4085
+ source: finishSource(layer2, pending, duration, offset),
4086
+ ...layer2.filters ? {
4087
+ filters: {
4088
+ mode: layer2.filters.mode,
4089
+ bank: pending.filters.map(({ spec, frequency, q, gain, level: filterLevel }) => ({
4090
+ type: spec.type,
4091
+ frequency: finishAutomation(frequency, duration),
4092
+ ...q === void 0 ? {} : { q },
4093
+ ...gain === void 0 ? {} : { gain },
4094
+ ...filterLevel === void 0 ? {} : { level: filterLevel }
4095
+ }))
4096
+ }
4097
+ } : {},
4098
+ modulators: pending.modulators.map((modulator) => finishModulator(modulator, sourceFrequency)),
4099
+ envelope: finishEnvelope(layer2.envelope, duration, levelInEnvelope ? level : 1)
4100
+ };
4101
+ if (!layer2.repeat) return [planned];
4102
+ return Array.from({ length: layer2.repeat.count }, (_, index) => ({ ...planned, at: planned.at + index * (layer2.repeat?.spacing ?? 0) }));
4103
+ }
4104
+ function scaleAutomation(automation, divisor) {
4105
+ const scale2 = (value) => value === void 0 ? void 0 : value / divisor;
4106
+ const scaled2 = { ...automation };
4107
+ for (const key of ["value", "from", "to"]) if (automation[key] !== void 0) scaled2[key] = scale2(automation[key]);
4108
+ return scaled2;
4109
+ }
4110
+ function muffle(layer2, divisor) {
4111
+ if (divisor === 1 || !layer2.filters) return layer2;
4112
+ const bank = layer2.filters.bank.map((filter) => filter.type === "lowpass" ? { ...filter, frequency: scaleAutomation(filter.frequency, divisor) } : filter);
4113
+ return { ...layer2, filters: { ...layer2.filters, bank } };
4114
+ }
4115
+ var layerEnd = (layer2) => layer2.at + layer2.duration + layer2.tail;
4116
+ function planSound(recipe, input = {}) {
4117
+ const listener = input.listener ?? DEFAULT_LISTENER;
4118
+ const distance4 = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0);
4119
+ const occlusion = Math.min(1, Math.max(0, input.occlusion ?? 0));
4120
+ const order = recipe.order ?? "layered";
4121
+ const falloffLevel = falloffGain(recipe.falloff, input.volume ?? 1, distance4);
4122
+ const level = occlusion > 0 ? falloffLevel * (1 - OCCLUSION_GAIN * occlusion) : falloffLevel;
4123
+ const silent = isSilent(recipe.falloff, falloffLevel);
4124
+ const base = {
4125
+ id: recipe.id,
4126
+ bus: recipe.bus,
4127
+ order,
4128
+ ...recipe.group === void 0 ? {} : { group: recipe.group },
4129
+ priority: recipe.priority ?? busPriority(recipe.bus),
4130
+ level,
4131
+ distance: distance4,
4132
+ ...recipe.pan ? { pan: input.position ? panFor(listener, input.position) : 0 } : {}
4133
+ };
4134
+ if (silent) return recipe.keepSilent ? { ...base, silent: true, length: 0, layers: [] } : null;
4135
+ const scope = { random: input.random ?? createRng2(1), params: { ...input.params } };
4136
+ for (const [name, range2] of Object.entries(recipe.vars ?? {})) if (scope.params[name] === void 0) scope.params[name] = draw(range2, scope);
4137
+ const defaults = {
4138
+ ...recipe.duration === void 0 ? {} : { duration: draw(recipe.duration, scope) },
4139
+ ...recipe.tail === void 0 ? {} : { tail: recipe.tail },
4140
+ noiseSeconds: input.noiseSeconds ?? DEFAULT_NOISE_SECONDS
4141
+ };
4142
+ const divisor = cutoffDivisor(recipe.falloff, distance4) / (1 - OCCLUSION_CUTOFF * occlusion);
4143
+ const layers = recipe.layers.flatMap((layer2) => planLayer(layer2, scope, defaults, order !== "layered", level)).map((layer2) => muffle(layer2, divisor));
4144
+ return { ...base, silent: false, length: Math.max(0, ...layers.map(layerEnd)), layers };
4145
+ }
4146
+ var LOOP_START_ABOVE = 2e-3;
4147
+ var LOOP_SMOOTHING = { volume: 0.15, frequency: 0.08 };
4148
+ function createLoopState(recipe, open = false) {
4149
+ const startAbove = recipe.startAbove ?? LOOP_START_ABOVE;
4150
+ let created = open;
4151
+ let volume = recipe.volume ?? 0;
4152
+ return {
4153
+ set(state) {
4154
+ volume = state.volume ?? volume;
4155
+ const create = !created;
4156
+ if (create && !(volume >= startAbove)) return null;
4157
+ created = true;
4158
+ const frequency = recipe.source.kind === "osc" && state.frequency ? state.frequency : void 0;
4159
+ return { create, volume, ...frequency === void 0 ? {} : { frequency } };
4160
+ },
4161
+ get created() {
4162
+ return created;
4163
+ },
4164
+ reset() {
4165
+ created = false;
4166
+ }
4167
+ };
4168
+ }
4169
+ var mix_profiles_default = [
4170
+ {
4171
+ id: "game",
4172
+ master: 0.5,
4173
+ noise: { seconds: 1.5, create: "lazy" },
4174
+ 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"]
4175
+ },
4176
+ {
4177
+ id: "art_scene",
4178
+ master: 0.85,
4179
+ compressor: { threshold: -12, ratio: 5 },
4180
+ reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
4181
+ noise: { seconds: 2, create: "eager" },
4182
+ sources: [
4183
+ "cena:L1308 compressor -12 dB, ratio 5",
4184
+ "cena:L1309 master 0.85",
4185
+ "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
4186
+ "cena:L1311 wet 0.4",
4187
+ "cena:L1312 noise 2 s made with the chain"
4188
+ ]
4189
+ }
4190
+ ];
4191
+ var MIX_PROFILES = Object.fromEntries(
4192
+ mix_profiles_default.map((profile) => [profile.id, profile])
4193
+ );
4194
+ var DEFAULT_MIX = "art_scene";
4195
+ function resolveMix(mix, profiles = MIX_PROFILES) {
4196
+ if (typeof mix !== "string") return mix;
4197
+ const profile = profiles[mix];
4198
+ if (!profile) throw new Error(`game-audio: unknown mix profile "${mix}" (known: ${Object.keys(profiles).join(", ")})`);
4199
+ return profile;
4200
+ }
4201
+ function busGain(setting) {
4202
+ if (!setting) return 1;
4203
+ return setting.muted ? 0 : setting.gain ?? 1;
4204
+ }
4205
+ function isPlainObject2(value) {
4206
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4207
+ }
4208
+ function mergeRecords(base, patch) {
4209
+ const merged = { ...base };
4210
+ for (const [key, value] of Object.entries(patch)) {
4211
+ if (value === void 0) continue;
4212
+ const current = merged[key];
4213
+ merged[key] = isPlainObject2(current) && isPlainObject2(value) ? mergeRecords(current, value) : value;
4214
+ }
4215
+ return merged;
4216
+ }
4217
+ function createSoundRegistry(banks = []) {
4218
+ const sounds = /* @__PURE__ */ new Map();
4219
+ const loops = /* @__PURE__ */ new Map();
4220
+ const owners = /* @__PURE__ */ new Map();
4221
+ const registry = {
4222
+ register(bank) {
4223
+ for (const sound of bank.sounds ?? []) {
4224
+ sounds.set(sound.id, sound);
4225
+ owners.set(sound.id, bank.id);
4226
+ }
4227
+ for (const loop of bank.loops ?? []) {
4228
+ loops.set(loop.id, loop);
4229
+ owners.set(loop.id, bank.id);
4230
+ }
4231
+ },
4232
+ patch(id, patch) {
4233
+ const sound = sounds.get(id);
4234
+ if (sound) sounds.set(id, mergeRecords(sound, patch));
4235
+ const loop = loops.get(id);
4236
+ if (loop) loops.set(id, mergeRecords(loop, patch));
4237
+ },
4238
+ remove(id) {
4239
+ sounds.delete(id);
4240
+ loops.delete(id);
4241
+ owners.delete(id);
4242
+ },
4243
+ sound: (id) => sounds.get(id),
4244
+ loop: (id) => loops.get(id),
4245
+ soundIds: () => [...sounds.keys()],
4246
+ loopIds: () => [...loops.keys()],
4247
+ bankOf: (id) => owners.get(id)
4248
+ };
4249
+ for (const bank of banks) registry.register(bank);
4250
+ return registry;
4251
+ }
4252
+ var game_sfx_default = {
4253
+ sources: ["jogo:L62089-62107"],
4254
+ 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`.",
4255
+ classes: [
4256
+ { id: "pistol", team: "pistola", bus: "weapons", cutoff: 1800, gain: 0.9, seconds: 0.18 },
4257
+ { id: "shotgun", team: "escopeta", bus: "weapons", cutoff: 900, gain: 1.2, seconds: 0.35 },
4258
+ { id: "rifle", team: "fuzil", bus: "weapons", cutoff: 1400, gain: 1.1, seconds: 0.28 },
4259
+ { id: "long_rifle", team: "rifle", bus: "weapons", cutoff: 1100, gain: 1.3, seconds: 0.45 },
4260
+ { id: "impact", team: "impacto", bus: "world", cutoff: 2500, gain: 0.25, seconds: 0.05 },
4261
+ { id: "flesh", team: "carne", bus: "world", cutoff: 600, gain: 0.35, seconds: 0.08 },
4262
+ { id: "step", team: "passo", bus: "world", cutoff: 400, gain: 0.08, seconds: 0.05 },
4263
+ { id: "glass", team: "vidro", bus: "world", cutoff: 5e3, gain: 0.5, seconds: 0.3 },
4264
+ { id: "wood", team: "madeira", bus: "world", cutoff: 700, gain: 0.5, seconds: 0.2 },
4265
+ { id: "reload", team: "recarga", bus: "weapons", cutoff: 3e3, gain: 0.15, seconds: 0.06 },
4266
+ { id: "empty", team: "vazio", bus: "weapons", cutoff: 4e3, gain: 0.12, seconds: 0.03 },
4267
+ { id: "explosion", team: "explosao", bus: "world", cutoff: 200, gain: 1.6, seconds: 1.4 }
4268
+ ],
4269
+ fallback: { id: "unknown", team: "(any other name)", bus: "world", cutoff: 1e3, gain: 0.5, seconds: 0.1 }
4270
+ };
4271
+ var GAME_SFX = game_sfx_default;
4272
+ var GAME_SFX_CLASSES = GAME_SFX.classes;
4273
+ var GAME_SFX_FALLBACK = GAME_SFX.fallback;
4274
+ function noiseClassRecipe(row, id = `sfx_${row.id}`) {
4275
+ return {
4276
+ id,
4277
+ bus: row.bus,
4278
+ order: "burst",
4279
+ duration: row.seconds,
4280
+ tail: 0.05,
4281
+ layers: [
4282
+ {
4283
+ source: { kind: "noise", offset: "random" },
4284
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: row.cutoff } }] },
4285
+ envelope: [
4286
+ [0, row.gain, "set"],
4287
+ ["1d", 1e-3, "exp", "fixed"]
4288
+ ]
4289
+ }
4290
+ ],
4291
+ falloff: DISTANCE_CURVES.game_sfx,
4292
+ sources: [...GAME_SFX.sources.map((source) => `${source} ${row.team}`)]
4293
+ };
4294
+ }
4295
+ var GAME_SFX_BANK = { id: "game_sfx", sounds: [...GAME_SFX_CLASSES, GAME_SFX_FALLBACK].map((row) => noiseClassRecipe(row)) };
4296
+ function fillNoise(random, length3) {
4297
+ const samples = new Float32Array(length3);
4298
+ for (let index = 0; index < length3; index++) samples[index] = random() * 2 - 1;
4299
+ return samples;
4300
+ }
4301
+ function fillImpulse(random, options) {
4302
+ const size = Math.floor(options.length);
4303
+ return Array.from({ length: options.channels }, () => {
4304
+ const samples = new Float32Array(size);
4305
+ for (let index = 0; index < options.length; index++) {
4306
+ const value = (random() * 2 - 1) * Math.exp(options.decay * Math.log(1 - index / options.length));
4307
+ if (index < size) samples[index] = value;
4308
+ }
4309
+ return samples;
4310
+ });
4311
+ }
4312
+ function bufferOf(context, channels, requestedLength) {
4313
+ const buffer = context.createBuffer(channels.length, requestedLength, context.sampleRate);
4314
+ channels.forEach((samples, channel) => buffer.getChannelData(channel).set(samples));
4315
+ return buffer;
4316
+ }
4317
+ function buildChain(context, profile, random) {
4318
+ let compressor;
4319
+ if (profile.compressor) {
4320
+ compressor = context.createDynamicsCompressor();
4321
+ compressor.threshold.value = profile.compressor.threshold;
4322
+ compressor.ratio.value = profile.compressor.ratio;
4323
+ if (profile.compressor.knee !== void 0) compressor.knee.value = profile.compressor.knee;
4324
+ if (profile.compressor.attack !== void 0) compressor.attack.value = profile.compressor.attack;
4325
+ if (profile.compressor.release !== void 0) compressor.release.value = profile.compressor.release;
4326
+ compressor.connect(context.destination);
4327
+ }
4328
+ const master = context.createGain();
4329
+ master.gain.value = profile.master;
4330
+ master.connect(compressor ?? context.destination);
4331
+ if (!profile.reverb) {
4332
+ return {
4333
+ master,
4334
+ disconnect: () => {
4335
+ master.disconnect();
4336
+ compressor?.disconnect();
4337
+ }
4338
+ };
4339
+ }
4340
+ const convolver = context.createConvolver();
4341
+ const length3 = context.sampleRate * profile.reverb.seconds;
4342
+ convolver.buffer = bufferOf(context, fillImpulse(random, { channels: 2, length: length3, decay: profile.reverb.decay }), length3);
4343
+ const wet = context.createGain();
4344
+ wet.gain.value = profile.reverb.wet;
4345
+ wet.connect(convolver);
4346
+ convolver.connect(master);
4347
+ return {
4348
+ master,
4349
+ wet,
4350
+ disconnect: () => {
4351
+ wet.disconnect();
4352
+ convolver.disconnect();
4353
+ master.disconnect();
4354
+ compressor?.disconnect();
4355
+ }
4356
+ };
4357
+ }
4358
+ function globalContext() {
4359
+ const scope = globalThis;
4360
+ const Constructor = scope.AudioContext ?? scope.webkitAudioContext;
4361
+ return Constructor ? new Constructor() : null;
4362
+ }
4363
+ function unlockContext(context) {
4364
+ if (context.state === "suspended") void context.resume();
4365
+ }
4366
+ function assign(param2, automation, time) {
4367
+ if (automation.value !== void 0) param2.value = automation.value;
4368
+ if (automation.from !== void 0) param2.setValueAtTime(automation.from, time);
4369
+ }
4370
+ function ramp(param2, curve, value, at3) {
4371
+ if (curve === "set") param2.setValueAtTime(value, at3);
4372
+ else if (curve === "exp") param2.exponentialRampToValueAtTime(value, at3);
4373
+ else param2.linearRampToValueAtTime(value, at3);
4374
+ }
4375
+ function rampTo(param2, automation, time) {
4376
+ if (automation.to !== void 0 && automation.at !== void 0) ramp(param2, automation.curve ?? "linear", automation.to, time + automation.at);
4377
+ }
4378
+ function applyEnvelope(param2, points, time) {
4379
+ for (const point of points) ramp(param2, point.curve, point.value, time + point.time);
4380
+ }
4381
+ function noiseSource(target, loop) {
4382
+ const source = target.context.createBufferSource();
4383
+ source.buffer = target.noise();
4384
+ if (loop) source.loop = true;
4385
+ return source;
4386
+ }
4387
+ function filterNode(context, filter, time) {
4388
+ const node = context.createBiquadFilter();
4389
+ node.type = filter.type;
4390
+ assign(node.frequency, filter.frequency, time);
4391
+ if (filter.q !== void 0) node.Q.value = filter.q;
4392
+ if (filter.gain !== void 0) node.gain.value = filter.gain;
4393
+ return node;
4394
+ }
4395
+ function gainNode(context, value) {
4396
+ const node = context.createGain();
4397
+ node.gain.value = value;
4398
+ return node;
4399
+ }
4400
+ function voiceOf(sources) {
4401
+ return {
4402
+ sources,
4403
+ stop() {
4404
+ for (const source of sources) {
4405
+ try {
4406
+ source.stop();
4407
+ } catch {
4408
+ }
4409
+ }
4410
+ }
4411
+ };
4412
+ }
4413
+ var fixed = (range2) => typeof range2 === "number" ? range2 : 0;
4414
+ var fixedFrequency = (automation) => fixed("set" in automation ? automation.set : automation.from);
4415
+ function sourceOf(recipe, target) {
4416
+ if (recipe.source.kind === "noise") return { source: noiseSource(target, true) };
4417
+ const oscillator = target.context.createOscillator();
4418
+ oscillator.type = recipe.source.wave;
4419
+ if (recipe.source.frequency) oscillator.frequency.value = fixedFrequency(recipe.source.frequency);
4420
+ return { source: oscillator, frequency: oscillator.frequency };
4421
+ }
4422
+ function stopAll(sources) {
4423
+ return () => {
4424
+ for (const source of sources) {
4425
+ try {
4426
+ source.stop();
4427
+ } catch {
4428
+ }
4429
+ }
4430
+ };
4431
+ }
4432
+ function realizeGameLoop(recipe, target) {
4433
+ const { context } = target;
4434
+ const gain = context.createGain();
4435
+ const filters = (recipe.filters?.bank ?? []).map(() => context.createBiquadFilter());
4436
+ gain.gain.value = 0;
4437
+ const last = filters.reduce((previous, filter) => previous ? previous.connect(filter) : filter, void 0);
4438
+ last?.connect(gain);
4439
+ gain.connect(target.output);
4440
+ const { source, frequency } = sourceOf(recipe, target);
4441
+ recipe.filters?.bank.forEach((spec, index) => {
4442
+ const filter = filters[index];
4443
+ filter.type = spec.type;
4444
+ filter.frequency.value = fixedFrequency(spec.frequency);
4445
+ if (spec.q !== void 0) filter.Q.value = fixed(spec.q);
4446
+ });
4447
+ const sources = [source];
4448
+ for (const modulator of recipe.modulators ?? []) {
4449
+ const lfo = context.createOscillator();
4450
+ const depth = context.createGain();
4451
+ const stage = context.createGain();
4452
+ if (modulator.wave) lfo.type = modulator.wave;
4453
+ lfo.frequency.value = fixed(modulator.rate);
4454
+ depth.gain.value = fixed(modulator.depth);
4455
+ stage.gain.value = modulator.base ?? 0;
4456
+ lfo.connect(depth);
4457
+ depth.connect(stage.gain);
4458
+ last?.disconnect();
4459
+ last?.connect(stage);
4460
+ stage.connect(gain);
4461
+ lfo.start();
4462
+ sources.push(lfo);
4463
+ }
4464
+ source.connect(filters[0] ?? gain);
4465
+ source.start();
4466
+ return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll(sources) };
4467
+ }
4468
+ function realizeChainLoop(recipe, target, volume) {
4469
+ const { context } = target;
4470
+ const { source, frequency } = sourceOf(recipe, target);
4471
+ const filters = (recipe.filters?.bank ?? []).map(
4472
+ (spec) => filterNode(context, { type: spec.type, frequency: { value: fixedFrequency(spec.frequency) }, ...spec.q === void 0 ? {} : { q: fixed(spec.q) } }, target.time)
4473
+ );
4474
+ const gain = gainNode(context, volume);
4475
+ const lfos = (recipe.modulators ?? []).map((modulator) => {
4476
+ const lfo = context.createOscillator();
4477
+ if (modulator.wave) lfo.type = modulator.wave;
4478
+ lfo.frequency.value = fixed(modulator.rate);
4479
+ const depth = gainNode(context, fixed(modulator.depth));
4480
+ lfo.connect(depth);
4481
+ depth.connect(gain.gain);
4482
+ return lfo;
4483
+ });
4484
+ let tail = source;
4485
+ for (const filter of filters) tail = tail.connect(filter);
4486
+ tail.connect(gain);
4487
+ gain.connect(target.output);
4488
+ source.start();
4489
+ for (const lfo of lfos) lfo.start();
4490
+ return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll([source, ...lfos]) };
4491
+ }
4492
+ var INERT = { set() {
4493
+ }, stop() {
4494
+ } };
4495
+ function createLoopHandle(recipe, host, volume) {
4496
+ if (!recipe) return INERT;
4497
+ const loopRecipe = recipe;
4498
+ const isChain = loopRecipe.order === "chain_loop";
4499
+ const opening = volume ?? loopRecipe.volume ?? 0;
4500
+ const smoothing = loopRecipe.smoothing ?? LOOP_SMOOTHING;
4501
+ let state = createLoopState(loopRecipe);
4502
+ let nodes;
4503
+ const entry = {
4504
+ get created() {
4505
+ return nodes !== void 0;
4506
+ },
4507
+ rewire() {
4508
+ nodes?.gain.disconnect();
4509
+ nodes?.gain.connect(host.output(loopRecipe.bus));
4510
+ },
4511
+ stop() {
4512
+ nodes?.stop();
4513
+ nodes?.gain.disconnect();
4514
+ nodes = void 0;
4515
+ state = createLoopState(loopRecipe);
4516
+ host.unregister(entry);
4517
+ }
4518
+ };
4519
+ const create = (context, startVolume) => {
4520
+ if (loopRecipe.source.kind === "noise") host.prepareNoise();
4521
+ const target = { context, output: host.output(loopRecipe.bus), noise: host.noise, time: context.currentTime };
4522
+ const made = isChain ? realizeChainLoop(loopRecipe, target, startVolume) : realizeGameLoop(loopRecipe, target);
4523
+ host.register(entry);
4524
+ return made;
4525
+ };
4526
+ const open = () => {
4527
+ const context = host.ready();
4528
+ if (!context) return;
4529
+ nodes = create(context, opening);
4530
+ state = createLoopState({ ...loopRecipe, volume: opening }, true);
4531
+ };
4532
+ if (isChain) open();
4533
+ return {
4534
+ set(next) {
4535
+ if (isChain && !nodes) open();
4536
+ const context = host.ready();
4537
+ if (!context) return;
4538
+ const action = state.set(next);
4539
+ if (!action) return;
4540
+ nodes ?? (nodes = create(context, 0));
4541
+ nodes.gain.gain.setTargetAtTime(action.volume, context.currentTime, smoothing.volume);
4542
+ if (action.frequency !== void 0 && nodes.frequency) nodes.frequency.setTargetAtTime(action.frequency, context.currentTime, smoothing.frequency);
4543
+ },
4544
+ stop: entry.stop
4545
+ };
4546
+ }
4547
+ function createNoiseStore(random) {
4548
+ const store = {
4549
+ random,
4550
+ reset() {
4551
+ store.samples = void 0;
4552
+ store.buffer = void 0;
4553
+ }
4554
+ };
4555
+ return store;
4556
+ }
4557
+ function prepareNoise(store, context, seconds) {
4558
+ store.samples ?? (store.samples = fillNoise(store.random, Math.floor(context.sampleRate * seconds)));
4559
+ return store.samples;
4560
+ }
4561
+ function noiseBuffer(store, context, seconds) {
4562
+ const samples = prepareNoise(store, context, seconds);
4563
+ store.buffer ?? (store.buffer = bufferOf(context, [samples], context.sampleRate * seconds));
4564
+ return store.buffer;
4565
+ }
4566
+ var end = (layer2, start) => start + layer2.duration + layer2.tail;
4567
+ var firstFilter = (layer2) => layer2.filters?.bank[0];
4568
+ var offsetOf = (layer2) => layer2.source.kind === "noise" ? layer2.source.offset : 0;
4569
+ function setFilter(node, filter, time) {
4570
+ if (!filter) return;
4571
+ node.type = filter.type;
4572
+ assign(node.frequency, filter.frequency, time);
4573
+ if (filter.q !== void 0) node.Q.value = filter.q;
4574
+ }
4575
+ function realizeBurst(plan, target) {
4576
+ const { context } = target;
4577
+ const layer2 = plan.layers[0];
4578
+ const time = target.time + layer2.at;
4579
+ const gain = context.createGain();
4580
+ gain.connect(target.output);
4581
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
4582
+ const filter = context.createBiquadFilter();
4583
+ source.connect(filter);
4584
+ filter.connect(gain);
4585
+ setFilter(filter, firstFilter(layer2), time);
4586
+ applyEnvelope(gain.gain, layer2.envelope, time);
4587
+ source.start(time, offsetOf(layer2));
4588
+ source.stop(end(layer2, time));
4589
+ return voiceOf([source]);
4590
+ }
4591
+ function realizeScream(plan, target) {
4592
+ const { context } = target;
4593
+ const layer2 = plan.layers[0];
4594
+ const time = target.time + layer2.at;
4595
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
4596
+ const gain = context.createGain();
4597
+ gain.connect(target.output);
4598
+ for (const band of layer2.filters?.bank ?? []) {
4599
+ const filter = context.createBiquadFilter();
4600
+ filter.type = band.type;
4601
+ if (band.q !== void 0) filter.Q.value = band.q;
4602
+ assign(filter.frequency, band.frequency, time);
4603
+ rampTo(filter.frequency, band.frequency, time);
4604
+ const level = context.createGain();
4605
+ level.gain.value = band.level ?? 1;
4606
+ source.connect(filter);
4607
+ filter.connect(level);
4608
+ level.connect(gain);
4609
+ }
4610
+ const sources = [source];
4611
+ const lfos = layer2.modulators.map((modulator) => {
4612
+ const lfo = context.createOscillator();
4613
+ const depth = context.createGain();
4614
+ if (modulator.wave) lfo.type = modulator.wave;
4615
+ lfo.frequency.value = modulator.rate;
4616
+ depth.gain.value = modulator.depth;
4617
+ lfo.connect(depth);
4618
+ depth.connect(gain.gain);
4619
+ return lfo;
4620
+ });
4621
+ applyEnvelope(gain.gain, layer2.envelope, time);
4622
+ source.start(time, offsetOf(layer2));
4623
+ source.stop(end(layer2, time));
4624
+ for (const lfo of lfos) {
4625
+ lfo.start(time);
4626
+ lfo.stop(end(layer2, time));
4627
+ sources.push(lfo);
4628
+ }
4629
+ return voiceOf(sources);
4630
+ }
4631
+ function realizeGroan(plan, target) {
4632
+ const { context } = target;
4633
+ const layer2 = plan.layers[0];
4634
+ const time = target.time + layer2.at;
4635
+ const oscillator = context.createOscillator();
4636
+ const gain = context.createGain();
4637
+ const filter = context.createBiquadFilter();
4638
+ const band = firstFilter(layer2);
4639
+ if (layer2.source.kind === "osc") oscillator.type = layer2.source.wave;
4640
+ if (band) {
4641
+ filter.type = band.type;
4642
+ if (band.q !== void 0) filter.Q.value = band.q;
4643
+ }
4644
+ if (layer2.source.kind === "osc") {
4645
+ assign(oscillator.frequency, layer2.source.frequency, time);
4646
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
4647
+ }
4648
+ if (band) assign(filter.frequency, band.frequency, time);
4649
+ applyEnvelope(gain.gain, layer2.envelope, time);
4650
+ oscillator.connect(filter);
4651
+ filter.connect(gain);
4652
+ gain.connect(target.output);
4653
+ oscillator.start(time);
4654
+ oscillator.stop(end(layer2, time));
4655
+ return voiceOf([oscillator]);
4656
+ }
4657
+ function realizeBoom(plan, target) {
4658
+ const { context } = target;
4659
+ const layer2 = plan.layers[0];
4660
+ const time = target.time + layer2.at;
4661
+ const oscillator = context.createOscillator();
4662
+ const gain = context.createGain();
4663
+ if (layer2.source.kind === "osc") {
4664
+ oscillator.type = layer2.source.wave;
4665
+ assign(oscillator.frequency, layer2.source.frequency, time);
4666
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
4667
+ }
4668
+ applyEnvelope(gain.gain, layer2.envelope, time);
4669
+ oscillator.connect(gain);
4670
+ gain.connect(target.output);
4671
+ oscillator.start(time);
4672
+ oscillator.stop(end(layer2, time));
4673
+ return voiceOf([oscillator]);
4674
+ }
4675
+ function realizeStatic(plan, target) {
4676
+ const { context } = target;
4677
+ const layer2 = plan.layers[0];
4678
+ const time = target.time + layer2.at;
4679
+ const gain = context.createGain();
4680
+ const source = context.createBufferSource();
4681
+ const filter = context.createBiquadFilter();
4682
+ source.buffer = target.noise();
4683
+ setFilter(filter, firstFilter(layer2), time);
4684
+ applyEnvelope(gain.gain, layer2.envelope, time);
4685
+ source.connect(filter);
4686
+ filter.connect(gain);
4687
+ gain.connect(target.output);
4688
+ source.start(time, offsetOf(layer2));
4689
+ source.stop(end(layer2, time));
4690
+ return voiceOf([source]);
4691
+ }
4692
+ function realizeChord(plan, target) {
4693
+ const { context } = target;
4694
+ const layers = plan.layers;
4695
+ const oscillators = layers.map(() => context.createOscillator());
4696
+ const gain = context.createGain();
4697
+ for (let index = layers.length - 1; index >= 0; index--) {
4698
+ const source = layers[index].source;
4699
+ if (source.kind === "osc") oscillators[index].type = source.wave;
4700
+ }
4701
+ layers.forEach((layer2, index) => {
4702
+ if (layer2.source.kind === "osc") assign(oscillators[index].frequency, layer2.source.frequency, target.time + layer2.at);
4703
+ });
4704
+ applyEnvelope(gain.gain, layers[0].envelope, target.time + layers[0].at);
4705
+ for (const oscillator of oscillators) oscillator.connect(gain);
4706
+ gain.connect(target.output);
4707
+ layers.forEach((layer2, index) => oscillators[index].start(target.time + layer2.at));
4708
+ layers.forEach((layer2, index) => oscillators[index].stop(end(layer2, target.time + layer2.at)));
4709
+ return voiceOf(oscillators);
4710
+ }
4711
+ function realizeCalls(plan, target) {
4712
+ const { context } = target;
4713
+ const gain = context.createGain();
4714
+ const filter = context.createBiquadFilter();
4715
+ if (plan.silent) return voiceOf([]);
4716
+ const band = firstFilter(plan.layers[0]);
4717
+ if (band) filter.type = band.type;
4718
+ filter.connect(gain);
4719
+ gain.connect(target.output);
4720
+ const oscillators = plan.layers.map((layer2) => {
4721
+ const time = target.time + layer2.at;
4722
+ const oscillator = context.createOscillator();
4723
+ if (layer2.source.kind === "osc") {
4724
+ oscillator.type = layer2.source.wave;
4725
+ assign(oscillator.frequency, layer2.source.frequency, time);
4726
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
4727
+ }
4728
+ if (band) {
4729
+ assign(filter.frequency, band.frequency, time);
4730
+ if (band.q !== void 0) filter.Q.value = band.q;
4731
+ }
4732
+ const envelope2 = context.createGain();
4733
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
4734
+ oscillator.connect(envelope2);
4735
+ envelope2.connect(filter);
4736
+ oscillator.start(time);
4737
+ oscillator.stop(end(layer2, time));
4738
+ return oscillator;
4739
+ });
4740
+ return voiceOf(oscillators);
4741
+ }
4742
+ var end2 = (layer2, start) => start + layer2.duration + layer2.tail;
4743
+ function outputStage(plan, layer2, target) {
4744
+ const { context } = target;
4745
+ const level = gainNode(context, layer2.level);
4746
+ let node = level;
4747
+ if (plan.pan !== void 0 && typeof context.createStereoPanner === "function") {
4748
+ const panner = context.createStereoPanner();
4749
+ panner.pan.value = plan.pan;
4750
+ level.connect(panner);
4751
+ node = panner;
4752
+ }
4753
+ node.connect(target.output);
4754
+ if (layer2.wet && target.wet) {
4755
+ const send = gainNode(context, layer2.wet);
4756
+ node.connect(send);
4757
+ send.connect(target.wet);
4758
+ }
4759
+ return level;
4760
+ }
4761
+ function realizeNoiseLayer(plan, layer2, target) {
4762
+ const { context } = target;
4763
+ const time = target.time + layer2.at;
4764
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
4765
+ const filters = (layer2.filters?.bank ?? []).map((filter) => {
4766
+ const node = filterNode(context, filter, time);
4767
+ rampTo(node.frequency, filter.frequency, time);
4768
+ return node;
4769
+ });
4770
+ const envelope2 = context.createGain();
4771
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
4772
+ let tail = source;
4773
+ for (const filter of filters) tail = tail.connect(filter);
4774
+ tail.connect(envelope2);
4775
+ envelope2.connect(outputStage(plan, layer2, target));
4776
+ source.start(time, layer2.source.kind === "noise" ? layer2.source.offset : 0);
4777
+ source.stop(end2(layer2, time));
4778
+ return [source];
4779
+ }
4780
+ function realizeToneLayer(plan, layer2, target) {
4781
+ const { context } = target;
4782
+ const time = target.time + layer2.at;
4783
+ const oscillator = context.createOscillator();
4784
+ if (layer2.source.kind === "osc") {
4785
+ oscillator.type = layer2.source.wave;
4786
+ assign(oscillator.frequency, layer2.source.frequency, time);
4787
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
4788
+ }
4789
+ const envelope2 = context.createGain();
4790
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
4791
+ oscillator.connect(envelope2);
4792
+ envelope2.connect(outputStage(plan, layer2, target));
4793
+ oscillator.start(time);
4794
+ oscillator.stop(end2(layer2, time));
4795
+ return [oscillator];
4796
+ }
4797
+ function realizeVoiceLayer(plan, layer2, target) {
4798
+ const source = layer2.source;
4799
+ if (source.kind !== "formant_voice") return [];
4800
+ const { context } = target;
4801
+ const time = target.time + layer2.at;
4802
+ const oscillator = context.createOscillator();
4803
+ oscillator.type = source.wave;
4804
+ assign(oscillator.frequency, source.frequency, time);
4805
+ rampTo(oscillator.frequency, source.frequency, time);
4806
+ const vibrato = context.createOscillator();
4807
+ vibrato.frequency.value = source.vibrato.rate;
4808
+ const vibratoDepth = gainNode(context, source.vibrato.depth);
4809
+ vibrato.connect(vibratoDepth);
4810
+ vibratoDepth.connect(oscillator.frequency);
4811
+ const formants = source.formants.map((formant) => filterNode(context, { type: "bandpass", frequency: { value: formant.frequency }, q: formant.q }, time));
4812
+ const breath = noiseSource(target, true);
4813
+ const breathBand = filterNode(context, { type: "bandpass", frequency: { value: source.breath.frequency }, q: source.breath.q }, time);
4814
+ const breathLevel = gainNode(context, source.breath.gain);
4815
+ const mix = gainNode(context, source.mix);
4816
+ for (const formant of formants) oscillator.connect(formant);
4817
+ breath.connect(breathBand);
4818
+ breathBand.connect(breathLevel);
4819
+ for (const formant of formants) formant.connect(mix);
4820
+ breathLevel.connect(mix);
4821
+ const envelope2 = context.createGain();
4822
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
4823
+ mix.connect(envelope2);
4824
+ envelope2.connect(outputStage(plan, layer2, target));
4825
+ const stop = end2(layer2, time);
4826
+ oscillator.start(time);
4827
+ vibrato.start(time);
4828
+ breath.start(time, source.breath.offset);
4829
+ oscillator.stop(stop);
4830
+ vibrato.stop(stop);
4831
+ breath.stop(stop);
4832
+ return [oscillator, vibrato, breath];
4833
+ }
4834
+ function realizeLayered(plan, target) {
4835
+ const sources = plan.layers.flatMap((layer2) => {
4836
+ if (layer2.source.kind === "noise") return realizeNoiseLayer(plan, layer2, target);
4837
+ if (layer2.source.kind === "osc") return realizeToneLayer(plan, layer2, target);
4838
+ return realizeVoiceLayer(plan, layer2, target);
4839
+ });
4840
+ return voiceOf(sources);
4841
+ }
4842
+ function realizeSiren(plan, target) {
4843
+ const { context } = target;
4844
+ const layer2 = plan.layers[0];
4845
+ const time = target.time + layer2.at;
4846
+ const oscillator = context.createOscillator();
4847
+ if (layer2.source.kind === "osc") {
4848
+ oscillator.type = layer2.source.wave;
4849
+ assign(oscillator.frequency, layer2.source.frequency, time);
4850
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
4851
+ }
4852
+ const depths = layer2.modulators.map((modulator) => {
4853
+ const lfo = context.createOscillator();
4854
+ if (modulator.wave) lfo.type = modulator.wave;
4855
+ lfo.frequency.value = modulator.rate;
4856
+ const depth = gainNode(context, modulator.depth);
4857
+ lfo.connect(depth);
4858
+ if (modulator.target === "frequency") depth.connect(oscillator.frequency);
4859
+ return { lfo, depth, target: modulator.target };
4860
+ });
4861
+ const lfos = depths.map(({ lfo }) => lfo);
4862
+ const envelope2 = context.createGain();
4863
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
4864
+ for (const { depth, target: modulated } of depths) if (modulated === "gain") depth.connect(envelope2.gain);
4865
+ const filters = (layer2.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
4866
+ let tail = oscillator;
4867
+ for (const filter of filters) tail = tail.connect(filter);
4868
+ tail.connect(envelope2);
4869
+ envelope2.connect(target.output);
4870
+ if (layer2.wet && target.wet) {
4871
+ const send = gainNode(context, layer2.wet);
4872
+ envelope2.connect(send);
4873
+ send.connect(target.wet);
4874
+ }
4875
+ const stop = end2(layer2, time);
4876
+ oscillator.start(time);
4877
+ for (const lfo of lfos) lfo.start(time);
4878
+ oscillator.stop(stop);
4879
+ for (const lfo of lfos) lfo.stop(stop);
4880
+ return voiceOf([oscillator, ...lfos]);
4881
+ }
4882
+ function noiseLayer(layer2, target, time) {
4883
+ const { context } = target;
4884
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
4885
+ const filters = (layer2.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
4886
+ const envelope2 = context.createGain();
4887
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
4888
+ let tail = source;
4889
+ for (const filter of filters) tail = tail.connect(filter);
4890
+ tail.connect(envelope2).connect(target.output);
4891
+ source.start(time, layer2.source.kind === "noise" ? layer2.source.offset : 0);
4892
+ source.stop(time + layer2.duration + layer2.tail);
4893
+ return source;
4894
+ }
4895
+ function toneLayer(layer2, target, time) {
4896
+ const { context } = target;
4897
+ const oscillator = context.createOscillator();
4898
+ if (layer2.source.kind === "osc") {
4899
+ oscillator.type = layer2.source.wave;
4900
+ assign(oscillator.frequency, layer2.source.frequency, time);
4901
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
4902
+ }
4903
+ const envelope2 = context.createGain();
4904
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
4905
+ oscillator.connect(envelope2).connect(target.output);
4906
+ oscillator.start(time);
4907
+ oscillator.stop(time + layer2.duration + layer2.tail);
4908
+ return oscillator;
4909
+ }
4910
+ function realizeWeapon(plan, target) {
4911
+ const sources = plan.layers.map((layer2) => {
4912
+ const time = target.time + layer2.at;
4913
+ return layer2.source.kind === "noise" ? noiseLayer(layer2, target, time) : toneLayer(layer2, target, time);
4914
+ });
4915
+ return voiceOf(sources);
4916
+ }
4917
+ var BUILDERS = {
4918
+ layered: realizeLayered,
4919
+ burst: realizeBurst,
4920
+ scream: realizeScream,
4921
+ groan: realizeGroan,
4922
+ boom: realizeBoom,
4923
+ static: realizeStatic,
4924
+ chord: realizeChord,
4925
+ calls: realizeCalls,
4926
+ siren: realizeSiren,
4927
+ weapon: realizeWeapon
4928
+ };
4929
+ function realize(plan, target) {
4930
+ return BUILDERS[plan.order](plan, target);
4931
+ }
4932
+ var usesNoise = (recipe) => recipe.layers.some((layer2) => layer2.source.kind !== "osc");
4933
+ var planUsesNoise = (plan) => plan.layers.some((layer2) => layer2.source.kind !== "osc");
4934
+ function createAudioEngine(options = {}) {
4935
+ const seed = options.seed ?? 1;
4936
+ const registry = createSoundRegistry(options.banks);
4937
+ const voices = createVoiceManager({ ...options.groups ? { groups: options.groups } : {}, ...options.maxVoices === void 0 ? {} : { maxVoices: options.maxVoices } });
4938
+ const engineRandom = createRng2(seed);
4939
+ const shared = createNoiseStore(engineRandom);
4940
+ const busSettings = /* @__PURE__ */ new Map();
4941
+ const busNodes = /* @__PURE__ */ new Map();
4942
+ const meters = /* @__PURE__ */ new Map();
4943
+ const active = /* @__PURE__ */ new Map();
4944
+ const loops = /* @__PURE__ */ new Set();
4945
+ const named = /* @__PURE__ */ new Map();
4946
+ let mix = resolveMix(options.mix ?? DEFAULT_MIX, options.profiles);
4947
+ let context = null;
4948
+ let chain = null;
4949
+ let listener = DEFAULT_LISTENER;
4950
+ let lastTime = 0;
4951
+ const outputFor = (bus) => busNodes.get(bus) ?? chain?.master;
4952
+ const watched = /* @__PURE__ */ new Set();
4953
+ const meterOf = (bus) => {
4954
+ const existing = meters.get(bus);
4955
+ if (existing) return existing;
4956
+ const node = busNodes.get(bus);
4957
+ if (!context || !node || typeof context.createAnalyser !== "function") return void 0;
4958
+ const analyser = context.createAnalyser();
4959
+ analyser.fftSize = 1024;
4960
+ node.connect(analyser);
4961
+ const made = { analyser, data: new Float32Array(analyser.fftSize) };
4962
+ meters.set(bus, made);
4963
+ return made;
4964
+ };
4965
+ const ensureBus = (bus) => {
4966
+ if (!context || !chain || busNodes.has(bus)) return;
4967
+ const node = context.createGain();
4968
+ node.connect(chain.master);
4969
+ busNodes.set(bus, node);
4970
+ if (watched.has(bus)) meterOf(bus);
4971
+ };
4972
+ const ready = () => {
4973
+ if (context) return context;
4974
+ const created = (options.createContext ?? globalContext)();
4975
+ if (!created) return null;
4976
+ context = created;
4977
+ chain = buildChain(created, mix, engineRandom);
4978
+ if (mix.noise.create === "eager") noiseBuffer(shared, created, mix.noise.seconds);
4979
+ for (const [bus, setting] of Object.entries(mix.buses ?? {})) {
4980
+ busSettings.set(bus, { ...setting, ...busSettings.get(bus) });
4981
+ }
4982
+ for (const [bus, setting] of busSettings) {
4983
+ ensureBus(bus);
4984
+ busNodes.get(bus).gain.value = busGain(setting);
4985
+ }
4986
+ lastTime = created.currentTime;
4987
+ return created;
4988
+ };
4989
+ const advance = () => {
4990
+ if (!context) return;
4991
+ const elapsed = context.currentTime - lastTime;
4992
+ lastTime = context.currentTime;
4993
+ if (elapsed > 0) for (const id of voices.advance(elapsed)) active.delete(id);
4994
+ };
4995
+ const inputFor = (recipe, playOptions, random) => ({
4996
+ random,
4997
+ listener,
4998
+ noiseSeconds: mix.noise.seconds,
4999
+ ...options.distance ? { distanceMode: options.distance } : {},
5000
+ ...playOptions.volume === void 0 ? {} : { volume: playOptions.volume },
5001
+ ...playOptions.distance === void 0 ? {} : { distance: playOptions.distance },
5002
+ ...playOptions.position ? { position: playOptions.position } : {},
5003
+ ...playOptions.params ? { params: playOptions.params } : {},
5004
+ ...options.occlusion && playOptions.position && recipe.falloff?.model !== "none" ? { occlusion: options.occlusion(playOptions.position) } : {}
5005
+ });
5006
+ const start = (plan, store, voiceId, delay = 0) => {
5007
+ const audio = context;
5008
+ const realized = realize(plan, {
5009
+ context: audio,
5010
+ output: outputFor(plan.bus),
5011
+ ...chain?.wet ? { wet: chain.wet } : {},
5012
+ noise: () => noiseBuffer(store, audio, mix.noise.seconds),
5013
+ time: audio.currentTime + delay
5014
+ });
5015
+ if (voiceId === void 0) return null;
5016
+ voices.setLength(voiceId, plan.length + delay);
5017
+ active.set(voiceId, realized);
5018
+ return {
5019
+ id: voiceId,
5020
+ plan,
5021
+ stop() {
5022
+ realized.stop();
5023
+ voices.release(voiceId);
5024
+ active.delete(voiceId);
5025
+ }
5026
+ };
5027
+ };
5028
+ const admit = (recipe, gain, distance4) => {
5029
+ const decision = voices.request({ ...recipe.group ? { group: recipe.group } : {}, priority: recipe.priority ?? busPriority(recipe.bus), gain, distance: distance4, length: 0 });
5030
+ if (!decision.accepted) return null;
5031
+ for (const id of decision.evicted) {
5032
+ active.get(id)?.stop();
5033
+ active.delete(id);
5034
+ }
5035
+ return decision.id;
5036
+ };
5037
+ const playWith = (id, playOptions, random, store) => {
5038
+ const recipe = registry.sound(id);
5039
+ if (!recipe || busSettings.get(recipe.bus)?.muted) return null;
5040
+ const distance4 = playOptions.distance ?? (playOptions.position ? listenerDistance(listener, playOptions.position, options.distance) : 0);
5041
+ const level = falloffGain(recipe.falloff, playOptions.volume ?? 1, distance4);
5042
+ const silent = isSilent(recipe.falloff, level);
5043
+ if (silent && !recipe.keepSilent) return null;
5044
+ const audio = ready();
5045
+ if (!audio) return null;
5046
+ unlockContext(audio);
5047
+ advance();
5048
+ const voiceId = silent ? void 0 : admit(recipe, level, distance4);
5049
+ if (voiceId === null) return null;
5050
+ if (!silent && usesNoise(recipe)) prepareNoise(store, audio, mix.noise.seconds);
5051
+ const plan = planSound(recipe, inputFor(recipe, playOptions, playOptions.seed === void 0 ? random : createRng2(playOptions.seed)));
5052
+ return plan ? start(plan, store, voiceId, playOptions.delay) : null;
5053
+ };
5054
+ const randomFor = (playOptions) => {
5055
+ if (playOptions.channel === void 0) return engineRandom;
5056
+ const existing = named.get(playOptions.channel);
5057
+ if (existing) return existing;
5058
+ const stream = streamFor(seed, playOptions.channel);
5059
+ named.set(playOptions.channel, stream);
5060
+ return stream;
5061
+ };
5062
+ const planWith = (id, playOptions, random) => {
5063
+ const recipe = registry.sound(id);
5064
+ return recipe ? planSound(recipe, inputFor(recipe, playOptions, random)) : null;
5065
+ };
5066
+ const loopHost = {
5067
+ ready,
5068
+ output: outputFor,
5069
+ prepareNoise: () => context && prepareNoise(shared, context, mix.noise.seconds),
5070
+ noise: () => noiseBuffer(shared, context, mix.noise.seconds),
5071
+ register: (loop) => void loops.add(loop),
5072
+ unregister: (loop) => void loops.delete(loop)
5073
+ };
5074
+ return {
5075
+ registry,
5076
+ register: (bank) => registry.register(bank),
5077
+ plan: (id, playOptions = {}) => planWith(id, playOptions, createRng2(playOptions.seed ?? seed)),
5078
+ play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared),
5079
+ playPlan(plan, playOptions = {}) {
5080
+ const audio = ready();
5081
+ if (!audio) return null;
5082
+ advance();
5083
+ const recipe = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...plan.group ? { group: plan.group } : {} };
5084
+ const voiceId = plan.silent ? void 0 : admit(recipe, plan.level, plan.distance);
5085
+ if (voiceId === null) return null;
5086
+ if (planUsesNoise(plan)) prepareNoise(shared, audio, mix.noise.seconds);
5087
+ return start(plan, shared, voiceId, playOptions.delay);
5088
+ },
5089
+ loop: (id, loopOptions = {}) => createLoopHandle(registry.loop(id), loopHost, loopOptions.volume),
5090
+ channel(channelOptions) {
5091
+ const random = channelOptions.random ?? (channelOptions.seed === void 0 ? engineRandom : createRng2(channelOptions.seed));
5092
+ const store = channelOptions.ownNoise ? createNoiseStore(random) : shared;
5093
+ return {
5094
+ random,
5095
+ play: (id, playOptions = {}) => playWith(id, playOptions, random, store),
5096
+ plan: (id, playOptions = {}) => planWith(id, playOptions, createRng2(playOptions.seed ?? seed))
5097
+ };
5098
+ },
5099
+ setListener(next) {
5100
+ listener = next;
5101
+ },
5102
+ setBus(id, setting) {
5103
+ busSettings.set(id, { ...busSettings.get(id), ...setting });
5104
+ if (!context) return;
5105
+ ensureBus(id);
5106
+ busNodes.get(id).gain.value = busGain(busSettings.get(id));
5107
+ },
5108
+ setMix(next) {
5109
+ const previous = mix;
5110
+ mix = resolveMix(next, options.profiles);
5111
+ if (!context || !chain) return;
5112
+ const old = chain;
5113
+ chain = buildChain(context, mix, engineRandom);
5114
+ if (mix.noise.seconds !== previous.noise.seconds) shared.reset();
5115
+ for (const node of busNodes.values()) {
5116
+ node.disconnect();
5117
+ node.connect(chain.master);
5118
+ }
5119
+ for (const loop of loops) loop.rewire();
5120
+ old.disconnect();
5121
+ },
5122
+ setMaster(gain) {
5123
+ mix = { ...mix, master: gain };
5124
+ if (chain) chain.master.gain.value = gain;
5125
+ },
5126
+ unlock() {
5127
+ const audio = ready();
5128
+ if (audio) unlockContext(audio);
5129
+ },
5130
+ level(bus) {
5131
+ watched.add(bus);
5132
+ ensureBus(bus);
5133
+ const meter = meterOf(bus);
5134
+ if (!meter) return 0;
5135
+ meter.analyser.getFloatTimeDomainData(meter.data);
5136
+ let sum2 = 0;
5137
+ for (const sample of meter.data) sum2 += sample * sample;
5138
+ return Math.sqrt(sum2 / meter.data.length);
5139
+ },
5140
+ stats() {
5141
+ advance();
5142
+ const voiceStats = voices.stats();
5143
+ return {
5144
+ context: context ? context.state : "none",
5145
+ voices: voiceStats.active,
5146
+ peak: voiceStats.peak,
5147
+ dropped: voiceStats.dropped,
5148
+ maxVoices: voiceStats.maxVoices,
5149
+ loops: [...loops].filter((loop) => loop.created).length
5150
+ };
5151
+ },
5152
+ dispose() {
5153
+ for (const id of active.keys()) voices.release(id);
5154
+ for (const loop of [...loops]) loop.stop();
5155
+ active.clear();
5156
+ void context?.close();
5157
+ context = null;
5158
+ chain = null;
5159
+ busNodes.clear();
5160
+ meters.clear();
5161
+ shared.reset();
5162
+ }
5163
+ };
5164
+ }
5165
+ function createAudioSink(engine) {
5166
+ const playing = /* @__PURE__ */ new Map();
5167
+ return {
5168
+ engine,
5169
+ play(request) {
5170
+ const handle = engine.play(request.sound, {
5171
+ ...request.position ? { position: request.position } : {},
5172
+ ...request.volume === void 0 ? {} : { volume: request.volume },
5173
+ ...request.params ? { params: request.params } : {},
5174
+ ...request.seed === void 0 ? {} : { seed: request.seed }
5175
+ });
5176
+ if (!handle) return null;
5177
+ playing.set(handle.id, handle);
5178
+ return { id: handle.id };
5179
+ },
5180
+ stop(handle) {
5181
+ playing.get(handle.id)?.stop();
5182
+ playing.delete(handle.id);
5183
+ },
5184
+ loop(sound) {
5185
+ const loop = engine.loop(sound);
5186
+ return { set: (state) => loop.set({ ...state.volume === void 0 ? {} : { volume: state.volume }, ...state.frequency === void 0 ? {} : { frequency: state.frequency } }), stop: () => loop.stop() };
5187
+ },
5188
+ setListener(listener) {
5189
+ engine.setListener(listener);
5190
+ },
5191
+ register(bank) {
5192
+ engine.register(bank);
5193
+ }
5194
+ };
5195
+ }
5196
+ var BENCH_BANK = {
5197
+ id: "audio_bench",
5198
+ sounds: [
5199
+ {
5200
+ id: "bench_ping",
5201
+ bus: "ui",
5202
+ pan: true,
5203
+ duration: 0.25,
5204
+ layers: [
5205
+ {
5206
+ gain: 0.5,
5207
+ wet: 0.3,
5208
+ source: { kind: "osc", wave: "sine", frequency: { from: 880 } },
5209
+ envelope: [
5210
+ [0, 1e-4, "set", "fixed"],
5211
+ [2e-3, 1, "exp"],
5212
+ ["1d", 1e-4, "exp", "fixed"]
5213
+ ]
5214
+ }
5215
+ ],
5216
+ falloff: DISTANCE_CURVES.art_scene,
5217
+ sources: ["sdk: bench probe, a 880 Hz ping through the art scene output stage"]
5218
+ },
5219
+ {
5220
+ id: "bench_hiss",
5221
+ bus: "ui",
5222
+ pan: true,
5223
+ duration: [0.3, 0.9],
5224
+ layers: [
5225
+ {
5226
+ gain: 0.4,
5227
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
5228
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3e3 }, q: 0.7 }] },
5229
+ envelope: [
5230
+ [0, 1e-4, "set", "fixed"],
5231
+ [2e-3, 1, "exp"],
5232
+ ["1d", 1e-4, "exp", "fixed"]
5233
+ ]
5234
+ }
5235
+ ],
5236
+ falloff: DISTANCE_CURVES.art_scene,
5237
+ sources: ["sdk: bench probe, a highpass hiss of random length"]
5238
+ }
5239
+ ]
5240
+ };
5241
+
3770
5242
  // ../game-vfx/dist/index.js
3771
5243
  var __defProp2 = Object.defineProperty;
3772
5244
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -6974,7 +8446,7 @@ var GameTestRoom = (() => {
6974
8446
  mixed ^= mixed >>> 16;
6975
8447
  return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1;
6976
8448
  }
6977
- function streamFor(eventSeed) {
8449
+ function streamFor2(eventSeed) {
6978
8450
  return lehmer(mixEventSeed(eventSeed));
6979
8451
  }
6980
8452
  var TABLE = {
@@ -7070,10 +8542,10 @@ var GameTestRoom = (() => {
7070
8542
  if (!best) return;
7071
8543
  const offsetX = clamp22(x - best.x, -0.2, 0.2);
7072
8544
  const offsetY = clamp22(y - best.y, -0.17, 0.17);
7073
- for (let draw = 0; draw < OBJECT_ID_DRAWS2; draw++) random.next();
8545
+ for (let draw2 = 0; draw2 < OBJECT_ID_DRAWS2; draw2++) random.next();
7074
8546
  const chipTurn = random.between(0, 6);
7075
8547
  const chipScale = random.between(0.8, 1.3) * scale2;
7076
- for (let draw = 0; draw < OBJECT_ID_DRAWS2; draw++) random.next();
8548
+ for (let draw2 = 0; draw2 < OBJECT_ID_DRAWS2; draw2++) random.next();
7077
8549
  best.holes.push({ x: offsetX, y: offsetY, scale: scale2, chipTurn, chipScale });
7078
8550
  impact(host.engine, "concrete", [ORIGIN2[0] + x, y, ORIGIN2[2] + 0.13], [0, 0, 1], 0.8);
7079
8551
  } else if (action === "collapse") {
@@ -7972,7 +9444,7 @@ var GameTestRoom = (() => {
7972
9444
  }
7973
9445
  cue(step) {
7974
9446
  const args = this.value(step.args);
7975
- for (let draw = 0; draw < (step.draws ?? 0); draw++) this.deps.engine.random.next();
9447
+ for (let draw2 = 0; draw2 < (step.draws ?? 0); draw2++) this.deps.engine.random.next();
7976
9448
  this.deps.onCue?.({ cue: step.cue, args, time: this.time });
7977
9449
  }
7978
9450
  scheduleSteps(step) {
@@ -9023,9 +10495,9 @@ var GameTestRoom = (() => {
9023
10495
  this.color[i3] = lerp4(this.rgbStart[i3], this.rgbEnd[i3], t);
9024
10496
  this.color[i3 + 1] = lerp4(this.rgbStart[i3 + 1], this.rgbEnd[i3 + 1], t);
9025
10497
  this.color[i3 + 2] = lerp4(this.rgbStart[i3 + 2], this.rgbEnd[i3 + 2], t);
9026
- let envelope = t < def.fadeIn ? t / def.fadeIn : 1 - (t - def.fadeIn) / (1 - def.fadeIn);
9027
- if (def.flicker) envelope *= 1 - def.flicker * (0.5 + 0.5 * Math.sin(age * 38 + this.seed[i] * 50));
9028
- this.alpha[i] = clamp4(envelope, 0, 1) * def.alpha * this.alphaMul[i];
10498
+ let envelope2 = t < def.fadeIn ? t / def.fadeIn : 1 - (t - def.fadeIn) / (1 - def.fadeIn);
10499
+ if (def.flicker) envelope2 *= 1 - def.flicker * (0.5 + 0.5 * Math.sin(age * 38 + this.seed[i] * 50));
10500
+ this.alpha[i] = clamp4(envelope2, 0, 1) * def.alpha * this.alphaMul[i];
9029
10501
  top = i + 1;
9030
10502
  }
9031
10503
  this.high = top;
@@ -9730,10 +11202,10 @@ var GameTestRoom = (() => {
9730
11202
  this.velocity[offset + 1] = this.direction.y * speed + (options.v ? options.v[1] : 0);
9731
11203
  this.velocity[offset + 2] = this.direction.z * speed + (options.v ? options.v[2] : 0);
9732
11204
  const start = hexToRgb(options.c0 ?? 16777215);
9733
- const end = options.c1 != null ? hexToRgb(options.c1) : start;
11205
+ const end3 = options.c1 != null ? hexToRgb(options.c1) : start;
9734
11206
  for (let channel = 0; channel < FLOATS_PER_VECTOR; channel++) {
9735
11207
  this.colorStart[offset + channel] = start[channel];
9736
- this.colorEnd[offset + channel] = end[channel];
11208
+ this.colorEnd[offset + channel] = end3[channel];
9737
11209
  }
9738
11210
  this.life[index] = this.maxLife[index] = drawRange(random, options.life || 1);
9739
11211
  this.startSize[index] = drawRange(random, options.size || 0.1);
@@ -9868,10 +11340,10 @@ var GameTestRoom = (() => {
9868
11340
  this.velocity[offset + 1] = this.direction.y * speed + (options.v ? options.v[1] : 0);
9869
11341
  this.velocity[offset + 2] = this.direction.z * speed + (options.v ? options.v[2] : 0);
9870
11342
  const start = hexToRgb(options.c0 ?? 16765056);
9871
- const end = options.c1 != null ? hexToRgb(options.c1) : start;
11343
+ const end3 = options.c1 != null ? hexToRgb(options.c1) : start;
9872
11344
  for (let channel = 0; channel < FLOATS_PER_VECTOR2; channel++) {
9873
11345
  this.colorStart[offset + channel] = start[channel];
9874
- this.colorEnd[offset + channel] = end[channel];
11346
+ this.colorEnd[offset + channel] = end3[channel];
9875
11347
  }
9876
11348
  this.life[index] = this.maxLife[index] = drawRange(random, options.life || 0.4);
9877
11349
  this.length[index] = options.len || 0.02;
@@ -10255,7 +11727,7 @@ var GameTestRoom = (() => {
10255
11727
  function streamOf(seed) {
10256
11728
  if (randomMode === "shared") return sharedRandom;
10257
11729
  handles.events++;
10258
- return streamFor(seed ?? Math.imul(handles.events, AUTO_SEED) + (options.seed ?? 0));
11730
+ return streamFor2(seed ?? Math.imul(handles.events, AUTO_SEED) + (options.seed ?? 0));
10259
11731
  }
10260
11732
  function detonate(def, point, call3) {
10261
11733
  composeExplosion(core, def, { x: point[0], y: point[1], z: point[2], direction: call3.direction, random: call3.random, handle: call3.handle });
@@ -10834,12 +12306,12 @@ void main(){
10834
12306
  if (typeof document === "undefined") return blankCanvas();
10835
12307
  return document.createElement("canvas");
10836
12308
  }
10837
- function paint(createCanvas, draw) {
12309
+ function paint(createCanvas, draw2) {
10838
12310
  const canvas = createCanvas();
10839
12311
  canvas.width = TEXTURE_SIZE;
10840
12312
  canvas.height = TEXTURE_SIZE;
10841
12313
  const context = canvas.getContext("2d");
10842
- if (context) draw(context, TEXTURE_SIZE);
12314
+ if (context) draw2(context, TEXTURE_SIZE);
10843
12315
  return canvas;
10844
12316
  }
10845
12317
  function drawScorch(context, size, random) {
@@ -11697,7 +13169,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11697
13169
  var __defProp3 = Object.defineProperty;
11698
13170
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
11699
13171
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
11700
- function createRng2(seed) {
13172
+ function createRng3(seed) {
11701
13173
  let a = seed >>> 0;
11702
13174
  return () => {
11703
13175
  a = a + 1831565813 >>> 0;
@@ -11745,7 +13217,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11745
13217
  __publicField2(this, "m");
11746
13218
  __publicField2(this, "v");
11747
13219
  __publicField2(this, "w");
11748
- __publicField2(this, "rng", createRng2(1374496523));
13220
+ __publicField2(this, "rng", createRng3(1374496523));
11749
13221
  __publicField2(this, "markGeometry", null);
11750
13222
  __publicField2(this, "markMaterial", null);
11751
13223
  this.box = new THREE.BoxGeometry(1, 1, 1);
@@ -11776,7 +13248,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11776
13248
  const idx = seed % 6;
11777
13249
  let g = this.shardGeos[idx];
11778
13250
  if (!g) {
11779
- const r = createRng2(1e3 + idx);
13251
+ const r = createRng3(1e3 + idx);
11780
13252
  const n = 3 + Math.floor(r() * 3);
11781
13253
  const shape = new this.THREE.Shape();
11782
13254
  const pts = [];
@@ -11981,9 +13453,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11981
13453
  };
11982
13454
  var MAX_RADIUS = 1.2;
11983
13455
  var GroundSpots = class {
11984
- constructor(THREE, scene, groundAt, cap) {
13456
+ constructor(THREE, scene2, groundAt, cap) {
11985
13457
  __publicField2(this, "THREE", THREE);
11986
- __publicField2(this, "scene", scene);
13458
+ __publicField2(this, "scene", scene2);
11987
13459
  __publicField2(this, "groundAt", groundAt);
11988
13460
  __publicField2(this, "cap", cap);
11989
13461
  __publicField2(this, "spots", []);
@@ -12381,9 +13853,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12381
13853
  }
12382
13854
  };
12383
13855
  var RebarCages = class {
12384
- constructor(THREE, scene, radius = 4e-3) {
13856
+ constructor(THREE, scene2, radius = 4e-3) {
12385
13857
  __publicField2(this, "THREE", THREE);
12386
- __publicField2(this, "scene", scene);
13858
+ __publicField2(this, "scene", scene2);
12387
13859
  __publicField2(this, "cages", /* @__PURE__ */ new Map());
12388
13860
  __publicField2(this, "geometry");
12389
13861
  __publicField2(this, "material");
@@ -12990,7 +14462,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12990
14462
  /** A star of jagged lines: `rays` rays up to `length` metres, seeded so the same event always draws the same star. */
12991
14463
  addCrack(mv, worldPoint, worldNormal, rays, length3, color, seed) {
12992
14464
  const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 2e-3);
12993
- const rnd2 = createRng2(seed);
14465
+ const rnd2 = createRng3(seed);
12994
14466
  const verts = [];
12995
14467
  const n = clamp6(Math.round(rays), 3, 16);
12996
14468
  const L = Math.max(0.02, length3) / s;
@@ -13191,10 +14663,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13191
14663
  const sy = lerp5(1, 0.25, melt);
13192
14664
  const sxz = 1 + 0.18 * melt;
13193
14665
  for (let i = range2[0] * 3, v = 0; i < range2[1] * 3; i += 3, v++) {
13194
- const noise = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
14666
+ const noise2 = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
13195
14667
  arr[i] = cx + (work[i] - cx) * sxz;
13196
14668
  arr[i + 2] = cz + (work[i + 2] - cz) * sxz;
13197
- arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise * melt * h * 0.04);
14669
+ arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise2 * melt * h * 0.04);
13198
14670
  }
13199
14671
  }
13200
14672
  /** The colour attribute of the host's current geometry (created white when there is none) with vertex colours switched on in its material. */
@@ -13517,10 +14989,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13517
14989
  __publicField2(this, "cages");
13518
14990
  __publicField2(this, "active", /* @__PURE__ */ new Map());
13519
14991
  __publicField2(this, "fires", /* @__PURE__ */ new Map());
14992
+ __publicField2(this, "primedBurning", /* @__PURE__ */ new Set());
13520
14993
  __publicField2(this, "leaks", []);
13521
14994
  __publicField2(this, "homeTags", []);
13522
14995
  __publicField2(this, "throttles", /* @__PURE__ */ new Map());
13523
- __publicField2(this, "rng", createRng2(12648430));
14996
+ __publicField2(this, "rng", createRng3(12648430));
13524
14997
  __publicField2(this, "vfx");
13525
14998
  __publicField2(this, "owner", `destruction-${++rendererCount}`);
13526
14999
  __publicField2(this, "stopLanding");
@@ -13856,8 +15329,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13856
15329
  case "primed":
13857
15330
  if (ev.trigger !== "explosion") {
13858
15331
  this.vfx.fire({ point: ev.point, radius: PRIMED_FIRE_RADIUS, life: ev.delay });
15332
+ this.primedBurning.add(`${ev.object}|${ev.part}`);
13859
15333
  this.activate(ev.object, ev.part);
13860
15334
  this.sound("ignite", ev.point, 0.7);
15335
+ this.sound("burn_loop", ev.point, 0.5);
13861
15336
  }
13862
15337
  break;
13863
15338
  case "spall":
@@ -13875,16 +15350,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13875
15350
  break;
13876
15351
  case "extinguish": {
13877
15352
  const f = this.fires.get(`${ev.object}|${ev.part}`);
15353
+ const primed = this.primedBurning.delete(`${ev.object}|${ev.part}`);
13878
15354
  if (f) {
13879
15355
  this.stopFire(`${ev.object}|${ev.part}`);
13880
15356
  this.vfx.burst("steam", f.point, [0, 1, 0], 14, 1.2);
13881
15357
  }
15358
+ if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5);
13882
15359
  this.activate(ev.object, ev.part);
13883
15360
  this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
13884
15361
  break;
13885
15362
  }
13886
15363
  case "burnout": {
13887
15364
  this.stopFire(`${ev.object}|${ev.part}`);
15365
+ this.primedBurning.delete(`${ev.object}|${ev.part}`);
15366
+ this.sound("burn_end", ev.point, 0.5);
13888
15367
  this.vfx.burst("ash", ev.point, [0, 1, 0], 16, 1);
13889
15368
  this.vfx.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
13890
15369
  const pv = this.partVis(ev.object, ev.part);
@@ -13926,6 +15405,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13926
15405
  break;
13927
15406
  }
13928
15407
  case "explode":
15408
+ this.primedBurning.delete(`${ev.object}|${ev.part}`);
13929
15409
  this.onExplode(ev);
13930
15410
  break;
13931
15411
  case "destroyed":
@@ -14009,7 +15489,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14009
15489
  if (!b || !pv) return;
14010
15490
  const def = this.world.materials.get(pv.material);
14011
15491
  const n = this.worldDir(b, ev.hole.normal);
14012
- const draw = (point, normal) => {
15492
+ const draw2 = (point, normal) => {
14013
15493
  const wp = new this.THREE.Vector3(point[0], point[1], point[2]);
14014
15494
  const mv = this.nearestMesh(pv, wp);
14015
15495
  if (!mv) return;
@@ -14020,8 +15500,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14020
15500
  this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
14021
15501
  }
14022
15502
  };
14023
- draw(ev.worldPoint, n);
14024
- if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
15503
+ draw2(ev.worldPoint, n);
15504
+ if (ev.hole.through && ev.exitPoint) draw2(ev.exitPoint, n.clone().negate());
14025
15505
  if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
14026
15506
  this.sound(impactKey(def), ev.worldPoint, 0.6);
14027
15507
  }
@@ -14416,6 +15896,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14416
15896
  this.fragments.clear();
14417
15897
  this.ground.clear();
14418
15898
  this.cages.clear();
15899
+ this.primedBurning.clear();
14419
15900
  this.vfx.sprites().clear();
14420
15901
  this.ownVfx?.world.clear();
14421
15902
  }
@@ -14444,12 +15925,197 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14444
15925
  this.disposed = true;
14445
15926
  }
14446
15927
  };
15928
+ var ATTACK = 2e-3;
15929
+ var lengthOf = (d, attack) => {
15930
+ if (typeof d === "number") return attack + d;
15931
+ if (Array.isArray(d)) return [attack + d[0], attack + d[1]];
15932
+ return { ...d, add: (d.add ?? 0) + attack };
15933
+ };
15934
+ var envelope = (attack) => [
15935
+ [0, 1e-4, "set", "fixed"],
15936
+ [attack, 1, "exp"],
15937
+ ["1d", 1e-4, "exp", "fixed"]
15938
+ ];
15939
+ function noise(o) {
15940
+ const a = o.a ?? ATTACK;
15941
+ const over = typeof o.d === "number" ? o.d : 0;
15942
+ return {
15943
+ filters: {
15944
+ mode: "serial",
15945
+ bank: [{ type: o.type ?? "lowpass", frequency: o.f2 === void 0 ? { set: o.f } : { set: o.f, to: o.f2, curve: "exp", over }, q: o.q ?? 0.7 }]
15946
+ },
15947
+ duration: lengthOf(o.d, a),
15948
+ gain: o.vol,
15949
+ ...o.delay === void 0 ? {} : { at: o.delay },
15950
+ ...o.wet === void 0 ? {} : { wet: o.wet },
15951
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
15952
+ envelope: envelope(a)
15953
+ };
15954
+ }
15955
+ function tone(o) {
15956
+ return {
15957
+ source: { kind: "osc", wave: o.wave ?? "sine", frequency: { from: o.f, to: o.f2 ?? "from", curve: "exp", over: "duration" } },
15958
+ duration: lengthOf(o.d, ATTACK),
15959
+ gain: o.vol,
15960
+ ...o.delay === void 0 ? {} : { at: o.delay },
15961
+ ...o.wet === void 0 ? {} : { wet: o.wet },
15962
+ envelope: envelope(ATTACK)
15963
+ };
15964
+ }
15965
+ var many = (count, make) => Array.from({ length: count }, make);
15966
+ var scene = (id, layers, line, vars) => ({
15967
+ id,
15968
+ bus: "world",
15969
+ pan: true,
15970
+ layers,
15971
+ falloff: DISTANCE_CURVES.art_scene,
15972
+ ...vars ? { vars } : {},
15973
+ sources: [`cena:${line}`]
15974
+ });
15975
+ var boomLayers = (big) => [
15976
+ noise({ f: 1200, f2: 120, d: 1.8, vol: 2, wet: 1.2 }),
15977
+ tone({ f: 65, f2: 24, d: 1, vol: 1.8 }),
15978
+ noise({ type: "highpass", f: 1500, d: 0.2, vol: 0.8 }),
15979
+ ...big ? [noise({ f: 500, f2: 90, a: 0.2, d: 2.4, vol: 1.2, wet: 1, delay: 0.15 })] : [],
15980
+ ...many(8, () => tone({ f: [2600, 5600], d: 0.05, vol: 0.08, delay: [0.4, 1.6] }))
15981
+ ];
15982
+ var SCENE_SOUNDS = [
15983
+ scene(
15984
+ "glass_shatter",
15985
+ [noise({ type: "highpass", f: 2800, d: 0.45, vol: 0.9, wet: 0.5 }), ...many(12, () => tone({ f: [3e3, 7500], d: [0.06, 0.14], vol: 0.18, delay: [0, 0.55] }))],
15986
+ "L1359 glass"
15987
+ ),
15988
+ scene(
15989
+ "wood_hit",
15990
+ [noise({ type: "bandpass", f: 850, q: 1, d: 0.25, vol: 0.9, wet: 0.3 }), tone({ f: 210, f2: 90, d: 0.1, vol: 0.6 }), noise({ type: "highpass", f: 2e3, d: 0.04, vol: 0.5 })],
15991
+ "L1360 wood"
15992
+ ),
15993
+ scene("thunk", [noise({ type: "bandpass", f: 700, q: 2, d: 0.07, vol: 0.4 })], "L1361 thunk"),
15994
+ scene("ping", [tone({ f: [2600, 3500], f2: 1400, d: 0.14, vol: 0.18, wet: 0.3 })], "L1362 ping"),
15995
+ scene("clang", [tone({ f: [520, 720], f2: 480, d: 0.5, vol: 0.35, wet: 0.4 }), tone({ f: 1370, f2: 1300, d: 0.3, vol: 0.15 }), noise({ type: "highpass", f: 2e3, d: 0.05, vol: 0.5 })], "L1363 clang"),
15996
+ scene("chip", [noise({ type: "bandpass", f: 1600, q: 1.2, d: 0.06, vol: 0.5 })], "L1364 chip"),
15997
+ scene(
15998
+ "crumble",
15999
+ [noise({ f: 900, f2: 200, d: 0.6, vol: 0.9, wet: 0.3 }), ...many(5, () => noise({ type: "bandpass", f: [500, 1400], q: 2, d: 0.05, vol: 0.4, delay: [0.1, 0.5] }))],
16000
+ "L1365 crumble"
16001
+ ),
16002
+ // `thud(x, z, v)`: `v` is the `scale` param (default 1)
16003
+ scene("thud", [tone({ f: 75, f2: 38, d: 0.22, vol: { param: "scale", scale: 0.6 } }), noise({ f: 420, d: 0.16, vol: { param: "scale", scale: 0.45 } })], "L1358 thud", { scale: 1 }),
16004
+ // `hiss(x, z, d)`: `seconds` is the param `d` (default 1.2)
16005
+ scene("hiss", [noise({ type: "highpass", f: 3500, a: 0.05, d: { param: "seconds" }, vol: 0.35 })], "L1366 hiss", { seconds: 1.2 }),
16006
+ scene("boom", boomLayers(false), "L1367 boom"),
16007
+ scene("boom_big", boomLayers(true), "L1367 boom (big)"),
16008
+ scene("burst", [noise({ f: 700, f2: 120, d: 0.7, vol: 1.4, wet: 0.5 }), noise({ type: "highpass", f: 3e3, a: 0.05, d: 1.2, vol: 0.4, delay: 0.1 })], "L1357 burst"),
16009
+ scene("crack", [noise({ type: "highpass", f: 1800, d: 0.05, vol: 0.8 }), tone({ f: 420, f2: 180, d: 0.06, vol: 0.4 })], "L1354 crack")
16010
+ ];
16011
+ var EXPLOSION_BOOM = {
16012
+ id: "explosion_boom",
16013
+ bus: "world",
16014
+ order: "boom",
16015
+ duration: 1.6,
16016
+ tail: 0.1,
16017
+ layers: [
16018
+ {
16019
+ source: { kind: "osc", wave: "sine", frequency: { from: 70, to: 28, curve: "exp", over: 1.2 } },
16020
+ envelope: [
16021
+ [0, 1, "set"],
16022
+ ["1d", 1e-3, "exp", "fixed"]
16023
+ ]
16024
+ }
16025
+ ],
16026
+ falloff: DISTANCE_CURVES.game_boom,
16027
+ sources: ["jogo:L68278-68285 boom"]
16028
+ };
16029
+ var BURN_LOOP = {
16030
+ id: "burn_loop",
16031
+ bus: "world",
16032
+ order: "chain_loop",
16033
+ source: { kind: "noise", loop: true },
16034
+ filters: { mode: "serial", bank: [{ type: "bandpass", frequency: { set: 1800 }, q: 0.6 }] },
16035
+ volume: 0,
16036
+ sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
16037
+ };
16038
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
16039
+ var DESTRUCTION_SOUND_KEYS = {
16040
+ art_scene: {
16041
+ impact_metal: { sound: "clang" },
16042
+ impact_wood: { sound: "wood_hit" },
16043
+ impact_glass: { sound: "ping" },
16044
+ impact_masonry: { sound: "chip" },
16045
+ impact_soft: { sound: "thud" },
16046
+ shatter_glass: { sound: "glass_shatter" },
16047
+ crumble: { sound: "crumble" },
16048
+ splinter: { sound: "crack" },
16049
+ ricochet: { sound: "ping" },
16050
+ unscrew: { sound: "chip" },
16051
+ detach: { sound: "thunk" },
16052
+ ignite: { sound: "hiss", params: { seconds: 0.5 } },
16053
+ extinguish: { sound: "hiss" },
16054
+ melt_sizzle: { sound: "hiss", params: { seconds: 0.6 } },
16055
+ explode: { sound: "boom", above: { volume: 0.9, sound: "boom_big" } }
16056
+ },
16057
+ game: {
16058
+ impact_metal: { sound: "sfx_impact" },
16059
+ impact_wood: { sound: "sfx_wood" },
16060
+ impact_glass: { sound: "sfx_glass" },
16061
+ impact_masonry: { sound: "sfx_impact" },
16062
+ impact_soft: { sound: "sfx_flesh" },
16063
+ shatter_glass: { sound: "sfx_glass" },
16064
+ crumble: { sound: "sfx_impact" },
16065
+ splinter: { sound: "sfx_wood" },
16066
+ explode: { sound: "explosion_boom" }
16067
+ }
16068
+ };
16069
+ var DEFAULT_FIRE = { perFire: 0.05, max: 0.2 };
16070
+ function createDestructionSound(engine, options = {}) {
16071
+ engine.register(DESTRUCTION_SOUND_BANK);
16072
+ engine.register(GAME_SFX_BANK);
16073
+ const fire = { ...DEFAULT_FIRE, ...options.fire };
16074
+ const rules = { ...DESTRUCTION_SOUND_KEYS.art_scene, ...options.style === "game" ? DESTRUCTION_SOUND_KEYS.game : {}, ...options.keys };
16075
+ let loop;
16076
+ let burning = 0;
16077
+ const setFire = () => {
16078
+ if (options.burnLoop === false) return;
16079
+ if (burning <= 0) {
16080
+ loop?.stop();
16081
+ loop = void 0;
16082
+ return;
16083
+ }
16084
+ loop ?? (loop = engine.loop("burn_loop"));
16085
+ loop.set({ volume: Math.min(fire.max, fire.perFire * burning) });
16086
+ };
16087
+ return {
16088
+ onSound(key, point, volume) {
16089
+ if (key === "burn_loop") {
16090
+ burning += 1;
16091
+ setFire();
16092
+ return;
16093
+ }
16094
+ if (key === "burn_end") {
16095
+ if (burning > 0) burning -= 1;
16096
+ setFire();
16097
+ return;
16098
+ }
16099
+ const rule = rules[key];
16100
+ if (!rule) return;
16101
+ const sound = rule.above && volume >= rule.above.volume ? rule.above.sound : rule.sound;
16102
+ engine.play(sound, { position: point, volume: volume * (rule.volumeScale ?? 1), ...rule.params ? { params: rule.params } : {} });
16103
+ },
16104
+ rule: (key) => rules[key] ?? void 0,
16105
+ burning: () => burning,
16106
+ dispose() {
16107
+ loop?.stop();
16108
+ loop = void 0;
16109
+ burning = 0;
16110
+ }
16111
+ };
16112
+ }
14447
16113
 
14448
16114
  // ../game-weapons/dist/index.js
14449
16115
  var __defProp4 = Object.defineProperty;
14450
16116
  var __defNormalProp3 = (obj, key, value) => key in obj ? __defProp4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
14451
16117
  var __publicField3 = (obj, key, value) => __defNormalProp3(obj, typeof key !== "symbol" ? key + "" : key, value);
14452
- var createRng3 = (seed) => {
16118
+ var createRng4 = (seed) => {
14453
16119
  let a = seed >>> 0;
14454
16120
  return () => {
14455
16121
  a = a + 1831565813 >>> 0;
@@ -26598,9 +28264,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
26598
28264
  this.caliber = init.caliber;
26599
28265
  this.action = init.def.action;
26600
28266
  this.rules = init.rules;
26601
- this.rng = createRng3(init.seed);
26602
- this.faultRng = createRng3((init.seed ^ FAULT_RNG_SALT) >>> 0);
26603
- this.fuseRng = createRng3((init.seed ^ FUSE_RNG_SALT) >>> 0);
28267
+ this.rng = createRng4(init.seed);
28268
+ this.faultRng = createRng4((init.seed ^ FAULT_RNG_SALT) >>> 0);
28269
+ this.fuseRng = createRng4((init.seed ^ FUSE_RNG_SALT) >>> 0);
26604
28270
  this.explosive = init.explosive ?? null;
26605
28271
  this._safety = init.rules.startSafe;
26606
28272
  this._condition = findById(init.rules.conditions, init.condition ?? init.rules.defaultCondition, "condition");
@@ -26862,7 +28528,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
26862
28528
  this.isCompatible = init.isCompatible;
26863
28529
  this.makeUnderbarrel = init.makeUnderbarrel;
26864
28530
  this.seed = init.seed;
26865
- this.breakRng = createRng3((init.seed ^ BREAK_RNG_SALT) >>> 0);
28531
+ this.breakRng = createRng4((init.seed ^ BREAK_RNG_SALT) >>> 0);
26866
28532
  this.fitted = (init.snapshot?.attachments ?? init.def.fitted ?? []).map(copyOf);
26867
28533
  if (init.snapshot) this.stockState = init.snapshot.stock;
26868
28534
  this.fitted = this.fitted.map((part) => part.shotsLeft === void 0 ? this.withDurability(part) : part);
@@ -27121,10 +28787,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27121
28787
  __publicField3(this, "lastRoundAt", Number.NEGATIVE_INFINITY);
27122
28788
  }
27123
28789
  /** 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. */
27124
- spunUpAt(spin, start, end) {
28790
+ spunUpAt(spin, start, end3) {
27125
28791
  if (!spin || start - this.lastRoundAt <= spin.holdSeconds + EPS3) return 0;
27126
28792
  if (this.spinSince === void 0 || start - this.spinFrameEnd > SPIN_PULL_GAP) this.spinSince = start;
27127
- this.spinFrameEnd = end;
28793
+ this.spinFrameEnd = end3;
27128
28794
  return this.spinSince + spin.upSeconds;
27129
28795
  }
27130
28796
  oncePerPull(held) {
@@ -27133,7 +28799,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27133
28799
  this.latched = true;
27134
28800
  return true;
27135
28801
  }
27136
- fireFirearm(a, origin, dir, start, end, held, source) {
28802
+ fireFirearm(a, origin, dir, start, end3, held, source) {
27137
28803
  if (this._reloading) {
27138
28804
  if (this.loadStep !== null && this._ammo > 0) this.stopReload(start);
27139
28805
  else return result("reloading");
@@ -27143,10 +28809,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27143
28809
  this.emit({ type: "empty", at: start });
27144
28810
  return result("empty");
27145
28811
  }
27146
- const t0 = Math.max(start, this.readyAt, this.spunUpAt(a.spin, start, end));
27147
- if (t0 > end + EPS3) return result("cooldown");
28812
+ const t0 = Math.max(start, this.readyAt, this.spunUpAt(a.spin, start, end3));
28813
+ if (t0 > end3 + EPS3) return result("cooldown");
27148
28814
  const res = result("ok");
27149
- const times = planRounds(a, this.mode, this._ammo, t0, end);
28815
+ const times = planRounds(a, this.mode, this._ammo, t0, end3);
27150
28816
  let lastAt = t0;
27151
28817
  for (const t of times) {
27152
28818
  const fault = rollFault({ def: this.def, caliber: this.caliber, condition: this._condition, ammoQuality: this._ammoQuality, heat: this._heat, rules: this.faultRules(), random: this.faultRng });
@@ -27262,7 +28928,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27262
28928
  this.pend = { src: "trigger", amt: d };
27263
28929
  }
27264
28930
  const start = this.clock - overlap;
27265
- const end = start + d;
28931
+ const end3 = start + d;
27266
28932
  this.settle(start);
27267
28933
  const dir = normalize2(direction);
27268
28934
  let res;
@@ -27273,10 +28939,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27273
28939
  else if (launcher) res = launcher.trigger(origin, dir, d, held, source);
27274
28940
  else switch (a.kind) {
27275
28941
  case "firearm":
27276
- res = this.fireFirearm(a, origin, dir, start, end, held, source);
28942
+ res = this.fireFirearm(a, origin, dir, start, end3, held, source);
27277
28943
  break;
27278
28944
  case "melee":
27279
- res = this.fireMelee(a, origin, dir, start, end);
28945
+ res = this.fireMelee(a, origin, dir, start, end3);
27280
28946
  break;
27281
28947
  case "flame":
27282
28948
  case "heat":
@@ -27286,7 +28952,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27286
28952
  res = this.fireExtinguisher(a, origin, dir, d);
27287
28953
  break;
27288
28954
  case "throwable":
27289
- res = this.fireThrowable(a, origin, dir, held, start, end);
28955
+ res = this.fireThrowable(a, origin, dir, held, start, end3);
27290
28956
  break;
27291
28957
  case "launcher":
27292
28958
  res = this.fireLauncher(a, origin, dir, start, held);
@@ -27298,7 +28964,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27298
28964
  res = this.oncePerPull(held) ? result("ok", { uses: [{ kind: "hand", reach: a.reach, origin, direction: dir }] }) : result("cooldown");
27299
28965
  break;
27300
28966
  }
27301
- this.advance(Math.max(0, end - this.clock));
28967
+ this.advance(Math.max(0, end3 - this.clock));
27302
28968
  this.settle(this.clock);
27303
28969
  return res;
27304
28970
  }
@@ -27330,9 +28996,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27330
28996
  this.emit({ type: "shot", shots: 0, rounds: 1, mode: this.mode, at: t });
27331
28997
  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] });
27332
28998
  }
27333
- fireMelee(a, origin, dir, start, end) {
28999
+ fireMelee(a, origin, dir, start, end3) {
27334
29000
  const t0 = Math.max(start, this.readyAt);
27335
- if (t0 > end + EPS3) return result("cooldown");
29001
+ if (t0 > end3 + EPS3) return result("cooldown");
27336
29002
  this.readyAt = t0 + 60 / a.rpm;
27337
29003
  const use = { kind: "melee", damage: a.damage, energy: a.energy, radius: a.radius, reach: a.reach, origin, direction: dir };
27338
29004
  return result("ok", { uses: [use], noise: a.noise });
@@ -27366,8 +29032,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27366
29032
  return result("ok", { uses: [throwableUse({ action: a, explosive: this.explosive, origin, direction: dir, fuse, rules: this.rules.explosives, ...extra })], noise: a.noise });
27367
29033
  }
27368
29034
  /** A timer throwable is cooked from the pull; impact, remote and wire ones (and a long fuse that sets down) act at once. */
27369
- fireThrowable(a, origin, dir, held, start, end) {
27370
- if (this.cook) return this.cookStep(a, origin, dir, end);
29035
+ fireThrowable(a, origin, dir, held, start, end3) {
29036
+ if (this.cook) return this.cookStep(a, origin, dir, end3);
27371
29037
  if (!this.oncePerPull(held)) return result("cooldown");
27372
29038
  if (this._ammo <= 0) return result("empty");
27373
29039
  const fuse = this.sampleThrowFuse(a);
@@ -27377,7 +29043,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27377
29043
  }
27378
29044
  this.cook = { start, fuse, origin, direction: dir };
27379
29045
  this.emit({ type: "cook", phase: "start", fuse, at: start });
27380
- return this.cookStep(a, origin, dir, end);
29046
+ return this.cookStep(a, origin, dir, end3);
27381
29047
  }
27382
29048
  /** The trigger stays down on a cooked throwable: nothing happens until the fuse runs out in the hand. */
27383
29049
  cookStep(a, origin, dir, now) {
@@ -29734,17 +31400,17 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
29734
31400
  }
29735
31401
  }
29736
31402
  const start = time;
29737
- const end = Math.min(def.duration, time + Math.max(0, dt));
31403
+ const end3 = Math.min(def.duration, time + Math.max(0, dt));
29738
31404
  for (let i = 0; i < keys.length && !done; i++) {
29739
31405
  const key = keys[i];
29740
31406
  const spanStart = key.t;
29741
31407
  const spanEnd = i + 1 < keys.length ? keys[i + 1].t : def.duration;
29742
- if ((key.action === "open" || key.action === "close") && !fired.has(i) && end >= spanStart) {
31408
+ if ((key.action === "open" || key.action === "close") && !fired.has(i) && end3 >= spanStart) {
29743
31409
  fired.add(i);
29744
31410
  if (key.action === "open") world.open(containerId);
29745
31411
  else world.close(containerId);
29746
31412
  }
29747
- const overlap = Math.min(end, spanEnd) - Math.max(start, spanStart);
31413
+ const overlap = Math.min(end3, spanEnd) - Math.max(start, spanStart);
29748
31414
  if (overlap <= 0) continue;
29749
31415
  if (key.action === "drink") {
29750
31416
  const ev = world.drink(containerId, (key.rate ?? DEFAULT_DRINK_RATE) * overlap);
@@ -29758,7 +31424,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
29758
31424
  }
29759
31425
  }
29760
31426
  if (!done) {
29761
- time = end;
31427
+ time = end3;
29762
31428
  done = time >= def.duration;
29763
31429
  }
29764
31430
  follow(dt);
@@ -30360,8 +32026,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
30360
32026
  bedHeight = roadY * 1e3 + z * 14 - (x - params.roadSlopeAnchorX) * 5;
30361
32027
  roughness = 0.55;
30362
32028
  }
30363
- const noise = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
30364
- bed[index] = bedHeight + (noise - 0.5) * roughness * 2;
32029
+ const noise2 = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
32030
+ bed[index] = bedHeight + (noise2 - 0.5) * roughness * 2;
30365
32031
  vertical[index] = region === 0 || region === 2 ? 0.95 : 1;
30366
32032
  pin[index] = 0.62 + 0.76 * (valueNoise(column * 0.21 + 40, row * 0.21) * 0.5 + hashCell(column, row) * 0.5) * (region === 3 ? 1.25 : 1);
30367
32033
  }
@@ -30518,8 +32184,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
30518
32184
  for (let column = 0; column < columns; column++) {
30519
32185
  const index = (region.firstRow + row) * columns + column;
30520
32186
  const centre = add2(add2(region.origin, scale(columnAxis, (column + 0.5) * cell)), scale(region.rowAxis, (row + 0.5) * cell));
30521
- const noise = valueNoise(column * 0.35, (region.firstRow + row) * 0.35) * 0.6 + valueNoise(column * 1.3, (region.firstRow + row) * 1.3) * 0.4;
30522
- bed[index] = centre[1] * 1e3 + (noise - 0.5) * strip.roughness * 2;
32187
+ const noise2 = valueNoise(column * 0.35, (region.firstRow + row) * 0.35) * 0.6 + valueNoise(column * 1.3, (region.firstRow + row) * 1.3) * 0.4;
32188
+ bed[index] = centre[1] * 1e3 + (noise2 - 0.5) * strip.roughness * 2;
30523
32189
  vertical[index] = region.isVertical ? 0.95 : 1;
30524
32190
  pin[index] = 0.62 + 0.76 * (valueNoise(column * 0.21 + 40, (region.firstRow + row) * 0.21) * 0.5 + valueNoise(column * 0.9, row * 0.9) * 0.5);
30525
32191
  }
@@ -31432,8 +33098,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
31432
33098
  const b2x = ty * b1z - tz * b1y;
31433
33099
  const b2y = tz * b1x - tx * b1z;
31434
33100
  const b2z = tx * b1y - ty * b1x;
31435
- const end = Math.min(1, 0.35 + Math.min(k, 1 - k) * 8);
31436
- const r = this.radiusAt(state, gravity, t, r0, speed0) * end;
33101
+ const end3 = Math.min(1, 0.35 + Math.min(k, 1 - k) * 8);
33102
+ const r = this.radiusAt(state, gravity, t, r0, speed0) * end3;
31437
33103
  for (let j = 0; j < sides; j++) {
31438
33104
  const a = j / sides * Math.PI * 2;
31439
33105
  const c = Math.cos(a);
@@ -31467,15 +33133,15 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
31467
33133
  };
31468
33134
  var rendererCount2 = 0;
31469
33135
  function createFluidRenderer(THREE, options) {
31470
- const { scene, fluids } = options;
33136
+ const { scene: scene2, fluids } = options;
31471
33137
  const quality = options.quality ?? "medium";
31472
33138
  const profile = profileOf(quality);
31473
33139
  const materials = new MaterialCache(THREE, options.envMap ?? null);
31474
33140
  const root = new THREE.Group();
31475
33141
  root.name = "fluid_renderer";
31476
- scene.add(root);
33142
+ scene2.add(root);
31477
33143
  const vfx = options.vfx ?? createVfx({ profile: quality, gravity: fluids.gravity, sprites: { capacity: profile.maxParticles } });
31478
- const ownVfx = options.vfx ? void 0 : createVfxRenderer(THREE, { scene, camera: options.camera ?? new THREE.PerspectiveCamera(), vfx, pointScale: options.pointScale ?? 600 });
33144
+ const ownVfx = options.vfx ? void 0 : createVfxRenderer(THREE, { scene: scene2, camera: options.camera ?? new THREE.PerspectiveCamera(), vfx, pointScale: options.pointScale ?? 600 });
31479
33145
  const particles = new FluidSprites(vfx, `fluids-${++rendererCount2}`);
31480
33146
  const dropGeometry = new THREE.SphereGeometry(1, 8, 6);
31481
33147
  const bodies = /* @__PURE__ */ new Map();
@@ -31612,7 +33278,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
31612
33278
  ownVfx?.dispose();
31613
33279
  dropGeometry.dispose();
31614
33280
  materials.dispose();
31615
- scene.remove(root);
33281
+ scene2.remove(root);
31616
33282
  root.clear();
31617
33283
  },
31618
33284
  stats() {
@@ -33454,10 +35120,10 @@ void main(){
33454
35120
  }
33455
35121
  return { name: "shelf", parts, joints, tags: ["furniture"] };
33456
35122
  };
33457
- function createArena(THREE, size, scene) {
35123
+ function createArena(THREE, size, scene2) {
33458
35124
  const group = new THREE.Group();
33459
35125
  group.name = "test-room-arena";
33460
- scene.background = new THREE.Color(10466504);
35126
+ scene2.background = new THREE.Color(10466504);
33461
35127
  group.add(new THREE.HemisphereLight(13624319, 7037527, 0.75));
33462
35128
  const sun = new THREE.DirectionalLight(16777215, 2.1);
33463
35129
  sun.position.set(7, 13, 8);
@@ -33732,7 +35398,7 @@ void main(){
33732
35398
  var CONE_BLAST_RADIUS = 0.5;
33733
35399
  var toArray = (vector) => [vector.x, vector.y, vector.z];
33734
35400
  function createExplosives(context) {
33735
- const { THREE, scene, camera: camera2, world, renderer, arsenal, translate, format, action } = context;
35401
+ const { THREE, scene: scene2, camera: camera2, world, renderer, arsenal, translate, format, action } = context;
33736
35402
  const projectiles = [];
33737
35403
  const meshes = [];
33738
35404
  const up = new THREE.Vector3(0, 1, 0);
@@ -33745,7 +35411,7 @@ void main(){
33745
35411
  const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color, roughness: 0.6, metalness }));
33746
35412
  mesh.castShadow = true;
33747
35413
  mesh.position.set(at3[0], at3[1], at3[2]);
33748
- scene.add(mesh);
35414
+ scene2.add(mesh);
33749
35415
  meshes.push(mesh);
33750
35416
  return mesh;
33751
35417
  };
@@ -33800,7 +35466,7 @@ void main(){
33800
35466
  flag.position.y = 0.46;
33801
35467
  marker.add(stake, flag);
33802
35468
  marker.position.set(at3[0] + 0.12, at3[1] - 0.03, at3[2]);
33803
- scene.add(marker);
35469
+ scene2.add(marker);
33804
35470
  meshes.push(marker);
33805
35471
  return marker;
33806
35472
  };
@@ -33808,7 +35474,7 @@ void main(){
33808
35474
  const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(...from), new THREE.Vector3(...to)]);
33809
35475
  const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 15262920 }));
33810
35476
  line.name = "charge_wire";
33811
- scene.add(line);
35477
+ scene2.add(line);
33812
35478
  meshes.push(line);
33813
35479
  return line;
33814
35480
  };
@@ -34483,13 +36149,13 @@ void main(){
34483
36149
  if (!g || g.done) break;
34484
36150
  g.done = true;
34485
36151
  const [a, b] = ev.parts;
34486
- const many = g.list.length > 1;
36152
+ const many2 = g.list.length > 1;
34487
36153
  c.log.add({
34488
36154
  channel: "event",
34489
36155
  type: "detach",
34490
36156
  objectId: ev.object,
34491
36157
  itemId: c.itemIdOf(ev.object),
34492
- text: many ? t(`ev.detach.many.${ev.by === "tool" ? "tool" : "break"}`, { obj: c.nameOf(ev.object), joints: joints(g.list.length), pieces: pieces(g.groups) }) : t("ev.detach", { obj: c.nameOf(ev.object), a, b, by: t(`by.${ev.by}`), pieces: pieces(g.groups) }),
36158
+ text: many2 ? t(`ev.detach.many.${ev.by === "tool" ? "tool" : "break"}`, { obj: c.nameOf(ev.object), joints: joints(g.list.length), pieces: pieces(g.groups) }) : t("ev.detach", { obj: c.nameOf(ev.object), a, b, by: t(`by.${ev.by}`), pieces: pieces(g.groups) }),
34493
36159
  data: { joint: ev.joint, joints: g.list.map((j) => j.joint), parts: ev.parts, by: ev.by, groups: g.groups }
34494
36160
  });
34495
36161
  break;
@@ -35490,23 +37156,23 @@ void main(){
35490
37156
  }
35491
37157
  if (sheet.timeline) {
35492
37158
  const { segments, ticks, metric } = sheet.timeline;
35493
- const end = Math.max(1e-3, ...segments.map((segment) => segment.end), ...ticks);
37159
+ const end3 = Math.max(1e-3, ...segments.map((segment) => segment.end), ...ticks);
35494
37160
  const strip = mk("div", "tr-timeline");
35495
37161
  for (const segment of segments) {
35496
37162
  const bar2 = mk("div", "tr-segment", segment.name);
35497
- bar2.style.left = `${segment.start / end * 100}%`;
35498
- bar2.style.width = `${(segment.end - segment.start) / end * 100}%`;
37163
+ bar2.style.left = `${segment.start / end3 * 100}%`;
37164
+ bar2.style.width = `${(segment.end - segment.start) / end3 * 100}%`;
35499
37165
  bar2.style.background = segment.color;
35500
37166
  strip.appendChild(bar2);
35501
37167
  }
35502
37168
  for (const tick of ticks) {
35503
37169
  const mark = mk("u", "tr-tick");
35504
- mark.style.left = `${tick / end * 100}%`;
37170
+ mark.style.left = `${tick / end3 * 100}%`;
35505
37171
  strip.appendChild(mark);
35506
37172
  }
35507
37173
  const head = mk("s", "tr-playhead");
35508
37174
  strip.appendChild(head);
35509
- playhead = { node: head, metric, end };
37175
+ playhead = { node: head, metric, end: end3 };
35510
37176
  card.appendChild(strip);
35511
37177
  }
35512
37178
  if (sheet.note) card.appendChild(mk("p", "sub", sheet.note));
@@ -36187,9 +37853,9 @@ void main(){
36187
37853
  gl.setSize(width(), height());
36188
37854
  const dom = gl.domElement;
36189
37855
  if (dom && !dom.hasAttribute?.("tabindex")) dom.setAttribute?.("tabindex", "0");
36190
- const scene = options.scene ?? new THREE.Scene();
37856
+ const scene2 = options.scene ?? new THREE.Scene();
36191
37857
  const camera2 = options.camera ?? new THREE.PerspectiveCamera(72, width() / height(), 0.05, 140);
36192
- if (!options.camera) scene.add(camera2);
37858
+ if (!options.camera) scene2.add(camera2);
36193
37859
  const resize = () => {
36194
37860
  const w = width();
36195
37861
  const h = height();
@@ -36200,8 +37866,8 @@ void main(){
36200
37866
  const onResize = () => resize();
36201
37867
  window.addEventListener("resize", onResize);
36202
37868
  const arenaSize = options.arenaSize ?? 14;
36203
- const arena = createArena(THREE, arenaSize, scene);
36204
- scene.add(arena.group);
37869
+ const arena = createArena(THREE, arenaSize, scene2);
37870
+ scene2.add(arena.group);
36205
37871
  const ownWorld = !options.world;
36206
37872
  const world = options.world ?? createDestruction({ seed: 1, ...options.worldOptions, materials: [...DEMO_MATERIALS, ...options.worldOptions?.materials ?? []] });
36207
37873
  const pristineWorld = ownWorld ? world.serialize() : null;
@@ -36229,15 +37895,22 @@ void main(){
36229
37895
  let simTime = 0;
36230
37896
  const log = createLog({ exportCancelled: () => t("log.export_cancelled") });
36231
37897
  log.setClock(() => simTime);
37898
+ const ownAudio = options.audio === void 0;
37899
+ const audio = options.audio === false ? null : options.audio ?? createAudioEngine();
37900
+ const destructionSound = audio ? createDestructionSound(audio) : null;
37901
+ const playSound = (key, point, volume) => {
37902
+ destructionSound?.onSound(key, point, volume);
37903
+ options.onSound?.(key, point, volume);
37904
+ };
36232
37905
  const craterField = options.craters ? createGroundField() : void 0;
36233
37906
  const craterPatch = craterField ? createGroundPatch(THREE, { field: craterField, material: new THREE.MeshStandardMaterial({ color: 9342602, roughness: 0.95 }) }) : void 0;
36234
37907
  if (craterPatch) {
36235
37908
  const floor2 = arena.group.getObjectByName("floor");
36236
37909
  if (floor2) floor2.visible = false;
36237
- scene.add(craterPatch.mesh);
37910
+ scene2.add(craterPatch.mesh);
36238
37911
  }
36239
37912
  const vfx = createVfxRenderer(THREE, {
36240
- scene,
37913
+ scene: scene2,
36241
37914
  camera: camera2,
36242
37915
  overlay: container,
36243
37916
  quality,
@@ -36249,13 +37922,13 @@ void main(){
36249
37922
  })
36250
37923
  });
36251
37924
  const renderer = createDestructionRenderer(THREE, {
36252
- scene,
37925
+ scene: scene2,
36253
37926
  world,
36254
37927
  vfx: vfx.world,
36255
37928
  quality,
36256
37929
  groundAt,
36257
37930
  pointScale,
36258
- onSound: options.onSound,
37931
+ onSound: playSound,
36259
37932
  onPiece: (piece) => {
36260
37933
  rendererPieces.set(piece.objectId, piece);
36261
37934
  loose.push(piece);
@@ -36277,10 +37950,10 @@ void main(){
36277
37950
  if (best) splitOf.set(piece.objectId, best);
36278
37951
  }
36279
37952
  });
36280
- const fluidView = createFluidRenderer(THREE, { scene, fluids, quality, pointScale, vfx: vfx.world });
37953
+ const fluidView = createFluidRenderer(THREE, { scene: scene2, fluids, quality, pointScale, vfx: vfx.world });
36281
37954
  const filmRandom = createFilmLcg(1);
36282
37955
  const film = createFilmManager(THREE, {
36283
- scene,
37956
+ scene: scene2,
36284
37957
  camera: camera2,
36285
37958
  random: filmRandom,
36286
37959
  fluids,
@@ -36477,7 +38150,7 @@ void main(){
36477
38150
  for (const o of arena.objects) {
36478
38151
  const root = buildFromSpec(THREE, o.spec, { colors: o.colors });
36479
38152
  root.position.set(o.position[0], o.position[1], o.position[2]);
36480
- scene.add(root);
38153
+ scene2.add(root);
36481
38154
  root.updateMatrixWorld(true);
36482
38155
  world.add(structuredCloneSpec(o.spec), { id: o.id, position: o.position });
36483
38156
  registerEntry(o.id, o.id, root, o.spec, S.text[`arena.${o.id}`] ?? o.id, void 0, void 0, true, false);
@@ -36504,7 +38177,7 @@ void main(){
36504
38177
  root.position.set(pos[0], pos[1], pos[2]);
36505
38178
  root.rotation.y = rotationY ?? player.yaw;
36506
38179
  root.name = item.id;
36507
- scene.add(root);
38180
+ scene2.add(root);
36508
38181
  root.updateMatrixWorld(true);
36509
38182
  const objectId = `${item.id}_${++counter}`;
36510
38183
  let own;
@@ -36691,7 +38364,7 @@ void main(){
36691
38364
  g.setAttribute("position", new THREE.Float32BufferAttribute(6, 3));
36692
38365
  const line = new THREE.Line(g, tracerMat);
36693
38366
  line.frustumCulled = false;
36694
- scene.add(line);
38367
+ scene2.add(line);
36695
38368
  tr = { line, life: 0 };
36696
38369
  tracers.push(tr);
36697
38370
  }
@@ -36807,15 +38480,15 @@ void main(){
36807
38480
  energy = Math.max(energy, s.energy);
36808
38481
  const results = world.shoot(s.origin, s.direction, s.energy, s.caliber, "player");
36809
38482
  const r0 = results.find((r) => r.part && r.point);
36810
- let end = [s.origin[0] + s.direction[0] * 40, s.origin[1] + s.direction[1] * 40, s.origin[2] + s.direction[2] * 40];
38483
+ let end3 = [s.origin[0] + s.direction[0] * 40, s.origin[1] + s.direction[1] * 40, s.origin[2] + s.direction[2] * 40];
36811
38484
  if (r0?.point && r0.part) {
36812
38485
  hits++;
36813
- end = r0.point;
38486
+ end3 = r0.point;
36814
38487
  if (!first) first = { object: r0.object, part: r0.part, point: r0.point, normal: [0, 1, 0], distance: dist3(s.origin, r0.point) };
36815
38488
  const speed = Math.max(80, s.speed);
36816
38489
  pushPiece(r0.object, s.direction, 2 * s.energy / speed, r0.point);
36817
38490
  }
36818
- if (shots.length <= 3 || rnd2() < 0.34) tracer(m, end);
38491
+ if (shots.length <= 3 || rnd2() < 0.34) tracer(m, end3);
36819
38492
  }
36820
38493
  });
36821
38494
  player.kick(recoil[0], recoil[1]);
@@ -36981,7 +38654,7 @@ void main(){
36981
38654
  }
36982
38655
  const explosives = createExplosives({
36983
38656
  THREE,
36984
- scene,
38657
+ scene: scene2,
36985
38658
  camera: camera2,
36986
38659
  world,
36987
38660
  renderer,
@@ -37478,6 +39151,10 @@ void main(){
37478
39151
  s.update(dt);
37479
39152
  flushEvents(id, s);
37480
39153
  }
39154
+ if (audio) {
39155
+ const look = camera2.getWorldDirection(listenerDirection);
39156
+ audio.setListener({ position: [camera2.position.x, camera2.position.y, camera2.position.z], yaw: Math.atan2(-look.x, -look.z) });
39157
+ }
37481
39158
  for (const update2 of frameListeners) update2(dt);
37482
39159
  for (const tr of tracers) {
37483
39160
  if (tr.life > 0) {
@@ -37587,7 +39264,7 @@ void main(){
37587
39264
  const ev = evs[pl.fired++];
37588
39265
  action("animation_event", t("act.anim_event", { name: pl.entry.name, clip: pl.clip.id, type: ev.type }), { objectId: pl.entry.so.objectId, itemId: pl.entry.so.itemId, data: { clip: pl.clip.id, type: ev.type, at: ev.t, ...ev.data ?? {} } });
37589
39266
  const key = ev.type === "sound" ? ev.data?.key : void 0;
37590
- if (typeof key === "string") options.onSound?.(key, v3(pl.entry.so.root.getWorldPosition(tmpV2)), 1);
39267
+ if (typeof key === "string") playSound(key, v3(pl.entry.so.root.getWorldPosition(tmpV2)), 1);
37591
39268
  }
37592
39269
  if (pl.t >= pl.clip.duration) {
37593
39270
  playing.splice(i, 1);
@@ -37680,6 +39357,9 @@ void main(){
37680
39357
  };
37681
39358
  const hostListeners = /* @__PURE__ */ new Set();
37682
39359
  const frameListeners = /* @__PURE__ */ new Set();
39360
+ const listenerDirection = new THREE.Vector3();
39361
+ const environment = options.environment ?? createEnvironmentBus();
39362
+ const audioSink = audio ? createAudioSink(audio) : void 0;
37683
39363
  function emitHost(event) {
37684
39364
  for (const listener of hostListeners) listener(event);
37685
39365
  }
@@ -37726,9 +39406,11 @@ void main(){
37726
39406
  }
37727
39407
  const host = {
37728
39408
  THREE,
37729
- scene,
39409
+ scene: scene2,
37730
39410
  camera: camera2,
37731
39411
  locale,
39412
+ ...audioSink ? { audio: audioSink } : {},
39413
+ environment,
37732
39414
  groundAt,
37733
39415
  effects: vfx,
37734
39416
  tool: () => ({
@@ -37977,11 +39659,13 @@ void main(){
37977
39659
  const room = {
37978
39660
  world,
37979
39661
  renderer,
37980
- scene,
39662
+ scene: scene2,
37981
39663
  camera: camera2,
37982
39664
  arsenal,
37983
39665
  fluids,
37984
39666
  fluidRenderer: fluidView,
39667
+ audio,
39668
+ environment,
37985
39669
  log,
37986
39670
  get tool() {
37987
39671
  return tool;
@@ -38075,6 +39759,8 @@ void main(){
38075
39759
  return { room: roomMetrics.sample(), module: moduleMetrics() };
38076
39760
  },
38077
39761
  dispose() {
39762
+ destructionSound?.dispose();
39763
+ if (ownAudio) audio?.dispose();
38078
39764
  for (const detach of detachers.values()) detach();
38079
39765
  detachers.clear();
38080
39766
  for (const store of benches.values()) store.dispose();
@@ -38179,7 +39865,7 @@ void main(){
38179
39865
  last = now;
38180
39866
  if (!scenarioRunning) update(dt);
38181
39867
  const restoreCamera = vfx.applyCamera();
38182
- gl.render(scene, camera2);
39868
+ gl.render(scene2, camera2);
38183
39869
  restoreCamera();
38184
39870
  roomMetrics.rendered();
38185
39871
  };