@adatechnology/game-destruction-three 0.13.0 → 0.15.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.
@@ -23,33 +23,93 @@ var GameDestructionThree = (() => {
23
23
  // src/index.ts
24
24
  var index_exports = {};
25
25
  __export(index_exports, {
26
+ BIG_BLAST_RADIUS: () => BIG_BLAST_RADIUS,
27
+ BLAST_FALLOFF: () => BLAST_FALLOFF,
28
+ BLAST_FAR_DISTANCE: () => BLAST_FAR_DISTANCE,
29
+ BLAST_FAR_FALLOFF: () => BLAST_FAR_FALLOFF,
30
+ BLAST_FIRE_SAMPLES: () => BLAST_FIRE_SAMPLES,
31
+ BLAST_KINDS: () => BLAST_KINDS,
32
+ BLAST_KIND_OF: () => BLAST_KIND_OF,
33
+ BLAST_SOUNDS: () => BLAST_SOUNDS,
34
+ BLAST_SOUND_OF: () => BLAST_SOUND_OF,
26
35
  BURN_LOOP: () => BURN_LOOP,
36
+ DEFAULT_CRACKLES: () => DEFAULT_CRACKLES,
27
37
  DEFAULT_FIRE: () => DEFAULT_FIRE,
38
+ DEFAULT_INDOOR_CONFINEMENT: () => DEFAULT_INDOOR_CONFINEMENT,
28
39
  DESTRUCTION_SOUND_BANK: () => DESTRUCTION_SOUND_BANK,
29
40
  DESTRUCTION_SOUND_KEYS: () => DESTRUCTION_SOUND_KEYS,
30
41
  DRAG_SOUNDS: () => DRAG_SOUNDS,
31
42
  EXPLOSION_BOOM: () => EXPLOSION_BOOM,
43
+ FIRE_FALLOFF: () => FIRE_FALLOFF,
44
+ FIRE_LOOP: () => FIRE_LOOP,
45
+ FIRE_SOUNDS: () => FIRE_SOUNDS,
46
+ IMPACT_FAMILY_TAGS: () => IMPACT_FAMILY_TAGS,
47
+ IMPACT_SOUNDS: () => IMPACT_SOUNDS,
48
+ IMPACT_SOUND_KEYS: () => IMPACT_SOUND_KEYS,
32
49
  SCENE_SOUNDS: () => SCENE_SOUNDS,
50
+ blastFireSampleBank: () => blastFireSampleBank,
51
+ breakSoundKeys: () => breakSoundKeys,
33
52
  createDestructionRenderer: () => createDestructionRenderer,
34
53
  createDestructionSound: () => createDestructionSound,
35
54
  dragSoundKey: () => dragSoundKey,
36
55
  heatColor: () => heatColor,
56
+ impactFamily: () => impactFamily,
37
57
  impactKey: () => impactKey,
58
+ impactSoundKeys: () => impactSoundKeys,
59
+ ricochetSoundKeys: () => ricochetSoundKeys,
60
+ shotKey: () => shotKey,
38
61
  specFromObject3D: () => specFromObject3D
39
62
  });
40
63
 
64
+ // src/audio/impact-keys.ts
65
+ var IMPACT_FAMILY_TAGS = [
66
+ ["metal", "metal"],
67
+ ["glass", "glass"],
68
+ ["ceramic", "ceramic"],
69
+ ["soil", "soil"],
70
+ ["masonry", "masonry"],
71
+ ["wood", "wood"],
72
+ ["plant", "plant"],
73
+ ["plastic", "plastic"],
74
+ ["rubber", "rubber"],
75
+ ["wax", "wax"],
76
+ ["fabric", "soft"]
77
+ ];
78
+ function impactFamily(def) {
79
+ const tags = def?.tags ?? [];
80
+ for (const [tag, family] of IMPACT_FAMILY_TAGS) if (tags.includes(tag)) return family;
81
+ return void 0;
82
+ }
83
+ var ordered = (prefix, material, family, blow3) => {
84
+ const keys = [];
85
+ if (material && blow3) keys.push(`${prefix}_${material}_${blow3}`);
86
+ if (material) keys.push(`${prefix}_${material}`);
87
+ if (family && blow3) keys.push(`${prefix}_${family}_${blow3}`);
88
+ if (family) keys.push(`${prefix}_${family}`);
89
+ return keys;
90
+ };
91
+ function impactSoundKeys(material, def, blow3) {
92
+ return { keys: ordered("impact", material, impactFamily(def), blow3) };
93
+ }
94
+ function breakSoundKeys(material, def, blow3) {
95
+ return { keys: ordered("break", material, impactFamily(def), blow3) };
96
+ }
97
+ function ricochetSoundKeys(material, def) {
98
+ return { keys: ordered("ricochet", material, impactFamily(def), void 0) };
99
+ }
100
+
41
101
  // ../game-contracts/dist/index.js
42
102
  var DEFAULT_BLAST_SHOVE = { coupling: 0.02, maxSpeed: 20, lift: 0.3, minSpeed: 0.3, falloffPower: 2 };
43
- function blastShove(blast, target, options = {}) {
103
+ function blastShove(blast3, target, options = {}) {
44
104
  const o = { ...DEFAULT_BLAST_SHOVE, ...options };
45
- const dx = target.position[0] - blast.point[0];
46
- const dy = target.position[1] - blast.point[1];
47
- const dz = target.position[2] - blast.point[2];
105
+ const dx = target.position[0] - blast3.point[0];
106
+ const dy = target.position[1] - blast3.point[1];
107
+ const dz = target.position[2] - blast3.point[2];
48
108
  const distance = Math.hypot(dx, dy, dz);
49
- if (!(blast.radius > 0) || distance > blast.radius) return void 0;
50
- const falloff = (1 - distance / blast.radius) ** o.falloffPower;
109
+ if (!(blast3.radius > 0) || distance > blast3.radius) return void 0;
110
+ const falloff = (1 - distance / blast3.radius) ** o.falloffPower;
51
111
  const mass = Math.max(target.mass, 0.05);
52
- const speed = Math.min(o.maxSpeed, Math.sqrt(2 * blast.energy * (blast.reach ?? 1) * o.coupling / mass)) * falloff;
112
+ const speed = Math.min(o.maxSpeed, Math.sqrt(2 * blast3.energy * (blast3.reach ?? 1) * o.coupling / mass)) * falloff;
53
113
  if (!(speed >= o.minSpeed)) return void 0;
54
114
  const direction = distance > 1e-4 ? [dx / distance, dy / distance + o.lift, dz / distance] : [0, 1, 0];
55
115
  const length = Math.hypot(direction[0], direction[1], direction[2]);
@@ -1873,48 +1933,48 @@ var GameDestructionThree = (() => {
1873
1933
  }));
1874
1934
  }
1875
1935
  function spawnFire(pool, random, init, handle) {
1876
- const fire = pool.acquire();
1877
- fire.x = init.x ?? 0;
1878
- fire.y = init.y ?? 0;
1879
- fire.z = init.z ?? 0;
1880
- fire.r = init.r ?? 0.6;
1881
- fire.r0 = init.startRadius ?? fire.r;
1882
- fire.grow = init.grow ?? 0;
1883
- fire.life = init.life ?? 6;
1884
- fire.t = 0;
1885
- fire.rate = init.rate ?? 22;
1886
- fire.acc = 0;
1887
- fire.smoke = init.smoke ?? 0.6;
1888
- fire.smokeAcc = 0;
1889
- fire.fade = 1;
1890
- fire.follow = init.follow ?? null;
1891
- fire.fx = init.fx ?? 0;
1892
- fire.fy = init.fy ?? 0;
1893
- fire.fz = init.fz ?? 0;
1894
- fire.color = init.color ?? SMOKE_COLORS.black;
1895
- fire.rng = random;
1896
- fire.handle = handle;
1897
- return fire;
1936
+ const fire2 = pool.acquire();
1937
+ fire2.x = init.x ?? 0;
1938
+ fire2.y = init.y ?? 0;
1939
+ fire2.z = init.z ?? 0;
1940
+ fire2.r = init.r ?? 0.6;
1941
+ fire2.r0 = init.startRadius ?? fire2.r;
1942
+ fire2.grow = init.grow ?? 0;
1943
+ fire2.life = init.life ?? 6;
1944
+ fire2.t = 0;
1945
+ fire2.rate = init.rate ?? 22;
1946
+ fire2.acc = 0;
1947
+ fire2.smoke = init.smoke ?? 0.6;
1948
+ fire2.smokeAcc = 0;
1949
+ fire2.fade = 1;
1950
+ fire2.follow = init.follow ?? null;
1951
+ fire2.fx = init.fx ?? 0;
1952
+ fire2.fy = init.fy ?? 0;
1953
+ fire2.fz = init.fz ?? 0;
1954
+ fire2.color = init.color ?? SMOKE_COLORS.black;
1955
+ fire2.rng = random;
1956
+ fire2.handle = handle;
1957
+ return fire2;
1898
1958
  }
1899
1959
  function addFire(core, random, init, handle) {
1900
- const fire = spawnFire(core.fires, random, init, handle);
1901
- core.trace?.fire?.(fire);
1902
- return fire;
1960
+ const fire2 = spawnFire(core.fires, random, init, handle);
1961
+ core.trace?.fire?.(fire2);
1962
+ return fire2;
1903
1963
  }
1904
1964
  function positionOf(follow) {
1905
1965
  return "position" in follow ? follow.position : follow;
1906
1966
  }
1907
- function emitFlames(core, fire, radius, dt) {
1908
- const random = fire.rng;
1909
- fire.acc += dt * fire.rate * (0.5 + radius * 0.9) * fire.fade * core.clock.lod;
1910
- while (fire.acc >= 1) {
1911
- fire.acc -= 1;
1967
+ function emitFlames(core, fire2, radius, dt) {
1968
+ const random = fire2.rng;
1969
+ fire2.acc += dt * fire2.rate * (0.5 + radius * 0.9) * fire2.fade * core.clock.lod;
1970
+ while (fire2.acc >= 1) {
1971
+ fire2.acc -= 1;
1912
1972
  const angle = random.next() * Math.PI * 2;
1913
1973
  const distance = Math.sqrt(random.next()) * radius;
1914
1974
  addPuff(core, random, {
1915
- x: fire.x + Math.cos(angle) * distance,
1916
- y: fire.y + 0.05,
1917
- z: fire.z + Math.sin(angle) * distance,
1975
+ x: fire2.x + Math.cos(angle) * distance,
1976
+ y: fire2.y + 0.05,
1977
+ z: fire2.z + Math.sin(angle) * distance,
1918
1978
  vx: random.between(-0.2, 0.2),
1919
1979
  vy: random.between(0.6, 1.4),
1920
1980
  vz: random.between(-0.2, 0.2),
@@ -1926,21 +1986,21 @@ var GameDestructionThree = (() => {
1926
1986
  hl: random.between(0.35, 0.6),
1927
1987
  drag: 1.5,
1928
1988
  buoy: 3.2,
1929
- color: fire.color,
1989
+ color: fire2.color,
1930
1990
  kind: 0,
1931
1991
  wind: 0.4
1932
1992
  });
1933
1993
  }
1934
1994
  }
1935
- function emitSmoke(core, fire, radius, dt) {
1936
- const random = fire.rng;
1937
- fire.smokeAcc += dt * fire.smoke * (1 + radius * 0.8) * fire.fade * core.clock.lod;
1938
- while (fire.smokeAcc >= 1 && core.layers.smoke) {
1939
- fire.smokeAcc -= 1;
1995
+ function emitSmoke(core, fire2, radius, dt) {
1996
+ const random = fire2.rng;
1997
+ fire2.smokeAcc += dt * fire2.smoke * (1 + radius * 0.8) * fire2.fade * core.clock.lod;
1998
+ while (fire2.smokeAcc >= 1 && core.layers.smoke) {
1999
+ fire2.smokeAcc -= 1;
1940
2000
  addPuff(core, random, {
1941
- x: fire.x + random.between(-radius, radius) * 0.5,
1942
- y: fire.y + 0.6 + radius * 0.3,
1943
- z: fire.z + random.between(-radius, radius) * 0.5,
2001
+ x: fire2.x + random.between(-radius, radius) * 0.5,
2002
+ y: fire2.y + 0.6 + radius * 0.3,
2003
+ z: fire2.z + random.between(-radius, radius) * 0.5,
1944
2004
  vy: random.between(0.8, 1.5),
1945
2005
  s0: 0.25,
1946
2006
  s1: 0.6 + Math.min(radius, 2) * 0.3,
@@ -1950,18 +2010,18 @@ var GameDestructionThree = (() => {
1950
2010
  hl: 0.25,
1951
2011
  drag: 0.6,
1952
2012
  buoy: 0.55,
1953
- color: fire.color,
2013
+ color: fire2.color,
1954
2014
  kind: 0
1955
2015
  });
1956
2016
  }
1957
2017
  }
1958
- function emitEmber(core, fire, radius, dt) {
1959
- const random = fire.rng;
1960
- if (!(random.next() < dt * 6 * fire.fade && core.layers.shrapnel)) return;
2018
+ function emitEmber(core, fire2, radius, dt) {
2019
+ const random = fire2.rng;
2020
+ if (!(random.next() < dt * 6 * fire2.fade && core.layers.shrapnel)) return;
1961
2021
  addSpark(core, {
1962
- x: fire.x + random.between(-radius, radius) * 0.6,
1963
- y: fire.y + 0.4,
1964
- z: fire.z + random.between(-radius, radius) * 0.6,
2022
+ x: fire2.x + random.between(-radius, radius) * 0.6,
2023
+ y: fire2.y + 0.4,
2024
+ z: fire2.z + random.between(-radius, radius) * 0.6,
1965
2025
  vx: random.between(-0.4, 0.4),
1966
2026
  vy: random.between(1.5, 3.2),
1967
2027
  vz: random.between(-0.4, 0.4),
@@ -1976,24 +2036,24 @@ var GameDestructionThree = (() => {
1976
2036
  function updateFires(core, dt) {
1977
2037
  const pool = core.fires;
1978
2038
  for (let index = pool.items.length - 1; index >= 0; index--) {
1979
- const fire = pool.items[index];
1980
- fire.t += dt;
1981
- if (fire.follow) {
1982
- const point = positionOf(fire.follow);
1983
- fire.x = point.x + fire.fx;
1984
- fire.y = point.y + fire.fy;
1985
- fire.z = point.z + fire.fz;
1986
- }
1987
- const radius = fire.grow ? lerp(fire.r0, fire.r, Math.min(1, fire.t / fire.grow)) : fire.r;
1988
- fire.fade = fire.t < fire.life - 1.5 ? Math.min(1, fire.t * 3) : Math.max(0, (fire.life - fire.t) / 1.5);
1989
- if (fire.t >= fire.life) {
2039
+ const fire2 = pool.items[index];
2040
+ fire2.t += dt;
2041
+ if (fire2.follow) {
2042
+ const point = positionOf(fire2.follow);
2043
+ fire2.x = point.x + fire2.fx;
2044
+ fire2.y = point.y + fire2.fy;
2045
+ fire2.z = point.z + fire2.fz;
2046
+ }
2047
+ const radius = fire2.grow ? lerp(fire2.r0, fire2.r, Math.min(1, fire2.t / fire2.grow)) : fire2.r;
2048
+ fire2.fade = fire2.t < fire2.life - 1.5 ? Math.min(1, fire2.t * 3) : Math.max(0, (fire2.life - fire2.t) / 1.5);
2049
+ if (fire2.t >= fire2.life) {
1990
2050
  pool.removeAt(index);
1991
2051
  continue;
1992
2052
  }
1993
2053
  if (!core.layers.fire) continue;
1994
- emitFlames(core, fire, radius, dt);
1995
- emitSmoke(core, fire, radius, dt);
1996
- emitEmber(core, fire, radius, dt);
2054
+ emitFlames(core, fire2, radius, dt);
2055
+ emitSmoke(core, fire2, radius, dt);
2056
+ emitEmber(core, fire2, radius, dt);
1997
2057
  }
1998
2058
  }
1999
2059
  var call = (name) => (...args) => ({ $: name, a: args });
@@ -4458,35 +4518,35 @@ var GameDestructionThree = (() => {
4458
4518
  var NOOP = () => {
4459
4519
  };
4460
4520
  function createSchedule() {
4461
- const entries = [];
4521
+ const entries2 = [];
4462
4522
  const spare = [];
4463
4523
  return {
4464
4524
  at(time, action) {
4465
4525
  const entry = spare.pop() ?? { time: 0, action: NOOP };
4466
4526
  entry.time = time;
4467
4527
  entry.action = action;
4468
- entries.push(entry);
4528
+ entries2.push(entry);
4469
4529
  },
4470
4530
  update(now) {
4471
- for (let index = entries.length - 1; index >= 0; index--) {
4472
- const entry = entries[index];
4531
+ for (let index = entries2.length - 1; index >= 0; index--) {
4532
+ const entry = entries2[index];
4473
4533
  if (!entry || entry.time > now) continue;
4474
4534
  const action = entry.action;
4475
- entries.splice(index, 1);
4535
+ entries2.splice(index, 1);
4476
4536
  entry.action = NOOP;
4477
4537
  spare.push(entry);
4478
4538
  action();
4479
4539
  }
4480
4540
  },
4481
4541
  clear() {
4482
- for (const entry of entries) {
4542
+ for (const entry of entries2) {
4483
4543
  entry.action = NOOP;
4484
4544
  spare.push(entry);
4485
4545
  }
4486
- entries.length = 0;
4546
+ entries2.length = 0;
4487
4547
  },
4488
4548
  get pending() {
4489
- return entries.length;
4549
+ return entries2.length;
4490
4550
  }
4491
4551
  };
4492
4552
  }
@@ -5174,9 +5234,9 @@ var GameDestructionThree = (() => {
5174
5234
  }
5175
5235
  function largestFire(fires, skip) {
5176
5236
  let best = null;
5177
- for (const fire of fires) {
5178
- if (fire === skip) continue;
5179
- if (best === null || fire.r > best.r) best = fire;
5237
+ for (const fire2 of fires) {
5238
+ if (fire2 === skip) continue;
5239
+ if (best === null || fire2.r > best.r) best = fire2;
5180
5240
  }
5181
5241
  return best;
5182
5242
  }
@@ -5199,17 +5259,17 @@ var GameDestructionThree = (() => {
5199
5259
  const second = largestFire(step.fires, first);
5200
5260
  const largest = [first, second];
5201
5261
  for (let index = 0; index < FIRE_LIGHTS; index++) {
5202
- const fire = largest[index];
5262
+ const fire2 = largest[index];
5203
5263
  const light = pool.fire[index];
5204
- if (!fire) {
5264
+ if (!fire2) {
5205
5265
  light.intensity = 0;
5206
5266
  continue;
5207
5267
  }
5208
- light.x = fire.x;
5209
- light.y = fire.y + Math.max(0.8, fire.r * 0.6);
5210
- light.z = fire.z;
5211
- light.intensity = (1.4 + fire.r * 0.6) * (0.75 + 0.25 * Math.sin(step.clock * 17 + index * 3) + 0.1 * Math.sin(step.clock * 41)) * fire.fade;
5212
- light.distance = 10 + fire.r * 4;
5268
+ light.x = fire2.x;
5269
+ light.y = fire2.y + Math.max(0.8, fire2.r * 0.6);
5270
+ light.z = fire2.z;
5271
+ light.intensity = (1.4 + fire2.r * 0.6) * (0.75 + 0.25 * Math.sin(step.clock * 17 + index * 3) + 0.1 * Math.sin(step.clock * 41)) * fire2.fade;
5272
+ light.distance = 10 + fire2.r * 4;
5213
5273
  }
5214
5274
  if (brightest && brightness > 0.05) {
5215
5275
  pool.glow.x = brightest.x;
@@ -5241,12 +5301,12 @@ var GameDestructionThree = (() => {
5241
5301
  var SCORCH_VARIANTS = 3;
5242
5302
  var RIM_COLORS = [2829359, 3815478];
5243
5303
  function createMarkStore() {
5244
- const entries = [];
5304
+ const entries2 = [];
5245
5305
  let version = 0;
5246
5306
  let bloodCount = 0;
5247
5307
  const listeners = /* @__PURE__ */ new Set();
5248
5308
  return {
5249
- entries,
5309
+ entries: entries2,
5250
5310
  get version() {
5251
5311
  return version;
5252
5312
  },
@@ -5255,13 +5315,13 @@ var GameDestructionThree = (() => {
5255
5315
  },
5256
5316
  lift: 0,
5257
5317
  add(mark) {
5258
- entries.push(mark);
5318
+ entries2.push(mark);
5259
5319
  if (mark.kind === "blood") bloodCount++;
5260
5320
  version++;
5261
5321
  for (const listener of listeners) listener({ type: "add", mark });
5262
5322
  },
5263
5323
  clear() {
5264
- entries.length = 0;
5324
+ entries2.length = 0;
5265
5325
  bloodCount = 0;
5266
5326
  this.lift = 0;
5267
5327
  version++;
@@ -5715,18 +5775,18 @@ var GameDestructionThree = (() => {
5715
5775
  }
5716
5776
  if (def.mushroom && core.layers.fire) composeMushroom(context, def.mushroom);
5717
5777
  if (def.residualFires && core.layers.fire) {
5718
- for (const fire of def.residualFires) {
5778
+ for (const fire2 of def.residualFires) {
5719
5779
  addFire(core, random, {
5720
- x: x + (fire.dx ?? 0),
5780
+ x: x + (fire2.dx ?? 0),
5721
5781
  y: 0,
5722
- z: z + (fire.dz ?? 0),
5723
- r: fire.radius,
5724
- life: fire.life,
5725
- rate: fire.rate,
5726
- smoke: fire.smoke,
5727
- startRadius: fire.startRadius,
5728
- grow: fire.grow,
5729
- color: fire.smokeColor !== void 0 ? hexToLinearRgb(fire.smokeColor) : void 0
5782
+ z: z + (fire2.dz ?? 0),
5783
+ r: fire2.radius,
5784
+ life: fire2.life,
5785
+ rate: fire2.rate,
5786
+ smoke: fire2.smoke,
5787
+ startRadius: fire2.startRadius,
5788
+ grow: fire2.grow,
5789
+ color: fire2.smokeColor !== void 0 ? hexToLinearRgb(fire2.smokeColor) : void 0
5730
5790
  }, context.call.handle);
5731
5791
  }
5732
5792
  }
@@ -7398,7 +7458,7 @@ var GameDestructionThree = (() => {
7398
7458
  function scaleDefinition(def, factor) {
7399
7459
  if (factor === 1) return def;
7400
7460
  const mushroom = def.mushroom && { ...def.mushroom, height: def.mushroom.height * factor, radius: def.mushroom.radius * factor };
7401
- const residual = def.residualFires?.map((fire) => ({ ...fire, radius: fire.radius * factor, dx: (fire.dx ?? 0) * factor, dz: (fire.dz ?? 0) * factor }));
7461
+ const residual = def.residualFires?.map((fire2) => ({ ...fire2, radius: fire2.radius * factor, dx: (fire2.dx ?? 0) * factor, dz: (fire2.dz ?? 0) * factor }));
7402
7462
  return {
7403
7463
  ...def,
7404
7464
  lethal: def.lethal * factor,
@@ -7968,13 +8028,13 @@ void main(){
7968
8028
  function createLightLayer(THREE, pool, scale, budget) {
7969
8029
  const group = new THREE.Group();
7970
8030
  if (budget) return createBudgetedLightLayer(group, pool, scale, budget);
7971
- const blast = pool.blast.map(() => {
8031
+ const blast3 = pool.blast.map(() => {
7972
8032
  const light = new THREE.PointLight(16752720, 0, 30, 2);
7973
8033
  light.position.set(0, -50, 0);
7974
8034
  group.add(light);
7975
8035
  return light;
7976
8036
  });
7977
- const fire = pool.fire.map(() => {
8037
+ const fire2 = pool.fire.map(() => {
7978
8038
  const light = new THREE.PointLight(16742954, 0, 16, 2);
7979
8039
  light.position.set(0, -50, 0);
7980
8040
  group.add(light);
@@ -7985,7 +8045,7 @@ void main(){
7985
8045
  sync() {
7986
8046
  let lit = 0;
7987
8047
  pool.blast.forEach((entry, index) => {
7988
- const light = blast[index];
8048
+ const light = blast3[index];
7989
8049
  light.position.set(entry.x, entry.y, entry.z);
7990
8050
  light.distance = entry.distance;
7991
8051
  light.color.setRGB(entry.color[0], entry.color[1], entry.color[2], THREE.LinearSRGBColorSpace);
@@ -7993,7 +8053,7 @@ void main(){
7993
8053
  if (entry.intensity > 0.05) lit++;
7994
8054
  });
7995
8055
  pool.fire.forEach((entry, index) => {
7996
- const light = fire[index];
8056
+ const light = fire2[index];
7997
8057
  light.position.set(entry.x, entry.y, entry.z);
7998
8058
  light.distance = entry.distance;
7999
8059
  light.intensity = entry.intensity * scale;
@@ -8002,31 +8062,31 @@ void main(){
8002
8062
  return lit;
8003
8063
  },
8004
8064
  dispose() {
8005
- for (const light of [...blast, ...fire]) light.dispose();
8065
+ for (const light of [...blast3, ...fire2]) light.dispose();
8006
8066
  }
8007
8067
  };
8008
8068
  }
8009
8069
  function createBudgetedLightLayer(group, pool, scale, budget) {
8010
- const blast = pool.blast.map(() => null);
8011
- const fire = pool.fire.map(() => null);
8070
+ const blast3 = pool.blast.map(() => null);
8071
+ const fire2 = pool.fire.map(() => null);
8012
8072
  return {
8013
8073
  group,
8014
8074
  sync() {
8015
8075
  let lit = 0;
8016
8076
  pool.blast.forEach((entry, index) => {
8017
8077
  const request = { owner: "vfx.blast", priority: LIGHT_PRIORITY.blast, urgent: true, color: entry.color, intensity: entry.intensity * scale, distance: entry.distance, decay: BLAST_DECAY, position: [entry.x, entry.y, entry.z] };
8018
- followEntry(budget, blast, index, entry, request);
8078
+ followEntry(budget, blast3, index, entry, request);
8019
8079
  if (entry.intensity > LIGHT_ON_THRESHOLD) lit++;
8020
8080
  });
8021
8081
  pool.fire.forEach((entry, index) => {
8022
8082
  const request = { owner: "vfx.fire", priority: LIGHT_PRIORITY.fire, color: FIRE_COLOR, intensity: entry.intensity * scale, distance: entry.distance, decay: FIRE_DECAY, position: [entry.x, entry.y, entry.z] };
8023
- followEntry(budget, fire, index, entry, request);
8083
+ followEntry(budget, fire2, index, entry, request);
8024
8084
  if (entry.intensity > LIGHT_ON_THRESHOLD) lit++;
8025
8085
  });
8026
8086
  return lit;
8027
8087
  },
8028
8088
  dispose() {
8029
- for (const handle of [...blast, ...fire]) handle?.release({ fadeMs: 0 });
8089
+ for (const handle of [...blast3, ...fire2]) handle?.release({ fadeMs: 0 });
8030
8090
  }
8031
8091
  };
8032
8092
  }
@@ -8924,7 +8984,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8924
8984
  }
8925
8985
  function budgetedLights(engine, lightScale, budget) {
8926
8986
  const flash2 = engine.lights.lights.map(() => null);
8927
- const fire = engine.lights.fire.map(() => null);
8987
+ const fire2 = engine.lights.fire.map(() => null);
8928
8988
  return {
8929
8989
  sync() {
8930
8990
  engine.lights.lights.forEach((source, index) => {
@@ -8933,11 +8993,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8933
8993
  });
8934
8994
  engine.lights.fire.forEach((source, index) => {
8935
8995
  const request = { owner: "vfx.workshop", priority: LIGHT_PRIORITY.fire, color: FIRE_LIGHT_COLOR, intensity: source.intensity * lightScale, distance: FIRE_LIGHT_DISTANCE, position: [source.x, source.y, source.z] };
8936
- followEntry(budget, fire, index, source, request);
8996
+ followEntry(budget, fire2, index, source, request);
8937
8997
  });
8938
8998
  },
8939
8999
  dispose() {
8940
- for (const handle of [...flash2, ...fire]) handle?.release({ fadeMs: 0 });
9000
+ for (const handle of [...flash2, ...fire2]) handle?.release({ fadeMs: 0 });
8941
9001
  }
8942
9002
  };
8943
9003
  }
@@ -9301,10 +9361,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9301
9361
  */
9302
9362
  warmMeshes() {
9303
9363
  const opaque = this.materialFor("warm", 8421504, false);
9304
- const glass = this.materialFor("warm", 8421504, true);
9364
+ const glass2 = this.materialFor("warm", 8421504, true);
9305
9365
  const geometries = [this.box, ...Array.from({ length: SHARD_SHAPES }, (_, index) => this.shardGeometry(index))];
9306
9366
  const { geometry, material } = this.markParts();
9307
- return [...geometries.flatMap((shared) => [new this.THREE.Mesh(shared, opaque), new this.THREE.Mesh(shared, glass)]), new this.THREE.Mesh(geometry, material)];
9367
+ return [...geometries.flatMap((shared) => [new this.THREE.Mesh(shared, opaque), new this.THREE.Mesh(shared, glass2)]), new this.THREE.Mesh(geometry, material)];
9308
9368
  }
9309
9369
  /**
9310
9370
  * Bullet holes that stay on a fragment (`Fragment.marks`): dark discs that are children of its mesh, so they fall, spin and settle with it. The mesh is
@@ -9655,6 +9715,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9655
9715
  };
9656
9716
 
9657
9717
  // src/pieces.ts
9718
+ var SLAM_MIN_SPEED = 2;
9719
+ var SLAM_BOUNCE = 0.2;
9720
+ var SLAM_FLOOR_NORMAL = 0.7;
9658
9721
  var SUB_DT = 1 / 120;
9659
9722
  var MU = 0.7;
9660
9723
  var SLEEP_AFTER = 0.5;
@@ -9889,9 +9952,34 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9889
9952
  const fixed = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
9890
9953
  return Math.max(top, fixed);
9891
9954
  }
9955
+ /** Stops a piece that ran into something on its way from `start` to `pos` (`sweep`): moves it back to where it met it and takes the speed into it. */
9956
+ sweepOn(b, start, pos) {
9957
+ const travel = Math.hypot(pos[0] - start[0], pos[1] - start[1], pos[2] - start[2]);
9958
+ if (travel < 1e-3) return;
9959
+ const hit = this.o.sweep?.(b, start, pos);
9960
+ if (!hit || hit.normal[1] > SLAM_FLOOR_NORMAL) return;
9961
+ const v = b.velocity;
9962
+ const n = hit.normal;
9963
+ const into = -(v[0] * n[0] + v[1] * n[1] + v[2] * n[2]);
9964
+ if (into <= 0) return;
9965
+ const k = Math.min(1, hit.distance / travel);
9966
+ for (let i = 0; i < 3; i++) {
9967
+ pos[i] = start[i] + (pos[i] - start[i]) * k;
9968
+ v[i] += (1 + SLAM_BOUNCE) * into * n[i];
9969
+ }
9970
+ const w = b.angularVelocity;
9971
+ w[0] *= 0.5;
9972
+ w[1] *= 0.5;
9973
+ w[2] *= 0.5;
9974
+ if (into >= SLAM_MIN_SPEED && this.time - b.lastHit > 0.15) {
9975
+ b.lastHit = this.time;
9976
+ this.o.onSlam?.(b, hit, into);
9977
+ }
9978
+ }
9892
9979
  integrate(b, dt) {
9893
9980
  const c = this.centerOf(b, this.p);
9894
9981
  const pos = [c.x, c.y, c.z];
9982
+ const start = [c.x, c.y, c.z];
9895
9983
  const q = this.q.copy(b.root.quaternion);
9896
9984
  const v = b.velocity;
9897
9985
  const w = b.angularVelocity;
@@ -10001,11 +10089,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10001
10089
  if (contactedLast) touched = true;
10002
10090
  if (!contactedLast) continue;
10003
10091
  pos[1] += Math.max(0, maxPen - SLOP * 0.5);
10004
- let impact2 = 0;
10005
- for (const r of rs) impact2 = Math.max(impact2, -(v[1] + w[2] * r[0] - w[0] * r[2]));
10006
- const restitution = impact2 > 2 ? 0.25 : 0;
10007
- if (impact2 > landing2) {
10008
- landing2 = impact2;
10092
+ let impact3 = 0;
10093
+ for (const r of rs) impact3 = Math.max(impact3, -(v[1] + w[2] * r[0] - w[0] * r[2]));
10094
+ const restitution = impact3 > 2 ? 0.25 : 0;
10095
+ if (impact3 > landing2) {
10096
+ landing2 = impact3;
10009
10097
  let sx = 0;
10010
10098
  let sz = 0;
10011
10099
  for (const r of rs) {
@@ -10136,6 +10224,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10136
10224
  v[0] *= fd;
10137
10225
  v[2] *= fd;
10138
10226
  }
10227
+ if (this.o.sweep && !b.held) this.sweepOn(b, start, pos);
10139
10228
  b.root.quaternion.copy(q);
10140
10229
  const off = this.ax.copy(b.centerV).applyQuaternion(q);
10141
10230
  b.root.position.set(pos[0] - off.x, pos[1] - off.y, pos[2] - off.z);
@@ -11724,7 +11813,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11724
11813
  const fallback2 = options.fallback ?? "plastic";
11725
11814
  const minSize = options.minSize ?? 0.015;
11726
11815
  const inv = specFrameInverse(THREE, root);
11727
- const entries = [];
11816
+ const entries2 = [];
11728
11817
  const byKey = /* @__PURE__ */ new Map();
11729
11818
  const visit = (o) => {
11730
11819
  if (!o.visible || o.userData?.destructionIgnore) return;
@@ -11761,8 +11850,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11761
11850
  if (powder) existing.powder = powder;
11762
11851
  if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
11763
11852
  } else {
11764
- const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...powder ? { powder } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
11765
- entries.push(e);
11853
+ const e = { key: explicit ?? `m${entries2.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...powder ? { powder } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
11854
+ entries2.push(e);
11766
11855
  if (explicit) byKey.set(explicit, e);
11767
11856
  }
11768
11857
  }
@@ -11780,7 +11869,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11780
11869
  const largest = (e) => Math.max(...sizeOf(e.box));
11781
11870
  const kept = [];
11782
11871
  const tiny = [];
11783
- for (const e of entries) (e.skin || !e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
11872
+ for (const e of entries2) (e.skin || !e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
11784
11873
  for (const t of tiny) {
11785
11874
  let best = null;
11786
11875
  let bestV = 0;
@@ -12939,12 +13028,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12939
13028
  var QUEUE_CAP = 4e3;
12940
13029
  var MIN_FRAGMENTS_PER_SLICE = 8;
12941
13030
  var STATIC_REFRESH = 0.5;
13031
+ var STATIC_REFRESH_SLICE_MS = 1;
12942
13032
  var STATIC_SPAN = 3;
12943
13033
  var FEET_BAND = 0.03;
12944
13034
  var TIP_MIN_HEIGHT = 0.3;
12945
13035
  var TIP_NARROW = 0.3;
12946
13036
  var FIRE_CEILING_SECONDS = 3600;
12947
13037
  var GENERIC_REFERENCE_RADIUS = 15;
13038
+ var SWEEP_CORE = 0.15;
12948
13039
  var PRIMED_FIRE_RADIUS = 0.2;
12949
13040
  var FLUID_COLORS = {
12950
13041
  water: 9226495,
@@ -12987,6 +13078,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12987
13078
  if (def.fracture === "tear") return "impact_soft";
12988
13079
  return def.fracture === "none" ? "impact_metal" : "impact_wood";
12989
13080
  }
13081
+ var shotKey = (def) => impactKey(def).replace("impact_", "shot_");
12990
13082
  var dragSoundKey = (def) => impactKey(def).replace("impact_", "drag_");
12991
13083
  function fractureKey(def, style) {
12992
13084
  switch (style) {
@@ -13036,6 +13128,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13036
13128
  /** Objects to measure again; `staticAll` measures every one. */
13037
13129
  __publicField(this, "staticDirty", /* @__PURE__ */ new Set());
13038
13130
  __publicField(this, "staticAll", true);
13131
+ /** The periodic refresh: the objects still to measure again in this round, a slice per call. */
13132
+ __publicField(this, "staticRound", []);
13039
13133
  /** Boxes of the pieces' meshes (as `staticBoxes`), with the time they were measured. */
13040
13134
  __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
13041
13135
  __publicField(this, "staticAt", 0);
@@ -13127,7 +13221,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13127
13221
  staticTopAt: (x, z, y, step) => this.staticTopAt(x, z, y, step),
13128
13222
  gravity: this.gravity,
13129
13223
  scene: o.scene,
13130
- onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
13224
+ onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed),
13225
+ sweep: (p, from, to) => this.sweepPiece(p, from, to),
13226
+ onSlam: (p, hit, speed) => this.slam(p, hit, speed)
13131
13227
  });
13132
13228
  this.unsubscribe = this.world.on("*", (ev) => {
13133
13229
  if (this.queue.length < QUEUE_CAP) this.queue.push(ev);
@@ -13317,26 +13413,58 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13317
13413
  const p = this.pieceSys.get(objectId);
13318
13414
  if (p) this.pieceSys.impulse(p, impulse, point);
13319
13415
  }
13416
+ /**
13417
+ * What a piece moving from `from` to `to` runs into: the core of its box (its thinnest half each way, at most `SWEEP_CORE`) swept through the world, the piece itself left out,
13418
+ * so a crate thrown by a blast stops at the wall instead of passing through it. Null for a world that cannot sweep.
13419
+ */
13420
+ sweepPiece(p, from, to) {
13421
+ if (typeof this.world.sweepBox !== "function") return null;
13422
+ const d = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
13423
+ const travel = Math.hypot(d[0], d[1], d[2]);
13424
+ if (travel < 1e-4) return null;
13425
+ const core = Math.min(SWEEP_CORE, ...p.halfExtents);
13426
+ const box = { min: [from[0] - core, from[1] - core, from[2] - core], max: [from[0] + core, from[1] + core, from[2] + core] };
13427
+ const hit = this.world.sweepBox(box, d, travel, { ignore: p.objectId, spacing: Math.max(0.05, core) });
13428
+ if (!hit) return null;
13429
+ return { object: hit.object, part: hit.part, distance: hit.distance, point: hit.point, normal: hit.normal };
13430
+ }
13431
+ /** A piece thrown into a wall: the wall takes the blow of its weight where it met it, the piece takes it too, and each sounds as its material does. */
13432
+ slam(p, hit, speed) {
13433
+ const energy = 0.5 * p.mass * speed * speed;
13434
+ const direction = [-hit.normal[0], -hit.normal[1], -hit.normal[2]];
13435
+ this.landingSound(hit.object, hit.point, clamp6(speed / 10, 0.2, 1));
13436
+ this.syncBound(p.objectId);
13437
+ try {
13438
+ this.world.hitPart(hit.object, hit.part, { type: "blunt", point: hit.point, energy, radius: 0.1, direction, normal: hit.normal, source: p.objectId });
13439
+ this.world.hit(p.objectId, { type: "blunt", point: hit.point, energy, radius: 0.08, direction: hit.normal, source: "slam" });
13440
+ } catch {
13441
+ }
13442
+ }
13320
13443
  hardLanding(p, point, speed) {
13321
- const key = this.impactSoundOf(p.objectId);
13322
- this.sound(key, point, clamp6(speed / 10, 0.2, 1));
13444
+ this.landingSound(p.objectId, point, clamp6(speed / 10, 0.2, 1));
13323
13445
  this.syncBound(p.objectId);
13324
13446
  try {
13325
13447
  this.world.hit(p.objectId, { type: "blunt", point, energy: 0.5 * p.mass * speed * speed, radius: 0.08, direction: [0, -1, 0], source: "fall" });
13326
13448
  } catch {
13327
13449
  }
13328
13450
  }
13329
- impactSoundOf(objectId) {
13451
+ /** The impact of the heaviest part of an object (a piece landing): its material and the blunt blow of the fall. */
13452
+ landingSound(objectId, point, volume) {
13330
13453
  const st = this.world.state(objectId);
13331
- let best;
13454
+ let best = "";
13332
13455
  let mass = -1;
13333
13456
  for (const p of st?.parts ?? []) {
13334
13457
  if (p.mass > mass) {
13335
13458
  mass = p.mass;
13336
- best = this.world.materials.get(p.material);
13459
+ best = p.material;
13337
13460
  }
13338
13461
  }
13339
- return impactKey(best);
13462
+ this.impactSound(best, point, volume, "blunt");
13463
+ }
13464
+ /** The impact on a material by a blow: the legacy family key (`impactKey`) with the more specific keys to try first (`impactSoundKeys`). */
13465
+ impactSound(material, point, volume, blow3) {
13466
+ const def = this.world.materials.get(material);
13467
+ this.sound(impactKey(def), point, volume, impactSoundKeys(material, def, blow3));
13340
13468
  }
13341
13469
  // ----------------------------------------------------------------------------------------------------------------------------------
13342
13470
  // Update
@@ -13468,9 +13596,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13468
13596
  staticNow() {
13469
13597
  if (this.time - this.staticAt > STATIC_REFRESH) {
13470
13598
  this.staticAt = this.time;
13471
- this.staticAll = true;
13599
+ if (this.staticRound.length === 0) this.staticRound = [...this.bounds.keys()].reverse();
13472
13600
  }
13473
- if (!this.staticAll && this.staticDirty.size === 0) return this.staticBoxes;
13601
+ if (!this.staticAll && this.staticDirty.size === 0 && this.staticRound.length === 0) return this.staticBoxes;
13474
13602
  const box = new this.THREE.Box3();
13475
13603
  const measure = (b) => {
13476
13604
  if (b.piece || this.pieceSys.get(b.id)) {
@@ -13490,14 +13618,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13490
13618
  }
13491
13619
  this.staticBoxes.set(b.id, out.length > 6 ? out : []);
13492
13620
  };
13621
+ const measureId = (id) => {
13622
+ const b = this.bounds.get(id);
13623
+ if (b) measure(b);
13624
+ else this.staticBoxes.delete(id);
13625
+ };
13493
13626
  if (this.staticAll) {
13494
13627
  this.staticBoxes.clear();
13495
13628
  for (const b of this.bounds.values()) measure(b);
13629
+ this.staticRound = [];
13496
13630
  } else {
13497
- for (const id of this.staticDirty) {
13498
- const b = this.bounds.get(id);
13499
- if (b) measure(b);
13500
- else this.staticBoxes.delete(id);
13631
+ for (const id of this.staticDirty) measureId(id);
13632
+ const until = performance.now() + STATIC_REFRESH_SLICE_MS;
13633
+ while (this.staticRound.length > 0) {
13634
+ measureId(this.staticRound.pop());
13635
+ if (performance.now() >= until) break;
13501
13636
  }
13502
13637
  }
13503
13638
  this.staticAll = false;
@@ -13557,7 +13692,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13557
13692
  case "ricochet": {
13558
13693
  const d = ev.direction;
13559
13694
  this.vfx.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
13560
- this.sound("ricochet", ev.point, 0.7);
13695
+ const glanced = this.partVis(ev.object, ev.part)?.material ?? "";
13696
+ this.sound("ricochet", ev.point, 0.7, ricochetSoundKeys(glanced, this.world.materials.get(glanced)));
13561
13697
  break;
13562
13698
  }
13563
13699
  case "debris":
@@ -13592,15 +13728,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13592
13728
  this.startFire(ev);
13593
13729
  this.activate(ev.object, ev.part);
13594
13730
  this.sound("ignite", ev.point, 0.7);
13595
- this.sound("burn_loop", ev.point, 0.5);
13731
+ this.sound("burn_loop", ev.point, 0.5, this.burning(ev.object, ev.part));
13596
13732
  break;
13597
13733
  case "primed":
13598
13734
  if (ev.trigger !== "explosion") {
13599
13735
  this.vfx.fire({ point: ev.point, radius: PRIMED_FIRE_RADIUS, life: ev.delay });
13600
13736
  this.primedBurning.add(`${ev.object}|${ev.part}`);
13601
13737
  this.activate(ev.object, ev.part);
13602
- this.sound("ignite", ev.point, 0.7);
13603
- this.sound("burn_loop", ev.point, 0.5);
13738
+ this.sound("primed", ev.point, 0.7, { seconds: ev.delay });
13739
+ this.sound("burn_loop", ev.point, 0.5, { fuel: "gas" });
13604
13740
  }
13605
13741
  break;
13606
13742
  case "spall":
@@ -13626,15 +13762,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13626
13762
  this.stopFire(`${ev.object}|${ev.part}`);
13627
13763
  this.vfx.burst("steam", f.point, [0, 1, 0], 14, 1.2);
13628
13764
  }
13629
- if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5);
13765
+ if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5, primed ? { fuel: "gas" } : this.burning(ev.object, ev.part));
13630
13766
  this.activate(ev.object, ev.part);
13631
13767
  this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
13632
13768
  break;
13633
13769
  }
13634
13770
  case "burnout": {
13635
13771
  this.stopFire(`${ev.object}|${ev.part}`);
13636
- this.primedBurning.delete(`${ev.object}|${ev.part}`);
13637
- this.sound("burn_end", ev.point, 0.5);
13772
+ const primed = this.primedBurning.delete(`${ev.object}|${ev.part}`);
13773
+ this.sound("burn_end", ev.point, 0.5, primed ? { fuel: "gas" } : this.burning(ev.object, ev.part));
13638
13774
  this.vfx.burst("ash", ev.point, [0, 1, 0], 16, 1);
13639
13775
  this.vfx.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
13640
13776
  const pv = this.partVis(ev.object, ev.part);
@@ -13702,7 +13838,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13702
13838
  break;
13703
13839
  }
13704
13840
  case "explode":
13705
- this.primedBurning.delete(`${ev.object}|${ev.part}`);
13841
+ if (this.primedBurning.delete(`${ev.object}|${ev.part}`)) this.sound("burn_end", ev.point, 0.5, { fuel: "gas" });
13706
13842
  this.onExplode(ev);
13707
13843
  break;
13708
13844
  case "destroyed":
@@ -13713,8 +13849,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13713
13849
  const b = this.bounds.get(objectId);
13714
13850
  return b ? [b.root.position.x, b.root.position.y, b.root.position.z] : [0, 0, 0];
13715
13851
  }
13716
- sound(key, point, volume) {
13717
- this.o.onSound?.(key, point, volume);
13852
+ sound(key, point, volume, detail) {
13853
+ if (detail) this.o.onSound?.(key, point, volume, detail);
13854
+ else this.o.onSound?.(key, point, volume);
13855
+ }
13856
+ /** What a burning part is made of, for the fire's sound. */
13857
+ burning(objectId, partId) {
13858
+ const material = this.partVis(objectId, partId)?.material || this.world.part(objectId, partId)?.material;
13859
+ return material ? { material } : void 0;
13718
13860
  }
13719
13861
  partVis(objectId, partId) {
13720
13862
  return this.bounds.get(objectId)?.parts.get(partId) ?? null;
@@ -13779,7 +13921,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13779
13921
  const reach = shown.reduce((widest, mv) => Math.max(widest, this.visuals.dentReach(mv, wp, ev.dent)), ev.dent.radius);
13780
13922
  this.visuals.applyPartDent(shown, wp, wn, ev.dent, reach);
13781
13923
  if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [wn.x, wn.y, wn.z], 4 + Math.min(8, ev.dent.depth * 150), 3.2);
13782
- this.sound(impactKey(def), ev.worldPoint, clamp6(0.3 + ev.dent.depth * 12, 0.3, 1));
13924
+ this.impactSound(pv.material, ev.worldPoint, clamp6(0.3 + ev.dent.depth * 12, 0.3, 1), ev.blow);
13783
13925
  }
13784
13926
  onHole(ev) {
13785
13927
  const b = this.bounds.get(ev.object);
@@ -13808,7 +13950,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13808
13950
  this.vfx.burst("dust", ev.worldPoint, [n.x, n.y, n.z], this.low ? 5 : 10, 0.7, puff3);
13809
13951
  if (ev.hole.through && ev.exitPoint) this.vfx.burst("dust", ev.exitPoint, [-n.x, -n.y, -n.z], this.low ? 10 : 20, 1.3, puff3);
13810
13952
  }
13811
- this.sound(impactKey(def), ev.worldPoint, 0.6);
13953
+ this.sound(shotKey(def), ev.worldPoint, 0.6, impactSoundKeys(pv.material, def, ev.blow));
13812
13954
  }
13813
13955
  onCrack(ev) {
13814
13956
  const b = this.bounds.get(ev.object);
@@ -13818,13 +13960,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13818
13960
  const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
13819
13961
  const mv = this.nearestMesh(pv, wp);
13820
13962
  if (mv) {
13821
- const glass = !!def?.visual?.transparent;
13963
+ const glass2 = !!def?.visual?.transparent;
13822
13964
  const own = this.partColor(pv, def) ?? fallbackColor(pv.material, def);
13823
- const color = glass ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
13824
- const length = glass ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
13965
+ const color = glass2 ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
13966
+ const length = glass2 ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
13825
13967
  this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, length, color, hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]));
13826
13968
  }
13827
- this.sound(impactKey(def), ev.worldPoint, 0.5);
13969
+ this.impactSound(pv.material, ev.worldPoint, 0.5, ev.blow);
13828
13970
  }
13829
13971
  /** Outward normal of the face of `mv` nearest to a world point: the thin axis of its world box (right for a wall; a sloped face is left to the vertex-range binding). */
13830
13972
  faceNormal(mv, point) {
@@ -13881,7 +14023,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13881
14023
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), false), ev.object, maxAlive);
13882
14024
  }
13883
14025
  if (!played) this.vfx.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
13884
- this.sound(impactKey(def), ev.opening.center, 0.8);
14026
+ this.impactSound(pv?.material ?? "", ev.opening.center, 0.8, ev.blow);
13885
14027
  }
13886
14028
  /**
13887
14029
  * A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust, and the marks drawn on it (bullet holes, spalls, the impact
@@ -13899,7 +14041,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13899
14041
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone, false), ev.object, maxAlive);
13900
14042
  }
13901
14043
  if (ev.torn) {
13902
- this.sound(impactKey(def), ev.center, 0.5);
14044
+ this.impactSound(pv?.material ?? "", ev.center, 0.5, ev.blow);
13903
14045
  return;
13904
14046
  }
13905
14047
  const T = this.THREE;
@@ -13909,7 +14051,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13909
14051
  for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation2);
13910
14052
  this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
13911
14053
  this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
13912
- this.sound(impactKey(def), ev.center, 0.7);
14054
+ this.impactSound(pv?.material ?? "", ev.center, 0.7, ev.blow);
13913
14055
  }
13914
14056
  onFracture(ev) {
13915
14057
  const b = this.bounds.get(ev.object);
@@ -13950,7 +14092,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13950
14092
  default:
13951
14093
  this.vfx.burst("sparks", ev.point, up, 10, 4);
13952
14094
  }
13953
- this.sound(fractureKey(def, ev.style), ev.point, clamp6(0.4 + ev.fragments.length * 0.03, 0.4, 1));
14095
+ this.sound(fractureKey(def, ev.style), ev.point, clamp6(0.4 + ev.fragments.length * 0.03, 0.4, 1), breakSoundKeys(matId, def, ev.blow));
13954
14096
  }
13955
14097
  /** Spawns the fragments of a fracture from index `from`; what does not fit in this frame's budget is spawned by the next frames. */
13956
14098
  spawnFragments(ev, from, look) {
@@ -14087,10 +14229,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14087
14229
  const report = options.report;
14088
14230
  const confinement = report?.find((r) => r.blast !== void 0)?.blast?.confinement ?? 1;
14089
14231
  const energy = ev.energy * confinement;
14090
- const reachOf = (objectId) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? 1;
14232
+ const reachOf = (objectId, at3) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? this.world.blastReach?.(p, at3, { ignore: ev.object ? [ev.object, objectId] : [objectId] }) ?? 1;
14091
14233
  const shove = (pc) => {
14092
14234
  this.pieceSys.centerOf(pc, c);
14093
- const hit = blastShove({ point: p, energy, radius: ev.radius, reach: reachOf(pc.objectId) }, { position: [c.x, c.y, c.z], mass: pc.mass }, this.o.blastShove);
14235
+ if (c.distanceTo(this.v2.set(p[0], p[1], p[2])) > ev.radius) return;
14236
+ const hit = blastShove({ point: p, energy, radius: ev.radius, reach: reachOf(pc.objectId, [c.x, c.y, c.z]) }, { position: [c.x, c.y, c.z], mass: pc.mass }, this.o.blastShove);
14094
14237
  if (!hit) return;
14095
14238
  const spread = pc.halfExtents[0];
14096
14239
  const momentum = hit.speed * pc.mass;
@@ -14099,7 +14242,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14099
14242
  for (const pc of [...this.pieceSys.list]) shove(pc);
14100
14243
  if (this.o.shoveable) this.loosen(p, ev.radius, energy, reachOf);
14101
14244
  this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(energy) * 0.08));
14102
- this.sound("explode", p, clamp6(0.5 + ev.radius / 10, 0.5, 1));
14245
+ this.sound("explode", p, clamp6(0.5 + ev.radius / 10, 0.5, 1), {
14246
+ radius: ev.radius,
14247
+ ...ev.effect !== void 0 ? { effect: ev.effect } : {},
14248
+ ...options.effect !== void 0 ? { effect: options.effect } : {},
14249
+ ...report ? { confinement } : {}
14250
+ });
14103
14251
  }
14104
14252
  /** The bound objects without a piece that the host lets a blast move (`shoveable`): the ones it reaches with enough speed become pieces and get the shove. */
14105
14253
  loosen(point, radius, energy, reachOf) {
@@ -14110,7 +14258,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14110
14258
  const { half, center } = this.measure(b.root, this.meshesOf(b));
14111
14259
  const world = center.clone().applyQuaternion(b.root.quaternion).add(b.root.position);
14112
14260
  const mass = this.massOf(b.id);
14113
- const hit = blastShove({ point, energy, radius, reach: reachOf(b.id) }, { position: [world.x, world.y, world.z], mass }, this.o.blastShove);
14261
+ if (world.distanceTo(this.v2.set(point[0], point[1], point[2])) > radius) continue;
14262
+ const hit = blastShove({ point, energy, radius, reach: reachOf(b.id, [world.x, world.y, world.z]) }, { position: [world.x, world.y, world.z], mass }, this.o.blastShove);
14114
14263
  if (!hit) continue;
14115
14264
  const piece = this.makePiece(b.id, { mass });
14116
14265
  const body = this.pieceSys.get(b.id);
@@ -14134,9 +14283,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14134
14283
  this.fires.set(key, { object: ev.object, part: ev.part, point: ev.point, follow: { x: ev.point[0], y: ev.point[1], z: ev.point[2] } });
14135
14284
  }
14136
14285
  stopFire(key) {
14137
- const fire = this.fires.get(key);
14138
- if (!fire) return;
14139
- if (fire.handle !== void 0) this.vfx.stop(fire.handle);
14286
+ const fire2 = this.fires.get(key);
14287
+ if (!fire2) return;
14288
+ if (fire2.handle !== void 0) this.vfx.stop(fire2.handle);
14140
14289
  this.fires.delete(key);
14141
14290
  }
14142
14291
  // ----------------------------------------------------------------------------------------------------------------------------------
@@ -14173,9 +14322,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14173
14322
  if (ps.status === "melted") this.onGone(a.object, pv, "melted", def);
14174
14323
  else if (ps.status === "ashes") this.onGone(a.object, pv, "ashes", def);
14175
14324
  }
14176
- const fire = this.fires.get(key);
14177
- if (fire) {
14178
- if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def);
14325
+ const fire2 = this.fires.get(key);
14326
+ if (fire2) {
14327
+ if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire2, pv, def);
14179
14328
  else this.stopFire(key);
14180
14329
  }
14181
14330
  const hot = def?.visual?.glows === true && ps.temperature > GLOW_FROM_C;
@@ -14305,10 +14454,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14305
14454
  this.scene.remove(b.root);
14306
14455
  this.pieceSys.remove(b.id);
14307
14456
  this.bounds.delete(b.id);
14457
+ this.staticDirty.add(b.id);
14308
14458
  }
14309
14459
  }
14310
14460
  }
14311
14461
  clear() {
14462
+ this.staticAll = true;
14312
14463
  this.queue.length = 0;
14313
14464
  this.slices.length = 0;
14314
14465
  this.active.clear();
@@ -14335,6 +14486,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14335
14486
  this.rng = createRng(RNG_SEED);
14336
14487
  this.time = 0;
14337
14488
  this.staticAt = 0;
14489
+ this.staticRound = [];
14490
+ this.staticAll = true;
14338
14491
  this.throttles.clear();
14339
14492
  this.fragments.reset();
14340
14493
  this.pieceSys.reset();
@@ -14391,7 +14544,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14391
14544
  }
14392
14545
  };
14393
14546
 
14394
- // ../game-audio/dist/chunk-DZITVFMJ.js
14547
+ // ../game-audio/dist/chunk-7TLFY7VO.js
14395
14548
  var DISTANCE_CURVES = {
14396
14549
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
14397
14550
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -14454,15 +14607,31 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14454
14607
  {
14455
14608
  id: "game",
14456
14609
  master: 0.5,
14457
- noise: { seconds: 1.5, create: "lazy" },
14458
- 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"]
14610
+ noise: {
14611
+ seconds: 1.5,
14612
+ create: "lazy"
14613
+ },
14614
+ sources: [
14615
+ "jogo:L62077-62081 master 0.5 to the output",
14616
+ "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"
14617
+ ]
14459
14618
  },
14460
14619
  {
14461
14620
  id: "art_scene",
14462
14621
  master: 0.85,
14463
- compressor: { threshold: -12, ratio: 5 },
14464
- reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
14465
- noise: { seconds: 2, create: "eager" },
14622
+ compressor: {
14623
+ threshold: -12,
14624
+ ratio: 5
14625
+ },
14626
+ reverb: {
14627
+ seconds: 2.2,
14628
+ decay: 3.4,
14629
+ wet: 0.4
14630
+ },
14631
+ noise: {
14632
+ seconds: 2,
14633
+ create: "eager"
14634
+ },
14466
14635
  sources: [
14467
14636
  "cena:L1308 compressor -12 dB, ratio 5",
14468
14637
  "cena:L1309 master 0.85",
@@ -14470,6 +14639,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14470
14639
  "cena:L1311 wet 0.4",
14471
14640
  "cena:L1312 noise 2 s made with the chain"
14472
14641
  ]
14642
+ },
14643
+ {
14644
+ id: "kit",
14645
+ master: 0.85,
14646
+ compressor: {
14647
+ threshold: -12,
14648
+ ratio: 5
14649
+ },
14650
+ reverb: {
14651
+ seconds: 2.2,
14652
+ decay: 3.4,
14653
+ wet: 0.4
14654
+ },
14655
+ noise: {
14656
+ seconds: 2,
14657
+ create: "eager"
14658
+ },
14659
+ sources: [
14660
+ "sdk: the art scene chain (cena L1308-L1312) with a limiter after the compressor: the page has none, and a blast with its debris and a fire catching reached +7 dBFS"
14661
+ ],
14662
+ limiter: {
14663
+ ceiling: -1,
14664
+ knee: -6
14665
+ }
14473
14666
  }
14474
14667
  ];
14475
14668
  var MIX_PROFILES = Object.fromEntries(
@@ -14520,8 +14713,395 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14520
14713
  }
14521
14714
  ]
14522
14715
  };
14716
+ var entries = (category, list) => list.map(([id, priority, current]) => ({ id, category, priority, ...current ? { current } : {} }));
14717
+ var SFX_CATALOG = [
14718
+ ...entries("armas", [
14719
+ ["pistol_22lr", 2, "sfx_pistol"],
14720
+ ["pistol_9mm", 1, "sfx_pistol"],
14721
+ ["pistol_45", 1, "sfx_pistol"],
14722
+ ["revolver_38_357", 2, "sfx_pistol"],
14723
+ ["revolver_44_50ae", 3, "sfx_pistol"],
14724
+ ["smg_9mm_auto", 1, "sfx_pistol"],
14725
+ ["shotgun_12", 1, "sfx_shotgun"],
14726
+ ["rifle_5_56", 1, "sfx_rifle"],
14727
+ ["rifle_7_62x39", 1, "sfx_rifle"],
14728
+ ["rifle_auto_burst", 1, "sfx_rifle"],
14729
+ ["rifle_308_3006", 2, "sfx_rifle"],
14730
+ ["sniper_338_300wm", 2, "sfx_rifle"],
14731
+ ["antimateriel_50bmg", 3, "sfx_rifle"],
14732
+ ["suppressed_pistol", 2],
14733
+ ["suppressed_rifle", 2],
14734
+ ["gunshot_far_pistol", 1],
14735
+ ["gunshot_far_rifle", 1],
14736
+ ["gunshot_far_shotgun", 1],
14737
+ ["gunshot_far_heavy", 2],
14738
+ ["gunshot_tail_indoor", 1],
14739
+ ["gunshot_tail_outdoor", 1],
14740
+ ["bullet_crack_by", 2],
14741
+ ["bullet_whiz", 2],
14742
+ ["ricochet_metal", 2, "ping"],
14743
+ ["dry_fire", 1],
14744
+ ["mag_out", 1],
14745
+ ["mag_in", 1],
14746
+ ["slide_pull_release", 1],
14747
+ ["pump_cycle", 1],
14748
+ ["casing_drop_concreto_pistol", 1],
14749
+ ["casing_drop_concreto_rifle", 1],
14750
+ ["casing_drop_concreto_shell", 1],
14751
+ ["casing_drop_madeira_pistol", 1],
14752
+ ["casing_drop_madeira_rifle", 1],
14753
+ ["casing_drop_madeira_shell", 1],
14754
+ ["casing_drop_metal_pistol", 1],
14755
+ ["casing_drop_metal_rifle", 1],
14756
+ ["casing_drop_metal_shell", 1],
14757
+ ["casing_drop_terra_pistol", 1],
14758
+ ["casing_drop_terra_rifle", 1],
14759
+ ["casing_drop_terra_shell", 1],
14760
+ ["rocket_fire", 2]
14761
+ ]),
14762
+ ...entries("explosoes-fogo", [
14763
+ ["explosion_grenade", 1, "boom"],
14764
+ ["explosion_big", 1, "boom_big"],
14765
+ ["explosion_car", 2, "boom_big"],
14766
+ ["explosion_far", 1],
14767
+ ["debris_rain_stone", 1],
14768
+ ["molotov_break_ignite", 1, "glass_shatter"],
14769
+ ["fire_ignite", 1, "hiss"],
14770
+ ["fire_loop_small", 1],
14771
+ ["fire_loop_large", 1],
14772
+ ["fire_extinguish", 2, "hiss"],
14773
+ ["gas_leak_hiss", 2],
14774
+ ["structure_collapse", 2, "crumble"]
14775
+ ]),
14776
+ ...entries("personagem", [
14777
+ ["footstep_concrete_walk", 1, "sfx_step"],
14778
+ ["footstep_wood_walk", 1, "sfx_step"],
14779
+ ["footstep_dirt_walk", 1, "sfx_step"],
14780
+ ["footstep_grass_walk", 1, "sfx_step"],
14781
+ ["footstep_gravel_walk", 1, "sfx_step"],
14782
+ ["land_concrete", 1, "sfx_step"],
14783
+ ["punch_swing", 1],
14784
+ ["punch_hit_body", 1, "sfx_flesh"],
14785
+ ["kick_hit", 1, "sfx_flesh"],
14786
+ ["knife_stab", 1],
14787
+ ["blunt_hit", 1],
14788
+ ["bite", 1, "sfx_flesh"],
14789
+ ["body_fall", 1],
14790
+ ["player_hurt", 2]
14791
+ ]),
14792
+ ...entries("veiculos", [
14793
+ ["engine_loop_car", 1],
14794
+ ["engine_start", 1],
14795
+ ["crash_light", 1, "crash"],
14796
+ ["crash_heavy", 1, "crash"],
14797
+ ["car_door_open", 1, "latch"],
14798
+ ["car_door_close", 1, "shut_metal"],
14799
+ ["horn_car", 2, "horn"],
14800
+ ["tire_skid_asphalt", 2]
14801
+ ]),
14802
+ ...entries("itens-portas", [
14803
+ ["item_pickup_metal", 1],
14804
+ ["can_open", 1],
14805
+ ["can_crush", 1],
14806
+ ["door_wood_open", 1, "latch"],
14807
+ ["door_wood_close", 1, "shut_wood"],
14808
+ ["door_creak", 1, "creak_wood"],
14809
+ ["door_locked_rattle", 1, "rattle"],
14810
+ ["drawer_open", 2, "slide"],
14811
+ ["fridge_close", 2, "shut_metal"]
14812
+ ]),
14813
+ ...entries("ambiente", [
14814
+ ["rain_loop", 2],
14815
+ ["wind_loop", 2],
14816
+ ["thunder_near", 2],
14817
+ ["thunder_far", 2],
14818
+ ["water_splash", 2]
14819
+ ])
14820
+ ];
14821
+ var SFX_MATERIALS = [
14822
+ ["steel", "metal"],
14823
+ ["aluminium", "metal"],
14824
+ ["wood", "wood"],
14825
+ ["plywood", "wood"],
14826
+ ["mdf", "wood"],
14827
+ ["glass", "glass"],
14828
+ ["tempered_glass", "glass"],
14829
+ ["ceramic", "glass"],
14830
+ ["concrete", "masonry"],
14831
+ ["brick", "masonry"],
14832
+ ["plaster", "masonry"],
14833
+ ["plastic", "soft"],
14834
+ ["fabric", "soft"],
14835
+ ["paper", "soft"],
14836
+ ["foliage", "soft"],
14837
+ ["soil", "soft"],
14838
+ ["flesh", "soft"]
14839
+ ];
14840
+ var SFX_BLOWS = ["bullet", "blunt", "blade", "break", "drag"];
14841
+ var CURRENT_BY_GROUP = {
14842
+ metal: { bullet: "clang", blunt: "clang", blade: "clang", break: "clang", drag: "drag_metal" },
14843
+ wood: { bullet: "wood_hit", blunt: "wood_hit", blade: "wood_hit", break: "crack", drag: "drag_wood" },
14844
+ glass: { bullet: "ping", blunt: "ping", break: "glass_shatter", drag: "drag_glass" },
14845
+ masonry: { bullet: "chip", blunt: "chip", blade: "chip", break: "crumble", drag: "drag_masonry" },
14846
+ soft: { bullet: "thud", blunt: "thud", blade: "thud", break: "thud", drag: "drag_soft" }
14847
+ };
14848
+ var impactSoundId = (material, blow3) => `impact_${material}_${blow3}`;
14849
+ var SFX_IMPACTS = SFX_MATERIALS.flatMap(
14850
+ ([material, group]) => SFX_BLOWS.map((blow3) => {
14851
+ const current = CURRENT_BY_GROUP[group]?.[blow3];
14852
+ return { id: impactSoundId(material, blow3), category: "impactos", priority: ["steel", "wood", "glass", "concrete", "brick", "soil", "flesh"].includes(material) && blow3 === "bullet" ? 1 : 2, ...current ? { current } : {} };
14853
+ })
14854
+ );
14855
+ var CALIBRATION_ID = "calibration_shot";
14856
+ var PREFIX = "calibration/";
14857
+ var url = (name) => `${PREFIX}${name}`;
14858
+ var CALIBRATION_SOUND = {
14859
+ id: CALIBRATION_ID,
14860
+ bus: "weapons",
14861
+ close: ["close_1.wav", "close_2.wav", "close_3.wav", "close_4.wav"].map((name) => ({ url: url(name) })),
14862
+ far: ["far_1.wav", "far_2.wav"].map((name) => ({ url: url(name) })),
14863
+ tiers: [
14864
+ { name: "light", from: 0, close: ["light_1.wav", "light_2.wav"].map((name) => ({ url: url(name) })) },
14865
+ { name: "medium", from: 0.34, close: ["close_1.wav", "close_2.wav", "close_3.wav", "close_4.wav"].map((name) => ({ url: url(name) })) },
14866
+ { name: "heavy", from: 0.67, close: ["heavy_1.wav", "heavy_2.wav"].map((name) => ({ url: url(name) })) }
14867
+ ],
14868
+ tail: { indoor: [{ url: url("tail_in.wav") }], outdoor: [{ url: url("tail_out.wav") }] },
14869
+ pitch: 0.04,
14870
+ choke: { voices: 2 },
14871
+ fallback: "sfx_pistol",
14872
+ sources: ["sdk: calibration takes synthesized by the harness (measure/calibration.ts), no file, no licence"]
14873
+ };
14874
+ var CALIBRATION_LOOP_ID = "calibration_loop";
14875
+ var CALIBRATION_LOOP_SOUND = {
14876
+ id: CALIBRATION_LOOP_ID,
14877
+ bus: "ambience",
14878
+ close: ["loop_1.wav", "loop_2.wav", "loop_3.wav"].map((name) => ({ url: url(name) })),
14879
+ loop: { crossfade: 0.15 },
14880
+ sources: ["sdk: calibration takes synthesized by the harness (measure/calibration.ts), no file, no licence"]
14881
+ };
14882
+ var CRITERIA_TARGETS = {
14883
+ /** C1: event to the first sample, and onset to the peak (one 60 fps frame). */
14884
+ syncMs: 20,
14885
+ peakAfterOnsetMs: 1e3 / 60,
14886
+ /** C3: distances measured, metres. */
14887
+ distances: [1, 10, 50, 200],
14888
+ /** C5: plays, distinct takes and the pitch range (fraction). */
14889
+ plays: 10,
14890
+ minTakes: 4,
14891
+ pitch: [0.03, 0.05],
14892
+ /** C6: the highest peak, dBFS. */
14893
+ peakDb: -1,
14894
+ /** C8: the end of each file against its peak, dB. */
14895
+ endDropDb: -40,
14896
+ /** C9: a burst of rapid fire. */
14897
+ burst: 20,
14898
+ burstSpacing: 0.06
14899
+ };
14900
+ var PT = {
14901
+ group: "Som",
14902
+ name: "Painel de som (arquivos e C1\u2013C10)",
14903
+ controls: {
14904
+ category: "1 \xB7 Categoria",
14905
+ sound: "1 \xB7 Som",
14906
+ play: "1 \xB7 \u25B6 Tocar",
14907
+ material: "2 \xB7 Material",
14908
+ blow: "2 \xB7 Golpe",
14909
+ strike: "2 \xB7 \u25B6 Tocar golpe",
14910
+ distance: "3 \xB7 Dist\xE2ncia",
14911
+ bearing: "3 \xB7 Dire\xE7\xE3o",
14912
+ environment: "3 \xB7 Lugar",
14913
+ strength: "3 \xB7 For\xE7a",
14914
+ wall: "3 \xB7 Atr\xE1s da parede",
14915
+ burst: "3 \xB7 Rajada 20\xD7",
14916
+ play_a: "4 \xB7 \u25B6 A (atual)",
14917
+ play_b: "4 \xB7 \u25B6 B (novo)",
14918
+ ab_cycle: "4 \xB7 Alternar A/B",
14919
+ blind: "4 \xB7 \xC0s cegas (X)",
14920
+ reveal: "4 \xB7 Revelar X",
14921
+ prefer_a: "4 \xB7 Prefiro A",
14922
+ prefer_b: "4 \xB7 Prefiro B",
14923
+ measure: "5 \xB7 Medir C1\u2013C10",
14924
+ record: "5 \xB7 Gravar 3 s",
14925
+ approve: "5 \xB7 Aprovo (C10)"
14926
+ },
14927
+ hints: {
14928
+ sound: "A = arquivo, S = sintetizado, F = faltando. O que tem arquivo toca o arquivo; o resto toca o som atual.",
14929
+ strike: "Toca impact_<material>_<golpe>: o arquivo se existir, sen\xE3o o som atual daquele material.",
14930
+ wall: "P\xF5e a parede de casa entre voc\xEA e a fonte: o som fica abafado (C4).",
14931
+ burst: "Vinte vezes a cada 60 ms, como uma rajada (C9).",
14932
+ ab_cycle: "A, B, A, B, um por segundo.",
14933
+ blind: "Toca A ou B sem dizer qual; depois revele.",
14934
+ measure: "Mede o som escolhido no motor real (contexto offline): sincronia, som certo, dist\xE2ncia, lado, varia\xE7\xE3o, volume, for\xE7a, cauda e rajada.",
14935
+ record: "Grava 3 s da sa\xEDda com o som tocando e baixa o arquivo (C10).",
14936
+ approve: "Marca o C10 do som escolhido como aprovado pelo seu ouvido."
14937
+ },
14938
+ categories: {
14939
+ armas: "Armas",
14940
+ impactos: "Impactos",
14941
+ "explosoes-fogo": "Explos\xF5es e fogo",
14942
+ personagem: "Personagem",
14943
+ veiculos: "Ve\xEDculos",
14944
+ "itens-portas": "Itens e portas",
14945
+ ambiente: "Ambiente",
14946
+ calibracao: "Calibra\xE7\xE3o (SDK)"
14947
+ },
14948
+ blows: { bullet: "tiro", blunt: "soco/chute", blade: "faca", break: "quebra", drag: "arrasto/queda" },
14949
+ materials: {
14950
+ steel: "a\xE7o",
14951
+ aluminium: "alum\xEDnio",
14952
+ wood: "madeira",
14953
+ plywood: "compensado",
14954
+ mdf: "MDF",
14955
+ glass: "vidro",
14956
+ tempered_glass: "vidro temperado",
14957
+ ceramic: "cer\xE2mica",
14958
+ concrete: "concreto",
14959
+ brick: "tijolo",
14960
+ plaster: "gesso",
14961
+ plastic: "pl\xE1stico",
14962
+ fabric: "tecido",
14963
+ paper: "papel\xE3o",
14964
+ foliage: "folhagem",
14965
+ soil: "terra",
14966
+ flesh: "carne"
14967
+ },
14968
+ environments: { indoor: "interior", outdoor: "exterior" },
14969
+ strengths: { light: "leve", medium: "m\xE9dio", heavy: "forte" },
14970
+ states: { file: "A", synth: "S", missing: "F" },
14971
+ criteria: { C1: "C1 Sincronia", C2: "C2 Certo p/ a\xE7\xE3o", C3: "C3 Dist\xE2ncia", C4: "C4 Posi\xE7\xE3o", C5: "C5 Varia\xE7\xE3o", C6: "C6 Volume", C7: "C7 Intensidade", C8: "C8 Cauda", C9: "C9 Rajada", C10: "C10 Escuta" },
14972
+ metrics: { chosen: "1 \xB7 Escolhido", matrix: "2 \xB7 Matriz", last: "\xDAltimo tocado", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Prefer\xEAncias", state: "5 \xB7 Medi\xE7\xE3o", count: "1 \xB7 Cat\xE1logo" },
14973
+ run: { idle: "parada", measuring: "medindo\u2026", done: "pronta", failed: "falhou", recording: "gravando\u2026", recorded: "grava\xE7\xE3o baixada" },
14974
+ words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono" },
14975
+ probe: "Fonte do som",
14976
+ wall: "Parede (oclus\xE3o)",
14977
+ scenarios: { pipeline: "Arquivo \u2192 decodifica \u2192 toca (round-robin, cauda)", criteria: "Crit\xE9rios C1\u2013C9 no som de calibra\xE7\xE3o", loop: "Loop de arquivo: emenda sem buraco (crossfade)", panel: "Painel: categorias, matriz, A/B e parede" }
14978
+ };
14979
+ var EN2 = {
14980
+ ...PT,
14981
+ group: "Sound",
14982
+ name: "Sound panel (files and C1\u2013C10)",
14983
+ controls: {
14984
+ category: "1 \xB7 Category",
14985
+ sound: "1 \xB7 Sound",
14986
+ play: "1 \xB7 \u25B6 Play",
14987
+ material: "2 \xB7 Material",
14988
+ blow: "2 \xB7 Blow",
14989
+ strike: "2 \xB7 \u25B6 Play blow",
14990
+ distance: "3 \xB7 Distance",
14991
+ bearing: "3 \xB7 Bearing",
14992
+ environment: "3 \xB7 Place",
14993
+ strength: "3 \xB7 Strength",
14994
+ wall: "3 \xB7 Behind the wall",
14995
+ burst: "3 \xB7 Burst 20\xD7",
14996
+ play_a: "4 \xB7 \u25B6 A (current)",
14997
+ play_b: "4 \xB7 \u25B6 B (new)",
14998
+ ab_cycle: "4 \xB7 Alternate A/B",
14999
+ blind: "4 \xB7 Blind (X)",
15000
+ reveal: "4 \xB7 Reveal X",
15001
+ prefer_a: "4 \xB7 Prefer A",
15002
+ prefer_b: "4 \xB7 Prefer B",
15003
+ measure: "5 \xB7 Measure C1\u2013C10",
15004
+ record: "5 \xB7 Record 3 s",
15005
+ approve: "5 \xB7 Approve (C10)"
15006
+ },
15007
+ hints: {
15008
+ sound: "A = file, S = synthesized, F = missing. A file plays the file; the rest plays the current sound.",
15009
+ strike: "Plays impact_<material>_<blow>: the file if there is one, else the current sound of that material.",
15010
+ wall: "Puts the house wall between you and the source: the sound is muffled (C4).",
15011
+ burst: "Twenty times every 60 ms, like a burst (C9).",
15012
+ ab_cycle: "A, B, A, B, one a second.",
15013
+ blind: "Plays A or B without saying which; reveal it after.",
15014
+ measure: "Measures the chosen sound through the real engine (offline context): sync, right sound, distance, side, variation, level, strength, tail and burst.",
15015
+ record: "Records 3 s of the output with the sound playing and downloads it (C10).",
15016
+ approve: "Marks C10 of the chosen sound as approved by your ear."
15017
+ },
15018
+ categories: { armas: "Weapons", impactos: "Impacts", "explosoes-fogo": "Explosions and fire", personagem: "Character", veiculos: "Vehicles", "itens-portas": "Items and doors", ambiente: "Ambience", calibracao: "Calibration (SDK)" },
15019
+ blows: { bullet: "bullet", blunt: "punch/kick", blade: "knife", break: "break", drag: "drag/fall" },
15020
+ materials: {
15021
+ steel: "steel",
15022
+ aluminium: "aluminium",
15023
+ wood: "wood",
15024
+ plywood: "plywood",
15025
+ mdf: "MDF",
15026
+ glass: "glass",
15027
+ tempered_glass: "tempered glass",
15028
+ ceramic: "ceramic",
15029
+ concrete: "concrete",
15030
+ brick: "brick",
15031
+ plaster: "plaster",
15032
+ plastic: "plastic",
15033
+ fabric: "fabric",
15034
+ paper: "cardboard",
15035
+ foliage: "foliage",
15036
+ soil: "soil",
15037
+ flesh: "flesh"
15038
+ },
15039
+ environments: { indoor: "indoor", outdoor: "outdoor" },
15040
+ strengths: { light: "light", medium: "medium", heavy: "heavy" },
15041
+ criteria: { C1: "C1 Sync", C2: "C2 Right sound", C3: "C3 Distance", C4: "C4 Position", C5: "C5 Variation", C6: "C6 Level", C7: "C7 Strength", C8: "C8 Tail", C9: "C9 Burst", C10: "C10 Listening" },
15042
+ metrics: { chosen: "1 \xB7 Chosen", matrix: "2 \xB7 Matrix", last: "Last played", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Preferences", state: "5 \xB7 Measurement", count: "1 \xB7 Catalogue" },
15043
+ run: { idle: "idle", measuring: "measuring\u2026", done: "done", failed: "failed", recording: "recording\u2026", recorded: "recording downloaded" },
15044
+ words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner" },
15045
+ probe: "Sound source",
15046
+ wall: "Wall (occlusion)",
15047
+ scenarios: { pipeline: "File \u2192 decode \u2192 play (round-robin, tail)", criteria: "Criteria C1\u2013C9 on the calibration sound", loop: "File loop: seamless joins (crossfade)", panel: "Panel: categories, matrix, A/B and wall" }
15048
+ };
14523
15049
 
14524
15050
  // ../game-audio/dist/index.js
15051
+ var SFX_TIERS = { light: 0, medium: 0.34, heavy: 0.67 };
15052
+ function splitTier(id) {
15053
+ const match = /^(.*)_(light|medium|heavy)$/.exec(id);
15054
+ return match ? { base: match[1], tier: match[2] } : { base: id };
15055
+ }
15056
+ var refsOf = (entries2, baseUrl) => (entries2 ?? []).map((entry) => ({ url: `${baseUrl}${entry.file}` }));
15057
+ function sampleBankFromManifest(manifest, options) {
15058
+ const baseUrl = options.baseUrl ?? "";
15059
+ const samples = [];
15060
+ const tiersOf = /* @__PURE__ */ new Map();
15061
+ for (const sound of manifest.sounds) {
15062
+ const { base, tier } = splitTier(sound.id);
15063
+ if (tier) tiersOf.set(base, [...tiersOf.get(base) ?? [], sound]);
15064
+ }
15065
+ const bases = /* @__PURE__ */ new Map();
15066
+ for (const sound of manifest.sounds) {
15067
+ if (!splitTier(sound.id).tier) bases.set(sound.id, sound);
15068
+ }
15069
+ for (const [base, tiers] of tiersOf) {
15070
+ if (!bases.has(base)) bases.set(base, { id: base, category: tiers[0]?.category ?? "", layers: { close: tiers[0]?.layers.close ?? [] } });
15071
+ }
15072
+ for (const sound of bases.values()) {
15073
+ const numbers = options.sounds?.[sound.id] ?? options.defaults?.(sound);
15074
+ const close = refsOf(sound.layers.close, baseUrl);
15075
+ if (!numbers || close.length === 0) continue;
15076
+ const tiers = (tiersOf.get(sound.id) ?? []).map((tier) => {
15077
+ const name = splitTier(tier.id).tier;
15078
+ const tierFar = refsOf(tier.layers.far, baseUrl);
15079
+ return { name, from: SFX_TIERS[name] ?? 0, close: refsOf(tier.layers.close, baseUrl), ...tierFar.length > 0 ? { far: tierFar } : {} };
15080
+ }).sort((a, b) => a.from - b.from);
15081
+ const far = refsOf(sound.layers.far, baseUrl);
15082
+ const indoor = refsOf(sound.layers.tail_in, baseUrl);
15083
+ const outdoor = refsOf(sound.layers.tail_out, baseUrl);
15084
+ const files = [sound, ...tiersOf.get(sound.id) ?? []].flatMap((entry) => Object.values(entry.layers).flatMap((entries2) => entries2 ?? []));
15085
+ samples.push({
15086
+ id: sound.id,
15087
+ bus: numbers.bus,
15088
+ close,
15089
+ ...far.length > 0 ? { far } : {},
15090
+ ...tiers.length > 0 ? { tiers } : {},
15091
+ ...numbers.crossover ? { crossover: numbers.crossover } : {},
15092
+ ...indoor.length > 0 || outdoor.length > 0 ? { tail: { ...indoor.length > 0 ? { indoor } : {}, ...outdoor.length > 0 ? { outdoor } : {}, ...numbers.tailGain === void 0 ? {} : { gain: numbers.tailGain } } } : {},
15093
+ ...numbers.pitch === void 0 ? {} : { pitch: numbers.pitch },
15094
+ ...numbers.gain === void 0 ? {} : { gain: numbers.gain },
15095
+ ...numbers.falloff ? { falloff: numbers.falloff } : {},
15096
+ ...numbers.cutoffK === void 0 ? {} : { cutoffK: numbers.cutoffK },
15097
+ ...numbers.group ? { group: numbers.group } : {},
15098
+ ...numbers.priority === void 0 ? {} : { priority: numbers.priority },
15099
+ ...numbers.fallback ? { fallback: numbers.fallback } : {},
15100
+ sources: files.map((file) => `${file.file}: ${file.source} ${file.author} ${file.license}`)
15101
+ });
15102
+ }
15103
+ return { id: options.id, samples };
15104
+ }
14525
15105
  var ATTACK = 2e-3;
14526
15106
  function lengthOf(d, attack) {
14527
15107
  if (typeof d === "number") return attack + d;
@@ -14597,6 +15177,569 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14597
15177
  sources: [`cena:${line}`]
14598
15178
  };
14599
15179
  }
15180
+ var FOLEY_FALLOFF = {
15181
+ model: "inverse_square",
15182
+ k: 0.02,
15183
+ cutoffK: 0.04,
15184
+ minGain: 4e-3
15185
+ };
15186
+ var vary = (f, pct = 0.04) => [Math.round(f * (1 - pct)), Math.round(f * (1 + pct))];
15187
+ var foley = (id, layers, note) => ({
15188
+ id,
15189
+ bus: "world",
15190
+ pan: true,
15191
+ layers,
15192
+ falloff: FOLEY_FALLOFF,
15193
+ sources: [`sdk: ${note}`]
15194
+ });
15195
+ var click = (f, vol, delay = 0) => [
15196
+ sceneNoise({
15197
+ type: "bandpass",
15198
+ f: vary(f, 0.08),
15199
+ q: 4,
15200
+ d: [0.018, 0.03],
15201
+ vol: [vol * 0.85, vol * 1.1],
15202
+ delay
15203
+ }),
15204
+ sceneTone({
15205
+ f: vary(f * 0.8, 0.06),
15206
+ f2: Math.round(f * 0.62),
15207
+ d: [0.015, 0.025],
15208
+ vol: vol * 0.18,
15209
+ delay
15210
+ })
15211
+ ];
15212
+ var meet = (body, panel, vol, latch) => [
15213
+ sceneTone({
15214
+ f: vary(body),
15215
+ f2: Math.round(body * 0.5),
15216
+ d: [0.12, 0.16],
15217
+ vol: [vol * 0.9, vol * 1.1]
15218
+ }),
15219
+ sceneNoise({
15220
+ type: "bandpass",
15221
+ f: vary(panel, 0.06),
15222
+ q: 1.2,
15223
+ d: [0.08, 0.12],
15224
+ vol: [vol * 0.9, vol * 1.1],
15225
+ wet: 0.25
15226
+ }),
15227
+ ...click(latch, vol * 0.45, [0.012, 0.028])
15228
+ ];
15229
+ var LEAF_FOLEY = [
15230
+ // a wooden house door or wardrobe door: latch, a short creak, the leaf on the frame (the hard slam adds the frame rattling)
15231
+ foley("door_wood_open", click(2400, 0.4), "the latch of a wooden door giving"),
15232
+ foley(
15233
+ "door_creak",
15234
+ [
15235
+ sceneTone({ f: [190, 240], f2: 310, d: [0.4, 0.55], vol: [0.04, 0.06] }),
15236
+ sceneNoise({
15237
+ type: "bandpass",
15238
+ f: [480, 560],
15239
+ q: 6,
15240
+ a: 0.05,
15241
+ d: [0.35, 0.5],
15242
+ vol: [0.1, 0.14]
15243
+ })
15244
+ ],
15245
+ "a wooden hinge turning"
15246
+ ),
15247
+ foley("door_wood_close", meet(120, 600, 0.55, 2400), "a wooden leaf meeting its frame"),
15248
+ foley(
15249
+ "door_wood_slam",
15250
+ [
15251
+ ...meet(110, 560, 0.85, 2300),
15252
+ ...sceneMany(
15253
+ 3,
15254
+ () => sceneNoise({
15255
+ type: "bandpass",
15256
+ f: [900, 1500],
15257
+ q: 3,
15258
+ d: 0.03,
15259
+ vol: [0.08, 0.14],
15260
+ delay: [0.05, 0.18]
15261
+ })
15262
+ )
15263
+ ],
15264
+ "a wooden door slammed: the frame and the hinges rattle after the hit"
15265
+ ),
15266
+ // a metal door or gate: a hollow panel that rings a little
15267
+ foley("door_metal_open_close", [...meet(160, 900, 0.55, 2e3), sceneTone({ f: vary(1250), f2: 1150, d: [0.1, 0.16], vol: 0.06, wet: 0.3 })], "a metal door or gate meeting its frame"),
15268
+ foley(
15269
+ "door_creak_metal",
15270
+ [
15271
+ sceneTone({ f: [620, 760], f2: 900, d: [0.3, 0.4], vol: [0.03, 0.045] }),
15272
+ sceneNoise({
15273
+ type: "bandpass",
15274
+ f: [1300, 1500],
15275
+ q: 8,
15276
+ a: 0.04,
15277
+ d: [0.25, 0.35],
15278
+ vol: [0.07, 0.09]
15279
+ })
15280
+ ],
15281
+ "a metal hinge turning"
15282
+ ),
15283
+ // the handle of a locked door shaken: the latch knocking in its keeper, five to seven times in half a second
15284
+ foley(
15285
+ "door_locked_rattle",
15286
+ [
15287
+ ...sceneMany(
15288
+ 6,
15289
+ () => sceneNoise({
15290
+ type: "bandpass",
15291
+ f: [1600, 2600],
15292
+ q: 3,
15293
+ d: [0.025, 0.04],
15294
+ vol: [0.22, 0.34],
15295
+ delay: [0, 0.45]
15296
+ })
15297
+ ),
15298
+ ...sceneMany(
15299
+ 3,
15300
+ () => sceneTone({
15301
+ f: [170, 210],
15302
+ f2: 120,
15303
+ d: 0.05,
15304
+ vol: [0.12, 0.18],
15305
+ delay: [0.02, 0.4]
15306
+ })
15307
+ )
15308
+ ],
15309
+ "the handle of a locked door shaken: latch knocks and the leaf thumping in its frame"
15310
+ ),
15311
+ // a fridge: the rubber seal lets go with a soft suck, the bottles in the door clink; closing it is a soft thump and the seal pulling shut
15312
+ foley(
15313
+ "fridge_open",
15314
+ [
15315
+ sceneNoise({
15316
+ type: "lowpass",
15317
+ f: [320, 380],
15318
+ a: 0.03,
15319
+ d: [0.14, 0.2],
15320
+ vol: [0.32, 0.4]
15321
+ }),
15322
+ sceneNoise({
15323
+ type: "bandpass",
15324
+ f: [800, 950],
15325
+ f2: 1400,
15326
+ q: 2,
15327
+ a: 0.02,
15328
+ d: 0.12,
15329
+ vol: [0.08, 0.12]
15330
+ }),
15331
+ ...sceneMany(
15332
+ 2,
15333
+ () => sceneTone({
15334
+ f: [2600, 4200],
15335
+ d: [0.04, 0.08],
15336
+ vol: [0.015, 0.03],
15337
+ delay: [0.08, 0.22]
15338
+ })
15339
+ )
15340
+ ],
15341
+ "a fridge door: the seal letting go and the bottles in the door"
15342
+ ),
15343
+ foley(
15344
+ "fridge_close",
15345
+ [
15346
+ sceneTone({ f: vary(85), f2: 45, d: [0.1, 0.13], vol: [0.38, 0.46] }),
15347
+ sceneNoise({
15348
+ type: "lowpass",
15349
+ f: [520, 620],
15350
+ d: [0.08, 0.12],
15351
+ vol: [0.3, 0.36]
15352
+ }),
15353
+ sceneNoise({
15354
+ type: "lowpass",
15355
+ f: [260, 320],
15356
+ a: 0.02,
15357
+ d: 0.1,
15358
+ vol: [0.12, 0.18],
15359
+ delay: [0.04, 0.07]
15360
+ }),
15361
+ ...sceneMany(
15362
+ 2,
15363
+ () => sceneTone({
15364
+ f: [2600, 4200],
15365
+ d: [0.04, 0.07],
15366
+ vol: [0.015, 0.03],
15367
+ delay: [0.03, 0.12]
15368
+ })
15369
+ )
15370
+ ],
15371
+ "a fridge door shut: a soft thump, the seal pulling and the bottles"
15372
+ ),
15373
+ // a microwave: the spring catch of the door, plastic on plastic
15374
+ foley(
15375
+ "microwave_open",
15376
+ [
15377
+ ...click(1800, 0.34),
15378
+ sceneTone({
15379
+ f: [3e3, 3400],
15380
+ f2: 2800,
15381
+ d: 0.05,
15382
+ vol: 0.025,
15383
+ delay: [0.01, 0.02]
15384
+ })
15385
+ ],
15386
+ "the spring catch of a microwave door"
15387
+ ),
15388
+ foley(
15389
+ "microwave_close",
15390
+ [
15391
+ sceneNoise({
15392
+ type: "bandpass",
15393
+ f: [1200, 1450],
15394
+ q: 2.5,
15395
+ d: [0.04, 0.06],
15396
+ vol: [0.36, 0.44]
15397
+ }),
15398
+ sceneTone({ f: vary(180), f2: 120, d: 0.05, vol: [0.16, 0.22] }),
15399
+ ...click(2200, 0.2, [8e-3, 0.016])
15400
+ ],
15401
+ "a microwave door clacking shut"
15402
+ ),
15403
+ foley("microwave_beep", [sceneTone({ wave: "square", f: 2e3, d: 0.12, vol: 0.05 })], "the beep of a microwave"),
15404
+ // drawers and sliding leaves: the runners, then the stop
15405
+ foley(
15406
+ "drawer_open",
15407
+ [
15408
+ sceneNoise({
15409
+ type: "bandpass",
15410
+ f: [620, 780],
15411
+ q: 2,
15412
+ a: 0.04,
15413
+ d: [0.28, 0.36],
15414
+ vol: [0.18, 0.24]
15415
+ }),
15416
+ sceneNoise({ type: "bandpass", f: [2e3, 2400], q: 5, d: 0.28, vol: 0.04 }),
15417
+ ...click(1400, 0.16, [0.28, 0.34])
15418
+ ],
15419
+ "a drawer on its runners and its stop"
15420
+ ),
15421
+ foley(
15422
+ "drawer_close",
15423
+ [
15424
+ sceneNoise({
15425
+ type: "bandpass",
15426
+ f: [620, 780],
15427
+ q: 2,
15428
+ a: 0.03,
15429
+ d: [0.2, 0.28],
15430
+ vol: [0.18, 0.24]
15431
+ }),
15432
+ sceneTone({
15433
+ f: vary(150),
15434
+ f2: 90,
15435
+ d: 0.08,
15436
+ vol: [0.22, 0.3],
15437
+ delay: [0.2, 0.26]
15438
+ })
15439
+ ],
15440
+ "a drawer pushed home"
15441
+ )
15442
+ ];
15443
+ var ITEM_FOLEY = [
15444
+ // the hand closing on the item: a touch of skin and cloth, then the item's own small sound
15445
+ foley(
15446
+ "item_pickup_metal",
15447
+ [
15448
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
15449
+ sceneTone({
15450
+ f: [1900, 2600],
15451
+ f2: 1700,
15452
+ d: [0.05, 0.08],
15453
+ vol: [0.06, 0.09],
15454
+ delay: [0.01, 0.03]
15455
+ })
15456
+ ],
15457
+ "picking up a metal thing"
15458
+ ),
15459
+ foley(
15460
+ "item_pickup_plastic",
15461
+ [
15462
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
15463
+ sceneNoise({
15464
+ type: "bandpass",
15465
+ f: [1500, 1900],
15466
+ q: 3,
15467
+ d: 0.03,
15468
+ vol: [0.15, 0.21],
15469
+ delay: [0.01, 0.03]
15470
+ })
15471
+ ],
15472
+ "picking up a plastic thing"
15473
+ ),
15474
+ foley(
15475
+ "item_pickup_glass",
15476
+ [
15477
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.15 }),
15478
+ sceneTone({
15479
+ f: [3200, 4200],
15480
+ d: [0.05, 0.08],
15481
+ vol: [0.038, 0.06],
15482
+ delay: [0.01, 0.03]
15483
+ })
15484
+ ],
15485
+ "picking up a glass thing"
15486
+ ),
15487
+ foley(
15488
+ "item_pickup_paper",
15489
+ [
15490
+ sceneNoise({
15491
+ type: "highpass",
15492
+ f: [2600, 3400],
15493
+ a: 0.01,
15494
+ d: [0.1, 0.14],
15495
+ vol: [0.12, 0.18]
15496
+ }),
15497
+ sceneNoise({ type: "bandpass", f: [800, 1e3], q: 1, d: 0.05, vol: 0.12 })
15498
+ ],
15499
+ "picking up paper or cardboard"
15500
+ ),
15501
+ foley(
15502
+ "item_pickup_fabric",
15503
+ [
15504
+ sceneNoise({
15505
+ type: "bandpass",
15506
+ f: [900, 1300],
15507
+ q: 0.8,
15508
+ a: 0.02,
15509
+ d: [0.12, 0.16],
15510
+ vol: [0.15, 0.21]
15511
+ })
15512
+ ],
15513
+ "picking up cloth"
15514
+ ),
15515
+ foley(
15516
+ "item_pickup_wood",
15517
+ [
15518
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
15519
+ sceneNoise({
15520
+ type: "bandpass",
15521
+ f: [900, 1100],
15522
+ q: 2,
15523
+ d: 0.04,
15524
+ vol: [0.15, 0.21],
15525
+ delay: [0.01, 0.03]
15526
+ })
15527
+ ],
15528
+ "picking up a wooden thing"
15529
+ ),
15530
+ foley(
15531
+ "item_pickup_food",
15532
+ [
15533
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
15534
+ sceneNoise({
15535
+ type: "lowpass",
15536
+ f: [500, 650],
15537
+ d: 0.05,
15538
+ vol: 0.12,
15539
+ delay: [0.01, 0.03]
15540
+ })
15541
+ ],
15542
+ "picking up food"
15543
+ ),
15544
+ foley(
15545
+ "item_throw",
15546
+ [
15547
+ sceneNoise({
15548
+ type: "bandpass",
15549
+ f: [450, 650],
15550
+ f2: 1400,
15551
+ q: 1.5,
15552
+ a: 0.05,
15553
+ d: 0.18,
15554
+ vol: [0.1, 0.14]
15555
+ })
15556
+ ],
15557
+ "the arm throwing something: air"
15558
+ ),
15559
+ // a can: the tab cracks, the gas hisses out; drinking is gulps; crushing it crumples the sheet
15560
+ foley(
15561
+ "can_open",
15562
+ [
15563
+ ...click(3200, 0.36),
15564
+ sceneNoise({
15565
+ type: "highpass",
15566
+ f: [3600, 4400],
15567
+ a: 5e-3,
15568
+ d: [0.22, 0.3],
15569
+ vol: [0.16, 0.22],
15570
+ delay: [0.012, 0.02]
15571
+ }),
15572
+ ...sceneMany(5, () => sceneTone({ f: [4e3, 7e3], d: 0.02, vol: 0.01, delay: [0.05, 0.4] }))
15573
+ ],
15574
+ "the tab of a can and the gas hissing out"
15575
+ ),
15576
+ foley(
15577
+ "can_crush",
15578
+ [
15579
+ sceneNoise({
15580
+ type: "bandpass",
15581
+ f: [1600, 2200],
15582
+ q: 1.2,
15583
+ d: [0.18, 0.25],
15584
+ vol: [0.3, 0.38]
15585
+ }),
15586
+ ...sceneMany(
15587
+ 6,
15588
+ () => sceneTone({
15589
+ f: [1200, 3400],
15590
+ f2: 900,
15591
+ d: 0.03,
15592
+ vol: [0.03, 0.06],
15593
+ delay: [0, 0.2]
15594
+ })
15595
+ )
15596
+ ],
15597
+ "a can crushed in the hand"
15598
+ ),
15599
+ foley(
15600
+ "can_puncture_spray",
15601
+ [
15602
+ sceneNoise({
15603
+ type: "highpass",
15604
+ f: [2400, 3e3],
15605
+ a: 5e-3,
15606
+ d: [0.5, 0.7],
15607
+ vol: [0.2, 0.26]
15608
+ }),
15609
+ sceneNoise({ type: "bandpass", f: [600, 800], q: 2, d: 0.4, vol: 0.06 })
15610
+ ],
15611
+ "a can holed: the jet hissing out"
15612
+ ),
15613
+ foley(
15614
+ "bottle_open",
15615
+ [
15616
+ ...sceneMany(
15617
+ 5,
15618
+ () => sceneNoise({
15619
+ type: "bandpass",
15620
+ f: [2200, 3e3],
15621
+ q: 4,
15622
+ d: 8e-3,
15623
+ vol: [0.1, 0.16],
15624
+ delay: [0, 0.18]
15625
+ })
15626
+ ),
15627
+ sceneNoise({
15628
+ type: "highpass",
15629
+ f: 3800,
15630
+ a: 5e-3,
15631
+ d: 0.12,
15632
+ vol: [0.06, 0.1],
15633
+ delay: [0.18, 0.22]
15634
+ })
15635
+ ],
15636
+ "the screw cap of a bottle turning off"
15637
+ ),
15638
+ foley(
15639
+ "drink_gulp",
15640
+ [
15641
+ sceneNoise({
15642
+ type: "lowpass",
15643
+ f: [280, 360],
15644
+ a: 0.02,
15645
+ d: [0.1, 0.14],
15646
+ vol: [0.24, 0.3]
15647
+ }),
15648
+ sceneTone({ f: [140, 170], f2: 85, d: [0.08, 0.11], vol: [0.12, 0.16] })
15649
+ ],
15650
+ "a gulp"
15651
+ ),
15652
+ foley(
15653
+ "eat_crunch",
15654
+ [
15655
+ ...sceneMany(
15656
+ 4,
15657
+ () => sceneNoise({
15658
+ type: "bandpass",
15659
+ f: [1800, 3200],
15660
+ q: 1.5,
15661
+ d: [0.02, 0.04],
15662
+ vol: [0.12, 0.2],
15663
+ delay: [0, 0.12]
15664
+ })
15665
+ )
15666
+ ],
15667
+ "a bite of something crunchy"
15668
+ ),
15669
+ foley(
15670
+ "eat_chew",
15671
+ [
15672
+ sceneNoise({
15673
+ type: "lowpass",
15674
+ f: [450, 600],
15675
+ a: 0.03,
15676
+ d: [0.1, 0.14],
15677
+ vol: [0.12, 0.16]
15678
+ }),
15679
+ sceneNoise({
15680
+ type: "bandpass",
15681
+ f: [1100, 1400],
15682
+ q: 2,
15683
+ d: 0.04,
15684
+ vol: 0.04,
15685
+ delay: [0.03, 0.06]
15686
+ })
15687
+ ],
15688
+ "chewing"
15689
+ ),
15690
+ foley(
15691
+ "bandage_wrap",
15692
+ [
15693
+ sceneNoise({
15694
+ type: "bandpass",
15695
+ f: [1400, 1800],
15696
+ f2: 2400,
15697
+ q: 1,
15698
+ a: 0.04,
15699
+ d: [0.3, 0.4],
15700
+ vol: [0.12, 0.16]
15701
+ })
15702
+ ],
15703
+ "a bandage unrolled round a limb"
15704
+ ),
15705
+ foley(
15706
+ "pills_shake",
15707
+ [
15708
+ ...sceneMany(
15709
+ 8,
15710
+ () => sceneTone({
15711
+ f: [2400, 3600],
15712
+ d: 0.015,
15713
+ vol: [0.03, 0.05],
15714
+ delay: [0, 0.25]
15715
+ })
15716
+ ),
15717
+ sceneNoise({ type: "bandpass", f: [1500, 1900], q: 2, d: 0.25, vol: 0.05 })
15718
+ ],
15719
+ "pills shaken in their bottle"
15720
+ ),
15721
+ foley(
15722
+ "medkit_use",
15723
+ [
15724
+ ...click(1700, 0.24),
15725
+ sceneNoise({
15726
+ type: "bandpass",
15727
+ f: [1e3, 1400],
15728
+ q: 1,
15729
+ a: 0.03,
15730
+ d: [0.25, 0.32],
15731
+ vol: [0.08, 0.12],
15732
+ delay: [0.05, 0.08]
15733
+ })
15734
+ ],
15735
+ "a first aid kit opened and rummaged"
15736
+ ),
15737
+ foley("flashlight_click", click(2600, 0.3), "the switch of a flashlight")
15738
+ ];
15739
+ var FOLEY_BANK = {
15740
+ id: "foley",
15741
+ sounds: [...LEAF_FOLEY, ...ITEM_FOLEY]
15742
+ };
14600
15743
  var TAKES = [
14601
15744
  { f: 1, t: 1, v: 1 },
14602
15745
  { f: 1.08, t: 0.9, v: 0.92 },
@@ -14764,6 +15907,243 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14764
15907
  };
14765
15908
  var BODY_FOLEY_SET_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
14766
15909
 
15910
+ // src/audio/blast-fire.ts
15911
+ var BLAST_FALLOFF = { model: "inverse_square", k: 12e-4, cutoffK: 6e-3, minGain: 2e-3 };
15912
+ var BLAST_FAR_FALLOFF = { model: "inverse_square", k: 8e-5, scale: 0.3, minGain: 1e-3 };
15913
+ var FIRE_FALLOFF = { model: "inverse_square", k: 0.01, cutoffK: 0.01, minGain: 5e-3 };
15914
+ var BLAST_FAR_DISTANCE = 60;
15915
+ var BLAST_KINDS = ["grenade", "big", "fuel", "car", "pipe_bomb", "rocket", "flashbang", "molotov"];
15916
+ var BLAST_KIND_OF = {
15917
+ m67: "grenade",
15918
+ vog_25: "grenade",
15919
+ m79_40mm: "grenade",
15920
+ claymore: "grenade",
15921
+ generic: "grenade",
15922
+ c4: "big",
15923
+ dynamite: "big",
15924
+ fuel_station_tank: "big",
15925
+ propane_p13: "fuel",
15926
+ fuel_barrel: "fuel",
15927
+ fuel_pump: "fuel",
15928
+ car_fuel_tank: "car",
15929
+ pipe_bomb: "pipe_bomb",
15930
+ m72_66mm: "rocket",
15931
+ at4_84mm: "rocket",
15932
+ carl_gustaf_84mm: "rocket",
15933
+ pg7v: "rocket",
15934
+ flashbang: "flashbang",
15935
+ molotov: "molotov"
15936
+ };
15937
+ var BIG_BLAST_RADIUS = 8;
15938
+ var blast = (id, layers, sources, falloff = BLAST_FALLOFF) => ({
15939
+ ...sceneRecipe(id, layers, ""),
15940
+ falloff,
15941
+ sources
15942
+ });
15943
+ var crack = (f, d, vol) => sceneNoise({ type: "highpass", f, a: 1e-3, d, vol });
15944
+ var debris2 = (count, from, to, vol) => [
15945
+ ...sceneMany(count, () => sceneNoise({ type: "bandpass", f: [300, 1400], q: 2.5, d: [0.03, 0.09], vol, delay: [from, to] })),
15946
+ ...sceneMany(Math.ceil(count / 3), () => sceneNoise({ type: "bandpass", f: [1800, 3600], q: 5, d: [0.02, 0.04], vol: [vol[0] * 0.6, vol[1] * 0.6], delay: [from + 0.1, to] }))
15947
+ ];
15948
+ var shrapnel = (count, to) => sceneMany(count, () => sceneNoise({ type: "bandpass", f: [2500, 6e3], q: 6, d: [0.015, 0.045], vol: [0.08, 0.16], delay: [0.02, to] }));
15949
+ var glass = (count, from) => [
15950
+ sceneNoise({ type: "highpass", f: [2600, 3e3], d: 0.4, vol: 0.7, wet: 0.5, delay: from }),
15951
+ ...sceneMany(count, () => sceneTone({ f: [3e3, 7500], d: [0.05, 0.13], vol: 0.12, delay: [from, from + 0.7] }))
15952
+ ];
15953
+ var BLAST_SOUNDS = [
15954
+ blast(
15955
+ "explosion_grenade",
15956
+ [
15957
+ crack([2300, 2700], 0.06, 2.4),
15958
+ sceneNoise({ f: [2500, 3e3], f2: 160, d: 0.7, vol: 2, wet: 0.35 }),
15959
+ sceneTone({ f: [80, 92], f2: 32, d: 0.5, vol: 1.6 }),
15960
+ sceneNoise({ type: "bandpass", f: [620, 760], q: 0.8, d: 0.18, vol: 1.1 }),
15961
+ ...shrapnel(14, 0.7),
15962
+ ...debris2(10, 0.35, 1.8, [0.1, 0.2])
15963
+ ],
15964
+ ["sdk: frag grenade (M67, VOG-25, 40 mm): sharp crack, short body, fragments striking, a little debris"]
15965
+ ),
15966
+ blast(
15967
+ "explosion_big",
15968
+ [
15969
+ crack([1900, 2300], 0.08, 2.4),
15970
+ sceneNoise({ f: [2200, 2600], f2: 90, d: 1.6, vol: 2.2, wet: 0.45 }),
15971
+ sceneTone({ f: [52, 60], f2: 22, d: 1.6, vol: 1.9 }),
15972
+ sceneNoise({ f: [280, 330], f2: 60, a: 0.25, d: 2.8, vol: 0.8, wet: 0.6, delay: 0.05 }),
15973
+ ...debris2(24, 0.5, 3.6, [0.12, 0.25])
15974
+ ],
15975
+ ["sdk: big charge (C4, dynamite, a fuel station tank): crack, long deep roar, debris falling for 2 to 4 s"]
15976
+ ),
15977
+ blast(
15978
+ "explosion_fuel",
15979
+ [
15980
+ sceneNoise({ type: "highpass", f: [1700, 2e3], a: 1e-3, d: 0.05, vol: 1 }),
15981
+ sceneTone({ f: [380, 450], f2: 300, d: 0.4, vol: 0.3 }),
15982
+ sceneNoise({ f: [1700, 2e3], f2: 150, a: 0.03, d: 1.4, vol: 2.1, wet: 0.4 }),
15983
+ sceneTone({ f: [48, 56], f2: 25, d: 1.2, vol: 1.7 }),
15984
+ sceneNoise({ type: "bandpass", f: [450, 560], q: 0.6, a: 0.15, d: 2.2, vol: 0.85, wet: 0.5, delay: 0.1 }),
15985
+ ...debris2(10, 0.5, 2.6, [0.08, 0.16])
15986
+ ],
15987
+ ['sdk: a fuel vessel bursting (propane P13, fuel barrel, pump): the shell tearing, a fireball "whoomp" and its roar']
15988
+ ),
15989
+ blast(
15990
+ "explosion_car",
15991
+ [
15992
+ sceneNoise({ type: "highpass", f: [1700, 2e3], a: 1e-3, d: 0.06, vol: 1.1 }),
15993
+ sceneNoise({ f: [1600, 1900], f2: 140, a: 0.03, d: 1.6, vol: 2.1, wet: 0.4 }),
15994
+ sceneTone({ f: [44, 52], f2: 24, d: 1.4, vol: 1.8 }),
15995
+ sceneNoise({ type: "bandpass", f: [420, 520], q: 0.6, a: 0.15, d: 2.6, vol: 0.9, wet: 0.5, delay: 0.1 }),
15996
+ ...glass(10, 0.04),
15997
+ sceneTone({ f: [130, 150], f2: 85, d: 1.1, vol: 0.18, delay: [0.3, 0.5] }),
15998
+ ...debris2(16, 0.6, 3.2, [0.1, 0.2])
15999
+ ],
16000
+ ["sdk: a car going up: the fuel tank fireball, every window breaking, the body groaning, parts falling"]
16001
+ ),
16002
+ blast(
16003
+ "explosion_pipe_bomb",
16004
+ [
16005
+ crack([2e3, 2400], 0.07, 2.2),
16006
+ sceneNoise({ f: [2e3, 2400], f2: 150, d: 0.8, vol: 1.9, wet: 0.35 }),
16007
+ sceneTone({ f: [70, 82], f2: 30, d: 0.6, vol: 1.5 }),
16008
+ ...sceneMany(6, () => sceneTone({ f: [1800, 3200], f2: 1200, d: [0.06, 0.12], vol: 0.06, delay: [0.05, 0.6] })),
16009
+ ...shrapnel(18, 0.9),
16010
+ ...debris2(8, 0.4, 1.8, [0.08, 0.16])
16011
+ ],
16012
+ ["sdk: pipe bomb: crack, body, the steel pipe flying apart in ringing pieces"]
16013
+ ),
16014
+ blast(
16015
+ "explosion_rocket",
16016
+ [
16017
+ crack([2100, 2500], 0.08, 2.4),
16018
+ sceneNoise({ f: [2400, 2800], f2: 120, d: 1.1, vol: 2.1, wet: 0.4 }),
16019
+ sceneTone({ f: [60, 70], f2: 26, d: 0.9, vol: 1.8 }),
16020
+ sceneNoise({ f: [350, 420], f2: 80, a: 0.08, d: 1.8, vol: 0.9, wet: 0.6, delay: 0.05 }),
16021
+ ...shrapnel(10, 0.8),
16022
+ ...debris2(16, 0.4, 2.6, [0.1, 0.2])
16023
+ ],
16024
+ ["sdk: rocket or anti-tank warhead (M72, AT4, RPG): hard crack, heavy body, debris"]
16025
+ ),
16026
+ blast(
16027
+ "explosion_flashbang",
16028
+ [crack([1400, 1700], 0.12, 2.8), sceneNoise({ f: [1900, 2200], f2: 300, d: 0.35, vol: 1.3, wet: 0.3 }), sceneTone({ f: [110, 130], f2: 60, d: 0.2, vol: 0.8 })],
16029
+ ["sdk: flashbang: a deafening crack with almost no body and no fragments"]
16030
+ ),
16031
+ blast(
16032
+ "molotov_break_ignite",
16033
+ [
16034
+ ...glass(8, 0),
16035
+ sceneNoise({ f: [280, 340], f2: 2200, a: 0.12, d: 0.9, vol: 1.5, wet: 0.6, delay: 0.05 }),
16036
+ sceneTone({ f: [85, 95], f2: 55, d: 0.4, vol: 0.5, delay: 0.08 })
16037
+ ],
16038
+ ['sdk: molotov: the bottle breaking and the fuel catching with a "fuuush"'],
16039
+ FIRE_FALLOFF
16040
+ ),
16041
+ blast(
16042
+ "explosion_far",
16043
+ [
16044
+ sceneNoise({ f: [550, 650], f2: 80, a: 0.03, d: 2.5, vol: 2, wet: 1.4 }),
16045
+ sceneTone({ f: [42, 48], f2: 25, d: 1.8, vol: 1.2 }),
16046
+ sceneNoise({ f: [350, 420], f2: 70, a: 0.1, d: 1.6, vol: 0.6, wet: 1.5, delay: [0.6, 1] })
16047
+ ],
16048
+ ["sdk: any blast far away: no crack left, a dull thud and the echo rolling back"],
16049
+ BLAST_FAR_FALLOFF
16050
+ ),
16051
+ blast(
16052
+ "explosion_indoor_tail",
16053
+ [
16054
+ ...sceneMany(3, () => sceneNoise({ f: [650, 800], d: 0.12, vol: 0.5, delay: [0.04, 0.2] })),
16055
+ sceneNoise({ f: [850, 950], f2: 120, a: 0.05, d: 2.6, vol: 0.9, wet: 1.6, delay: 0.08 })
16056
+ ],
16057
+ ["sdk: the room answering a blast inside it: slaps off the walls and a boomy tail"]
16058
+ )
16059
+ ];
16060
+ var FIRE_SOUNDS = [
16061
+ {
16062
+ ...blast(
16063
+ "fire_ignite",
16064
+ [sceneNoise({ f: [230, 270], f2: 1500, a: 0.07, d: 0.5, vol: 1.6 }), sceneTone({ f: [80, 90], f2: 55, d: 0.3, vol: 0.45 }), sceneNoise({ type: "highpass", f: 3e3, d: 0.3, vol: 0.1, delay: 0.1 })],
16065
+ ['sdk: fire catching, a soft "fuuump"'],
16066
+ FIRE_FALLOFF
16067
+ ),
16068
+ group: "fire_ignite"
16069
+ },
16070
+ {
16071
+ ...blast("gas_leak", [sceneNoise({ type: "bandpass", f: [3200, 3800], q: 0.7, a: 0.08, d: { param: "seconds" }, vol: 0.3 })], ["sdk: gas jetting from a punctured vessel"], FIRE_FALLOFF),
16072
+ vars: { seconds: 2 }
16073
+ },
16074
+ {
16075
+ ...blast(
16076
+ "fire_extinguish",
16077
+ [sceneNoise({ type: "highpass", f: [2300, 2700], a: 0.03, d: { param: "seconds" }, vol: 0.3 }), sceneNoise({ type: "bandpass", f: [800, 1e3], q: 0.5, d: 0.5, vol: 0.25 })],
16078
+ ["sdk: water or foam on a fire: a steam puff and a sizzle"],
16079
+ FIRE_FALLOFF
16080
+ ),
16081
+ vars: { seconds: 1.2 }
16082
+ },
16083
+ { ...blast("fire_crackle", [sceneNoise({ type: "highpass", f: [1500, 3e3], d: [0.01, 0.025], vol: [1.2, 2.2] })], ["sdk: a crackle of the fire"], FIRE_FALLOFF), group: "fire_crackle" },
16084
+ { ...blast("fire_pop", [sceneNoise({ type: "bandpass", f: [500, 1100], q: 1, d: [0.03, 0.06], vol: [3, 4.5] })], ["sdk: a log or a resin pocket popping"], FIRE_FALLOFF), group: "fire_crackle" }
16085
+ ];
16086
+ var FIRE_LOOP = {
16087
+ id: "fire_loop",
16088
+ bus: "world",
16089
+ order: "chain_loop",
16090
+ source: { kind: "noise", loop: true },
16091
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 950 }, q: 0.5 }] },
16092
+ volume: 0,
16093
+ sources: ["sdk: the roar under the crackles of a fire"]
16094
+ };
16095
+ var FIRE_FUEL_OF = {
16096
+ wood_soft: "wood",
16097
+ wood_hard: "wood",
16098
+ plywood: "wood",
16099
+ mdf: "wood",
16100
+ green_wood: "wood",
16101
+ foliage: "wood",
16102
+ cactus: "wood",
16103
+ paper: "fabric",
16104
+ fabric: "fabric",
16105
+ leather: "fabric",
16106
+ foam: "plastic",
16107
+ plastic: "plastic",
16108
+ rubber: "plastic",
16109
+ wax: "plastic"
16110
+ };
16111
+ var FIRE_LOOP_OF = {
16112
+ wood: "fire_loop",
16113
+ fabric: "fire_loop",
16114
+ plastic: "fire_loop_plastic",
16115
+ gas: "fire_loop_gas",
16116
+ liquid: "fire_loop_liquid"
16117
+ };
16118
+ var FIRE_CRACKLE_OF = { wood: 1, fabric: 0.5, plastic: 0.35, gas: 0, liquid: 0 };
16119
+ var roar = (id, bank, source) => ({
16120
+ id,
16121
+ bus: "world",
16122
+ order: "chain_loop",
16123
+ source: { kind: "noise", loop: true },
16124
+ filters: { mode: "serial", bank },
16125
+ volume: 0,
16126
+ sources: [source]
16127
+ });
16128
+ var FIRE_FUEL_LOOPS = [
16129
+ // a gas jet: a steady, broad roar, brighter than wood and without crackles
16130
+ roar("fire_loop_gas", [{ type: "highpass", frequency: { set: 220 }, q: 0.7 }, { type: "lowpass", frequency: { set: 2400 }, q: 0.6 }], "sdk: a gas jet burning (a vessel venting into the flame)"),
16131
+ // a fuel pool: a soft, low, rolling roar
16132
+ roar("fire_loop_liquid", [{ type: "highpass", frequency: { set: 90 }, q: 0.7 }, { type: "lowpass", frequency: { set: 650 }, q: 0.9 }], "sdk: a pool of fuel burning"),
16133
+ // plastic and rubber: a thin sizzle over a low burn
16134
+ roar("fire_loop_plastic", [{ type: "bandpass", frequency: { set: 1800 }, q: 0.6 }], "sdk: plastic and rubber melting in the fire")
16135
+ ];
16136
+ var BLAST_SOUND_OF = {
16137
+ grenade: "explosion_grenade",
16138
+ big: "explosion_big",
16139
+ fuel: "explosion_fuel",
16140
+ car: "explosion_car",
16141
+ pipe_bomb: "explosion_pipe_bomb",
16142
+ rocket: "explosion_rocket",
16143
+ flashbang: "explosion_flashbang",
16144
+ molotov: "molotov_break_ignite"
16145
+ };
16146
+
14767
16147
  // src/audio/recipes.ts
14768
16148
  var scene = (id, layers, line, vars) => sceneRecipe(id, layers, line, vars ? { vars } : {});
14769
16149
  var boomLayers = (big) => [
@@ -14846,11 +16226,111 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14846
16226
  leaf("shut_metal", [sceneTone({ f: 160, f2: 85, d: 0.18, vol: 0.55 }), sceneNoise({ type: "bandpass", f: 900, q: 1.6, d: 0.12, vol: 0.45, wet: 0.3 }), sceneTone({ f: 1250, f2: 1150, d: 0.12, vol: 0.06 })]),
14847
16227
  leaf("shut_soft", [sceneTone({ f: 95, f2: 55, d: 0.12, vol: 0.35 }), sceneNoise({ type: "lowpass", f: 500, d: 0.1, vol: 0.3 })]),
14848
16228
  leaf("rattle", sceneMany(5, () => sceneNoise({ type: "bandpass", f: [1600, 2600], q: 3, d: 0.035, vol: 0.32, delay: [0, 0.45] }))),
14849
- leaf("slide", [sceneNoise({ type: "bandpass", f: 700, q: 2, a: 0.04, d: 0.35, vol: 0.22 }), sceneNoise({ type: "bandpass", f: 2200, q: 5, d: 0.3, vol: 0.05 })])
16229
+ leaf("slide", [sceneNoise({ type: "bandpass", f: 700, q: 2, a: 0.04, d: 0.35, vol: 0.22 }), sceneNoise({ type: "bandpass", f: 2200, q: 5, d: 0.3, vol: 0.05 })]),
16230
+ // an item taken in the hand: the fingers on a hard surface and the sleeve moving, or a soft thing gathered up (the kit's recordings answer these ids)
16231
+ leaf("grab_hard", [sceneNoise({ type: "bandpass", f: 1800, q: 3, d: 0.03, vol: 0.18 }), sceneNoise({ type: "bandpass", f: 900, q: 1.2, a: 0.02, d: 0.14, vol: 0.08 })]),
16232
+ leaf("grab_soft", [sceneNoise({ type: "lowpass", f: 1400, a: 0.02, d: 0.16, vol: 0.14 })])
16233
+ ];
16234
+ var impact2 = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a blow on a material, by material and kind of blow (synthesised until the recorded samples arrive)"] });
16235
+ var IMPACT_SOUNDS = [
16236
+ // metal
16237
+ impact2("metal_bullet", [sceneNoise({ type: "highpass", f: 3e3, d: 0.03, vol: 0.8 }), sceneTone({ f: [2600, 3400], f2: 1800, d: 0.14, vol: 0.28, wet: 0.35 }), sceneTone({ f: [520, 720], f2: 480, d: 0.35, vol: 0.22, wet: 0.35 })]),
16238
+ impact2("metal_blunt", [sceneNoise({ type: "bandpass", f: 800, q: 1, d: 0.05, vol: 0.5 }), sceneTone({ f: [150, 210], f2: 120, d: 0.6, vol: 0.5, wet: 0.4 }), sceneTone({ f: [420, 520], f2: 400, d: 0.4, vol: 0.12 })]),
16239
+ impact2("metal_blade", [sceneNoise({ type: "bandpass", f: [3500, 4500], q: 6, a: 0.01, d: 0.18, vol: 0.45 }), sceneTone({ f: [2200, 2800], f2: 3e3, d: 0.15, vol: 0.06 })]),
16240
+ impact2("metal_thin", [sceneNoise({ type: "highpass", f: 2500, d: 0.04, vol: 0.6 }), sceneTone({ f: [900, 1300], f2: 700, d: 0.25, vol: 0.22, wet: 0.3 }), sceneNoise({ type: "bandpass", f: 1200, q: 2, d: 0.08, vol: 0.3 })]),
16241
+ // wood (the bullet keeps the art scene's `wood_hit`)
16242
+ impact2("wood_blunt", [sceneTone({ f: [110, 150], f2: 70, d: 0.14, vol: 0.6 }), sceneNoise({ type: "bandpass", f: 500, q: 1.2, d: 0.12, vol: 0.55, wet: 0.2 })]),
16243
+ impact2("wood_blade", [sceneNoise({ type: "bandpass", f: [1800, 2400], q: 2, a: 0.01, d: 0.12, vol: 0.5 }), ...sceneMany(3, () => sceneNoise({ type: "bandpass", f: [2500, 4e3], q: 3, d: 0.02, vol: 0.22, delay: [0.02, 0.1] }))]),
16244
+ impact2("mdf_puff", [sceneNoise({ type: "lowpass", f: 900, d: 0.08, vol: 1 }), sceneTone({ f: [180, 230], f2: 110, d: 0.06, vol: 0.3 }), sceneNoise({ type: "highpass", f: 3e3, a: 0.01, d: 0.2, vol: 0.1 })]),
16245
+ // glass and ceramic
16246
+ impact2("glass_bullet", [sceneNoise({ type: "highpass", f: 4e3, d: 0.05, vol: 0.7 }), sceneTone({ f: [3200, 4200], f2: 2600, d: 0.2, vol: 0.14, wet: 0.3 }), ...sceneMany(4, () => sceneTone({ f: [4e3, 7e3], d: 0.05, vol: 0.07, delay: [0.02, 0.2] }))]),
16247
+ impact2("glass_blunt", [sceneNoise({ type: "bandpass", f: 2200, q: 3, d: 0.05, vol: 0.8 }), sceneTone({ f: [1400, 1800], f2: 1300, d: 0.2, vol: 0.16, wet: 0.3 })]),
16248
+ impact2("glass_blade", [sceneNoise({ type: "bandpass", f: 5e3, q: 8, a: 0.02, d: 0.25, vol: 0.3 })]),
16249
+ impact2("ceramic_hit", [sceneNoise({ type: "bandpass", f: 3e3, q: 2, d: 0.03, vol: 0.6 }), sceneTone({ f: [1800, 2400], f2: 1600, d: 0.1, vol: 0.2 })]),
16250
+ impact2("ceramic_bullet", [sceneNoise({ type: "highpass", f: 2500, d: 0.06, vol: 0.85 }), ...sceneMany(5, () => sceneTone({ f: [2500, 5e3], d: 0.04, vol: 0.09, delay: [0, 0.15] }))]),
16251
+ // masonry and the ground
16252
+ impact2("masonry_bullet", [sceneNoise({ type: "bandpass", f: [1600, 2e3], q: 1, d: 0.05, vol: 1.8 }), sceneNoise({ type: "lowpass", f: 600, d: 0.12, vol: 0.9, wet: 0.2 }), ...sceneMany(4, () => sceneNoise({ type: "bandpass", f: [2e3, 4e3], q: 3, d: 0.02, vol: 0.28, delay: [0.03, 0.3] }))]),
16253
+ impact2("masonry_blunt", [sceneTone({ f: [70, 95], f2: 45, d: 0.12, vol: 0.55 }), sceneNoise({ type: "lowpass", f: 400, d: 0.1, vol: 0.5 })]),
16254
+ impact2("masonry_blade", [sceneNoise({ type: "bandpass", f: [2500, 3500], q: 4, a: 0.01, d: 0.15, vol: 0.4 })]),
16255
+ impact2("plaster_puff", [sceneNoise({ type: "lowpass", f: 900, d: 0.09, vol: 0.7 }), sceneNoise({ type: "highpass", f: 3e3, a: 0.01, d: 0.2, vol: 0.14 })]),
16256
+ impact2("soil_bullet", [sceneNoise({ type: "lowpass", f: 700, d: 0.08, vol: 2.2 }), ...sceneMany(3, () => sceneNoise({ type: "bandpass", f: [1200, 2200], q: 2, d: 0.03, vol: 0.18, delay: [0.02, 0.2] }))]),
16257
+ impact2("soil_blunt", [sceneTone({ f: [55, 70], f2: 40, d: 0.1, vol: 0.45 }), sceneNoise({ type: "lowpass", f: 300, d: 0.08, vol: 0.4 })]),
16258
+ impact2("sand_hit", [sceneNoise({ type: "bandpass", f: 3500, q: 0.8, a: 5e-3, d: 0.15, vol: 0.45 }), sceneNoise({ type: "lowpass", f: 500, d: 0.06, vol: 0.45 })]),
16259
+ impact2("gravel_hit", [sceneNoise({ type: "lowpass", f: 500, d: 0.06, vol: 0.45 }), ...sceneMany(6, () => sceneNoise({ type: "bandpass", f: [1500, 3500], q: 3, d: 0.025, vol: 0.28, delay: [0, 0.18] }))]),
16260
+ // plastic, rubber, wax
16261
+ impact2("plastic_hit", [sceneNoise({ type: "bandpass", f: 1600, q: 3, d: 0.04, vol: 0.5 }), sceneTone({ f: [380, 520], f2: 300, d: 0.08, vol: 0.32 })]),
16262
+ impact2("plastic_bullet", [sceneNoise({ type: "bandpass", f: 2500, q: 2, d: 0.03, vol: 1.3 }), sceneTone({ f: [600, 800], f2: 450, d: 0.06, vol: 0.4 })]),
16263
+ impact2("rubber_hit", [sceneTone({ f: [90, 130], f2: 60, d: 0.1, vol: 0.45 }), sceneNoise({ type: "lowpass", f: 350, d: 0.06, vol: 0.28 })]),
16264
+ impact2("wax_hit", [sceneNoise({ type: "lowpass", f: 800, d: 0.05, vol: 0.45 }), sceneTone({ f: [240, 280], f2: 180, d: 0.05, vol: 0.22 })]),
16265
+ // soft things, paper, plants
16266
+ impact2("soft_bullet", [sceneNoise({ type: "lowpass", f: 450, a: 3e-3, d: 0.1, vol: 1.8 }), sceneNoise({ type: "bandpass", f: 900, q: 1, d: 0.05, vol: 0.5 })]),
16267
+ impact2("fabric_rip", [sceneNoise({ type: "bandpass", f: [2e3, 2800], q: 1.5, a: 0.02, d: 0.25, vol: 0.4 }), ...sceneMany(4, () => sceneNoise({ type: "bandpass", f: [3e3, 5e3], q: 4, d: 0.02, vol: 0.14, delay: [0, 0.22] }))]),
16268
+ impact2("paper_hit", [sceneNoise({ type: "bandpass", f: 2200, q: 1, d: 0.05, vol: 0.45 })]),
16269
+ impact2("paper_tear", [sceneNoise({ type: "highpass", f: 2500, a: 0.02, d: 0.3, vol: 0.15 })]),
16270
+ impact2("leaf_rustle", sceneMany(6, () => sceneNoise({ type: "bandpass", f: [2500, 6e3], q: 2, d: 0.05, vol: 0.16, delay: [0, 0.3] }))),
16271
+ impact2("branch_cut", [sceneNoise({ type: "bandpass", f: 1500, q: 2, d: 0.04, vol: 0.55 }), sceneTone({ f: [260, 340], f2: 150, d: 0.05, vol: 0.22 })]),
16272
+ impact2("fruit_splat", [sceneNoise({ type: "lowpass", f: 1200, d: 0.12, vol: 0.55 }), sceneNoise({ type: "bandpass", f: 600, q: 0.7, d: 0.2, vol: 0.32 })]),
16273
+ // a round glancing off: steel whines down in pitch ("piiuu"), stone and concrete give a short crack with grit
16274
+ impact2("ricochet_metal", [sceneNoise({ type: "highpass", f: 3e3, d: 0.02, vol: 0.6 }), sceneTone({ f: [2800, 3600], f2: 900, d: 0.38, vol: 0.2, wet: 0.4 }), sceneTone({ f: [5200, 6400], f2: 2e3, d: 0.2, vol: 0.05 })]),
16275
+ impact2("ricochet_stone", [sceneNoise({ type: "bandpass", f: [2200, 2800], q: 2, d: 0.04, vol: 0.8 }), sceneTone({ f: [1800, 2400], f2: 1200, d: 0.08, vol: 0.1 }), ...sceneMany(3, () => sceneNoise({ type: "bandpass", f: [2500, 4500], q: 3, d: 0.015, vol: 0.2, delay: [0.02, 0.12] }))]),
16276
+ // breaks a material has of its own (the rest keep the art scene's shatter, crumble and splinter)
16277
+ impact2("ceramic_break", [sceneNoise({ type: "highpass", f: 1800, d: 0.3, vol: 0.85, wet: 0.4 }), ...sceneMany(8, () => sceneTone({ f: [1800, 4500], d: [0.04, 0.1], vol: 0.15, delay: [0, 0.4] }))]),
16278
+ impact2("tempered_burst", [sceneNoise({ type: "highpass", f: 3500, d: 0.6, vol: 0.9, wet: 0.4 }), ...sceneMany(16, () => sceneTone({ f: [4e3, 8e3], d: 0.04, vol: 0.08, delay: [0, 0.8] }))]),
16279
+ impact2("plastic_crack", [sceneNoise({ type: "bandpass", f: 1800, q: 2, d: 0.06, vol: 0.75 }), sceneTone({ f: [450, 550], f2: 250, d: 0.05, vol: 0.22 })]),
16280
+ impact2("mdf_crumble", [sceneNoise({ type: "lowpass", f: 700, d: 0.25, vol: 0.75, wet: 0.2 }), ...sceneMany(4, () => sceneNoise({ type: "lowpass", f: 500, d: 0.05, vol: 0.32, delay: [0.05, 0.3] }))])
14850
16281
  ];
14851
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
16282
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, ...IMPACT_SOUNDS, EXPLOSION_BOOM, ...BLAST_SOUNDS, ...FIRE_SOUNDS], loops: [BURN_LOOP, FIRE_LOOP, ...FIRE_FUEL_LOOPS] };
14852
16283
 
14853
16284
  // src/audio/sound.ts
16285
+ var IMPACT_SOUND_KEYS = {
16286
+ impact_metal_ballistic: { sound: "metal_bullet" },
16287
+ impact_metal_blunt: { sound: "metal_blunt" },
16288
+ impact_metal_slash: { sound: "metal_blade" },
16289
+ impact_aluminium_ballistic: { sound: "metal_thin" },
16290
+ impact_aluminium_blunt: { sound: "metal_thin" },
16291
+ impact_copper_ballistic: { sound: "metal_thin" },
16292
+ impact_copper_blunt: { sound: "metal_thin" },
16293
+ impact_wood_ballistic: { sound: "wood_hit" },
16294
+ impact_wood_blunt: { sound: "wood_blunt" },
16295
+ impact_wood_slash: { sound: "wood_blade" },
16296
+ impact_mdf_ballistic: { sound: "mdf_puff" },
16297
+ impact_mdf_blunt: { sound: "mdf_puff", volumeScale: 0.8 },
16298
+ impact_glass_ballistic: { sound: "glass_bullet" },
16299
+ impact_glass_blunt: { sound: "glass_blunt" },
16300
+ impact_glass_slash: { sound: "glass_blade" },
16301
+ impact_ceramic: { sound: "ceramic_hit" },
16302
+ impact_ceramic_ballistic: { sound: "ceramic_bullet" },
16303
+ impact_masonry_ballistic: { sound: "masonry_bullet" },
16304
+ impact_masonry_blunt: { sound: "masonry_blunt" },
16305
+ impact_masonry_slash: { sound: "masonry_blade" },
16306
+ impact_plaster: { sound: "plaster_puff" },
16307
+ impact_soil: { sound: "soil_blunt" },
16308
+ impact_soil_ballistic: { sound: "soil_bullet" },
16309
+ impact_sand: { sound: "sand_hit" },
16310
+ impact_gravel: { sound: "gravel_hit" },
16311
+ impact_plastic: { sound: "plastic_hit" },
16312
+ impact_plastic_ballistic: { sound: "plastic_bullet" },
16313
+ impact_rubber: { sound: "rubber_hit" },
16314
+ impact_wax: { sound: "wax_hit" },
16315
+ impact_soft_ballistic: { sound: "soft_bullet" },
16316
+ impact_soft_slash: { sound: "fabric_rip" },
16317
+ impact_paper: { sound: "paper_hit" },
16318
+ impact_paper_slash: { sound: "paper_tear" },
16319
+ impact_plant: { sound: "leaf_rustle" },
16320
+ impact_plant_slash: { sound: "branch_cut" },
16321
+ impact_flesh_fruit: { sound: "fruit_splat" },
16322
+ ricochet_metal: { sound: "ricochet_metal" },
16323
+ ricochet_masonry: { sound: "ricochet_stone" },
16324
+ ricochet_soil: { sound: "ricochet_stone" },
16325
+ ricochet_ceramic: { sound: "ricochet_stone" },
16326
+ break_ceramic: { sound: "ceramic_break" },
16327
+ break_tempered_glass: { sound: "tempered_burst" },
16328
+ break_plastic: { sound: "plastic_crack" },
16329
+ break_mdf: { sound: "mdf_crumble" },
16330
+ break_soft: { sound: "fabric_rip" },
16331
+ break_paper: { sound: "paper_tear" },
16332
+ break_plant: { sound: "branch_cut" }
16333
+ };
14854
16334
  var DESTRUCTION_SOUND_KEYS = {
14855
16335
  art_scene: {
14856
16336
  impact_metal: { sound: "clang" },
@@ -14858,6 +16338,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14858
16338
  impact_glass: { sound: "ping" },
14859
16339
  impact_masonry: { sound: "chip" },
14860
16340
  impact_soft: { sound: "thud" },
16341
+ // a round strikes: the steel rings high, glass cracks, wood and masonry chip, a soft part takes a dull thunk
16342
+ shot_metal: { sound: "ping" },
16343
+ shot_wood: { sound: "chip" },
16344
+ shot_glass: { sound: "crack" },
16345
+ shot_masonry: { sound: "chip" },
16346
+ shot_soft: { sound: "thunk" },
14861
16347
  drag_wood: { sound: "drag_wood" },
14862
16348
  drag_metal: { sound: "drag_metal" },
14863
16349
  drag_glass: { sound: "drag_glass" },
@@ -14883,22 +16369,38 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14883
16369
  hood_shut: { sound: "shut_metal" },
14884
16370
  trunk_shut: { sound: "shut_metal" },
14885
16371
  door_close: { sound: "shut_wood", volumeScale: 0.7 },
16372
+ // a vehicle's leaves (the vehicle bank of game-vehicles-three): a car door shuts with a padded thump and a latch, not like a fridge
16373
+ vehicle_door_open: { sound: "car_door_open", fallback: "latch" },
16374
+ vehicle_door_shut: { sound: "car_door_close", fallback: "shut_metal" },
16375
+ vehicle_trunk_open: { sound: "car_door_open", fallback: "latch" },
16376
+ vehicle_trunk_shut: { sound: "car_trunk_close", fallback: "shut_metal" },
16377
+ vehicle_hood_open: { sound: "car_door_open", fallback: "latch" },
16378
+ vehicle_hood_shut: { sound: "car_hood_close", fallback: "shut_metal" },
14886
16379
  drawer_open: { sound: "slide" },
14887
16380
  drawer_close: { sound: "slide" },
14888
16381
  lid_open: { sound: "latch" },
14889
16382
  lid_close: { sound: "shut_soft" },
14890
16383
  flap_open: { sound: "latch" },
14891
16384
  flap_close: { sound: "shut_soft" },
16385
+ // an item taken in the hand, by what it is made of (game-audio's KIT_SAMPLE_ALIASES puts recordings on these ids)
16386
+ grab_metal: { sound: "grab_hard" },
16387
+ grab_wood: { sound: "grab_hard" },
16388
+ grab_glass: { sound: "grab_hard", volumeScale: 0.7 },
16389
+ grab_masonry: { sound: "grab_hard" },
16390
+ grab_soft: { sound: "grab_soft" },
14892
16391
  shatter_glass: { sound: "glass_shatter" },
14893
16392
  crumble: { sound: "crumble" },
14894
16393
  splinter: { sound: "crack" },
14895
16394
  ricochet: { sound: "ping" },
14896
16395
  unscrew: { sound: "chip" },
14897
16396
  detach: { sound: "thunk" },
14898
- ignite: { sound: "hiss", params: { seconds: 0.5 } },
14899
- extinguish: { sound: "hiss" },
16397
+ ignite: { sound: "fire_ignite" },
16398
+ primed: { sound: "gas_leak" },
16399
+ extinguish: { sound: "fire_extinguish" },
14900
16400
  melt_sizzle: { sound: "hiss", params: { seconds: 0.6 } },
14901
- explode: { sound: "boom", above: { volume: 0.9, sound: "boom_big" } }
16401
+ // the blast family comes from the explosion's effect (`BLAST_KIND_OF`); this rule answers only an explosion the kit cannot place
16402
+ explode: { sound: "explosion_grenade", above: { volume: 0.9, sound: "explosion_big" } },
16403
+ ...IMPACT_SOUND_KEYS
14902
16404
  },
14903
16405
  game: {
14904
16406
  impact_metal: { sound: "sfx_impact" },
@@ -14912,50 +16414,174 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14912
16414
  explode: { sound: "explosion_boom" }
14913
16415
  }
14914
16416
  };
14915
- var DEFAULT_FIRE = { perFire: 0.05, max: 0.2 };
16417
+ var DEFAULT_FIRE = { perFire: 0.08, max: 0.45 };
16418
+ var DEFAULT_CRACKLES = { base: 2, perSqrtFire: 3, max: 14, volume: 0.5 };
16419
+ var DEFAULT_INDOOR_CONFINEMENT = 1.3;
14916
16420
  function createDestructionSound(engine, options = {}) {
14917
16421
  engine.register(DESTRUCTION_SOUND_BANK);
14918
16422
  engine.register(GAME_SFX_BANK);
14919
- const fire = { ...DEFAULT_FIRE, ...options.fire };
16423
+ engine.register(FOLEY_BANK);
16424
+ const fire2 = { ...DEFAULT_FIRE, ...options.fire };
16425
+ const crackles = options.crackles === false ? null : { ...DEFAULT_CRACKLES, ...options.crackles };
16426
+ const fireLoop = options.fireLoop ?? "fire_loop";
16427
+ const kinds = { ...BLAST_KIND_OF, ...options.blastKinds };
16428
+ const farDistance = options.blastFarDistance ?? BLAST_FAR_DISTANCE;
16429
+ const indoorFrom = options.indoorConfinement ?? DEFAULT_INDOOR_CONFINEMENT;
14920
16430
  const rules = { ...DESTRUCTION_SOUND_KEYS.art_scene, ...options.style === "game" ? DESTRUCTION_SOUND_KEYS.game : {}, ...options.keys };
14921
- let loop;
14922
- let burning = 0;
16431
+ const blastByKind = options.style !== "game" && !(options.keys && "explode" in options.keys);
16432
+ const channel = engine.channel({ seed: options.seed ?? 1 });
16433
+ const fuelOf = { ...FIRE_FUEL_OF, ...options.fuels };
16434
+ const loopOf = { ...FIRE_LOOP_OF, ...options.fireLoop ? { wood: fireLoop, fabric: fireLoop } : {}, ...options.fireLoops };
16435
+ const defaultFuel = options.defaultFuel ?? "wood";
16436
+ const loops = /* @__PURE__ */ new Map();
16437
+ const fires = [];
16438
+ let nextCrackle = 0;
16439
+ const fuelFor = (detail) => detail?.fuel ?? (detail?.material !== void 0 ? fuelOf[detail.material] : void 0) ?? defaultFuel;
14923
16440
  const setFire = () => {
14924
16441
  if (options.burnLoop === false) return;
14925
- if (burning <= 0) {
14926
- loop?.stop();
14927
- loop = void 0;
14928
- return;
16442
+ const count = /* @__PURE__ */ new Map();
16443
+ for (const f of fires) {
16444
+ const id = loopOf[f.fuel];
16445
+ count.set(id, (count.get(id) ?? 0) + 1);
16446
+ }
16447
+ for (const [id, handle] of loops) {
16448
+ if (count.has(id)) continue;
16449
+ handle.stop();
16450
+ loops.delete(id);
16451
+ }
16452
+ for (const [id, n] of count) {
16453
+ let handle = loops.get(id);
16454
+ if (!handle) {
16455
+ handle = engine.loop(id);
16456
+ loops.set(id, handle);
16457
+ }
16458
+ handle.set({ volume: Math.min(fire2.max, fire2.perFire * n) });
14929
16459
  }
14930
- loop ?? (loop = engine.loop("burn_loop"));
14931
- loop.set({ volume: Math.min(fire.max, fire.perFire * burning) });
16460
+ };
16461
+ const forget = (point, fuel) => {
16462
+ let best = -1;
16463
+ let bestD = Infinity;
16464
+ fires.forEach((f, i) => {
16465
+ if (fuel !== void 0 && f.fuel !== fuel) return;
16466
+ const d = (f.point[0] - point[0]) ** 2 + (f.point[1] - point[1]) ** 2 + (f.point[2] - point[2]) ** 2;
16467
+ if (d < bestD) {
16468
+ bestD = d;
16469
+ best = i;
16470
+ }
16471
+ });
16472
+ if (best < 0 && fuel !== void 0) forget(point, void 0);
16473
+ else if (best >= 0) fires.splice(best, 1);
16474
+ };
16475
+ const reset = () => {
16476
+ for (const handle of loops.values()) handle.stop();
16477
+ loops.clear();
16478
+ fires.length = 0;
16479
+ };
16480
+ const blastSound = (point, volume, detail) => {
16481
+ const named = detail?.effect === void 0 ? void 0 : kinds[detail.effect];
16482
+ const kind = named ?? ((detail?.radius ?? 0) >= BIG_BLAST_RADIUS || volume >= 0.9 ? "big" : "grenade");
16483
+ const sound = BLAST_SOUND_OF[kind];
16484
+ const distance = engine.plan(sound, { position: point, seed: 1 })?.distance ?? 0;
16485
+ return kind !== "molotov" && distance > farDistance ? "explosion_far" : sound;
14932
16486
  };
14933
16487
  return {
14934
- onSound(key, point, volume) {
16488
+ onSound(key, point, volume, detail) {
14935
16489
  if (key === "burn_loop") {
14936
- burning += 1;
16490
+ fires.push({ point, fuel: fuelFor(detail) });
14937
16491
  setFire();
14938
16492
  return;
14939
16493
  }
14940
16494
  if (key === "burn_end") {
14941
- if (burning > 0) burning -= 1;
16495
+ forget(point, detail?.fuel ?? (detail?.material !== void 0 ? fuelOf[detail.material] : void 0));
14942
16496
  setFire();
14943
16497
  return;
14944
16498
  }
14945
- const rule = rules[key];
14946
- if (!rule) return;
14947
- const sound = rule.above && volume >= rule.above.volume ? rule.above.sound : rule.sound;
14948
- engine.play(sound, { position: point, volume: volume * (rule.volumeScale ?? 1), ...rule.params ? { params: rule.params } : {} });
16499
+ if (key === "explode" && blastByKind) {
16500
+ const sound2 = blastSound(point, volume, detail);
16501
+ engine.play(sound2, { position: point, volume });
16502
+ if (sound2 !== "explosion_far" && sound2 !== "molotov_break_ignite" && (detail?.confinement ?? 1) >= indoorFrom) engine.play("explosion_indoor_tail", { position: point, volume });
16503
+ return;
16504
+ }
16505
+ let rule;
16506
+ for (const k of detail?.keys ?? []) {
16507
+ if (k in rules) {
16508
+ rule = rules[k];
16509
+ break;
16510
+ }
16511
+ }
16512
+ if (rule === void 0) rule = key in rules ? rules[key] : rules[key.replace(/^shot_/, "impact_")];
16513
+ if (rule === null) return;
16514
+ if (!rule) {
16515
+ if (engine.registry.sound(key)) engine.play(key, { position: point, volume });
16516
+ return;
16517
+ }
16518
+ const wanted = rule.above && volume >= rule.above.volume ? rule.above.sound : rule.sound;
16519
+ const sound = rule.fallback && !engine.registry.sound(wanted) ? rule.fallback : wanted;
16520
+ const params = detail?.seconds === void 0 ? rule.params : { ...rule.params, seconds: detail.seconds };
16521
+ engine.play(sound, { position: point, volume: volume * (rule.volumeScale ?? 1), ...params ? { params } : {} });
14949
16522
  },
14950
- rule: (key) => rules[key] ?? void 0,
14951
- burning: () => burning,
14952
- dispose() {
14953
- loop?.stop();
14954
- loop = void 0;
14955
- burning = 0;
14956
- }
16523
+ update(dt) {
16524
+ if (!crackles || fires.length === 0 || options.burnLoop === false) return;
16525
+ const weight = fires.reduce((sum, f) => sum + FIRE_CRACKLE_OF[f.fuel], 0);
16526
+ if (weight <= 0) return;
16527
+ nextCrackle -= dt;
16528
+ for (let i = 0; i < 4 && nextCrackle <= 0; i++) {
16529
+ const rate = Math.min(crackles.max, crackles.base * Math.min(1, weight) + crackles.perSqrtFire * Math.sqrt(weight));
16530
+ nextCrackle += (0.4 + channel.random() * 1.2) / rate;
16531
+ let pick3 = channel.random() * weight;
16532
+ const at3 = fires.find((f) => (pick3 -= FIRE_CRACKLE_OF[f.fuel]) <= 0) ?? fires[fires.length - 1];
16533
+ engine.play(channel.random() < 0.18 ? "fire_pop" : "fire_crackle", { position: at3.point, volume: crackles.volume });
16534
+ }
16535
+ if (nextCrackle < 0) nextCrackle = 0;
16536
+ },
16537
+ blastSound,
16538
+ rule(key, detail) {
16539
+ for (const k of detail?.keys ?? []) if (k in rules) return rules[k] ?? void 0;
16540
+ return rules[key] ?? void 0;
16541
+ },
16542
+ burning: () => fires.length,
16543
+ burningBy: () => {
16544
+ const by = {};
16545
+ for (const f of fires) by[f.fuel] = (by[f.fuel] ?? 0) + 1;
16546
+ return by;
16547
+ },
16548
+ reset,
16549
+ dispose: reset
14957
16550
  };
14958
16551
  }
16552
+
16553
+ // src/audio/samples.ts
16554
+ var blast2 = (gain) => ({ bus: "world", gain, falloff: BLAST_FALLOFF, cutoffK: 6e-3, priority: 3 });
16555
+ var fire = (gain, group) => ({ bus: "world", gain, falloff: FIRE_FALLOFF, cutoffK: 0.01, ...group ? { group } : {} });
16556
+ var BLAST_FIRE_SAMPLES = {
16557
+ explosion_grenade: blast2(1),
16558
+ explosion_big: blast2(1),
16559
+ explosion_car: blast2(1),
16560
+ explosion_flashbang: blast2(1),
16561
+ molotov_break_ignite: blast2(0.8),
16562
+ // the far takes are already muffled: no extra distance lowpass
16563
+ explosion_far: { bus: "world", gain: 1, falloff: BLAST_FAR_FALLOFF, cutoffK: 0, priority: 2 },
16564
+ fire_ignite: fire(0.8, "fire_ignite"),
16565
+ fire_crackle: fire(0.8, "fire_crackle"),
16566
+ fire_pop: fire(0.6, "fire_crackle"),
16567
+ fire_extinguish: fire(0.7),
16568
+ gas_leak: fire(0.5),
16569
+ // the roar: recorded fires chained with a crossfade (the level comes from `createDestructionSound`, by how much burns)
16570
+ fire_loop: { bus: "world", gain: 4, loop: { crossfade: 1 } },
16571
+ // each fuel has its own roar: a gas jet (burners and torches), a pool of fuel, plastic and rubber sizzling under the flames
16572
+ fire_loop_gas: { bus: "world", gain: 6, loop: { crossfade: 1 } },
16573
+ fire_loop_liquid: { bus: "world", gain: 3.5, loop: { crossfade: 1 } },
16574
+ fire_loop_plastic: { bus: "world", gain: 1, loop: { crossfade: 0.6 } }
16575
+ };
16576
+ function blastFireSampleBank(manifest, options = {}) {
16577
+ const sounds = options.sounds ?? BLAST_FIRE_SAMPLES;
16578
+ const bank = sampleBankFromManifest(manifest, { id: options.id ?? "destruction-samples", ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl }, sounds });
16579
+ const samples = (bank.samples ?? []).map((sample) => {
16580
+ const loop = sounds[sample.id]?.loop;
16581
+ return loop === void 0 ? sample : { ...sample, loop };
16582
+ });
16583
+ return { ...bank, samples };
16584
+ }
14959
16585
  return __toCommonJS(index_exports);
14960
16586
  })();
14961
16587
  //# sourceMappingURL=index.global.js.map