@adatechnology/game-weapons-three 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -290,16 +290,26 @@ var ACCESSORY_TEXT = {
290
290
  var accessoryTextOf = (locale) => ACCESSORY_TEXT[locale] ?? ACCESSORY_TEXT.en;
291
291
 
292
292
  // src/audio.ts
293
- import { createRng } from "@adatechnology/game-weapons";
293
+ import { MIX_PROFILES, createAudioEngine } from "@adatechnology/game-audio";
294
294
 
295
295
  // src/audio/mechanical-sounds.ts
296
296
  var metal = (at, frequency, gain) => ({ layer: "metal", at, frequency, gain });
297
297
  var sound = (at, soundClass, volume) => ({ layer: "class", at, soundClass, volume });
298
298
  var thud = (at, gain, frequency) => ({ layer: "thud", at, gain, frequency });
299
+ var sceneClick = (at, f, vol) => [
300
+ { layer: "scene_tone", at, wave: "square", f, f2: f * 0.7, d: 0.02, vol },
301
+ { layer: "scene_noise", at, type: "highpass", f: 3e3, q: 0.7, d: 0.03, vol: vol * 0.8 }
302
+ ];
303
+ var sceneNoise = (at, type, f, q, d, vol) => ({ layer: "scene_noise", at, type, f, q, d, vol });
304
+ var sceneEmpty = sceneClick(0, 3200, 0.35);
305
+ var sceneMagOut = [...sceneClick(0, 1600, 0.4), sceneNoise(0.03, "bandpass", 900, 2, 0.08, 0.3)];
306
+ var sceneMagIn = [...sceneClick(0, 1100, 0.5), { layer: "scene_tone", at: 0, wave: "sine", f: 320, f2: 200, d: 0.05, vol: 0.35 }];
307
+ var sceneBolt = [...sceneClick(0, 2200, 0.4), ...sceneClick(0.12, 2700, 0.4)];
308
+ var sceneShell = [...sceneClick(0, 1400, 0.35), sceneNoise(0.02, "bandpass", 1200, 3, 0.05, 0.25)];
299
309
  var slide = [metal(0, 900, 0.35), metal(0.14, 1300, 0.35)];
300
310
  var seat = [metal(0, 1100, 0.45), sound(0, "reload", 0.8)];
301
311
  var MECHANICAL_SOUNDS = {
302
- dry_fire: [sound(0, "empty", 1), metal(0, 2600, 0.25)],
312
+ dry_fire: [sound(0, "empty", 1), ...sceneEmpty],
303
313
  misfire: [sound(0, "empty", 0.8), metal(0, 2200, 0.2)],
304
314
  feed: [metal(0, 1900, 0.2)],
305
315
  cycle: [metal(0.22, 900, 0.35), metal(0.36, 1300, 0.35)],
@@ -312,11 +322,11 @@ var MECHANICAL_SOUNDS = {
312
322
  stock_fold: [metal(0, 800, 0.3), metal(0.18, 1200, 0.3), thud(0.3, 0.2, 600)],
313
323
  light_click: [metal(0, 2400, 0.2), metal(0.03, 1800, 0.12)],
314
324
  attach: [metal(0, 1400, 0.35), thud(0.04, 0.2, 700), metal(0.12, 1900, 0.25)],
315
- magazine_out: [metal(0, 1500, 0.35), sound(0, "reload", 0.6)],
316
- magazine_in: seat,
317
- charge: slide,
325
+ magazine_out: [...sceneMagOut, sound(0, "reload", 0.6)],
326
+ magazine_in: [...sceneMagIn, sound(0, "reload", 0.8)],
327
+ charge: sceneBolt,
318
328
  bolt_open: [metal(0, 900, 0.35)],
319
- round_in: [metal(0, 2e3, 0.2), sound(0, "reload", 0.6)],
329
+ round_in: [...sceneShell, sound(0, "reload", 0.6)],
320
330
  bolt_close: [metal(0, 1300, 0.35)],
321
331
  pump_cycle: slide,
322
332
  lever_cycle: [metal(0, 1e3, 0.35), metal(0.12, 1500, 0.35)],
@@ -371,6 +381,9 @@ var mechanicalSoundOf = (event) => {
371
381
  }
372
382
  };
373
383
 
384
+ // src/audio/recipes.ts
385
+ import { DISTANCE_CURVES } from "@adatechnology/game-audio";
386
+
374
387
  // src/audio/shot-sound.ts
375
388
  import { muzzleEnergy } from "@adatechnology/game-weapons";
376
389
  var GAME_SOUND_CLASSES = {
@@ -457,100 +470,185 @@ var computeShotSound = (input, options = {}) => {
457
470
  };
458
471
  };
459
472
 
473
+ // src/audio/recipes.ts
474
+ var CLASS_BURST_ID = "weapon_class_burst";
475
+ var BURST_IDS = { lowpass: "weapon_burst_lowpass", highpass: "weapon_burst_highpass" };
476
+ var SCENE_NOISE_IDS = { lowpass: "scene_noise_lowpass", highpass: "scene_noise_highpass", bandpass: "scene_noise_bandpass" };
477
+ var SCENE_TONE_IDS = { sine: "scene_tone_sine", square: "scene_tone_square" };
478
+ var TONE_IDS = { sine: "weapon_tone_sine", sine_sweep: "weapon_tone_sine_sweep", square: "weapon_tone_square" };
479
+ var SOURCE_CLASS_BURST = "jogo:L62097-62110 sfx: lowpass noise, gain set at once, exponential fall to 0.001, stopped 0.05 s later";
480
+ var SOURCE_SDK = "sdk: the weapon synth of weapons-three (burst and tone), moved onto game-audio with the same nodes";
481
+ var FIT = { kind: "noise", offset: "fit" };
482
+ function classBurstId(curve) {
483
+ const same = curve.gainFalloff === GAME_DISTANCE_CURVE.gainFalloff && curve.cutoffFalloff === GAME_DISTANCE_CURVE.cutoffFalloff && curve.minGain === GAME_DISTANCE_CURVE.minGain;
484
+ return same ? CLASS_BURST_ID : `${CLASS_BURST_ID}@${curve.gainFalloff},${curve.cutoffFalloff},${curve.minGain}`;
485
+ }
486
+ function classBurstRecipe(curve) {
487
+ return {
488
+ id: classBurstId(curve),
489
+ bus: "weapons",
490
+ order: "weapon",
491
+ duration: { param: "seconds" },
492
+ layers: [
493
+ {
494
+ gain: { param: "gain" },
495
+ source: FIT,
496
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: { param: "cutoff" } } }] },
497
+ envelope: [
498
+ [0, 1, "set"],
499
+ ["1d", 1e-3, "exp", "fixed"]
500
+ ]
501
+ }
502
+ ],
503
+ falloff: { model: "inverse_square", k: curve.gainFalloff, cutoffK: curve.cutoffFalloff, minGain: curve.minGain },
504
+ sources: [SOURCE_CLASS_BURST]
505
+ };
506
+ }
507
+ var burstLayer = (type) => ({
508
+ gain: { param: "gain" },
509
+ source: FIT,
510
+ filters: { mode: "serial", bank: [{ type, frequency: { set: { param: "frequency" } }, q: { param: "q" } }] },
511
+ envelope: [
512
+ [0, 1e-4, "set", "fixed"],
513
+ [4e-3, 1, "exp"],
514
+ ["1d", 1e-4, "exp", "fixed"]
515
+ ]
516
+ });
517
+ var burstRecipe = (type) => ({
518
+ id: BURST_IDS[type],
519
+ bus: "weapons",
520
+ order: "weapon",
521
+ duration: { param: "seconds" },
522
+ layers: [burstLayer(type)],
523
+ falloff: { model: "none" },
524
+ sources: [SOURCE_SDK]
525
+ });
526
+ var toneLayer = (wave, sweep) => ({
527
+ gain: { param: "gain" },
528
+ source: { kind: "osc", wave, frequency: sweep ? { from: { param: "frequency" }, to: { param: "to" }, curve: "exp", over: "duration" } : { from: { param: "frequency" } } },
529
+ envelope: [
530
+ [0, 1e-4, "set", "fixed"],
531
+ [3e-3, 1, "exp"],
532
+ ["1d", 1e-4, "exp", "fixed"]
533
+ ]
534
+ });
535
+ var toneRecipe = (id, wave, sweep) => ({
536
+ id,
537
+ bus: "weapons",
538
+ order: "weapon",
539
+ duration: { param: "seconds" },
540
+ layers: [toneLayer(wave, sweep)],
541
+ falloff: { model: "none" },
542
+ sources: [SOURCE_SDK]
543
+ });
544
+ var SOURCE_SCENE = "cena:L1323-L1324 sNoise and sTone, played at the listener (distance 0), through the art scene output stage";
545
+ var SCENE_ATTACK = 2e-3;
546
+ var sceneNoiseRecipe = (type, slide2) => ({
547
+ id: slide2 ? `${SCENE_NOISE_IDS[type]}_slide` : SCENE_NOISE_IDS[type],
548
+ bus: "weapons",
549
+ order: "layered",
550
+ duration: { param: "d", add: SCENE_ATTACK },
551
+ pan: true,
552
+ layers: [
553
+ {
554
+ gain: { param: "vol" },
555
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
556
+ filters: { mode: "serial", bank: [{ type, frequency: slide2 ? { set: { param: "f" }, to: { param: "f2" }, curve: "exp", over: { param: "d" } } : { set: { param: "f" } }, q: { param: "q" } }] },
557
+ envelope: [
558
+ [0, 1e-4, "set", "fixed"],
559
+ [SCENE_ATTACK, 1, "exp"],
560
+ ["1d", 1e-4, "exp", "fixed"]
561
+ ]
562
+ }
563
+ ],
564
+ falloff: DISTANCE_CURVES.art_scene,
565
+ sources: [SOURCE_SCENE]
566
+ });
567
+ var sceneToneRecipe = (wave) => ({
568
+ id: SCENE_TONE_IDS[wave],
569
+ bus: "weapons",
570
+ order: "layered",
571
+ duration: { param: "d", add: SCENE_ATTACK },
572
+ pan: true,
573
+ layers: [
574
+ {
575
+ gain: { param: "vol" },
576
+ source: { kind: "osc", wave, frequency: { from: { param: "f" }, to: { param: "f2" }, curve: "exp", over: "duration" } },
577
+ envelope: [
578
+ [0, 1e-4, "set", "fixed"],
579
+ [SCENE_ATTACK, 1, "exp"],
580
+ ["1d", 1e-4, "exp", "fixed"]
581
+ ]
582
+ }
583
+ ],
584
+ falloff: DISTANCE_CURVES.art_scene,
585
+ sources: [SOURCE_SCENE]
586
+ });
587
+ function weaponRecipes(curve) {
588
+ return [
589
+ classBurstRecipe(curve),
590
+ burstRecipe("lowpass"),
591
+ burstRecipe("highpass"),
592
+ toneRecipe(TONE_IDS.sine, "sine", false),
593
+ toneRecipe(TONE_IDS.sine_sweep, "sine", true),
594
+ toneRecipe(TONE_IDS.square, "square", false),
595
+ ...["lowpass", "highpass", "bandpass"].flatMap((type) => [sceneNoiseRecipe(type, false), sceneNoiseRecipe(type, true)]),
596
+ sceneToneRecipe("sine"),
597
+ sceneToneRecipe("square")
598
+ ];
599
+ }
600
+
460
601
  // src/audio.ts
461
602
  var NOISE_SECONDS = 1.5;
462
- var globalContext = () => {
463
- const scope2 = globalThis;
464
- const Ctor = scope2.AudioContext ?? scope2.webkitAudioContext;
465
- return Ctor ? new Ctor() : null;
466
- };
603
+ var DEFAULT_SEED = 1;
467
604
  function createWeaponAudio(volume = 0.6, options = {}) {
468
- const makeContext = options.createContext ?? globalContext;
469
- const rng = createRng(options.seed ?? 1);
605
+ const seed = options.seed ?? DEFAULT_SEED;
470
606
  const classes = { ...GAME_SOUND_CLASSES, ...options.classes };
471
607
  const distanceCurve = { ...GAME_DISTANCE_CURVE, ...options.distance };
472
608
  const mechanicalTable = { ...MECHANICAL_SOUNDS, ...options.mechanical };
473
- let ctx = null;
474
- let master = null;
475
- let noise = null;
476
- let level = volume;
477
- const ready = () => {
478
- if (!ctx) {
479
- const created = makeContext();
480
- if (!created) return null;
481
- ctx = created;
482
- master = ctx.createGain();
483
- master.gain.value = level;
484
- master.connect(ctx.destination);
485
- noise = ctx.createBuffer(1, Math.floor(ctx.sampleRate * NOISE_SECONDS), ctx.sampleRate);
486
- const data = noise.getChannelData(0);
487
- for (let i = 0; i < data.length; i++) data[i] = rng() * 2 - 1;
488
- }
489
- if (ctx.state === "suspended") void ctx.resume();
490
- return ctx;
491
- };
609
+ const shared = options.engine;
610
+ const engine = shared ?? createAudioEngine({
611
+ seed,
612
+ mix: { ...MIX_PROFILES.game, master: volume, noise: { seconds: NOISE_SECONDS, create: "eager" } },
613
+ maxVoices: Number.POSITIVE_INFINITY,
614
+ ...options.createContext ? { createContext: options.createContext } : {}
615
+ });
616
+ const channel = shared && options.seed !== void 0 ? shared.channel({ seed }) : engine.channel({});
617
+ engine.register({ id: `weapons_three@${classBurstId(distanceCurve)}`, sounds: weaponRecipes(distanceCurve) });
618
+ const classBurst = classBurstId(distanceCurve);
492
619
  const burst = (config) => {
493
- const audio = ready();
494
- if (!audio || !noise || !master) return;
495
- const start = audio.currentTime + (config.at ?? 0);
496
- const source = audio.createBufferSource();
497
- source.buffer = noise;
498
- const filter = audio.createBiquadFilter();
499
- filter.type = config.filter;
500
- filter.frequency.value = config.frequency;
501
- filter.Q.value = config.q ?? 0.8;
502
- const gain = audio.createGain();
503
- gain.gain.setValueAtTime(1e-4, start);
504
- gain.gain.exponentialRampToValueAtTime(Math.max(config.gain, 2e-4), start + 4e-3);
505
- gain.gain.exponentialRampToValueAtTime(1e-4, start + config.seconds);
506
- source.connect(filter).connect(gain).connect(master);
507
- source.start(start, rng() * Math.max(0, NOISE_SECONDS - config.seconds));
508
- source.stop(start + config.seconds + 0.05);
620
+ const id = config.filter === "highpass" ? BURST_IDS.highpass : BURST_IDS.lowpass;
621
+ const params = { frequency: config.frequency, q: config.q ?? 0.8, gain: Math.max(config.gain, 2e-4), seconds: config.seconds };
622
+ channel.play(id, { params, ...config.at ? { delay: config.at } : {} });
509
623
  };
510
- const classBurst = (at, cutoff, gain, seconds) => {
511
- const audio = ready();
512
- if (!audio || !noise || !master) return;
513
- const start = audio.currentTime + at;
514
- const source = audio.createBufferSource();
515
- source.buffer = noise;
516
- const filter = audio.createBiquadFilter();
517
- filter.type = "lowpass";
518
- filter.frequency.value = cutoff;
519
- const envelope = audio.createGain();
520
- envelope.gain.setValueAtTime(gain, start);
521
- envelope.gain.exponentialRampToValueAtTime(1e-3, start + seconds);
522
- source.connect(filter).connect(envelope).connect(master);
523
- source.start(start, rng() * Math.max(0, NOISE_SECONDS - seconds));
524
- source.stop(start + seconds + 0.05);
624
+ const classBurstAt = (at, cutoff, gain, seconds) => {
625
+ channel.play(classBurst, { params: { cutoff, gain, seconds }, ...at ? { delay: at } : {} });
525
626
  };
526
627
  const tone = (config) => {
527
- const audio = ready();
528
- if (!audio || !master) return;
529
- const start = audio.currentTime + (config.at ?? 0);
530
- const osc = audio.createOscillator();
531
- osc.type = config.type ?? "sine";
532
- osc.frequency.setValueAtTime(config.frequency, start);
533
- if (config.to) osc.frequency.exponentialRampToValueAtTime(config.to, start + config.seconds);
534
- const gain = audio.createGain();
535
- gain.gain.setValueAtTime(1e-4, start);
536
- gain.gain.exponentialRampToValueAtTime(Math.max(config.gain, 2e-4), start + 3e-3);
537
- gain.gain.exponentialRampToValueAtTime(1e-4, start + config.seconds);
538
- osc.connect(gain).connect(master);
539
- osc.start(start);
540
- osc.stop(start + config.seconds + 0.05);
628
+ const id = config.to ? TONE_IDS.sine_sweep : config.type === "square" ? TONE_IDS.square : TONE_IDS.sine;
629
+ const params = { frequency: config.frequency, gain: Math.max(config.gain, 2e-4), seconds: config.seconds, ...config.to ? { to: config.to } : {} };
630
+ channel.play(id, { params, ...config.at ? { delay: config.at } : {} });
541
631
  };
542
632
  const metal2 = (at, frequency, gain) => {
543
633
  burst({ at, seconds: 0.05, gain, filter: "highpass", frequency, q: 2 });
544
634
  tone({ at, seconds: 0.04, gain: gain * 0.6, frequency: frequency * 0.6, type: "square" });
545
635
  };
546
636
  const play = (soundClass, classVolume = 1, distance = 0, at = 0) => {
547
- const attenuation = classVolume / (1 + distance * distance * distanceCurve.gainFalloff);
548
- if (attenuation < distanceCurve.minGain) return;
549
637
  const [cutoff, gain, seconds] = classes[soundClass] ?? FALLBACK_SOUND_CLASS;
550
- classBurst(at, cutoff / (1 + distance * distanceCurve.cutoffFalloff), gain * attenuation, seconds);
638
+ channel.play(classBurst, { volume: classVolume, distance, params: { cutoff, gain, seconds }, ...at ? { delay: at } : {} });
639
+ };
640
+ const sceneTone = (entry) => {
641
+ channel.play(SCENE_TONE_IDS[entry.wave], { params: { f: entry.f, f2: entry.f2 ?? entry.f, d: entry.d, vol: entry.vol }, ...entry.at ? { delay: entry.at } : {} });
642
+ };
643
+ const sceneNoise2 = (entry) => {
644
+ const slide2 = entry.f2 !== void 0;
645
+ const id = slide2 ? `${SCENE_NOISE_IDS[entry.type]}_slide` : SCENE_NOISE_IDS[entry.type];
646
+ channel.play(id, { params: { f: entry.f, ...slide2 ? { f2: entry.f2 } : {}, q: entry.q, d: entry.d, vol: entry.vol }, ...entry.at ? { delay: entry.at } : {} });
551
647
  };
552
648
  const step = (entry) => {
553
649
  if (entry.layer === "metal") metal2(entry.at, entry.frequency, entry.gain);
650
+ else if (entry.layer === "scene_tone") sceneTone(entry);
651
+ else if (entry.layer === "scene_noise") sceneNoise2(entry);
554
652
  else if (entry.layer === "class") play(entry.soundClass, entry.volume, 0, entry.at);
555
653
  else burst({ at: entry.at, seconds: 0.09, gain: entry.gain, filter: "lowpass", frequency: entry.frequency });
556
654
  };
@@ -562,7 +660,7 @@ function createWeaponAudio(volume = 0.6, options = {}) {
562
660
  shot(energyJ, category, extras = {}) {
563
661
  const sound2 = shotSound({ ...extras, energyJ, category });
564
662
  if (!sound2.audible) return;
565
- classBurst(0, sound2.cutoff, sound2.gain, sound2.duration);
663
+ classBurstAt(0, sound2.cutoff, sound2.gain, sound2.duration);
566
664
  if (sound2.crack.gain > 0) burst({ seconds: sound2.crack.duration, gain: sound2.crack.gain, filter: "highpass", frequency: sound2.crack.cutoff });
567
665
  tone({ seconds: sound2.body.duration, gain: sound2.body.gain, frequency: sound2.body.frequency, to: sound2.body.to });
568
666
  },
@@ -590,28 +688,26 @@ function createWeaponAudio(volume = 0.6, options = {}) {
590
688
  mechanical("cycle");
591
689
  },
592
690
  clink() {
593
- const f = 3200 + rng() * 2e3;
594
- tone({ seconds: 0.09, gain: 0.12, frequency: f });
595
- tone({ seconds: 0.06, gain: 0.06, frequency: f * 2.7 });
691
+ const f = 4200 + channel.random() * 1500;
692
+ const at = 0.25 + channel.random() * 0.2;
693
+ sceneTone({ layer: "scene_tone", at, wave: "sine", f, f2: 3800, d: 0.06, vol: 0.06 });
596
694
  },
597
695
  thud() {
598
696
  burst({ seconds: 0.09, gain: 0.35, filter: "lowpass", frequency: 380 });
599
697
  },
600
698
  setVolume(next) {
601
- level = Math.max(0, Math.min(1, next));
602
- if (master) master.gain.value = level;
699
+ const level = Math.max(0, Math.min(1, next));
700
+ if (shared) shared.setBus("weapons", { gain: level });
701
+ else engine.setMaster(level);
603
702
  },
604
703
  dispose() {
605
- void ctx?.close();
606
- ctx = null;
607
- master = null;
608
- noise = null;
704
+ if (!shared) engine.dispose();
609
705
  }
610
706
  };
611
707
  }
612
708
 
613
709
  // src/box-fx.ts
614
- import { createRng as createRng2, muzzleEnergy as muzzleEnergy2 } from "@adatechnology/game-weapons";
710
+ import { createRng, muzzleEnergy as muzzleEnergy2 } from "@adatechnology/game-weapons";
615
711
 
616
712
  // src/fx/layers.ts
617
713
  var createLayers = () => ({ casings: true, magazines: true, muzzleFlash: true, attachments: true, beams: true });
@@ -3862,7 +3958,7 @@ var GRAVITY = 9.81;
3862
3958
  function createBoxFx(options) {
3863
3959
  const { THREE, scene, camera, arsenal, groundAt } = options;
3864
3960
  const audio = options.audio ?? null;
3865
- const random = createRng2(options.seed ?? 1);
3961
+ const random = createRng(options.seed ?? 1);
3866
3962
  const layers = options.layers ?? createLayers();
3867
3963
  const bodies = [];
3868
3964
  const brass = new THREE.MeshStandardMaterial({ color: 13214282, metalness: 0.8, roughness: 0.35 });
@@ -4029,7 +4125,7 @@ function createBoxFx(options) {
4029
4125
 
4030
4126
  // src/fx/viewmodel.ts
4031
4127
  import { createToolClipPlayer, toolAnimationStep } from "@adatechnology/game-contracts";
4032
- import { createRng as createRng3, muzzleEnergy as muzzleEnergy3 } from "@adatechnology/game-weapons";
4128
+ import { createRng as createRng2, muzzleEnergy as muzzleEnergy3 } from "@adatechnology/game-weapons";
4033
4129
 
4034
4130
  // src/animation/clip-builder.ts
4035
4131
  import { STAGE_EVENT } from "@adatechnology/game-contracts";
@@ -5520,7 +5616,7 @@ var caliberOf = (arsenal, def) => def.action.kind === "firearm" ? arsenal.calibe
5520
5616
  function createViewmodel(options) {
5521
5617
  const { THREE, scene, camera, arsenal, groundAt, audio } = options;
5522
5618
  const kit = modelKit(THREE);
5523
- const random = createRng3(options.seed ?? 1);
5619
+ const random = createRng2(options.seed ?? 1);
5524
5620
  const layers = options.layers ?? createLayers();
5525
5621
  const distance = () => options.soundDistance?.() ?? 0;
5526
5622
  const particles = createParticles(THREE, scene, groundAt);
@@ -6528,7 +6624,7 @@ var explosivePart = (def, shape) => ({
6528
6624
  shape: shape.shape,
6529
6625
  position: shape.position,
6530
6626
  ...SHELL,
6531
- explodes: { energy: def.energy, radius: def.injuryRadius, ...def.chain ? { chain: { ...def.chain } } : {} }
6627
+ explodes: { energy: def.energy, radius: def.injuryRadius, effect: def.id, ...def.chain ? { chain: { ...def.chain } } : {} }
6532
6628
  });
6533
6629
  var meshOf = (THREE, shape) => {
6534
6630
  const geometry = shape.shape.kind === "cylinder" ? new THREE.CylinderGeometry(shape.shape.radius, shape.shape.radius, shape.shape.height, 16) : shape.shape.kind === "box" ? new THREE.BoxGeometry(...shape.shape.size) : new THREE.SphereGeometry(0.2);
@@ -6556,6 +6652,51 @@ var worldExplosiveItem = (def, name) => {
6556
6652
  };
6557
6653
  };
6558
6654
 
6655
+ // src/data/world-effects.json
6656
+ var world_effects_default = {
6657
+ pistol: { muzzle: "muzzle_glock", casing: "casing_brass" },
6658
+ tiny: { muzzle: "muzzle_glock", casing: "casing_brass" },
6659
+ rifle: { muzzle: "muzzle_ak", casing: "casing_brass" },
6660
+ shell: { muzzle: "muzzle_r870", casing: "casing_shell" },
6661
+ big: { muzzle: "muzzle_barrett", casing: "casing_50" }
6662
+ };
6663
+
6664
+ // src/world-effects.ts
6665
+ var WORLD_EFFECTS = world_effects_default;
6666
+ var PORT_BACK = 0.35;
6667
+ var PORT_RIGHT = 0.04;
6668
+ function createWorldEffects(options) {
6669
+ const ids = options.ids ?? WORLD_EFFECTS;
6670
+ const caseTypeOf = options.caseTypeOf ?? ((weapon) => {
6671
+ const def = options.arsenal.weapons.get(weapon);
6672
+ return def && def.action.kind === "firearm" ? options.arsenal.calibers.get(def.action.caliber)?.caseType : void 0;
6673
+ });
6674
+ let counter = 0;
6675
+ return {
6676
+ /** Plays what the event asks for and answers how many effects started. */
6677
+ handle(event, sink) {
6678
+ if (!sink || event.type !== "shot" || !event.origin) return 0;
6679
+ const caseType = caseTypeOf(event.weapon) ?? "rifle";
6680
+ const entry = ids[caseType];
6681
+ if (!entry) return 0;
6682
+ const direction = event.direction ?? [0, 0, -1];
6683
+ const seed = ++counter;
6684
+ let started = 0;
6685
+ for (let shot = 0; shot < Math.max(1, event.rounds ?? 1); shot++) {
6686
+ if (sink.play({ effect: entry.muzzle, point: event.origin, direction, seed: seed * 8 + shot }) !== null) started++;
6687
+ }
6688
+ const side = [direction[2], 0, -direction[0]];
6689
+ const port = [
6690
+ event.origin[0] - direction[0] * PORT_BACK + side[0] * PORT_RIGHT,
6691
+ event.origin[1] - direction[1] * PORT_BACK,
6692
+ event.origin[2] - direction[2] * PORT_BACK + side[2] * PORT_RIGHT
6693
+ ];
6694
+ if (sink.play({ effect: entry.casing, point: port, direction, seed: seed * 8 + 7 }) !== null) started++;
6695
+ return started;
6696
+ }
6697
+ };
6698
+ }
6699
+
6559
6700
  // src/area.ts
6560
6701
  var WEAPON_ITEM_PREFIX = "weapon_";
6561
6702
  var COUNTER_ITEM_ID = "counter_weapons";
@@ -6724,7 +6865,8 @@ function createWeaponsTestModule(options = {}) {
6724
6865
  const arsenal = options.arsenal ?? createArsenal({ locale });
6725
6866
  const text = textOf(locale);
6726
6867
  const accessoryText = accessoryTextOf(locale);
6727
- const audio = options.sound === false ? null : createWeaponAudio(void 0, options.seed === void 0 ? {} : { seed: options.seed });
6868
+ const seedOption = options.seed === void 0 ? {} : { seed: options.seed };
6869
+ let audio = options.sound === false ? null : createWeaponAudio(void 0, seedOption);
6728
6870
  const weaponDefs = [...arsenal.weapons.values()].filter((def) => !BODY_ACTIONS.has(def.id));
6729
6871
  const thumbnailBase = options.thumbnailBase ?? DEFAULT_THUMBNAIL_BASE;
6730
6872
  const withThumbnail = (item, key) => {
@@ -6751,7 +6893,9 @@ function createWeaponsTestModule(options = {}) {
6751
6893
  id: "weapons_area",
6752
6894
  name: text.name,
6753
6895
  group: text.group,
6754
- audio,
6896
+ get audio() {
6897
+ return audio;
6898
+ },
6755
6899
  get fx() {
6756
6900
  return fx;
6757
6901
  },
@@ -6799,6 +6943,11 @@ function createWeaponsTestModule(options = {}) {
6799
6943
  ],
6800
6944
  attach(lent) {
6801
6945
  host = lent;
6946
+ const lentEngine = lent.audio?.engine;
6947
+ if (audio && lentEngine) {
6948
+ audio.dispose();
6949
+ audio = createWeaponAudio(void 0, { ...seedOption, engine: lentEngine });
6950
+ }
6802
6951
  const created = createWeaponFx({
6803
6952
  THREE: lent.THREE,
6804
6953
  scene: lent.scene,
@@ -6832,7 +6981,9 @@ function createWeaponsTestModule(options = {}) {
6832
6981
  } else if (change.kind === "control" && change.id === "sound_distance" && typeof change.value === "number") soundDistance = change.value;
6833
6982
  else if (change.kind === "control" && change.id === "fuse_length" && typeof change.value === "number") lent.configureTool?.({ fuse_length: String(change.value) });
6834
6983
  });
6984
+ const worldEffects = createWorldEffects({ arsenal });
6835
6985
  const offEvents = lent.on((event) => {
6986
+ worldEffects.handle(event, lent.effects);
6836
6987
  if (event.type === "explosion") {
6837
6988
  const away = Math.hypot(event.point[0] - camera.position.x, event.point[1] - camera.position.y, event.point[2] - camera.position.z);
6838
6989
  audio?.play("explosion", Math.min(1, EXPLOSION_VOLUME_BASE + event.radius * EXPLOSION_VOLUME_PER_METRE), away);
@@ -6872,6 +7023,7 @@ export {
6872
7023
  MOUNTED_M134_ITEM_ID,
6873
7024
  WEAPON_ITEM_PREFIX,
6874
7025
  WEAPON_THUMBNAILS,
7026
+ WORLD_EFFECTS,
6875
7027
  WORLD_EXPLOSIVE_IDS,
6876
7028
  WORLD_EXPLOSIVE_PREFIX,
6877
7029
  applyCondition,
@@ -6887,6 +7039,7 @@ export {
6887
7039
  createWeaponAudio,
6888
7040
  createWeaponFx,
6889
7041
  createWeaponsTestModule,
7042
+ createWorldEffects,
6890
7043
  deriveFrames,
6891
7044
  fitAttachmentModels,
6892
7045
  hasWeaponModel,