@adatechnology/game-destruction-three 0.12.1 → 0.14.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,38 +23,131 @@ 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]);
56
116
  return { speed, direction: [direction[0] / length, direction[1] / length, direction[2] / length], distance, falloff };
57
117
  }
118
+ var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
119
+ var DEFERRED_QUEUE_ID = {
120
+ DESTRUCTION_EVENTS: "destruction.events",
121
+ BREACH_PAINT: "floorplan.breach-paint",
122
+ AUDIO_VOICES: "audio.voices",
123
+ VEHICLE_CREASE: "vehicles.crease"
124
+ };
125
+ var DEFERRED_PRIORITY = {
126
+ [DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
127
+ [DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
128
+ [DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
129
+ [DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
130
+ };
131
+ var WARNED_KEY = /* @__PURE__ */ Symbol.for("@adatechnology/game-contracts.warned");
132
+ var warnedKeys = () => {
133
+ const holder = globalThis;
134
+ return holder[WARNED_KEY] ?? (holder[WARNED_KEY] = /* @__PURE__ */ new Set());
135
+ };
136
+ var isProduction = () => {
137
+ try {
138
+ return process.env.NODE_ENV === "production";
139
+ } catch {
140
+ return false;
141
+ }
142
+ };
143
+ var isQuiet = () => globalThis.__GAME_KIT_QUIET__ === true;
144
+ var warnOnce = (key, message) => {
145
+ if (isQuiet() || isProduction()) return;
146
+ const seen = warnedKeys();
147
+ if (seen.has(key)) return;
148
+ seen.add(key);
149
+ console.warn(`[game-kit] ${message}`);
150
+ };
58
151
  var KIT_ORIGIN_KEY = "kitOrigin";
59
152
  function markKitOrigin(object, origin) {
60
153
  const node = object;
@@ -69,6 +162,43 @@ var GameDestructionThree = (() => {
69
162
  }
70
163
  return null;
71
164
  }
165
+ var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
166
+ var GAIT_IDS = ["walk", "run"];
167
+ var LANDING_IDS = ["light", "heavy"];
168
+ var BODY_SOUND_IDS = [
169
+ "jump",
170
+ "cloth_rustle",
171
+ "body_fall",
172
+ "fall_damage",
173
+ "player_hurt",
174
+ "player_breath_run",
175
+ "player_death",
176
+ "bite",
177
+ "grab",
178
+ "punch_swing",
179
+ "punch_hit_body",
180
+ "punch_hit_face",
181
+ "punch_hit_object",
182
+ "kick_swing",
183
+ "kick_hit",
184
+ "kick_hit_object",
185
+ "shove_two_hands",
186
+ "knife_swing",
187
+ "knife_stab",
188
+ "knife_slash",
189
+ "knife_hit_object",
190
+ "blunt_swing",
191
+ "blunt_hit",
192
+ "blunt_hit_object",
193
+ "axe_swing",
194
+ "axe_chop",
195
+ "axe_hit_object"
196
+ ];
197
+ var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
198
+ var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
199
+ var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing2) => `land_${surface}_${landing2}`));
200
+ var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
201
+ var SWING_LEAD_SECONDS = { punch: 0.07, kick: 0.09, shove: 0, blade: 0.05, blunt: 0.1, chop: 0.11 };
72
202
 
73
203
  // ../game-vfx/dist/index.js
74
204
  var __defProp2 = Object.defineProperty;
@@ -1803,48 +1933,48 @@ var GameDestructionThree = (() => {
1803
1933
  }));
1804
1934
  }
1805
1935
  function spawnFire(pool, random, init, handle) {
1806
- const fire = pool.acquire();
1807
- fire.x = init.x ?? 0;
1808
- fire.y = init.y ?? 0;
1809
- fire.z = init.z ?? 0;
1810
- fire.r = init.r ?? 0.6;
1811
- fire.r0 = init.startRadius ?? fire.r;
1812
- fire.grow = init.grow ?? 0;
1813
- fire.life = init.life ?? 6;
1814
- fire.t = 0;
1815
- fire.rate = init.rate ?? 22;
1816
- fire.acc = 0;
1817
- fire.smoke = init.smoke ?? 0.6;
1818
- fire.smokeAcc = 0;
1819
- fire.fade = 1;
1820
- fire.follow = init.follow ?? null;
1821
- fire.fx = init.fx ?? 0;
1822
- fire.fy = init.fy ?? 0;
1823
- fire.fz = init.fz ?? 0;
1824
- fire.color = init.color ?? SMOKE_COLORS.black;
1825
- fire.rng = random;
1826
- fire.handle = handle;
1827
- 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;
1828
1958
  }
1829
1959
  function addFire(core, random, init, handle) {
1830
- const fire = spawnFire(core.fires, random, init, handle);
1831
- core.trace?.fire?.(fire);
1832
- return fire;
1960
+ const fire2 = spawnFire(core.fires, random, init, handle);
1961
+ core.trace?.fire?.(fire2);
1962
+ return fire2;
1833
1963
  }
1834
1964
  function positionOf(follow) {
1835
1965
  return "position" in follow ? follow.position : follow;
1836
1966
  }
1837
- function emitFlames(core, fire, radius, dt) {
1838
- const random = fire.rng;
1839
- fire.acc += dt * fire.rate * (0.5 + radius * 0.9) * fire.fade * core.clock.lod;
1840
- while (fire.acc >= 1) {
1841
- 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;
1842
1972
  const angle = random.next() * Math.PI * 2;
1843
1973
  const distance = Math.sqrt(random.next()) * radius;
1844
1974
  addPuff(core, random, {
1845
- x: fire.x + Math.cos(angle) * distance,
1846
- y: fire.y + 0.05,
1847
- 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,
1848
1978
  vx: random.between(-0.2, 0.2),
1849
1979
  vy: random.between(0.6, 1.4),
1850
1980
  vz: random.between(-0.2, 0.2),
@@ -1856,21 +1986,21 @@ var GameDestructionThree = (() => {
1856
1986
  hl: random.between(0.35, 0.6),
1857
1987
  drag: 1.5,
1858
1988
  buoy: 3.2,
1859
- color: fire.color,
1989
+ color: fire2.color,
1860
1990
  kind: 0,
1861
1991
  wind: 0.4
1862
1992
  });
1863
1993
  }
1864
1994
  }
1865
- function emitSmoke(core, fire, radius, dt) {
1866
- const random = fire.rng;
1867
- fire.smokeAcc += dt * fire.smoke * (1 + radius * 0.8) * fire.fade * core.clock.lod;
1868
- while (fire.smokeAcc >= 1 && core.layers.smoke) {
1869
- 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;
1870
2000
  addPuff(core, random, {
1871
- x: fire.x + random.between(-radius, radius) * 0.5,
1872
- y: fire.y + 0.6 + radius * 0.3,
1873
- 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,
1874
2004
  vy: random.between(0.8, 1.5),
1875
2005
  s0: 0.25,
1876
2006
  s1: 0.6 + Math.min(radius, 2) * 0.3,
@@ -1880,18 +2010,18 @@ var GameDestructionThree = (() => {
1880
2010
  hl: 0.25,
1881
2011
  drag: 0.6,
1882
2012
  buoy: 0.55,
1883
- color: fire.color,
2013
+ color: fire2.color,
1884
2014
  kind: 0
1885
2015
  });
1886
2016
  }
1887
2017
  }
1888
- function emitEmber(core, fire, radius, dt) {
1889
- const random = fire.rng;
1890
- 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;
1891
2021
  addSpark(core, {
1892
- x: fire.x + random.between(-radius, radius) * 0.6,
1893
- y: fire.y + 0.4,
1894
- 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,
1895
2025
  vx: random.between(-0.4, 0.4),
1896
2026
  vy: random.between(1.5, 3.2),
1897
2027
  vz: random.between(-0.4, 0.4),
@@ -1906,24 +2036,24 @@ var GameDestructionThree = (() => {
1906
2036
  function updateFires(core, dt) {
1907
2037
  const pool = core.fires;
1908
2038
  for (let index = pool.items.length - 1; index >= 0; index--) {
1909
- const fire = pool.items[index];
1910
- fire.t += dt;
1911
- if (fire.follow) {
1912
- const point = positionOf(fire.follow);
1913
- fire.x = point.x + fire.fx;
1914
- fire.y = point.y + fire.fy;
1915
- fire.z = point.z + fire.fz;
1916
- }
1917
- const radius = fire.grow ? lerp(fire.r0, fire.r, Math.min(1, fire.t / fire.grow)) : fire.r;
1918
- fire.fade = fire.t < fire.life - 1.5 ? Math.min(1, fire.t * 3) : Math.max(0, (fire.life - fire.t) / 1.5);
1919
- 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) {
1920
2050
  pool.removeAt(index);
1921
2051
  continue;
1922
2052
  }
1923
2053
  if (!core.layers.fire) continue;
1924
- emitFlames(core, fire, radius, dt);
1925
- emitSmoke(core, fire, radius, dt);
1926
- emitEmber(core, fire, radius, dt);
2054
+ emitFlames(core, fire2, radius, dt);
2055
+ emitSmoke(core, fire2, radius, dt);
2056
+ emitEmber(core, fire2, radius, dt);
1927
2057
  }
1928
2058
  }
1929
2059
  var call = (name) => (...args) => ({ $: name, a: args });
@@ -2564,22 +2694,22 @@ var GameDestructionThree = (() => {
2564
2694
  )
2565
2695
  ]
2566
2696
  };
2567
- function normalized(vector) {
2568
- const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]) || 1;
2697
+ function normalized(vector2) {
2698
+ const length = Math.sqrt(vector2[0] * vector2[0] + vector2[1] * vector2[1] + vector2[2] * vector2[2]) || 1;
2569
2699
  const inverse = 1 / length;
2570
- return [vector[0] * inverse, vector[1] * inverse, vector[2] * inverse];
2700
+ return [vector2[0] * inverse, vector2[1] * inverse, vector2[2] * inverse];
2571
2701
  }
2572
- function plus(vector, x, y, z) {
2573
- return [vector[0] + x, vector[1] + y, vector[2] + z];
2702
+ function plus(vector2, x, y, z) {
2703
+ return [vector2[0] + x, vector2[1] + y, vector2[2] + z];
2574
2704
  }
2575
- function scaledBy(vector, factor) {
2576
- return [vector[0] * factor, vector[1] * factor, vector[2] * factor];
2705
+ function scaledBy(vector2, factor) {
2706
+ return [vector2[0] * factor, vector2[1] * factor, vector2[2] * factor];
2577
2707
  }
2578
- function plusScaled(vector, direction, factor) {
2579
- return [vector[0] + direction[0] * factor, vector[1] + direction[1] * factor, vector[2] + direction[2] * factor];
2708
+ function plusScaled(vector2, direction, factor) {
2709
+ return [vector2[0] + direction[0] * factor, vector2[1] + direction[1] * factor, vector2[2] + direction[2] * factor];
2580
2710
  }
2581
- function negated(vector) {
2582
- return [-vector[0], -vector[1], -vector[2]];
2711
+ function negated(vector2) {
2712
+ return [-vector2[0], -vector2[1], -vector2[2]];
2583
2713
  }
2584
2714
  var FRAME_DUST = normalized([0, -0.3, 1]);
2585
2715
  var SPLINTER_DIRECTION = normalized([0, 0.3, 1]);
@@ -3882,9 +4012,32 @@ var GameDestructionThree = (() => {
3882
4012
  /** The leak caught fire (an incendiary or tracer round, a spark, a fire near): the page's whole sequence, the fire running along the pool to the tank and the blast. */
3883
4013
  aliasOf(FUEL_LEAK, "vehicle_leak_ignited", {})
3884
4014
  ];
4015
+ var vector = [variable("vx"), variable("vy"), variable("vz")];
4016
+ var range = (name) => [variable(name), variable(name)];
4017
+ var singleParticle = (id, system) => ({
4018
+ id,
4019
+ pageId: `particle:${id}`,
4020
+ team: "world",
4021
+ budget: { particles: 1, lights: 0 },
4022
+ loop: 1e-3,
4023
+ timeline: [
4024
+ spawn(system, {
4025
+ p: [0, 0, 0],
4026
+ v: vector,
4027
+ life: range("life"),
4028
+ size: range("size"),
4029
+ grow: variable("grow"),
4030
+ c0: variable("color"),
4031
+ a: variable("alpha"),
4032
+ g: variable("gravity"),
4033
+ drag: variable("drag")
4034
+ })
4035
+ ]
4036
+ });
4037
+ var SINGLE_PARTICLE_EFFECTS = [singleParticle("particle_puff", "normal"), singleParticle("particle_glow", "add"), singleParticle("particle_streak", "streak")];
3885
4038
  var ALL_EFFECTS = [BLOOD_HIT, HEADSHOT, DISMEMBER, BLOATER_CLOUD, SCREAMER_SHRIEK, CORPSE_DECAY, REANIMATION, EYES_IN_FOG, MUZZLE_FLASH, IMPACT_BY_MATERIAL, PENETRATION, CASINGS_AND_MAGAZINE, MOLOTOV, GRENADE, EXPLOSIVE_BARREL, SHOPFRONT_GLASS, DOOR_STAGES, BUILDING_COLLAPSE, BLOCK_WALL, METAL_DOOR_EFFECT, IRON_RAILING, WOODEN_FENCE, HIT_POINT_BREAKAGE, PRESSURE_CYLINDERS, BUILDING_FIRE, RAIN_AND_PUDDLES, FOG_AND_ASH, SCRAPING_SPARKS, ENGINE_SMOKE, FUEL_LEAK, CAR_EXPLOSION, WINDSHIELD, HEADLIGHTS_IN_RAIN, DAMAGE_AND_BITE, INFECTION_PHASES, PANIC, BLOATER_BLINDNESS, CAMPFIRE, COUGH_AND_BREATH];
3886
4039
  var WORKSHOP_EFFECTS = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]));
3887
- var GAME_EFFECTS = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS, ...VEHICLE_EFFECTS].map((effect) => [effect.id, effect])) };
4040
+ var GAME_EFFECTS = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS, ...VEHICLE_EFFECTS, ...SINGLE_PARTICLE_EFFECTS].map((effect) => [effect.id, effect])) };
3888
4041
  var TABLE = {
3889
4042
  concrete: { dust: 9078138, chip: 7236195, spark: 2 },
3890
4043
  metal: { dust: 0, chip: 0, spark: 18 },
@@ -4365,35 +4518,35 @@ var GameDestructionThree = (() => {
4365
4518
  var NOOP = () => {
4366
4519
  };
4367
4520
  function createSchedule() {
4368
- const entries = [];
4521
+ const entries2 = [];
4369
4522
  const spare = [];
4370
4523
  return {
4371
4524
  at(time, action) {
4372
4525
  const entry = spare.pop() ?? { time: 0, action: NOOP };
4373
4526
  entry.time = time;
4374
4527
  entry.action = action;
4375
- entries.push(entry);
4528
+ entries2.push(entry);
4376
4529
  },
4377
4530
  update(now) {
4378
- for (let index = entries.length - 1; index >= 0; index--) {
4379
- const entry = entries[index];
4531
+ for (let index = entries2.length - 1; index >= 0; index--) {
4532
+ const entry = entries2[index];
4380
4533
  if (!entry || entry.time > now) continue;
4381
4534
  const action = entry.action;
4382
- entries.splice(index, 1);
4535
+ entries2.splice(index, 1);
4383
4536
  entry.action = NOOP;
4384
4537
  spare.push(entry);
4385
4538
  action();
4386
4539
  }
4387
4540
  },
4388
4541
  clear() {
4389
- for (const entry of entries) {
4542
+ for (const entry of entries2) {
4390
4543
  entry.action = NOOP;
4391
4544
  spare.push(entry);
4392
4545
  }
4393
- entries.length = 0;
4546
+ entries2.length = 0;
4394
4547
  },
4395
4548
  get pending() {
4396
- return entries.length;
4549
+ return entries2.length;
4397
4550
  }
4398
4551
  };
4399
4552
  }
@@ -4554,8 +4707,8 @@ var GameDestructionThree = (() => {
4554
4707
  var UP12 = [0, 1, 0];
4555
4708
  var ZERO = [0, 0, 0];
4556
4709
  var FULL_TURN_DRAWN = 6.283;
4557
- function scaledSizes(range, factor) {
4558
- return [range[0] * Math.sqrt(factor), range[1] * Math.sqrt(factor)];
4710
+ function scaledSizes(range2, factor) {
4711
+ return [range2[0] * Math.sqrt(factor), range2[1] * Math.sqrt(factor)];
4559
4712
  }
4560
4713
  function fireEmit(engine, key, dt, centre, radius, factor, options = {}) {
4561
4714
  const wind = options.wind ?? ZERO;
@@ -5081,9 +5234,9 @@ var GameDestructionThree = (() => {
5081
5234
  }
5082
5235
  function largestFire(fires, skip) {
5083
5236
  let best = null;
5084
- for (const fire of fires) {
5085
- if (fire === skip) continue;
5086
- 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;
5087
5240
  }
5088
5241
  return best;
5089
5242
  }
@@ -5106,17 +5259,17 @@ var GameDestructionThree = (() => {
5106
5259
  const second = largestFire(step.fires, first);
5107
5260
  const largest = [first, second];
5108
5261
  for (let index = 0; index < FIRE_LIGHTS; index++) {
5109
- const fire = largest[index];
5262
+ const fire2 = largest[index];
5110
5263
  const light = pool.fire[index];
5111
- if (!fire) {
5264
+ if (!fire2) {
5112
5265
  light.intensity = 0;
5113
5266
  continue;
5114
5267
  }
5115
- light.x = fire.x;
5116
- light.y = fire.y + Math.max(0.8, fire.r * 0.6);
5117
- light.z = fire.z;
5118
- 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;
5119
- 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;
5120
5273
  }
5121
5274
  if (brightest && brightness > 0.05) {
5122
5275
  pool.glow.x = brightest.x;
@@ -5148,12 +5301,12 @@ var GameDestructionThree = (() => {
5148
5301
  var SCORCH_VARIANTS = 3;
5149
5302
  var RIM_COLORS = [2829359, 3815478];
5150
5303
  function createMarkStore() {
5151
- const entries = [];
5304
+ const entries2 = [];
5152
5305
  let version = 0;
5153
5306
  let bloodCount = 0;
5154
5307
  const listeners = /* @__PURE__ */ new Set();
5155
5308
  return {
5156
- entries,
5309
+ entries: entries2,
5157
5310
  get version() {
5158
5311
  return version;
5159
5312
  },
@@ -5162,13 +5315,13 @@ var GameDestructionThree = (() => {
5162
5315
  },
5163
5316
  lift: 0,
5164
5317
  add(mark) {
5165
- entries.push(mark);
5318
+ entries2.push(mark);
5166
5319
  if (mark.kind === "blood") bloodCount++;
5167
5320
  version++;
5168
5321
  for (const listener of listeners) listener({ type: "add", mark });
5169
5322
  },
5170
5323
  clear() {
5171
- entries.length = 0;
5324
+ entries2.length = 0;
5172
5325
  bloodCount = 0;
5173
5326
  this.lift = 0;
5174
5327
  version++;
@@ -5622,18 +5775,18 @@ var GameDestructionThree = (() => {
5622
5775
  }
5623
5776
  if (def.mushroom && core.layers.fire) composeMushroom(context, def.mushroom);
5624
5777
  if (def.residualFires && core.layers.fire) {
5625
- for (const fire of def.residualFires) {
5778
+ for (const fire2 of def.residualFires) {
5626
5779
  addFire(core, random, {
5627
- x: x + (fire.dx ?? 0),
5780
+ x: x + (fire2.dx ?? 0),
5628
5781
  y: 0,
5629
- z: z + (fire.dz ?? 0),
5630
- r: fire.radius,
5631
- life: fire.life,
5632
- rate: fire.rate,
5633
- smoke: fire.smoke,
5634
- startRadius: fire.startRadius,
5635
- grow: fire.grow,
5636
- 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
5637
5790
  }, context.call.handle);
5638
5791
  }
5639
5792
  }
@@ -6409,9 +6562,9 @@ var GameDestructionThree = (() => {
6409
6562
  var TWO_PI_ROUNDED = 6.2832;
6410
6563
  var WIDE_CONE = 3.1;
6411
6564
  var UP13 = [0, 1, 0];
6412
- function drawRange(random, range) {
6413
- if (Array.isArray(range)) return random.between(range[0], range[1]);
6414
- return range || 0;
6565
+ function drawRange(random, range2) {
6566
+ if (Array.isArray(range2)) return random.between(range2[0], range2[1]);
6567
+ return range2 || 0;
6415
6568
  }
6416
6569
  function randomUnit(random, out) {
6417
6570
  const up = random.next() * 2 - 1;
@@ -7178,20 +7331,20 @@ var GameDestructionThree = (() => {
7178
7331
  return [origin[0] + cos2 * x + sin2 * z, origin[1] + point[1] - anchor[1], origin[2] - sin2 * x + cos2 * z];
7179
7332
  }
7180
7333
  /** A direction or velocity of the recipe's space in the world. */
7181
- turn(vector) {
7334
+ turn(vector2) {
7182
7335
  const placement = this.placement;
7183
- if (!placement) return vector;
7184
- if (placement.orientation) return rotate(placement.orientation, vector);
7336
+ if (!placement) return vector2;
7337
+ if (placement.orientation) return rotate(placement.orientation, vector2);
7185
7338
  const cos2 = Math.cos(placement.yaw);
7186
7339
  const sin2 = Math.sin(placement.yaw);
7187
- return [cos2 * vector[0] + sin2 * vector[2], vector[1], -sin2 * vector[0] + cos2 * vector[2]];
7340
+ return [cos2 * vector2[0] + sin2 * vector2[2], vector2[1], -sin2 * vector2[0] + cos2 * vector2[2]];
7188
7341
  }
7189
7342
  placed(options) {
7190
7343
  if (!this.placement) return options;
7191
7344
  return { ...options, p: this.place(options.p), ...options.dir ? { dir: this.turn(options.dir) } : {}, ...options.v ? { v: this.turn(options.v) } : {} };
7192
7345
  }
7193
- spawn(system, recipe) {
7194
- const options = this.placed(recipe);
7346
+ spawn(system, recipe2) {
7347
+ const options = this.placed(recipe2);
7195
7348
  this.context.trace?.(system, [options]);
7196
7349
  if (system === "add") this.add.spawn(options);
7197
7350
  else if (system === "normal") this.normal.spawn(options);
@@ -7211,43 +7364,43 @@ var GameDestructionThree = (() => {
7211
7364
  this.accumulators.set(key, carried);
7212
7365
  }
7213
7366
  /** A solid particle (a cone, a sphere): the group sits at `p` turned by the placement, `v` is turned with it and `wv` is not. */
7214
- mesh(recipe) {
7367
+ mesh(recipe2) {
7215
7368
  const placement = this.placement;
7216
- const options = placement ? { ...recipe, p: this.place(recipe.p), ...recipe.v ? { v: this.turn(recipe.v) } : {} } : recipe;
7369
+ const options = placement ? { ...recipe2, p: this.place(recipe2.p), ...recipe2.v ? { v: this.turn(recipe2.v) } : {} } : recipe2;
7217
7370
  this.context.trace?.("mesh", [options]);
7218
- const grouped = recipe.at !== void 0 || recipe.roll !== void 0;
7371
+ const grouped = recipe2.at !== void 0 || recipe2.roll !== void 0;
7219
7372
  return this.meshes.spawn(options, placement && grouped ? placement.orientation ?? [0, Math.sin(placement.yaw / 2), 0, Math.cos(placement.yaw / 2)] : void 0);
7220
7373
  }
7221
- debrisPiece(recipe) {
7222
- const options = this.placed(recipe);
7374
+ debrisPiece(recipe2) {
7375
+ const options = this.placed(recipe2);
7223
7376
  this.context.trace?.("debris", [options]);
7224
7377
  this.debris.spawn(options);
7225
7378
  }
7226
- decal(recipe) {
7227
- let args = recipe;
7379
+ decal(recipe2) {
7380
+ let args = recipe2;
7228
7381
  if (this.placement) {
7229
- const [x, y, z] = this.place([recipe[0], recipe[1], recipe[2]]);
7230
- const options = recipe[5];
7231
- args = [x, y, z, recipe[3], recipe[4], options?.n ? { ...options, n: this.turn(options.n) } : options];
7382
+ const [x, y, z] = this.place([recipe2[0], recipe2[1], recipe2[2]]);
7383
+ const options = recipe2[5];
7384
+ args = [x, y, z, recipe2[3], recipe2[4], options?.n ? { ...options, n: this.turn(options.n) } : options];
7232
7385
  }
7233
7386
  this.context.trace?.("decal", args);
7234
7387
  this.decals.add(args[0], args[1], args[2], args[3], args[4], args[5]);
7235
7388
  }
7236
- ring(recipe) {
7237
- const args = this.placement ? [this.place(recipe[0]), recipe[1], recipe[2], recipe[3], recipe[4], recipe[5] && this.turn(recipe[5]), recipe[6]] : recipe;
7389
+ ring(recipe2) {
7390
+ const args = this.placement ? [this.place(recipe2[0]), recipe2[1], recipe2[2], recipe2[3], recipe2[4], recipe2[5] && this.turn(recipe2[5]), recipe2[6]] : recipe2;
7238
7391
  this.context.trace?.("ring", args);
7239
7392
  this.rings.add(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
7240
7393
  }
7241
7394
  /** `steady` (a seventh number): the light holds its intensity for its duration and goes out, where the Workshop's fades with the square of its age. */
7242
- flash(recipe) {
7243
- const args = this.placement ? [this.place(recipe[0]), recipe[1], recipe[2], recipe[3], recipe[4], recipe[5], recipe[6]] : recipe;
7395
+ flash(recipe2) {
7396
+ const args = this.placement ? [this.place(recipe2[0]), recipe2[1], recipe2[2], recipe2[3], recipe2[4], recipe2[5], recipe2[6]] : recipe2;
7244
7397
  this.context.trace?.("flash", args.slice(0, 6));
7245
7398
  this.lights.flash(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
7246
7399
  }
7247
7400
  /** `fireLight` (W400): sways with the clock and flickers with one draw. */
7248
- fireLight(slot, recipe, intensity, time) {
7401
+ fireLight(slot, recipe2, intensity, time) {
7249
7402
  const light = this.lights.fire[slot];
7250
- const point = this.place(recipe);
7403
+ const point = this.place(recipe2);
7251
7404
  light.x = point[0];
7252
7405
  light.y = point[1];
7253
7406
  light.z = point[2];
@@ -7305,7 +7458,7 @@ var GameDestructionThree = (() => {
7305
7458
  function scaleDefinition(def, factor) {
7306
7459
  if (factor === 1) return def;
7307
7460
  const mushroom = def.mushroom && { ...def.mushroom, height: def.mushroom.height * factor, radius: def.mushroom.radius * factor };
7308
- 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 }));
7309
7462
  return {
7310
7463
  ...def,
7311
7464
  lethal: def.lethal * factor,
@@ -7561,7 +7714,22 @@ var GameDestructionThree = (() => {
7561
7714
  return key === void 0 ? void 0 : entry[key];
7562
7715
  }
7563
7716
 
7564
- // ../game-vfx-three/dist/index.js
7717
+ // ../game-render-three/dist/index.js
7718
+ var DEFAULT_ANGLE = Math.PI / 3;
7719
+ var LIGHT_PRIORITY = {
7720
+ blast: 900,
7721
+ rocket: 800,
7722
+ muzzle: 750,
7723
+ fire: 600,
7724
+ flare: 500,
7725
+ headlight: 400,
7726
+ weaponLight: 400,
7727
+ lamp: 200,
7728
+ street: 150
7729
+ };
7730
+ var EFFECT_STEP_SECONDS = 1 / 50;
7731
+
7732
+ // ../game-vfx-three/dist/chunk-ALDCQ5RH.js
7565
7733
  var GLOW_VERTEX = `attribute vec4 aP;attribute vec4 aC;varying vec2 vU;varying vec4 vC;
7566
7734
  void main(){vec3 R=vec3(viewMatrix[0][0],viewMatrix[1][0],viewMatrix[2][0]);vec3 Up=vec3(viewMatrix[0][1],viewMatrix[1][1],viewMatrix[2][1]);
7567
7735
  vec3 wp=aP.xyz+(R*position.x+Up*position.y)*aP.w;vU=position.xy;vC=aC;gl_Position=projectionMatrix*viewMatrix*vec4(wp,1.);}`;
@@ -7840,15 +8008,33 @@ void main(){
7840
8008
  }
7841
8009
  };
7842
8010
  }
7843
- function createLightLayer(THREE, pool, scale) {
8011
+ var LIGHT_ON_THRESHOLD = 0.05;
8012
+ var FIRE_COLOR = 16742954;
8013
+ var FIRE_DECAY = 2;
8014
+ var BLAST_DECAY = 2;
8015
+ function followEntry(budget, handles, index, entry, request) {
8016
+ const handle = handles[index] ?? null;
8017
+ if (entry.intensity <= LIGHT_ON_THRESHOLD) {
8018
+ handle?.release({ fadeMs: 0 });
8019
+ handles[index] = null;
8020
+ return;
8021
+ }
8022
+ if (handle?.alive) {
8023
+ handle.update({ color: request.color, intensity: request.intensity, distance: request.distance, position: request.position });
8024
+ return;
8025
+ }
8026
+ handles[index] = budget.acquire(request);
8027
+ }
8028
+ function createLightLayer(THREE, pool, scale, budget) {
7844
8029
  const group = new THREE.Group();
7845
- const blast = pool.blast.map(() => {
8030
+ if (budget) return createBudgetedLightLayer(group, pool, scale, budget);
8031
+ const blast3 = pool.blast.map(() => {
7846
8032
  const light = new THREE.PointLight(16752720, 0, 30, 2);
7847
8033
  light.position.set(0, -50, 0);
7848
8034
  group.add(light);
7849
8035
  return light;
7850
8036
  });
7851
- const fire = pool.fire.map(() => {
8037
+ const fire2 = pool.fire.map(() => {
7852
8038
  const light = new THREE.PointLight(16742954, 0, 16, 2);
7853
8039
  light.position.set(0, -50, 0);
7854
8040
  group.add(light);
@@ -7859,7 +8045,7 @@ void main(){
7859
8045
  sync() {
7860
8046
  let lit = 0;
7861
8047
  pool.blast.forEach((entry, index) => {
7862
- const light = blast[index];
8048
+ const light = blast3[index];
7863
8049
  light.position.set(entry.x, entry.y, entry.z);
7864
8050
  light.distance = entry.distance;
7865
8051
  light.color.setRGB(entry.color[0], entry.color[1], entry.color[2], THREE.LinearSRGBColorSpace);
@@ -7867,7 +8053,7 @@ void main(){
7867
8053
  if (entry.intensity > 0.05) lit++;
7868
8054
  });
7869
8055
  pool.fire.forEach((entry, index) => {
7870
- const light = fire[index];
8056
+ const light = fire2[index];
7871
8057
  light.position.set(entry.x, entry.y, entry.z);
7872
8058
  light.distance = entry.distance;
7873
8059
  light.intensity = entry.intensity * scale;
@@ -7876,7 +8062,31 @@ void main(){
7876
8062
  return lit;
7877
8063
  },
7878
8064
  dispose() {
7879
- for (const light of [...blast, ...fire]) light.dispose();
8065
+ for (const light of [...blast3, ...fire2]) light.dispose();
8066
+ }
8067
+ };
8068
+ }
8069
+ function createBudgetedLightLayer(group, pool, scale, budget) {
8070
+ const blast3 = pool.blast.map(() => null);
8071
+ const fire2 = pool.fire.map(() => null);
8072
+ return {
8073
+ group,
8074
+ sync() {
8075
+ let lit = 0;
8076
+ pool.blast.forEach((entry, index) => {
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] };
8078
+ followEntry(budget, blast3, index, entry, request);
8079
+ if (entry.intensity > LIGHT_ON_THRESHOLD) lit++;
8080
+ });
8081
+ pool.fire.forEach((entry, index) => {
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] };
8083
+ followEntry(budget, fire2, index, entry, request);
8084
+ if (entry.intensity > LIGHT_ON_THRESHOLD) lit++;
8085
+ });
8086
+ return lit;
8087
+ },
8088
+ dispose() {
8089
+ for (const handle of [...blast3, ...fire2]) handle?.release({ fadeMs: 0 });
7880
8090
  }
7881
8091
  };
7882
8092
  }
@@ -8013,6 +8223,7 @@ void main(){
8013
8223
  group.add(mesh2);
8014
8224
  return mesh2;
8015
8225
  }
8226
+ let probeGeometry;
8016
8227
  const groundMarks = [];
8017
8228
  const spareGeometries = [];
8018
8229
  const blood = [];
@@ -8118,6 +8329,15 @@ void main(){
8118
8329
  rockMesh();
8119
8330
  plateMesh();
8120
8331
  },
8332
+ probes() {
8333
+ probeGeometry ?? (probeGeometry = new THREE.PlaneGeometry(1e-3, 1e-3).rotateX(-Math.PI / 2));
8334
+ return [...scorchMaterials, dirtMaterial].map((material) => {
8335
+ const mesh2 = new THREE.Mesh(probeGeometry, material);
8336
+ mesh2.receiveShadow = true;
8337
+ mesh2.frustumCulled = false;
8338
+ return mesh2;
8339
+ });
8340
+ },
8121
8341
  update(dt) {
8122
8342
  for (let index = 0; index < blood.length; index++) {
8123
8343
  const drawn = blood[index];
@@ -8130,6 +8350,7 @@ void main(){
8130
8350
  dispose() {
8131
8351
  clearMarks();
8132
8352
  for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) mesh2?.dispose();
8353
+ probeGeometry?.dispose();
8133
8354
  for (const item of owned) item.dispose();
8134
8355
  }
8135
8356
  };
@@ -8705,7 +8926,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8705
8926
  }
8706
8927
  };
8707
8928
  }
8708
- function createWorkshopLayers(THREE, engine, pointScale, lightScale) {
8929
+ function createWorkshopLayers(THREE, engine, pointScale, lightScale, budget) {
8709
8930
  const group = new THREE.Group();
8710
8931
  group.name = "workshop";
8711
8932
  const layers = [
@@ -8720,15 +8941,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8720
8941
  for (const layer2 of layers) group.add(layer2.object);
8721
8942
  const meshes = createMeshLayer(THREE, engine.meshes);
8722
8943
  group.add(meshes.group);
8723
- const flashLights = engine.lights.lights.map(() => new THREE.PointLight(16760944, 0, 14));
8724
- const fireLights = engine.lights.fire.map(() => new THREE.PointLight(FIRE_LIGHT_COLOR, 0, FIRE_LIGHT_DISTANCE));
8725
- for (const light of [...flashLights, ...fireLights]) group.add(light);
8944
+ const lights = budget ? budgetedLights(engine, lightScale, budget) : legacyLights(THREE, engine, lightScale, group);
8726
8945
  return {
8727
8946
  group,
8728
8947
  sync() {
8729
8948
  let drawCalls = 0;
8730
8949
  for (const layer2 of layers) if (layer2.sync()) drawCalls++;
8731
8950
  drawCalls += meshes.sync();
8951
+ lights.sync();
8952
+ return drawCalls;
8953
+ },
8954
+ dispose() {
8955
+ for (const layer2 of layers) layer2.dispose();
8956
+ meshes.dispose();
8957
+ lights.dispose();
8958
+ group.clear();
8959
+ }
8960
+ };
8961
+ }
8962
+ function legacyLights(THREE, engine, lightScale, group) {
8963
+ const flashLights = engine.lights.lights.map(() => new THREE.PointLight(16760944, 0, 14));
8964
+ const fireLights = engine.lights.fire.map(() => new THREE.PointLight(FIRE_LIGHT_COLOR, 0, FIRE_LIGHT_DISTANCE));
8965
+ for (const light of [...flashLights, ...fireLights]) group.add(light);
8966
+ return {
8967
+ sync() {
8732
8968
  engine.lights.lights.forEach((source, index) => {
8733
8969
  const light = flashLights[index];
8734
8970
  light.position.set(source.x, source.y, source.z);
@@ -8741,12 +8977,27 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8741
8977
  light.position.set(source.x, source.y, source.z);
8742
8978
  light.intensity = source.intensity * lightScale;
8743
8979
  });
8744
- return drawCalls;
8745
8980
  },
8746
8981
  dispose() {
8747
- for (const layer2 of layers) layer2.dispose();
8748
- meshes.dispose();
8749
- group.clear();
8982
+ }
8983
+ };
8984
+ }
8985
+ function budgetedLights(engine, lightScale, budget) {
8986
+ const flash2 = engine.lights.lights.map(() => null);
8987
+ const fire2 = engine.lights.fire.map(() => null);
8988
+ return {
8989
+ sync() {
8990
+ engine.lights.lights.forEach((source, index) => {
8991
+ const request = { owner: "vfx.workshop", priority: LIGHT_PRIORITY.blast, urgent: true, color: source.color, intensity: source.intensity * lightScale, distance: source.distance, position: [source.x, source.y, source.z] };
8992
+ followEntry(budget, flash2, index, source, request);
8993
+ });
8994
+ engine.lights.fire.forEach((source, index) => {
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] };
8996
+ followEntry(budget, fire2, index, source, request);
8997
+ });
8998
+ },
8999
+ dispose() {
9000
+ for (const handle of [...flash2, ...fire2]) handle?.release({ fadeMs: 0 });
8750
9001
  }
8751
9002
  };
8752
9003
  }
@@ -8804,7 +9055,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8804
9055
  const chunks = createChunkLayer(THREE, pools.chunks, { rock: rockGeometry, plate: plateGeometry }, colorOfChunk);
8805
9056
  const shocks = createShockLayer(THREE, pools.shocks);
8806
9057
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
8807
- const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
9058
+ const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI, options.lights);
8808
9059
  const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
8809
9060
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
8810
9061
  const stopMarks = world.onMarks((event) => marks.handle(event));
@@ -8837,7 +9088,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8837
9088
  workshopLayers?.dispose();
8838
9089
  if (workshopLayers) root.remove(workshopLayers.group);
8839
9090
  workshopEngine = engine;
8840
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
9091
+ workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1, options.lights);
8841
9092
  root.add(workshopLayers.group);
8842
9093
  applyLayers();
8843
9094
  }
@@ -8910,7 +9161,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8910
9161
  const meshes = createMeshLayerProbes(THREE);
8911
9162
  for (const mesh2 of meshes) mesh2.frustumCulled = false;
8912
9163
  const group = new THREE.Group();
8913
- group.add(...meshes);
9164
+ group.add(...meshes, ...marks.probes());
8914
9165
  anchors = { meshes, group };
8915
9166
  return { group, dispose: () => {
8916
9167
  } };
@@ -9028,6 +9279,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9028
9279
  var BIG_LENGTH = 0.2;
9029
9280
  var BIG_WIDTH = 0.03;
9030
9281
  var FRAGMENT_SEED = 1374496523;
9282
+ var SHARD_SHAPES = 6;
9031
9283
  var isBig = (size) => {
9032
9284
  const [, middle, longest] = [...size].sort((a, b) => a - b);
9033
9285
  return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
@@ -9077,7 +9329,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9077
9329
  return m;
9078
9330
  }
9079
9331
  shardGeometry(seed) {
9080
- const idx = seed % 6;
9332
+ const idx = seed % SHARD_SHAPES;
9081
9333
  let g = this.shardGeos[idx];
9082
9334
  if (!g) {
9083
9335
  const r = createRng(1e3 + idx);
@@ -9098,17 +9350,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9098
9350
  }
9099
9351
  return g;
9100
9352
  }
9353
+ markParts() {
9354
+ this.markGeometry ?? (this.markGeometry = new this.THREE.CircleGeometry(1, 10));
9355
+ this.markMaterial ?? (this.markMaterial = new this.THREE.MeshBasicMaterial({ color: 723465, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
9356
+ return { geometry: this.markGeometry, material: this.markMaterial };
9357
+ }
9358
+ /**
9359
+ * Meshes, never put in a scene by this class, that draw every shared piece of a fragment once: the box, the six shards, the bullet-hole disc, and the opaque and
9360
+ * glass materials. A host draws them at load so the first blast does not upload these buffers in the middle of a frame.
9361
+ */
9362
+ warmMeshes() {
9363
+ const opaque = this.materialFor("warm", 8421504, false);
9364
+ const glass2 = this.materialFor("warm", 8421504, true);
9365
+ const geometries = [this.box, ...Array.from({ length: SHARD_SHAPES }, (_, index) => this.shardGeometry(index))];
9366
+ const { geometry, material } = this.markParts();
9367
+ return [...geometries.flatMap((shared) => [new this.THREE.Mesh(shared, opaque), new this.THREE.Mesh(shared, glass2)]), new this.THREE.Mesh(geometry, material)];
9368
+ }
9101
9369
  /**
9102
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
9103
9371
  * a unit box scaled to the fragment, so each disc is scaled back; exact for a mark on a face, which is where a bullet hole is.
9104
9372
  */
9105
9373
  addMarks(mesh2, f) {
9106
9374
  if (!f.marks?.length) return;
9107
- this.markGeometry ?? (this.markGeometry = new this.THREE.CircleGeometry(1, 10));
9108
- this.markMaterial ?? (this.markMaterial = new this.THREE.MeshBasicMaterial({ color: 723465, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
9375
+ const { geometry, material } = this.markParts();
9109
9376
  const sc = mesh2.scale;
9110
9377
  for (const mark of f.marks) {
9111
- const disc = new this.THREE.Mesh(this.markGeometry, this.markMaterial);
9378
+ const disc = new this.THREE.Mesh(geometry, material);
9112
9379
  const radius = Math.max(4e-3, mark.diameter) * 0.6;
9113
9380
  const n = this.v.set(mark.normal[0], mark.normal[1], mark.normal[2]).normalize();
9114
9381
  disc.position.set(mark.point[0] / sc.x + n.x * 2e-3 / sc.x, mark.point[1] / sc.y + n.y * 2e-3 / sc.y, mark.point[2] / sc.z + n.z * 2e-3 / sc.z);
@@ -9448,6 +9715,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9448
9715
  };
9449
9716
 
9450
9717
  // src/pieces.ts
9718
+ var SLAM_MIN_SPEED = 2;
9719
+ var SLAM_BOUNCE = 0.2;
9720
+ var SLAM_FLOOR_NORMAL = 0.7;
9451
9721
  var SUB_DT = 1 / 120;
9452
9722
  var MU = 0.7;
9453
9723
  var SLEEP_AFTER = 0.5;
@@ -9682,16 +9952,41 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9682
9952
  const fixed = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
9683
9953
  return Math.max(top, fixed);
9684
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
+ }
9685
9979
  integrate(b, dt) {
9686
9980
  const c = this.centerOf(b, this.p);
9687
9981
  const pos = [c.x, c.y, c.z];
9982
+ const start = [c.x, c.y, c.z];
9688
9983
  const q = this.q.copy(b.root.quaternion);
9689
9984
  const v = b.velocity;
9690
9985
  const w = b.angularVelocity;
9691
9986
  const invM = 1 / b.mass;
9692
9987
  const steps = clamp6(Math.ceil(dt / SUB_DT), 1, 8);
9693
9988
  const h = dt / steps;
9694
- let landing = 0;
9989
+ let landing2 = 0;
9695
9990
  let landPoint = [pos[0], pos[1], pos[2]];
9696
9991
  let contactedLast = false;
9697
9992
  let touched = false;
@@ -9794,11 +10089,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9794
10089
  if (contactedLast) touched = true;
9795
10090
  if (!contactedLast) continue;
9796
10091
  pos[1] += Math.max(0, maxPen - SLOP * 0.5);
9797
- let impact2 = 0;
9798
- for (const r of rs) impact2 = Math.max(impact2, -(v[1] + w[2] * r[0] - w[0] * r[2]));
9799
- const restitution = impact2 > 2 ? 0.25 : 0;
9800
- if (impact2 > landing) {
9801
- landing = 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;
9802
10097
  let sx = 0;
9803
10098
  let sz = 0;
9804
10099
  for (const r of rs) {
@@ -9929,14 +10224,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9929
10224
  v[0] *= fd;
9930
10225
  v[2] *= fd;
9931
10226
  }
10227
+ if (this.o.sweep && !b.held) this.sweepOn(b, start, pos);
9932
10228
  b.root.quaternion.copy(q);
9933
10229
  const off = this.ax.copy(b.centerV).applyQuaternion(q);
9934
10230
  b.root.position.set(pos[0] - off.x, pos[1] - off.y, pos[2] - off.z);
9935
10231
  b.root.updateMatrixWorld(true);
9936
10232
  b.onGround = contactedLast;
9937
- if (landing > HARD_LANDING && this.time - b.lastHit > 0.15) {
10233
+ if (landing2 > HARD_LANDING && this.time - b.lastHit > 0.15) {
9938
10234
  b.lastHit = this.time;
9939
- this.o.onHardLanding(b, landPoint, landing);
10235
+ this.o.onHardLanding(b, landPoint, landing2);
9940
10236
  }
9941
10237
  const still = touched && !unstable && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
9942
10238
  if (still) b.still += dt;
@@ -11416,6 +11712,69 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11416
11712
  return out;
11417
11713
  };
11418
11714
  var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
11715
+ var assemblies_default = [
11716
+ {
11717
+ id: "brick_wall_plastered",
11718
+ name: "Plastered brick wall",
11719
+ core: { material: "brick", thickness: 0.09 },
11720
+ layers: [{ material: "plaster", thickness: 0.02, faces: "both" }],
11721
+ units: { size: [0.23, 0.075, 0.11], bond: "running", mortar: "plaster" },
11722
+ breach: { area: 0.25 }
11723
+ },
11724
+ {
11725
+ id: "concrete_block_wall",
11726
+ name: "Concrete block wall",
11727
+ core: { material: "concrete", thickness: 0.14 },
11728
+ fill: 0.55,
11729
+ units: { size: [0.39, 0.19, 0.14], bond: "running", mortar: "plaster" },
11730
+ breach: { area: 0.25 }
11731
+ },
11732
+ {
11733
+ id: "reinforced_concrete",
11734
+ name: "Reinforced concrete wall",
11735
+ core: { material: "concrete", thickness: 0.2 },
11736
+ reinforcement: { material: "steel", spacing: 0.18, diameter: 8e-3, residualCapacity: 0.15 },
11737
+ units: { size: [0.6, 0.42, 0.2], bond: "stack", mortar: "concrete" },
11738
+ breach: { area: 0.25 }
11739
+ },
11740
+ {
11741
+ id: "drywall",
11742
+ name: "Drywall partition on steel studs",
11743
+ core: { material: "plaster", thickness: 0.0125 },
11744
+ layers: [{ material: "plaster", thickness: 0.0125, faces: "+" }],
11745
+ depth: 0.095,
11746
+ breach: { area: 0.25 }
11747
+ },
11748
+ {
11749
+ id: "wood_frame_wall",
11750
+ name: "Wood frame wall",
11751
+ core: { material: "plywood", thickness: 0.012 },
11752
+ layers: [{ material: "plaster", thickness: 0.0125, faces: "-" }],
11753
+ depth: 0.1135,
11754
+ breach: { area: 0.25 }
11755
+ },
11756
+ {
11757
+ id: "log_wall",
11758
+ name: "Log wall",
11759
+ core: { material: "wood_soft", thickness: 0.2 },
11760
+ fill: 0.785,
11761
+ breach: { area: 0.25 }
11762
+ },
11763
+ {
11764
+ id: "sheet_metal_shack",
11765
+ name: "Corrugated sheet metal wall",
11766
+ core: { material: "steel", thickness: 6e-4 },
11767
+ depth: 0.02,
11768
+ breach: { area: 0.25 }
11769
+ },
11770
+ {
11771
+ id: "glass_pane",
11772
+ name: "Glass pane",
11773
+ core: { material: "glass", thickness: 6e-3 },
11774
+ breach: { area: 0.1 }
11775
+ }
11776
+ ];
11777
+ var SHIPPED = structuredClone(assemblies_default);
11419
11778
 
11420
11779
  // src/spec.ts
11421
11780
  function containsOf(value) {
@@ -11454,7 +11813,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11454
11813
  const fallback2 = options.fallback ?? "plastic";
11455
11814
  const minSize = options.minSize ?? 0.015;
11456
11815
  const inv = specFrameInverse(THREE, root);
11457
- const entries = [];
11816
+ const entries2 = [];
11458
11817
  const byKey = /* @__PURE__ */ new Map();
11459
11818
  const visit = (o) => {
11460
11819
  if (!o.visible || o.userData?.destructionIgnore) return;
@@ -11491,8 +11850,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11491
11850
  if (powder) existing.powder = powder;
11492
11851
  if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
11493
11852
  } else {
11494
- 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 };
11495
- 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);
11496
11855
  if (explicit) byKey.set(explicit, e);
11497
11856
  }
11498
11857
  }
@@ -11510,7 +11869,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11510
11869
  const largest = (e) => Math.max(...sizeOf(e.box));
11511
11870
  const kept = [];
11512
11871
  const tiny = [];
11513
- 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);
11514
11873
  for (const t of tiny) {
11515
11874
  let best = null;
11516
11875
  let bestV = 0;
@@ -11781,6 +12140,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11781
12140
  this.b = tmpV(THREE);
11782
12141
  this.m4 = new THREE.Matrix4();
11783
12142
  this.ray = new THREE.Raycaster();
12143
+ this.ray.layers.enableAll();
11784
12144
  this.box = new THREE.Box3();
11785
12145
  this.t = tmpV(THREE);
11786
12146
  this.lbox = new THREE.Box3();
@@ -11788,9 +12148,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11788
12148
  /**
11789
12149
  * @param range vertex range `[v0, v1)` of a merged mesh this part is drawn by (see `MeshRange`); `hosts` caches the shared state per mesh for one `bind`.
11790
12150
  */
11791
- makeMeshVis(mesh2, home, range, hosts) {
12151
+ makeMeshVis(mesh2, home, range2, hosts) {
11792
12152
  let host = null;
11793
- if (range) {
12153
+ if (range2) {
11794
12154
  host = hosts?.get(mesh2) ?? null;
11795
12155
  if (!host) {
11796
12156
  host = { mesh: mesh2, origGeometry: mesh2.geometry, origMaterial: mesh2.material, ownGeo: false, work: null, ownMat: false, sharers: [] };
@@ -11816,8 +12176,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11816
12176
  bottomY: 0,
11817
12177
  moved: false,
11818
12178
  host,
11819
- range: range ? [range[0], range[1]] : null,
11820
- merged: host && range ? { host, range: [range[0], range[1]] } : null,
12179
+ range: range2 ? [range2[0], range2[1]] : null,
12180
+ merged: host && range2 ? { host, range: [range2[0], range2[1]] } : null,
11821
12181
  loose: false,
11822
12182
  collapsed: false,
11823
12183
  hidden: false,
@@ -12386,24 +12746,24 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12386
12746
  geo.boundingBox = null;
12387
12747
  geo.boundingSphere = null;
12388
12748
  }
12389
- collapseRange(arr, range) {
12390
- const x = arr[range[0] * 3];
12391
- const y = arr[range[0] * 3 + 1];
12392
- const z = arr[range[0] * 3 + 2];
12393
- for (let i = range[0] * 3; i < range[1] * 3; i += 3) {
12749
+ collapseRange(arr, range2) {
12750
+ const x = arr[range2[0] * 3];
12751
+ const y = arr[range2[0] * 3 + 1];
12752
+ const z = arr[range2[0] * 3 + 2];
12753
+ for (let i = range2[0] * 3; i < range2[1] * 3; i += 3) {
12394
12754
  arr[i] = x;
12395
12755
  arr[i + 1] = y;
12396
12756
  arr[i + 2] = z;
12397
12757
  }
12398
12758
  }
12399
12759
  /** Melt of one range: shorter in Y (down to 25 %) with the bottom fixed, a bit wider around its centre, with the same noise as a whole mesh. */
12400
- meltRange(arr, work, range, melt) {
12760
+ meltRange(arr, work, range2, melt) {
12401
12761
  let minY = Infinity;
12402
12762
  let maxY = -Infinity;
12403
12763
  let cx = 0;
12404
12764
  let cz = 0;
12405
- const n = Math.max(1, range[1] - range[0]);
12406
- for (let i = range[0] * 3; i < range[1] * 3; i += 3) {
12765
+ const n = Math.max(1, range2[1] - range2[0]);
12766
+ for (let i = range2[0] * 3; i < range2[1] * 3; i += 3) {
12407
12767
  minY = Math.min(minY, work[i + 1]);
12408
12768
  maxY = Math.max(maxY, work[i + 1]);
12409
12769
  cx += work[i];
@@ -12414,7 +12774,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12414
12774
  const h = Math.max(1e-4, maxY - minY);
12415
12775
  const sy = lerp5(1, 0.25, melt);
12416
12776
  const sxz = 1 + 0.18 * melt;
12417
- for (let i = range[0] * 3, v = 0; i < range[1] * 3; i += 3, v++) {
12777
+ for (let i = range2[0] * 3, v = 0; i < range2[1] * 3; i += 3, v++) {
12418
12778
  const noise = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
12419
12779
  arr[i] = cx + (work[i] - cx) * sxz;
12420
12780
  arr[i + 2] = cz + (work[i + 2] - cz) * sxz;
@@ -12668,12 +13028,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12668
13028
  var QUEUE_CAP = 4e3;
12669
13029
  var MIN_FRAGMENTS_PER_SLICE = 8;
12670
13030
  var STATIC_REFRESH = 0.5;
13031
+ var STATIC_REFRESH_SLICE_MS = 1;
12671
13032
  var STATIC_SPAN = 3;
12672
13033
  var FEET_BAND = 0.03;
12673
13034
  var TIP_MIN_HEIGHT = 0.3;
12674
13035
  var TIP_NARROW = 0.3;
12675
13036
  var FIRE_CEILING_SECONDS = 3600;
12676
13037
  var GENERIC_REFERENCE_RADIUS = 15;
13038
+ var SWEEP_CORE = 0.15;
12677
13039
  var PRIMED_FIRE_RADIUS = 0.2;
12678
13040
  var FLUID_COLORS = {
12679
13041
  water: 9226495,
@@ -12716,6 +13078,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12716
13078
  if (def.fracture === "tear") return "impact_soft";
12717
13079
  return def.fracture === "none" ? "impact_metal" : "impact_wood";
12718
13080
  }
13081
+ var shotKey = (def) => impactKey(def).replace("impact_", "shot_");
12719
13082
  var dragSoundKey = (def) => impactKey(def).replace("impact_", "drag_");
12720
13083
  function fractureKey(def, style) {
12721
13084
  switch (style) {
@@ -12765,14 +13128,34 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12765
13128
  /** Objects to measure again; `staticAll` measures every one. */
12766
13129
  __publicField(this, "staticDirty", /* @__PURE__ */ new Set());
12767
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", []);
12768
13133
  /** Boxes of the pieces' meshes (as `staticBoxes`), with the time they were measured. */
12769
13134
  __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
12770
13135
  __publicField(this, "staticAt", 0);
12771
13136
  __publicField(this, "queue", []);
12772
13137
  /** Presentation work an event left for later frames (fragments still to spawn, loose groups still to split), run in order within the frame budget. */
12773
13138
  __publicField(this, "slices", []);
12774
- /** When the work of this frame must stop; `Infinity` outside `drainQueue`, where a call finishes what it started. */
13139
+ /** When the work of this frame must stop; `Infinity` outside a drain, where a call finishes what it started. */
12775
13140
  __publicField(this, "sliceDeadline", Infinity);
13141
+ /** The clock the drain reads; a budget lends its own while it runs the queue. */
13142
+ __publicField(this, "drainClock", () => performance.now());
13143
+ /** true once a frame budget owns the draining: `update` no longer drains by itself. */
13144
+ __publicField(this, "attached", false);
13145
+ __publicField(this, "eventQueue", {
13146
+ id: DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS,
13147
+ pending: () => this.queue.length + this.slices.length,
13148
+ run: (deadline, clock) => this.runBudgeted(deadline, clock),
13149
+ flush: () => {
13150
+ for (let guard = 0; guard < 16 && this.queue.length + this.slices.length > 0; guard++) this.runBudgeted(Infinity, () => 0);
13151
+ },
13152
+ attach: () => {
13153
+ this.attached = true;
13154
+ },
13155
+ detach: () => {
13156
+ this.attached = false;
13157
+ }
13158
+ });
12776
13159
  __publicField(this, "unsubscribe");
12777
13160
  __publicField(this, "visuals");
12778
13161
  __publicField(this, "fragments");
@@ -12818,6 +13201,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12818
13201
  const maxParticles = Math.max(8, Math.floor((o.maxParticles ?? 2500) * (this.low ? 0.5 : 1)));
12819
13202
  this.visuals = new Visuals(THREE, this.low, 600);
12820
13203
  this.ground = new GroundSpots(THREE, o.scene, this.groundAt, 48);
13204
+ if (!o.vfx) warnOnce("game-destruction-three.own-effects-world", "game-destruction-three made an effects world of its own because the host lent none (options.vfx); pass the host effects so the effects share one budget");
12821
13205
  this.vfx = o.vfx ?? createVfx({ profile: this.quality, gravity: this.gravity, groundAt: this.groundAt, sprites: { capacity: maxParticles, density: this.low ? 0.6 : 1 } });
12822
13206
  this.ownVfx = o.vfx ? void 0 : createVfxRenderer(THREE, { scene: o.scene, camera: o.camera ?? new THREE.PerspectiveCamera(), vfx: this.vfx, pointScale: o.pointScale ?? 700 });
12823
13207
  this.stopLanding = this.vfx.onLand(this.owner, (x, _y, z, color, tag) => {
@@ -12837,7 +13221,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12837
13221
  staticTopAt: (x, z, y, step) => this.staticTopAt(x, z, y, step),
12838
13222
  gravity: this.gravity,
12839
13223
  scene: o.scene,
12840
- 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)
12841
13227
  });
12842
13228
  this.unsubscribe = this.world.on("*", (ev) => {
12843
13229
  if (this.queue.length < QUEUE_CAP) this.queue.push(ev);
@@ -13027,26 +13413,58 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13027
13413
  const p = this.pieceSys.get(objectId);
13028
13414
  if (p) this.pieceSys.impulse(p, impulse, point);
13029
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
+ }
13030
13443
  hardLanding(p, point, speed) {
13031
- const key = this.impactSoundOf(p.objectId);
13032
- this.sound(key, point, clamp6(speed / 10, 0.2, 1));
13444
+ this.landingSound(p.objectId, point, clamp6(speed / 10, 0.2, 1));
13033
13445
  this.syncBound(p.objectId);
13034
13446
  try {
13035
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" });
13036
13448
  } catch {
13037
13449
  }
13038
13450
  }
13039
- 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) {
13040
13453
  const st = this.world.state(objectId);
13041
- let best;
13454
+ let best = "";
13042
13455
  let mass = -1;
13043
13456
  for (const p of st?.parts ?? []) {
13044
13457
  if (p.mass > mass) {
13045
13458
  mass = p.mass;
13046
- best = this.world.materials.get(p.material);
13459
+ best = p.material;
13047
13460
  }
13048
13461
  }
13049
- 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));
13050
13468
  }
13051
13469
  // ----------------------------------------------------------------------------------------------------------------------------------
13052
13470
  // Update
@@ -13055,7 +13473,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13055
13473
  if (this.disposed) return;
13056
13474
  dt = clamp6(dt, 0, 0.1);
13057
13475
  this.time += dt;
13058
- this.drainQueue();
13476
+ if (!this.attached) this.drainQueue();
13059
13477
  for (const b of this.bounds.values()) b.onFrame?.(dt);
13060
13478
  for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
13061
13479
  this.updateActive(dt);
@@ -13066,36 +13484,50 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13066
13484
  this.ownVfx?.update(dt);
13067
13485
  this.syncTransforms();
13068
13486
  }
13487
+ deferredQueue() {
13488
+ return this.eventQueue;
13489
+ }
13069
13490
  drainQueue() {
13491
+ this.runBudgeted(performance.now() + this.eventBudgetMs, () => performance.now());
13492
+ }
13493
+ /** Applies the waiting events in order until the deadline, after the slices earlier events left; always at least one unit when something waits. Returns the units run. */
13494
+ runBudgeted(deadline, clock) {
13070
13495
  const batch = this.queue.splice(0, this.queue.length);
13071
- const deadline = performance.now() + this.eventBudgetMs;
13496
+ const outerClock = this.drainClock;
13497
+ this.drainClock = clock;
13072
13498
  this.sliceDeadline = deadline;
13499
+ let ran = 0;
13073
13500
  try {
13074
- this.runSlices(deadline);
13501
+ ran = this.runSlices(deadline);
13075
13502
  for (let i = 0; i < batch.length; i++) {
13076
- if (i > 0 && performance.now() >= deadline) {
13503
+ if ((i > 0 || ran > 0 && this.attached) && clock() >= deadline) {
13077
13504
  this.queue.unshift(...batch.slice(i));
13078
- return;
13505
+ return ran;
13079
13506
  }
13080
13507
  try {
13081
13508
  this.apply(batch[i]);
13082
13509
  } catch (err) {
13083
13510
  if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", batch[i].type, err);
13084
13511
  }
13512
+ ran++;
13085
13513
  }
13086
13514
  } finally {
13087
13515
  this.sliceDeadline = Infinity;
13516
+ this.drainClock = outerClock;
13088
13517
  }
13518
+ return ran;
13089
13519
  }
13090
13520
  /** Runs the work left by earlier frames until the deadline; at least one piece per frame, so it always finishes. */
13091
13521
  runSlices(deadline) {
13092
- for (let ran = 0; this.slices.length > 0 && (ran === 0 || performance.now() < deadline); ran++) {
13522
+ let ran = 0;
13523
+ for (; this.slices.length > 0 && (ran === 0 || this.drainClock() < deadline); ran++) {
13093
13524
  try {
13094
13525
  this.slices.shift()();
13095
13526
  } catch (err) {
13096
13527
  if (typeof console !== "undefined") console.error("game-destruction-three: failed to run deferred work", err);
13097
13528
  }
13098
13529
  }
13530
+ return ran;
13099
13531
  }
13100
13532
  /**
13101
13533
  * The ground under an object settled or tilted: its parts are drawn under a node with the soil's offset (`ObjectState.soil.offset`), so the host's
@@ -13164,9 +13596,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13164
13596
  staticNow() {
13165
13597
  if (this.time - this.staticAt > STATIC_REFRESH) {
13166
13598
  this.staticAt = this.time;
13167
- this.staticAll = true;
13599
+ if (this.staticRound.length === 0) this.staticRound = [...this.bounds.keys()].reverse();
13168
13600
  }
13169
- 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;
13170
13602
  const box = new this.THREE.Box3();
13171
13603
  const measure = (b) => {
13172
13604
  if (b.piece || this.pieceSys.get(b.id)) {
@@ -13186,14 +13618,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13186
13618
  }
13187
13619
  this.staticBoxes.set(b.id, out.length > 6 ? out : []);
13188
13620
  };
13621
+ const measureId = (id) => {
13622
+ const b = this.bounds.get(id);
13623
+ if (b) measure(b);
13624
+ else this.staticBoxes.delete(id);
13625
+ };
13189
13626
  if (this.staticAll) {
13190
13627
  this.staticBoxes.clear();
13191
13628
  for (const b of this.bounds.values()) measure(b);
13629
+ this.staticRound = [];
13192
13630
  } else {
13193
- for (const id of this.staticDirty) {
13194
- const b = this.bounds.get(id);
13195
- if (b) measure(b);
13196
- 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;
13197
13636
  }
13198
13637
  }
13199
13638
  this.staticAll = false;
@@ -13253,7 +13692,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13253
13692
  case "ricochet": {
13254
13693
  const d = ev.direction;
13255
13694
  this.vfx.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
13256
- 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)));
13257
13697
  break;
13258
13698
  }
13259
13699
  case "debris":
@@ -13295,7 +13735,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13295
13735
  this.vfx.fire({ point: ev.point, radius: PRIMED_FIRE_RADIUS, life: ev.delay });
13296
13736
  this.primedBurning.add(`${ev.object}|${ev.part}`);
13297
13737
  this.activate(ev.object, ev.part);
13298
- this.sound("ignite", ev.point, 0.7);
13738
+ this.sound("primed", ev.point, 0.7, { seconds: ev.delay });
13299
13739
  this.sound("burn_loop", ev.point, 0.5);
13300
13740
  }
13301
13741
  break;
@@ -13409,8 +13849,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13409
13849
  const b = this.bounds.get(objectId);
13410
13850
  return b ? [b.root.position.x, b.root.position.y, b.root.position.z] : [0, 0, 0];
13411
13851
  }
13412
- sound(key, point, volume) {
13413
- 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);
13414
13855
  }
13415
13856
  partVis(objectId, partId) {
13416
13857
  return this.bounds.get(objectId)?.parts.get(partId) ?? null;
@@ -13475,7 +13916,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13475
13916
  const reach = shown.reduce((widest, mv) => Math.max(widest, this.visuals.dentReach(mv, wp, ev.dent)), ev.dent.radius);
13476
13917
  this.visuals.applyPartDent(shown, wp, wn, ev.dent, reach);
13477
13918
  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);
13478
- this.sound(impactKey(def), ev.worldPoint, clamp6(0.3 + ev.dent.depth * 12, 0.3, 1));
13919
+ this.impactSound(pv.material, ev.worldPoint, clamp6(0.3 + ev.dent.depth * 12, 0.3, 1), ev.blow);
13479
13920
  }
13480
13921
  onHole(ev) {
13481
13922
  const b = this.bounds.get(ev.object);
@@ -13504,7 +13945,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13504
13945
  this.vfx.burst("dust", ev.worldPoint, [n.x, n.y, n.z], this.low ? 5 : 10, 0.7, puff3);
13505
13946
  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);
13506
13947
  }
13507
- this.sound(impactKey(def), ev.worldPoint, 0.6);
13948
+ this.sound(shotKey(def), ev.worldPoint, 0.6, impactSoundKeys(pv.material, def, ev.blow));
13508
13949
  }
13509
13950
  onCrack(ev) {
13510
13951
  const b = this.bounds.get(ev.object);
@@ -13514,13 +13955,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13514
13955
  const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
13515
13956
  const mv = this.nearestMesh(pv, wp);
13516
13957
  if (mv) {
13517
- const glass = !!def?.visual?.transparent;
13958
+ const glass2 = !!def?.visual?.transparent;
13518
13959
  const own = this.partColor(pv, def) ?? fallbackColor(pv.material, def);
13519
- const color = glass ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
13520
- const length = glass ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
13960
+ const color = glass2 ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
13961
+ const length = glass2 ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
13521
13962
  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]));
13522
13963
  }
13523
- this.sound(impactKey(def), ev.worldPoint, 0.5);
13964
+ this.impactSound(pv.material, ev.worldPoint, 0.5, ev.blow);
13524
13965
  }
13525
13966
  /** 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). */
13526
13967
  faceNormal(mv, point) {
@@ -13577,7 +14018,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13577
14018
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), false), ev.object, maxAlive);
13578
14019
  }
13579
14020
  if (!played) this.vfx.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
13580
- this.sound(impactKey(def), ev.opening.center, 0.8);
14021
+ this.impactSound(pv?.material ?? "", ev.opening.center, 0.8, ev.blow);
13581
14022
  }
13582
14023
  /**
13583
14024
  * 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
@@ -13595,7 +14036,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13595
14036
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone, false), ev.object, maxAlive);
13596
14037
  }
13597
14038
  if (ev.torn) {
13598
- this.sound(impactKey(def), ev.center, 0.5);
14039
+ this.impactSound(pv?.material ?? "", ev.center, 0.5, ev.blow);
13599
14040
  return;
13600
14041
  }
13601
14042
  const T = this.THREE;
@@ -13605,7 +14046,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13605
14046
  for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation2);
13606
14047
  this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
13607
14048
  this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
13608
- this.sound(impactKey(def), ev.center, 0.7);
14049
+ this.impactSound(pv?.material ?? "", ev.center, 0.7, ev.blow);
13609
14050
  }
13610
14051
  onFracture(ev) {
13611
14052
  const b = this.bounds.get(ev.object);
@@ -13646,14 +14087,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13646
14087
  default:
13647
14088
  this.vfx.burst("sparks", ev.point, up, 10, 4);
13648
14089
  }
13649
- this.sound(fractureKey(def, ev.style), ev.point, clamp6(0.4 + ev.fragments.length * 0.03, 0.4, 1));
14090
+ this.sound(fractureKey(def, ev.style), ev.point, clamp6(0.4 + ev.fragments.length * 0.03, 0.4, 1), breakSoundKeys(matId, def, ev.blow));
13650
14091
  }
13651
14092
  /** Spawns the fragments of a fracture from index `from`; what does not fit in this frame's budget is spawned by the next frames. */
13652
14093
  spawnFragments(ev, from, look) {
13653
14094
  const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
13654
14095
  const step = this.low ? 2 : 1;
13655
14096
  for (let i = from; i < ev.fragments.length; i += step) {
13656
- if (i - from >= MIN_FRAGMENTS_PER_SLICE * step && performance.now() >= this.sliceDeadline) {
14097
+ if (i - from >= MIN_FRAGMENTS_PER_SLICE * step && this.drainClock() >= this.sliceDeadline) {
13657
14098
  this.slices.push(() => this.spawnFragments(ev, i, look));
13658
14099
  return;
13659
14100
  }
@@ -13677,7 +14118,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13677
14118
  if (!st || st.groups.length < 2) return;
13678
14119
  const loose = st.groups.length - 1;
13679
14120
  for (let i = 0; i < loose; i++) {
13680
- if (i > 0 && performance.now() >= this.sliceDeadline) {
14121
+ if (i > 0 && this.drainClock() >= this.sliceDeadline) {
13681
14122
  this.slices.push(() => {
13682
14123
  this.staticDirty.add(objectId);
13683
14124
  this.splitLoose(objectId);
@@ -13783,10 +14224,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13783
14224
  const report = options.report;
13784
14225
  const confinement = report?.find((r) => r.blast !== void 0)?.blast?.confinement ?? 1;
13785
14226
  const energy = ev.energy * confinement;
13786
- const reachOf = (objectId) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? 1;
14227
+ 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;
13787
14228
  const shove = (pc) => {
13788
14229
  this.pieceSys.centerOf(pc, c);
13789
- 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);
14230
+ if (c.distanceTo(this.v2.set(p[0], p[1], p[2])) > ev.radius) return;
14231
+ 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);
13790
14232
  if (!hit) return;
13791
14233
  const spread = pc.halfExtents[0];
13792
14234
  const momentum = hit.speed * pc.mass;
@@ -13795,7 +14237,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13795
14237
  for (const pc of [...this.pieceSys.list]) shove(pc);
13796
14238
  if (this.o.shoveable) this.loosen(p, ev.radius, energy, reachOf);
13797
14239
  this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(energy) * 0.08));
13798
- this.sound("explode", p, clamp6(0.5 + ev.radius / 10, 0.5, 1));
14240
+ this.sound("explode", p, clamp6(0.5 + ev.radius / 10, 0.5, 1), {
14241
+ radius: ev.radius,
14242
+ ...ev.effect !== void 0 ? { effect: ev.effect } : {},
14243
+ ...options.effect !== void 0 ? { effect: options.effect } : {},
14244
+ ...report ? { confinement } : {}
14245
+ });
13799
14246
  }
13800
14247
  /** 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. */
13801
14248
  loosen(point, radius, energy, reachOf) {
@@ -13806,7 +14253,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13806
14253
  const { half, center } = this.measure(b.root, this.meshesOf(b));
13807
14254
  const world = center.clone().applyQuaternion(b.root.quaternion).add(b.root.position);
13808
14255
  const mass = this.massOf(b.id);
13809
- const hit = blastShove({ point, energy, radius, reach: reachOf(b.id) }, { position: [world.x, world.y, world.z], mass }, this.o.blastShove);
14256
+ if (world.distanceTo(this.v2.set(point[0], point[1], point[2])) > radius) continue;
14257
+ 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);
13810
14258
  if (!hit) continue;
13811
14259
  const piece = this.makePiece(b.id, { mass });
13812
14260
  const body = this.pieceSys.get(b.id);
@@ -13830,9 +14278,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13830
14278
  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] } });
13831
14279
  }
13832
14280
  stopFire(key) {
13833
- const fire = this.fires.get(key);
13834
- if (!fire) return;
13835
- if (fire.handle !== void 0) this.vfx.stop(fire.handle);
14281
+ const fire2 = this.fires.get(key);
14282
+ if (!fire2) return;
14283
+ if (fire2.handle !== void 0) this.vfx.stop(fire2.handle);
13836
14284
  this.fires.delete(key);
13837
14285
  }
13838
14286
  // ----------------------------------------------------------------------------------------------------------------------------------
@@ -13869,9 +14317,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13869
14317
  if (ps.status === "melted") this.onGone(a.object, pv, "melted", def);
13870
14318
  else if (ps.status === "ashes") this.onGone(a.object, pv, "ashes", def);
13871
14319
  }
13872
- const fire = this.fires.get(key);
13873
- if (fire) {
13874
- if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def);
14320
+ const fire2 = this.fires.get(key);
14321
+ if (fire2) {
14322
+ if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire2, pv, def);
13875
14323
  else this.stopFire(key);
13876
14324
  }
13877
14325
  const hot = def?.visual?.glows === true && ps.temperature > GLOW_FROM_C;
@@ -14001,10 +14449,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14001
14449
  this.scene.remove(b.root);
14002
14450
  this.pieceSys.remove(b.id);
14003
14451
  this.bounds.delete(b.id);
14452
+ this.staticDirty.add(b.id);
14004
14453
  }
14005
14454
  }
14006
14455
  }
14007
14456
  clear() {
14457
+ this.staticAll = true;
14008
14458
  this.queue.length = 0;
14009
14459
  this.slices.length = 0;
14010
14460
  this.active.clear();
@@ -14031,6 +14481,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14031
14481
  this.rng = createRng(RNG_SEED);
14032
14482
  this.time = 0;
14033
14483
  this.staticAt = 0;
14484
+ this.staticRound = [];
14485
+ this.staticAll = true;
14034
14486
  this.throttles.clear();
14035
14487
  this.fragments.reset();
14036
14488
  this.pieceSys.reset();
@@ -14056,6 +14508,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14056
14508
  const materials = [kit.discMaterial().clone(), kit.patchMaterial(8421504).clone(), kit.lineMaterial(0).clone()];
14057
14509
  const group = new T.Group();
14058
14510
  group.add(new T.Mesh(kit.discGeometry(), materials[0]), new T.Mesh(kit.discGeometry(), materials[1]), new T.LineSegments(geometry, materials[2]));
14511
+ group.add(...this.fragments.warmMeshes());
14059
14512
  for (const probe of group.children) probe.frustumCulled = false;
14060
14513
  this.warmProbes = { group, geometry, materials };
14061
14514
  return { group, dispose: () => {
@@ -14086,7 +14539,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14086
14539
  }
14087
14540
  };
14088
14541
 
14089
- // ../game-audio/dist/index.js
14542
+ // ../game-audio/dist/chunk-7TLFY7VO.js
14090
14543
  var DISTANCE_CURVES = {
14091
14544
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
14092
14545
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -14101,31 +14554,6 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14101
14554
  /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
14102
14555
  animals: { model: "linear", range: 70, scale: 0.25, minGain: 5e-3 }
14103
14556
  };
14104
- var mix_profiles_default = [
14105
- {
14106
- id: "game",
14107
- master: 0.5,
14108
- noise: { seconds: 1.5, create: "lazy" },
14109
- 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"]
14110
- },
14111
- {
14112
- id: "art_scene",
14113
- master: 0.85,
14114
- compressor: { threshold: -12, ratio: 5 },
14115
- reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
14116
- noise: { seconds: 2, create: "eager" },
14117
- sources: [
14118
- "cena:L1308 compressor -12 dB, ratio 5",
14119
- "cena:L1309 master 0.85",
14120
- "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
14121
- "cena:L1311 wet 0.4",
14122
- "cena:L1312 noise 2 s made with the chain"
14123
- ]
14124
- }
14125
- ];
14126
- var MIX_PROFILES = Object.fromEntries(
14127
- mix_profiles_default.map((profile) => [profile.id, profile])
14128
- );
14129
14557
  var game_sfx_default = {
14130
14558
  sources: ["jogo:L62089-62107"],
14131
14559
  note: "The game's sfx table P: [lowpass cutoff Hz, peak gain, seconds]. Played as a noise burst with the game's distance curve (falloff game_sfx) and a 0.05 s tail. Team names in `team`.",
@@ -14170,6 +14598,505 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14170
14598
  };
14171
14599
  }
14172
14600
  var GAME_SFX_BANK = { id: "game_sfx", sounds: [...GAME_SFX_CLASSES, GAME_SFX_FALLBACK].map((row) => noiseClassRecipe(row)) };
14601
+ var mix_profiles_default = [
14602
+ {
14603
+ id: "game",
14604
+ master: 0.5,
14605
+ noise: {
14606
+ seconds: 1.5,
14607
+ create: "lazy"
14608
+ },
14609
+ sources: [
14610
+ "jogo:L62077-62081 master 0.5 to the output",
14611
+ "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"
14612
+ ]
14613
+ },
14614
+ {
14615
+ id: "art_scene",
14616
+ master: 0.85,
14617
+ compressor: {
14618
+ threshold: -12,
14619
+ ratio: 5
14620
+ },
14621
+ reverb: {
14622
+ seconds: 2.2,
14623
+ decay: 3.4,
14624
+ wet: 0.4
14625
+ },
14626
+ noise: {
14627
+ seconds: 2,
14628
+ create: "eager"
14629
+ },
14630
+ sources: [
14631
+ "cena:L1308 compressor -12 dB, ratio 5",
14632
+ "cena:L1309 master 0.85",
14633
+ "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
14634
+ "cena:L1311 wet 0.4",
14635
+ "cena:L1312 noise 2 s made with the chain"
14636
+ ]
14637
+ },
14638
+ {
14639
+ id: "kit",
14640
+ master: 0.85,
14641
+ compressor: {
14642
+ threshold: -12,
14643
+ ratio: 5
14644
+ },
14645
+ reverb: {
14646
+ seconds: 2.2,
14647
+ decay: 3.4,
14648
+ wet: 0.4
14649
+ },
14650
+ noise: {
14651
+ seconds: 2,
14652
+ create: "eager"
14653
+ },
14654
+ sources: [
14655
+ "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"
14656
+ ],
14657
+ limiter: {
14658
+ ceiling: -1,
14659
+ knee: -6
14660
+ }
14661
+ }
14662
+ ];
14663
+ var MIX_PROFILES = Object.fromEntries(
14664
+ mix_profiles_default.map((profile) => [profile.id, profile])
14665
+ );
14666
+ var BENCH_BANK = {
14667
+ id: "audio_bench",
14668
+ sounds: [
14669
+ {
14670
+ id: "bench_ping",
14671
+ bus: "ui",
14672
+ pan: true,
14673
+ duration: 0.25,
14674
+ layers: [
14675
+ {
14676
+ gain: 0.5,
14677
+ wet: 0.3,
14678
+ source: { kind: "osc", wave: "sine", frequency: { from: 880 } },
14679
+ envelope: [
14680
+ [0, 1e-4, "set", "fixed"],
14681
+ [2e-3, 1, "exp"],
14682
+ ["1d", 1e-4, "exp", "fixed"]
14683
+ ]
14684
+ }
14685
+ ],
14686
+ falloff: DISTANCE_CURVES.art_scene,
14687
+ sources: ["sdk: bench probe, a 880 Hz ping through the art scene output stage"]
14688
+ },
14689
+ {
14690
+ id: "bench_hiss",
14691
+ bus: "ui",
14692
+ pan: true,
14693
+ duration: [0.3, 0.9],
14694
+ layers: [
14695
+ {
14696
+ gain: 0.4,
14697
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
14698
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3e3 }, q: 0.7 }] },
14699
+ envelope: [
14700
+ [0, 1e-4, "set", "fixed"],
14701
+ [2e-3, 1, "exp"],
14702
+ ["1d", 1e-4, "exp", "fixed"]
14703
+ ]
14704
+ }
14705
+ ],
14706
+ falloff: DISTANCE_CURVES.art_scene,
14707
+ sources: ["sdk: bench probe, a highpass hiss of random length"]
14708
+ }
14709
+ ]
14710
+ };
14711
+ var entries = (category, list) => list.map(([id, priority, current]) => ({ id, category, priority, ...current ? { current } : {} }));
14712
+ var SFX_CATALOG = [
14713
+ ...entries("armas", [
14714
+ ["pistol_22lr", 2, "sfx_pistol"],
14715
+ ["pistol_9mm", 1, "sfx_pistol"],
14716
+ ["pistol_45", 1, "sfx_pistol"],
14717
+ ["revolver_38_357", 2, "sfx_pistol"],
14718
+ ["revolver_44_50ae", 3, "sfx_pistol"],
14719
+ ["smg_9mm_auto", 1, "sfx_pistol"],
14720
+ ["shotgun_12", 1, "sfx_shotgun"],
14721
+ ["rifle_5_56", 1, "sfx_rifle"],
14722
+ ["rifle_7_62x39", 1, "sfx_rifle"],
14723
+ ["rifle_auto_burst", 1, "sfx_rifle"],
14724
+ ["rifle_308_3006", 2, "sfx_rifle"],
14725
+ ["sniper_338_300wm", 2, "sfx_rifle"],
14726
+ ["antimateriel_50bmg", 3, "sfx_rifle"],
14727
+ ["suppressed_pistol", 2],
14728
+ ["suppressed_rifle", 2],
14729
+ ["gunshot_far_pistol", 1],
14730
+ ["gunshot_far_rifle", 1],
14731
+ ["gunshot_far_shotgun", 1],
14732
+ ["gunshot_far_heavy", 2],
14733
+ ["gunshot_tail_indoor", 1],
14734
+ ["gunshot_tail_outdoor", 1],
14735
+ ["bullet_crack_by", 2],
14736
+ ["bullet_whiz", 2],
14737
+ ["ricochet_metal", 2, "ping"],
14738
+ ["dry_fire", 1],
14739
+ ["mag_out", 1],
14740
+ ["mag_in", 1],
14741
+ ["slide_pull_release", 1],
14742
+ ["pump_cycle", 1],
14743
+ ["casing_drop_concreto_pistol", 1],
14744
+ ["casing_drop_concreto_rifle", 1],
14745
+ ["casing_drop_concreto_shell", 1],
14746
+ ["casing_drop_madeira_pistol", 1],
14747
+ ["casing_drop_madeira_rifle", 1],
14748
+ ["casing_drop_madeira_shell", 1],
14749
+ ["casing_drop_metal_pistol", 1],
14750
+ ["casing_drop_metal_rifle", 1],
14751
+ ["casing_drop_metal_shell", 1],
14752
+ ["casing_drop_terra_pistol", 1],
14753
+ ["casing_drop_terra_rifle", 1],
14754
+ ["casing_drop_terra_shell", 1],
14755
+ ["rocket_fire", 2]
14756
+ ]),
14757
+ ...entries("explosoes-fogo", [
14758
+ ["explosion_grenade", 1, "boom"],
14759
+ ["explosion_big", 1, "boom_big"],
14760
+ ["explosion_car", 2, "boom_big"],
14761
+ ["explosion_far", 1],
14762
+ ["debris_rain_stone", 1],
14763
+ ["molotov_break_ignite", 1, "glass_shatter"],
14764
+ ["fire_ignite", 1, "hiss"],
14765
+ ["fire_loop_small", 1],
14766
+ ["fire_loop_large", 1],
14767
+ ["fire_extinguish", 2, "hiss"],
14768
+ ["gas_leak_hiss", 2],
14769
+ ["structure_collapse", 2, "crumble"]
14770
+ ]),
14771
+ ...entries("personagem", [
14772
+ ["footstep_concrete_walk", 1, "sfx_step"],
14773
+ ["footstep_wood_walk", 1, "sfx_step"],
14774
+ ["footstep_dirt_walk", 1, "sfx_step"],
14775
+ ["footstep_grass_walk", 1, "sfx_step"],
14776
+ ["footstep_gravel_walk", 1, "sfx_step"],
14777
+ ["land_concrete", 1, "sfx_step"],
14778
+ ["punch_swing", 1],
14779
+ ["punch_hit_body", 1, "sfx_flesh"],
14780
+ ["kick_hit", 1, "sfx_flesh"],
14781
+ ["knife_stab", 1],
14782
+ ["blunt_hit", 1],
14783
+ ["bite", 1, "sfx_flesh"],
14784
+ ["body_fall", 1],
14785
+ ["player_hurt", 2]
14786
+ ]),
14787
+ ...entries("veiculos", [
14788
+ ["engine_loop_car", 1],
14789
+ ["engine_start", 1],
14790
+ ["crash_light", 1, "crash"],
14791
+ ["crash_heavy", 1, "crash"],
14792
+ ["car_door_open", 1, "latch"],
14793
+ ["car_door_close", 1, "shut_metal"],
14794
+ ["horn_car", 2, "horn"],
14795
+ ["tire_skid_asphalt", 2]
14796
+ ]),
14797
+ ...entries("itens-portas", [
14798
+ ["item_pickup_metal", 1],
14799
+ ["can_open", 1],
14800
+ ["can_crush", 1],
14801
+ ["door_wood_open", 1, "latch"],
14802
+ ["door_wood_close", 1, "shut_wood"],
14803
+ ["door_creak", 1, "creak_wood"],
14804
+ ["door_locked_rattle", 1, "rattle"],
14805
+ ["drawer_open", 2, "slide"],
14806
+ ["fridge_close", 2, "shut_metal"]
14807
+ ]),
14808
+ ...entries("ambiente", [
14809
+ ["rain_loop", 2],
14810
+ ["wind_loop", 2],
14811
+ ["thunder_near", 2],
14812
+ ["thunder_far", 2],
14813
+ ["water_splash", 2]
14814
+ ])
14815
+ ];
14816
+ var SFX_MATERIALS = [
14817
+ ["steel", "metal"],
14818
+ ["aluminium", "metal"],
14819
+ ["wood", "wood"],
14820
+ ["plywood", "wood"],
14821
+ ["mdf", "wood"],
14822
+ ["glass", "glass"],
14823
+ ["tempered_glass", "glass"],
14824
+ ["ceramic", "glass"],
14825
+ ["concrete", "masonry"],
14826
+ ["brick", "masonry"],
14827
+ ["plaster", "masonry"],
14828
+ ["plastic", "soft"],
14829
+ ["fabric", "soft"],
14830
+ ["paper", "soft"],
14831
+ ["foliage", "soft"],
14832
+ ["soil", "soft"],
14833
+ ["flesh", "soft"]
14834
+ ];
14835
+ var SFX_BLOWS = ["bullet", "blunt", "blade", "break", "drag"];
14836
+ var CURRENT_BY_GROUP = {
14837
+ metal: { bullet: "clang", blunt: "clang", blade: "clang", break: "clang", drag: "drag_metal" },
14838
+ wood: { bullet: "wood_hit", blunt: "wood_hit", blade: "wood_hit", break: "crack", drag: "drag_wood" },
14839
+ glass: { bullet: "ping", blunt: "ping", break: "glass_shatter", drag: "drag_glass" },
14840
+ masonry: { bullet: "chip", blunt: "chip", blade: "chip", break: "crumble", drag: "drag_masonry" },
14841
+ soft: { bullet: "thud", blunt: "thud", blade: "thud", break: "thud", drag: "drag_soft" }
14842
+ };
14843
+ var impactSoundId = (material, blow3) => `impact_${material}_${blow3}`;
14844
+ var SFX_IMPACTS = SFX_MATERIALS.flatMap(
14845
+ ([material, group]) => SFX_BLOWS.map((blow3) => {
14846
+ const current = CURRENT_BY_GROUP[group]?.[blow3];
14847
+ return { id: impactSoundId(material, blow3), category: "impactos", priority: ["steel", "wood", "glass", "concrete", "brick", "soil", "flesh"].includes(material) && blow3 === "bullet" ? 1 : 2, ...current ? { current } : {} };
14848
+ })
14849
+ );
14850
+ var CALIBRATION_ID = "calibration_shot";
14851
+ var PREFIX = "calibration/";
14852
+ var url = (name) => `${PREFIX}${name}`;
14853
+ var CALIBRATION_SOUND = {
14854
+ id: CALIBRATION_ID,
14855
+ bus: "weapons",
14856
+ close: ["close_1.wav", "close_2.wav", "close_3.wav", "close_4.wav"].map((name) => ({ url: url(name) })),
14857
+ far: ["far_1.wav", "far_2.wav"].map((name) => ({ url: url(name) })),
14858
+ tiers: [
14859
+ { name: "light", from: 0, close: ["light_1.wav", "light_2.wav"].map((name) => ({ url: url(name) })) },
14860
+ { name: "medium", from: 0.34, close: ["close_1.wav", "close_2.wav", "close_3.wav", "close_4.wav"].map((name) => ({ url: url(name) })) },
14861
+ { name: "heavy", from: 0.67, close: ["heavy_1.wav", "heavy_2.wav"].map((name) => ({ url: url(name) })) }
14862
+ ],
14863
+ tail: { indoor: [{ url: url("tail_in.wav") }], outdoor: [{ url: url("tail_out.wav") }] },
14864
+ pitch: 0.04,
14865
+ choke: { voices: 2 },
14866
+ fallback: "sfx_pistol",
14867
+ sources: ["sdk: calibration takes synthesized by the harness (measure/calibration.ts), no file, no licence"]
14868
+ };
14869
+ var CALIBRATION_LOOP_ID = "calibration_loop";
14870
+ var CALIBRATION_LOOP_SOUND = {
14871
+ id: CALIBRATION_LOOP_ID,
14872
+ bus: "ambience",
14873
+ close: ["loop_1.wav", "loop_2.wav", "loop_3.wav"].map((name) => ({ url: url(name) })),
14874
+ loop: { crossfade: 0.15 },
14875
+ sources: ["sdk: calibration takes synthesized by the harness (measure/calibration.ts), no file, no licence"]
14876
+ };
14877
+ var CRITERIA_TARGETS = {
14878
+ /** C1: event to the first sample, and onset to the peak (one 60 fps frame). */
14879
+ syncMs: 20,
14880
+ peakAfterOnsetMs: 1e3 / 60,
14881
+ /** C3: distances measured, metres. */
14882
+ distances: [1, 10, 50, 200],
14883
+ /** C5: plays, distinct takes and the pitch range (fraction). */
14884
+ plays: 10,
14885
+ minTakes: 4,
14886
+ pitch: [0.03, 0.05],
14887
+ /** C6: the highest peak, dBFS. */
14888
+ peakDb: -1,
14889
+ /** C8: the end of each file against its peak, dB. */
14890
+ endDropDb: -40,
14891
+ /** C9: a burst of rapid fire. */
14892
+ burst: 20,
14893
+ burstSpacing: 0.06
14894
+ };
14895
+ var PT = {
14896
+ group: "Som",
14897
+ name: "Painel de som (arquivos e C1\u2013C10)",
14898
+ controls: {
14899
+ category: "1 \xB7 Categoria",
14900
+ sound: "1 \xB7 Som",
14901
+ play: "1 \xB7 \u25B6 Tocar",
14902
+ material: "2 \xB7 Material",
14903
+ blow: "2 \xB7 Golpe",
14904
+ strike: "2 \xB7 \u25B6 Tocar golpe",
14905
+ distance: "3 \xB7 Dist\xE2ncia",
14906
+ bearing: "3 \xB7 Dire\xE7\xE3o",
14907
+ environment: "3 \xB7 Lugar",
14908
+ strength: "3 \xB7 For\xE7a",
14909
+ wall: "3 \xB7 Atr\xE1s da parede",
14910
+ burst: "3 \xB7 Rajada 20\xD7",
14911
+ play_a: "4 \xB7 \u25B6 A (atual)",
14912
+ play_b: "4 \xB7 \u25B6 B (novo)",
14913
+ ab_cycle: "4 \xB7 Alternar A/B",
14914
+ blind: "4 \xB7 \xC0s cegas (X)",
14915
+ reveal: "4 \xB7 Revelar X",
14916
+ prefer_a: "4 \xB7 Prefiro A",
14917
+ prefer_b: "4 \xB7 Prefiro B",
14918
+ measure: "5 \xB7 Medir C1\u2013C10",
14919
+ record: "5 \xB7 Gravar 3 s",
14920
+ approve: "5 \xB7 Aprovo (C10)"
14921
+ },
14922
+ hints: {
14923
+ sound: "A = arquivo, S = sintetizado, F = faltando. O que tem arquivo toca o arquivo; o resto toca o som atual.",
14924
+ strike: "Toca impact_<material>_<golpe>: o arquivo se existir, sen\xE3o o som atual daquele material.",
14925
+ wall: "P\xF5e a parede de casa entre voc\xEA e a fonte: o som fica abafado (C4).",
14926
+ burst: "Vinte vezes a cada 60 ms, como uma rajada (C9).",
14927
+ ab_cycle: "A, B, A, B, um por segundo.",
14928
+ blind: "Toca A ou B sem dizer qual; depois revele.",
14929
+ 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.",
14930
+ record: "Grava 3 s da sa\xEDda com o som tocando e baixa o arquivo (C10).",
14931
+ approve: "Marca o C10 do som escolhido como aprovado pelo seu ouvido."
14932
+ },
14933
+ categories: {
14934
+ armas: "Armas",
14935
+ impactos: "Impactos",
14936
+ "explosoes-fogo": "Explos\xF5es e fogo",
14937
+ personagem: "Personagem",
14938
+ veiculos: "Ve\xEDculos",
14939
+ "itens-portas": "Itens e portas",
14940
+ ambiente: "Ambiente",
14941
+ calibracao: "Calibra\xE7\xE3o (SDK)"
14942
+ },
14943
+ blows: { bullet: "tiro", blunt: "soco/chute", blade: "faca", break: "quebra", drag: "arrasto/queda" },
14944
+ materials: {
14945
+ steel: "a\xE7o",
14946
+ aluminium: "alum\xEDnio",
14947
+ wood: "madeira",
14948
+ plywood: "compensado",
14949
+ mdf: "MDF",
14950
+ glass: "vidro",
14951
+ tempered_glass: "vidro temperado",
14952
+ ceramic: "cer\xE2mica",
14953
+ concrete: "concreto",
14954
+ brick: "tijolo",
14955
+ plaster: "gesso",
14956
+ plastic: "pl\xE1stico",
14957
+ fabric: "tecido",
14958
+ paper: "papel\xE3o",
14959
+ foliage: "folhagem",
14960
+ soil: "terra",
14961
+ flesh: "carne"
14962
+ },
14963
+ environments: { indoor: "interior", outdoor: "exterior" },
14964
+ strengths: { light: "leve", medium: "m\xE9dio", heavy: "forte" },
14965
+ states: { file: "A", synth: "S", missing: "F" },
14966
+ 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" },
14967
+ 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" },
14968
+ run: { idle: "parada", measuring: "medindo\u2026", done: "pronta", failed: "falhou", recording: "gravando\u2026", recorded: "grava\xE7\xE3o baixada" },
14969
+ words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono" },
14970
+ probe: "Fonte do som",
14971
+ wall: "Parede (oclus\xE3o)",
14972
+ 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" }
14973
+ };
14974
+ var EN2 = {
14975
+ ...PT,
14976
+ group: "Sound",
14977
+ name: "Sound panel (files and C1\u2013C10)",
14978
+ controls: {
14979
+ category: "1 \xB7 Category",
14980
+ sound: "1 \xB7 Sound",
14981
+ play: "1 \xB7 \u25B6 Play",
14982
+ material: "2 \xB7 Material",
14983
+ blow: "2 \xB7 Blow",
14984
+ strike: "2 \xB7 \u25B6 Play blow",
14985
+ distance: "3 \xB7 Distance",
14986
+ bearing: "3 \xB7 Bearing",
14987
+ environment: "3 \xB7 Place",
14988
+ strength: "3 \xB7 Strength",
14989
+ wall: "3 \xB7 Behind the wall",
14990
+ burst: "3 \xB7 Burst 20\xD7",
14991
+ play_a: "4 \xB7 \u25B6 A (current)",
14992
+ play_b: "4 \xB7 \u25B6 B (new)",
14993
+ ab_cycle: "4 \xB7 Alternate A/B",
14994
+ blind: "4 \xB7 Blind (X)",
14995
+ reveal: "4 \xB7 Reveal X",
14996
+ prefer_a: "4 \xB7 Prefer A",
14997
+ prefer_b: "4 \xB7 Prefer B",
14998
+ measure: "5 \xB7 Measure C1\u2013C10",
14999
+ record: "5 \xB7 Record 3 s",
15000
+ approve: "5 \xB7 Approve (C10)"
15001
+ },
15002
+ hints: {
15003
+ sound: "A = file, S = synthesized, F = missing. A file plays the file; the rest plays the current sound.",
15004
+ strike: "Plays impact_<material>_<blow>: the file if there is one, else the current sound of that material.",
15005
+ wall: "Puts the house wall between you and the source: the sound is muffled (C4).",
15006
+ burst: "Twenty times every 60 ms, like a burst (C9).",
15007
+ ab_cycle: "A, B, A, B, one a second.",
15008
+ blind: "Plays A or B without saying which; reveal it after.",
15009
+ measure: "Measures the chosen sound through the real engine (offline context): sync, right sound, distance, side, variation, level, strength, tail and burst.",
15010
+ record: "Records 3 s of the output with the sound playing and downloads it (C10).",
15011
+ approve: "Marks C10 of the chosen sound as approved by your ear."
15012
+ },
15013
+ categories: { armas: "Weapons", impactos: "Impacts", "explosoes-fogo": "Explosions and fire", personagem: "Character", veiculos: "Vehicles", "itens-portas": "Items and doors", ambiente: "Ambience", calibracao: "Calibration (SDK)" },
15014
+ blows: { bullet: "bullet", blunt: "punch/kick", blade: "knife", break: "break", drag: "drag/fall" },
15015
+ materials: {
15016
+ steel: "steel",
15017
+ aluminium: "aluminium",
15018
+ wood: "wood",
15019
+ plywood: "plywood",
15020
+ mdf: "MDF",
15021
+ glass: "glass",
15022
+ tempered_glass: "tempered glass",
15023
+ ceramic: "ceramic",
15024
+ concrete: "concrete",
15025
+ brick: "brick",
15026
+ plaster: "plaster",
15027
+ plastic: "plastic",
15028
+ fabric: "fabric",
15029
+ paper: "cardboard",
15030
+ foliage: "foliage",
15031
+ soil: "soil",
15032
+ flesh: "flesh"
15033
+ },
15034
+ environments: { indoor: "indoor", outdoor: "outdoor" },
15035
+ strengths: { light: "light", medium: "medium", heavy: "heavy" },
15036
+ 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" },
15037
+ 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" },
15038
+ run: { idle: "idle", measuring: "measuring\u2026", done: "done", failed: "failed", recording: "recording\u2026", recorded: "recording downloaded" },
15039
+ words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner" },
15040
+ probe: "Sound source",
15041
+ wall: "Wall (occlusion)",
15042
+ 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" }
15043
+ };
15044
+
15045
+ // ../game-audio/dist/index.js
15046
+ var SFX_TIERS = { light: 0, medium: 0.34, heavy: 0.67 };
15047
+ function splitTier(id) {
15048
+ const match = /^(.*)_(light|medium|heavy)$/.exec(id);
15049
+ return match ? { base: match[1], tier: match[2] } : { base: id };
15050
+ }
15051
+ var refsOf = (entries2, baseUrl) => (entries2 ?? []).map((entry) => ({ url: `${baseUrl}${entry.file}` }));
15052
+ function sampleBankFromManifest(manifest, options) {
15053
+ const baseUrl = options.baseUrl ?? "";
15054
+ const samples = [];
15055
+ const tiersOf = /* @__PURE__ */ new Map();
15056
+ for (const sound of manifest.sounds) {
15057
+ const { base, tier } = splitTier(sound.id);
15058
+ if (tier) tiersOf.set(base, [...tiersOf.get(base) ?? [], sound]);
15059
+ }
15060
+ const bases = /* @__PURE__ */ new Map();
15061
+ for (const sound of manifest.sounds) {
15062
+ if (!splitTier(sound.id).tier) bases.set(sound.id, sound);
15063
+ }
15064
+ for (const [base, tiers] of tiersOf) {
15065
+ if (!bases.has(base)) bases.set(base, { id: base, category: tiers[0]?.category ?? "", layers: { close: tiers[0]?.layers.close ?? [] } });
15066
+ }
15067
+ for (const sound of bases.values()) {
15068
+ const numbers = options.sounds?.[sound.id] ?? options.defaults?.(sound);
15069
+ const close = refsOf(sound.layers.close, baseUrl);
15070
+ if (!numbers || close.length === 0) continue;
15071
+ const tiers = (tiersOf.get(sound.id) ?? []).map((tier) => {
15072
+ const name = splitTier(tier.id).tier;
15073
+ const tierFar = refsOf(tier.layers.far, baseUrl);
15074
+ return { name, from: SFX_TIERS[name] ?? 0, close: refsOf(tier.layers.close, baseUrl), ...tierFar.length > 0 ? { far: tierFar } : {} };
15075
+ }).sort((a, b) => a.from - b.from);
15076
+ const far = refsOf(sound.layers.far, baseUrl);
15077
+ const indoor = refsOf(sound.layers.tail_in, baseUrl);
15078
+ const outdoor = refsOf(sound.layers.tail_out, baseUrl);
15079
+ const files = [sound, ...tiersOf.get(sound.id) ?? []].flatMap((entry) => Object.values(entry.layers).flatMap((entries2) => entries2 ?? []));
15080
+ samples.push({
15081
+ id: sound.id,
15082
+ bus: numbers.bus,
15083
+ close,
15084
+ ...far.length > 0 ? { far } : {},
15085
+ ...tiers.length > 0 ? { tiers } : {},
15086
+ ...numbers.crossover ? { crossover: numbers.crossover } : {},
15087
+ ...indoor.length > 0 || outdoor.length > 0 ? { tail: { ...indoor.length > 0 ? { indoor } : {}, ...outdoor.length > 0 ? { outdoor } : {}, ...numbers.tailGain === void 0 ? {} : { gain: numbers.tailGain } } } : {},
15088
+ ...numbers.pitch === void 0 ? {} : { pitch: numbers.pitch },
15089
+ ...numbers.gain === void 0 ? {} : { gain: numbers.gain },
15090
+ ...numbers.falloff ? { falloff: numbers.falloff } : {},
15091
+ ...numbers.cutoffK === void 0 ? {} : { cutoffK: numbers.cutoffK },
15092
+ ...numbers.group ? { group: numbers.group } : {},
15093
+ ...numbers.priority === void 0 ? {} : { priority: numbers.priority },
15094
+ ...numbers.fallback ? { fallback: numbers.fallback } : {},
15095
+ sources: files.map((file) => `${file.file}: ${file.source} ${file.author} ${file.license}`)
15096
+ });
15097
+ }
15098
+ return { id: options.id, samples };
15099
+ }
14173
15100
  var ATTACK = 2e-3;
14174
15101
  function lengthOf(d, attack) {
14175
15102
  if (typeof d === "number") return attack + d;
@@ -14207,6 +15134,31 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14207
15134
  envelope: envelope(ATTACK)
14208
15135
  };
14209
15136
  }
15137
+ function sceneVoice(o) {
15138
+ return {
15139
+ duration: lengthOf(o.d, 0),
15140
+ gain: o.vol,
15141
+ wet: 0.45,
15142
+ source: {
15143
+ kind: "formant_voice",
15144
+ wave: o.wave ?? "sawtooth",
15145
+ frequency: { from: o.f0, to: o.f1, curve: "linear", over: "duration" },
15146
+ vibrato: { rate: [4, 10], depth: 0.08 },
15147
+ formants: [
15148
+ { frequency: o.fA, q: 4 },
15149
+ { frequency: o.fB, q: 6 }
15150
+ ],
15151
+ breath: { frequency: o.fB, q: 1.1, gain: o.noise },
15152
+ mix: 3.2
15153
+ },
15154
+ envelope: [
15155
+ [0, 1e-4, "set", "fixed"],
15156
+ [0.1, 1, "exp"],
15157
+ ["0.6d", 1, "set"],
15158
+ ["1d", 1e-4, "exp", "fixed"]
15159
+ ]
15160
+ };
15161
+ }
14210
15162
  var sceneMany = (count, make) => Array.from({ length: count }, make);
14211
15163
  function sceneRecipe(id, layers, line, options = {}) {
14212
15164
  return {
@@ -14220,50 +15172,930 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14220
15172
  sources: [`cena:${line}`]
14221
15173
  };
14222
15174
  }
14223
- var BENCH_BANK = {
14224
- id: "audio_bench",
14225
- sounds: [
14226
- {
14227
- id: "bench_ping",
14228
- bus: "ui",
14229
- pan: true,
14230
- duration: 0.25,
14231
- layers: [
14232
- {
14233
- gain: 0.5,
14234
- wet: 0.3,
14235
- source: { kind: "osc", wave: "sine", frequency: { from: 880 } },
14236
- envelope: [
14237
- [0, 1e-4, "set", "fixed"],
14238
- [2e-3, 1, "exp"],
14239
- ["1d", 1e-4, "exp", "fixed"]
14240
- ]
14241
- }
14242
- ],
14243
- falloff: DISTANCE_CURVES.art_scene,
14244
- sources: ["sdk: bench probe, a 880 Hz ping through the art scene output stage"]
14245
- },
14246
- {
14247
- id: "bench_hiss",
14248
- bus: "ui",
14249
- pan: true,
14250
- duration: [0.3, 0.9],
14251
- layers: [
14252
- {
14253
- gain: 0.4,
14254
- source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
14255
- filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3e3 }, q: 0.7 }] },
14256
- envelope: [
14257
- [0, 1e-4, "set", "fixed"],
14258
- [2e-3, 1, "exp"],
14259
- ["1d", 1e-4, "exp", "fixed"]
14260
- ]
14261
- }
14262
- ],
14263
- falloff: DISTANCE_CURVES.art_scene,
14264
- sources: ["sdk: bench probe, a highpass hiss of random length"]
14265
- }
14266
- ]
15175
+ var FOLEY_FALLOFF = {
15176
+ model: "inverse_square",
15177
+ k: 0.02,
15178
+ cutoffK: 0.04,
15179
+ minGain: 4e-3
15180
+ };
15181
+ var vary = (f, pct = 0.04) => [Math.round(f * (1 - pct)), Math.round(f * (1 + pct))];
15182
+ var foley = (id, layers, note) => ({
15183
+ id,
15184
+ bus: "world",
15185
+ pan: true,
15186
+ layers,
15187
+ falloff: FOLEY_FALLOFF,
15188
+ sources: [`sdk: ${note}`]
15189
+ });
15190
+ var click = (f, vol, delay = 0) => [
15191
+ sceneNoise({
15192
+ type: "bandpass",
15193
+ f: vary(f, 0.08),
15194
+ q: 4,
15195
+ d: [0.018, 0.03],
15196
+ vol: [vol * 0.85, vol * 1.1],
15197
+ delay
15198
+ }),
15199
+ sceneTone({
15200
+ f: vary(f * 0.8, 0.06),
15201
+ f2: Math.round(f * 0.62),
15202
+ d: [0.015, 0.025],
15203
+ vol: vol * 0.18,
15204
+ delay
15205
+ })
15206
+ ];
15207
+ var meet = (body, panel, vol, latch) => [
15208
+ sceneTone({
15209
+ f: vary(body),
15210
+ f2: Math.round(body * 0.5),
15211
+ d: [0.12, 0.16],
15212
+ vol: [vol * 0.9, vol * 1.1]
15213
+ }),
15214
+ sceneNoise({
15215
+ type: "bandpass",
15216
+ f: vary(panel, 0.06),
15217
+ q: 1.2,
15218
+ d: [0.08, 0.12],
15219
+ vol: [vol * 0.9, vol * 1.1],
15220
+ wet: 0.25
15221
+ }),
15222
+ ...click(latch, vol * 0.45, [0.012, 0.028])
15223
+ ];
15224
+ var LEAF_FOLEY = [
15225
+ // a wooden house door or wardrobe door: latch, a short creak, the leaf on the frame (the hard slam adds the frame rattling)
15226
+ foley("door_wood_open", click(2400, 0.4), "the latch of a wooden door giving"),
15227
+ foley(
15228
+ "door_creak",
15229
+ [
15230
+ sceneTone({ f: [190, 240], f2: 310, d: [0.4, 0.55], vol: [0.04, 0.06] }),
15231
+ sceneNoise({
15232
+ type: "bandpass",
15233
+ f: [480, 560],
15234
+ q: 6,
15235
+ a: 0.05,
15236
+ d: [0.35, 0.5],
15237
+ vol: [0.1, 0.14]
15238
+ })
15239
+ ],
15240
+ "a wooden hinge turning"
15241
+ ),
15242
+ foley("door_wood_close", meet(120, 600, 0.55, 2400), "a wooden leaf meeting its frame"),
15243
+ foley(
15244
+ "door_wood_slam",
15245
+ [
15246
+ ...meet(110, 560, 0.85, 2300),
15247
+ ...sceneMany(
15248
+ 3,
15249
+ () => sceneNoise({
15250
+ type: "bandpass",
15251
+ f: [900, 1500],
15252
+ q: 3,
15253
+ d: 0.03,
15254
+ vol: [0.08, 0.14],
15255
+ delay: [0.05, 0.18]
15256
+ })
15257
+ )
15258
+ ],
15259
+ "a wooden door slammed: the frame and the hinges rattle after the hit"
15260
+ ),
15261
+ // a metal door or gate: a hollow panel that rings a little
15262
+ 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"),
15263
+ foley(
15264
+ "door_creak_metal",
15265
+ [
15266
+ sceneTone({ f: [620, 760], f2: 900, d: [0.3, 0.4], vol: [0.03, 0.045] }),
15267
+ sceneNoise({
15268
+ type: "bandpass",
15269
+ f: [1300, 1500],
15270
+ q: 8,
15271
+ a: 0.04,
15272
+ d: [0.25, 0.35],
15273
+ vol: [0.07, 0.09]
15274
+ })
15275
+ ],
15276
+ "a metal hinge turning"
15277
+ ),
15278
+ // the handle of a locked door shaken: the latch knocking in its keeper, five to seven times in half a second
15279
+ foley(
15280
+ "door_locked_rattle",
15281
+ [
15282
+ ...sceneMany(
15283
+ 6,
15284
+ () => sceneNoise({
15285
+ type: "bandpass",
15286
+ f: [1600, 2600],
15287
+ q: 3,
15288
+ d: [0.025, 0.04],
15289
+ vol: [0.22, 0.34],
15290
+ delay: [0, 0.45]
15291
+ })
15292
+ ),
15293
+ ...sceneMany(
15294
+ 3,
15295
+ () => sceneTone({
15296
+ f: [170, 210],
15297
+ f2: 120,
15298
+ d: 0.05,
15299
+ vol: [0.12, 0.18],
15300
+ delay: [0.02, 0.4]
15301
+ })
15302
+ )
15303
+ ],
15304
+ "the handle of a locked door shaken: latch knocks and the leaf thumping in its frame"
15305
+ ),
15306
+ // 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
15307
+ foley(
15308
+ "fridge_open",
15309
+ [
15310
+ sceneNoise({
15311
+ type: "lowpass",
15312
+ f: [320, 380],
15313
+ a: 0.03,
15314
+ d: [0.14, 0.2],
15315
+ vol: [0.32, 0.4]
15316
+ }),
15317
+ sceneNoise({
15318
+ type: "bandpass",
15319
+ f: [800, 950],
15320
+ f2: 1400,
15321
+ q: 2,
15322
+ a: 0.02,
15323
+ d: 0.12,
15324
+ vol: [0.08, 0.12]
15325
+ }),
15326
+ ...sceneMany(
15327
+ 2,
15328
+ () => sceneTone({
15329
+ f: [2600, 4200],
15330
+ d: [0.04, 0.08],
15331
+ vol: [0.015, 0.03],
15332
+ delay: [0.08, 0.22]
15333
+ })
15334
+ )
15335
+ ],
15336
+ "a fridge door: the seal letting go and the bottles in the door"
15337
+ ),
15338
+ foley(
15339
+ "fridge_close",
15340
+ [
15341
+ sceneTone({ f: vary(85), f2: 45, d: [0.1, 0.13], vol: [0.38, 0.46] }),
15342
+ sceneNoise({
15343
+ type: "lowpass",
15344
+ f: [520, 620],
15345
+ d: [0.08, 0.12],
15346
+ vol: [0.3, 0.36]
15347
+ }),
15348
+ sceneNoise({
15349
+ type: "lowpass",
15350
+ f: [260, 320],
15351
+ a: 0.02,
15352
+ d: 0.1,
15353
+ vol: [0.12, 0.18],
15354
+ delay: [0.04, 0.07]
15355
+ }),
15356
+ ...sceneMany(
15357
+ 2,
15358
+ () => sceneTone({
15359
+ f: [2600, 4200],
15360
+ d: [0.04, 0.07],
15361
+ vol: [0.015, 0.03],
15362
+ delay: [0.03, 0.12]
15363
+ })
15364
+ )
15365
+ ],
15366
+ "a fridge door shut: a soft thump, the seal pulling and the bottles"
15367
+ ),
15368
+ // a microwave: the spring catch of the door, plastic on plastic
15369
+ foley(
15370
+ "microwave_open",
15371
+ [
15372
+ ...click(1800, 0.34),
15373
+ sceneTone({
15374
+ f: [3e3, 3400],
15375
+ f2: 2800,
15376
+ d: 0.05,
15377
+ vol: 0.025,
15378
+ delay: [0.01, 0.02]
15379
+ })
15380
+ ],
15381
+ "the spring catch of a microwave door"
15382
+ ),
15383
+ foley(
15384
+ "microwave_close",
15385
+ [
15386
+ sceneNoise({
15387
+ type: "bandpass",
15388
+ f: [1200, 1450],
15389
+ q: 2.5,
15390
+ d: [0.04, 0.06],
15391
+ vol: [0.36, 0.44]
15392
+ }),
15393
+ sceneTone({ f: vary(180), f2: 120, d: 0.05, vol: [0.16, 0.22] }),
15394
+ ...click(2200, 0.2, [8e-3, 0.016])
15395
+ ],
15396
+ "a microwave door clacking shut"
15397
+ ),
15398
+ foley("microwave_beep", [sceneTone({ wave: "square", f: 2e3, d: 0.12, vol: 0.05 })], "the beep of a microwave"),
15399
+ // drawers and sliding leaves: the runners, then the stop
15400
+ foley(
15401
+ "drawer_open",
15402
+ [
15403
+ sceneNoise({
15404
+ type: "bandpass",
15405
+ f: [620, 780],
15406
+ q: 2,
15407
+ a: 0.04,
15408
+ d: [0.28, 0.36],
15409
+ vol: [0.18, 0.24]
15410
+ }),
15411
+ sceneNoise({ type: "bandpass", f: [2e3, 2400], q: 5, d: 0.28, vol: 0.04 }),
15412
+ ...click(1400, 0.16, [0.28, 0.34])
15413
+ ],
15414
+ "a drawer on its runners and its stop"
15415
+ ),
15416
+ foley(
15417
+ "drawer_close",
15418
+ [
15419
+ sceneNoise({
15420
+ type: "bandpass",
15421
+ f: [620, 780],
15422
+ q: 2,
15423
+ a: 0.03,
15424
+ d: [0.2, 0.28],
15425
+ vol: [0.18, 0.24]
15426
+ }),
15427
+ sceneTone({
15428
+ f: vary(150),
15429
+ f2: 90,
15430
+ d: 0.08,
15431
+ vol: [0.22, 0.3],
15432
+ delay: [0.2, 0.26]
15433
+ })
15434
+ ],
15435
+ "a drawer pushed home"
15436
+ )
15437
+ ];
15438
+ var ITEM_FOLEY = [
15439
+ // the hand closing on the item: a touch of skin and cloth, then the item's own small sound
15440
+ foley(
15441
+ "item_pickup_metal",
15442
+ [
15443
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
15444
+ sceneTone({
15445
+ f: [1900, 2600],
15446
+ f2: 1700,
15447
+ d: [0.05, 0.08],
15448
+ vol: [0.06, 0.09],
15449
+ delay: [0.01, 0.03]
15450
+ })
15451
+ ],
15452
+ "picking up a metal thing"
15453
+ ),
15454
+ foley(
15455
+ "item_pickup_plastic",
15456
+ [
15457
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
15458
+ sceneNoise({
15459
+ type: "bandpass",
15460
+ f: [1500, 1900],
15461
+ q: 3,
15462
+ d: 0.03,
15463
+ vol: [0.15, 0.21],
15464
+ delay: [0.01, 0.03]
15465
+ })
15466
+ ],
15467
+ "picking up a plastic thing"
15468
+ ),
15469
+ foley(
15470
+ "item_pickup_glass",
15471
+ [
15472
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.15 }),
15473
+ sceneTone({
15474
+ f: [3200, 4200],
15475
+ d: [0.05, 0.08],
15476
+ vol: [0.038, 0.06],
15477
+ delay: [0.01, 0.03]
15478
+ })
15479
+ ],
15480
+ "picking up a glass thing"
15481
+ ),
15482
+ foley(
15483
+ "item_pickup_paper",
15484
+ [
15485
+ sceneNoise({
15486
+ type: "highpass",
15487
+ f: [2600, 3400],
15488
+ a: 0.01,
15489
+ d: [0.1, 0.14],
15490
+ vol: [0.12, 0.18]
15491
+ }),
15492
+ sceneNoise({ type: "bandpass", f: [800, 1e3], q: 1, d: 0.05, vol: 0.12 })
15493
+ ],
15494
+ "picking up paper or cardboard"
15495
+ ),
15496
+ foley(
15497
+ "item_pickup_fabric",
15498
+ [
15499
+ sceneNoise({
15500
+ type: "bandpass",
15501
+ f: [900, 1300],
15502
+ q: 0.8,
15503
+ a: 0.02,
15504
+ d: [0.12, 0.16],
15505
+ vol: [0.15, 0.21]
15506
+ })
15507
+ ],
15508
+ "picking up cloth"
15509
+ ),
15510
+ foley(
15511
+ "item_pickup_wood",
15512
+ [
15513
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
15514
+ sceneNoise({
15515
+ type: "bandpass",
15516
+ f: [900, 1100],
15517
+ q: 2,
15518
+ d: 0.04,
15519
+ vol: [0.15, 0.21],
15520
+ delay: [0.01, 0.03]
15521
+ })
15522
+ ],
15523
+ "picking up a wooden thing"
15524
+ ),
15525
+ foley(
15526
+ "item_pickup_food",
15527
+ [
15528
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
15529
+ sceneNoise({
15530
+ type: "lowpass",
15531
+ f: [500, 650],
15532
+ d: 0.05,
15533
+ vol: 0.12,
15534
+ delay: [0.01, 0.03]
15535
+ })
15536
+ ],
15537
+ "picking up food"
15538
+ ),
15539
+ foley(
15540
+ "item_throw",
15541
+ [
15542
+ sceneNoise({
15543
+ type: "bandpass",
15544
+ f: [450, 650],
15545
+ f2: 1400,
15546
+ q: 1.5,
15547
+ a: 0.05,
15548
+ d: 0.18,
15549
+ vol: [0.1, 0.14]
15550
+ })
15551
+ ],
15552
+ "the arm throwing something: air"
15553
+ ),
15554
+ // a can: the tab cracks, the gas hisses out; drinking is gulps; crushing it crumples the sheet
15555
+ foley(
15556
+ "can_open",
15557
+ [
15558
+ ...click(3200, 0.36),
15559
+ sceneNoise({
15560
+ type: "highpass",
15561
+ f: [3600, 4400],
15562
+ a: 5e-3,
15563
+ d: [0.22, 0.3],
15564
+ vol: [0.16, 0.22],
15565
+ delay: [0.012, 0.02]
15566
+ }),
15567
+ ...sceneMany(5, () => sceneTone({ f: [4e3, 7e3], d: 0.02, vol: 0.01, delay: [0.05, 0.4] }))
15568
+ ],
15569
+ "the tab of a can and the gas hissing out"
15570
+ ),
15571
+ foley(
15572
+ "can_crush",
15573
+ [
15574
+ sceneNoise({
15575
+ type: "bandpass",
15576
+ f: [1600, 2200],
15577
+ q: 1.2,
15578
+ d: [0.18, 0.25],
15579
+ vol: [0.3, 0.38]
15580
+ }),
15581
+ ...sceneMany(
15582
+ 6,
15583
+ () => sceneTone({
15584
+ f: [1200, 3400],
15585
+ f2: 900,
15586
+ d: 0.03,
15587
+ vol: [0.03, 0.06],
15588
+ delay: [0, 0.2]
15589
+ })
15590
+ )
15591
+ ],
15592
+ "a can crushed in the hand"
15593
+ ),
15594
+ foley(
15595
+ "can_puncture_spray",
15596
+ [
15597
+ sceneNoise({
15598
+ type: "highpass",
15599
+ f: [2400, 3e3],
15600
+ a: 5e-3,
15601
+ d: [0.5, 0.7],
15602
+ vol: [0.2, 0.26]
15603
+ }),
15604
+ sceneNoise({ type: "bandpass", f: [600, 800], q: 2, d: 0.4, vol: 0.06 })
15605
+ ],
15606
+ "a can holed: the jet hissing out"
15607
+ ),
15608
+ foley(
15609
+ "bottle_open",
15610
+ [
15611
+ ...sceneMany(
15612
+ 5,
15613
+ () => sceneNoise({
15614
+ type: "bandpass",
15615
+ f: [2200, 3e3],
15616
+ q: 4,
15617
+ d: 8e-3,
15618
+ vol: [0.1, 0.16],
15619
+ delay: [0, 0.18]
15620
+ })
15621
+ ),
15622
+ sceneNoise({
15623
+ type: "highpass",
15624
+ f: 3800,
15625
+ a: 5e-3,
15626
+ d: 0.12,
15627
+ vol: [0.06, 0.1],
15628
+ delay: [0.18, 0.22]
15629
+ })
15630
+ ],
15631
+ "the screw cap of a bottle turning off"
15632
+ ),
15633
+ foley(
15634
+ "drink_gulp",
15635
+ [
15636
+ sceneNoise({
15637
+ type: "lowpass",
15638
+ f: [280, 360],
15639
+ a: 0.02,
15640
+ d: [0.1, 0.14],
15641
+ vol: [0.24, 0.3]
15642
+ }),
15643
+ sceneTone({ f: [140, 170], f2: 85, d: [0.08, 0.11], vol: [0.12, 0.16] })
15644
+ ],
15645
+ "a gulp"
15646
+ ),
15647
+ foley(
15648
+ "eat_crunch",
15649
+ [
15650
+ ...sceneMany(
15651
+ 4,
15652
+ () => sceneNoise({
15653
+ type: "bandpass",
15654
+ f: [1800, 3200],
15655
+ q: 1.5,
15656
+ d: [0.02, 0.04],
15657
+ vol: [0.12, 0.2],
15658
+ delay: [0, 0.12]
15659
+ })
15660
+ )
15661
+ ],
15662
+ "a bite of something crunchy"
15663
+ ),
15664
+ foley(
15665
+ "eat_chew",
15666
+ [
15667
+ sceneNoise({
15668
+ type: "lowpass",
15669
+ f: [450, 600],
15670
+ a: 0.03,
15671
+ d: [0.1, 0.14],
15672
+ vol: [0.12, 0.16]
15673
+ }),
15674
+ sceneNoise({
15675
+ type: "bandpass",
15676
+ f: [1100, 1400],
15677
+ q: 2,
15678
+ d: 0.04,
15679
+ vol: 0.04,
15680
+ delay: [0.03, 0.06]
15681
+ })
15682
+ ],
15683
+ "chewing"
15684
+ ),
15685
+ foley(
15686
+ "bandage_wrap",
15687
+ [
15688
+ sceneNoise({
15689
+ type: "bandpass",
15690
+ f: [1400, 1800],
15691
+ f2: 2400,
15692
+ q: 1,
15693
+ a: 0.04,
15694
+ d: [0.3, 0.4],
15695
+ vol: [0.12, 0.16]
15696
+ })
15697
+ ],
15698
+ "a bandage unrolled round a limb"
15699
+ ),
15700
+ foley(
15701
+ "pills_shake",
15702
+ [
15703
+ ...sceneMany(
15704
+ 8,
15705
+ () => sceneTone({
15706
+ f: [2400, 3600],
15707
+ d: 0.015,
15708
+ vol: [0.03, 0.05],
15709
+ delay: [0, 0.25]
15710
+ })
15711
+ ),
15712
+ sceneNoise({ type: "bandpass", f: [1500, 1900], q: 2, d: 0.25, vol: 0.05 })
15713
+ ],
15714
+ "pills shaken in their bottle"
15715
+ ),
15716
+ foley(
15717
+ "medkit_use",
15718
+ [
15719
+ ...click(1700, 0.24),
15720
+ sceneNoise({
15721
+ type: "bandpass",
15722
+ f: [1e3, 1400],
15723
+ q: 1,
15724
+ a: 0.03,
15725
+ d: [0.25, 0.32],
15726
+ vol: [0.08, 0.12],
15727
+ delay: [0.05, 0.08]
15728
+ })
15729
+ ],
15730
+ "a first aid kit opened and rummaged"
15731
+ ),
15732
+ foley("flashlight_click", click(2600, 0.3), "the switch of a flashlight")
15733
+ ];
15734
+ var FOLEY_BANK = {
15735
+ id: "foley",
15736
+ sounds: [...LEAF_FOLEY, ...ITEM_FOLEY]
15737
+ };
15738
+ var TAKES = [
15739
+ { f: 1, t: 1, v: 1 },
15740
+ { f: 1.08, t: 0.9, v: 0.92 },
15741
+ { f: 0.93, t: 1.12, v: 1.05 },
15742
+ { f: 1.03, t: 1.05, v: 0.88 }
15743
+ ];
15744
+ var FOOTSTEP_FALLOFF = { model: "inverse_square", k: 0.03, cutoffK: 0.03, minGain: 4e-3 };
15745
+ var BODY_FALLOFF = { model: "inverse_square", k: 0.012, cutoffK: 0.015, minGain: 4e-3 };
15746
+ var SURFACES = {
15747
+ concrete: { heel: { f: 95, d: 0.05, vol: 0.55 }, roll: { type: "bandpass", f: 2300, q: 1.1, d: 0.05, vol: 0.38 }, grains: { count: 2, f: 4200, q: 2, spread: 0.04, vol: 0.08 } },
15748
+ asphalt: { heel: { f: 90, d: 0.05, vol: 0.5 }, roll: { type: "bandpass", f: 1700, q: 0.9, d: 0.06, vol: 0.36 }, grains: { count: 4, f: 3600, q: 2.5, spread: 0.06, vol: 0.1 } },
15749
+ wood: { heel: { f: 130, d: 0.08, vol: 0.55 }, roll: { type: "lowpass", f: 1300, q: 0.8, d: 0.06, vol: 0.3 }, ring: [{ f: 210, d: 0.1, vol: 0.18 }, { f: 470, d: 0.06, vol: 0.06 }] },
15750
+ tile: { heel: { f: 110, d: 0.04, vol: 0.45 }, roll: { type: "bandpass", f: 3600, q: 2, d: 0.03, vol: 0.42 }, ring: [{ f: 1900, d: 0.03, vol: 0.05 }] },
15751
+ dirt: { heel: { f: 75, d: 0.06, vol: 0.45 }, roll: { type: "lowpass", f: 750, q: 0.7, d: 0.09, vol: 0.4 }, grains: { count: 3, f: 1800, q: 1.5, spread: 0.07, vol: 0.06 } },
15752
+ grass: { heel: { f: 70, d: 0.05, vol: 0.32 }, roll: { type: "highpass", f: 2600, q: 0.7, d: 0.13, vol: 0.28 }, grains: { count: 4, f: 5200, q: 1.2, spread: 0.1, vol: 0.07 } },
15753
+ gravel: { heel: { f: 80, d: 0.05, vol: 0.4 }, roll: { type: "lowpass", f: 950, q: 0.8, d: 0.1, vol: 0.3 }, grains: { count: 8, f: 2600, q: 3, spread: 0.13, vol: 0.2 } },
15754
+ sand: { heel: { f: 65, d: 0.06, vol: 0.3 }, roll: { type: "bandpass", f: 1500, q: 0.6, d: 0.16, vol: 0.3 }, grains: { count: 5, f: 4800, q: 1.5, spread: 0.12, vol: 0.05 } },
15755
+ metal: { heel: { f: 120, d: 0.05, vol: 0.5 }, roll: { type: "bandpass", f: 2800, q: 1.5, d: 0.03, vol: 0.25 }, ring: [{ f: 420, d: 0.25, vol: 0.16 }, { f: 1130, d: 0.18, vol: 0.09 }, { f: 2470, d: 0.1, vol: 0.04 }] },
15756
+ water: { heel: { f: 70, d: 0.04, vol: 0.25 }, roll: { type: "bandpass", f: 3e3, q: 1.2, d: 0.1, vol: 0.18 }, splash: { f: 1100, f2: 380, d: 0.22, vol: 0.55 }, grains: { count: 4, f: 2400, q: 6, spread: 0.18, vol: 0.12 } },
15757
+ glass: { heel: { f: 90, d: 0.04, vol: 0.4 }, roll: { type: "highpass", f: 4500, q: 0.8, d: 0.06, vol: 0.2 }, grains: { count: 9, f: 6200, q: 5, spread: 0.12, vol: 0.22 }, ring: [{ f: 3900, d: 0.04, vol: 0.04 }, { f: 5300, d: 0.03, vol: 0.03 }] },
15758
+ carpet: { heel: { f: 70, d: 0.05, vol: 0.32 }, roll: { type: "lowpass", f: 420, q: 0.7, d: 0.07, vol: 0.3 } }
15759
+ };
15760
+ var GAITS = {
15761
+ walk: { vol: 1, bright: 1, toe: 0.075, short: 1 },
15762
+ run: { vol: 1.35, bright: 1.25, toe: 0.035, short: 0.75 }
15763
+ };
15764
+ var grainLayers = (g, take, start, scale) => Array.from(
15765
+ { length: g.count },
15766
+ (_, i) => sceneNoise({ type: "bandpass", f: [g.f * take.f * 0.85, g.f * take.f * 1.15], q: g.q, d: [8e-3, 0.02], vol: g.vol * scale * take.v, delay: start + (i + 0.5) / g.count * g.spread * take.t })
15767
+ );
15768
+ var ringLayers = (rings, take, scale, at3 = 0) => rings.map((r) => sceneTone({ f: r.f * take.f, d: r.d * take.t, vol: r.vol * scale * take.v, delay: at3 }));
15769
+ function footLayers(surface, take, gait, weight = 1) {
15770
+ const s = SURFACES[surface];
15771
+ const toe = gait.toe * take.t;
15772
+ return [
15773
+ sceneTone({ f: s.heel.f * take.f * weight ** -0.3, f2: s.heel.f * take.f * 0.6, d: s.heel.d * gait.short * take.t * weight ** 0.5, vol: s.heel.vol * gait.vol * take.v * weight ** 0.5 }),
15774
+ sceneNoise({ type: "lowpass", f: 500 * gait.bright, d: 0.02 * take.t, vol: 0.18 * gait.vol * take.v * weight ** 0.5 }),
15775
+ sceneNoise({ type: s.roll.type, f: [s.roll.f * take.f * gait.bright * 0.92, s.roll.f * take.f * gait.bright * 1.08], q: s.roll.q, d: s.roll.d * gait.short * take.t * weight ** 0.3, vol: s.roll.vol * gait.vol * take.v, delay: toe }),
15776
+ ...s.grains ? grainLayers(s.grains, take, toe * 0.5, gait.vol * weight ** 0.5) : [],
15777
+ ...s.ring ? ringLayers(s.ring, take, gait.vol * weight ** 0.5) : [],
15778
+ ...s.splash ? [sceneNoise({ type: "bandpass", f: s.splash.f * take.f, f2: s.splash.f2 * take.f, q: 1.4, d: s.splash.d * take.t * weight ** 0.4, vol: s.splash.vol * gait.vol * take.v * weight ** 0.5 })] : []
15779
+ ];
15780
+ }
15781
+ var quieter = (level, layers) => layers.map((layer2) => {
15782
+ const gain = layer2.gain ?? 1;
15783
+ if (typeof gain === "number") return { ...layer2, gain: gain * level };
15784
+ if (Array.isArray(gain)) return { ...layer2, gain: [gain[0] * level, gain[1] * level] };
15785
+ return layer2;
15786
+ });
15787
+ var recipe = (id, layers, falloff, bus = "world") => ({ id, bus, pan: true, layers, falloff, sources: ["sdk"] });
15788
+ var takeId = (id, index) => `${id}_${String(index + 1).padStart(2, "0")}`;
15789
+ function takes(id, layersOf, falloff, rate = [0.96, 1.04], bus) {
15790
+ const sounds = TAKES.map((take, index) => recipe(takeId(id, index), layersOf(take, index), falloff, bus));
15791
+ return { sounds, set: { id, variants: sounds.map((sound) => sound.id), rate } };
15792
+ }
15793
+ var footstep = (surface, gait) => takes(footstepSoundId(surface, gait), (take) => footLayers(surface, take, GAITS[gait]), FOOTSTEP_FALLOFF);
15794
+ var LANDINGS = { light: { weight: 1.4, cloth: 0.12 }, heavy: { weight: 2.4, cloth: 0.22 } };
15795
+ var LANDING_LEVEL = 0.65;
15796
+ var landing = (surface, landing2) => takes(
15797
+ `land_${surface}_${landing2}`,
15798
+ (take) => quieter(LANDING_LEVEL, [
15799
+ // both feet, a hair apart, then the knees taking the weight (a low body thump) and the clothes settling
15800
+ ...footLayers(surface, take, GAITS.run, LANDINGS[landing2].weight),
15801
+ ...footLayers(surface, { ...take, v: take.v * 0.6 }, GAITS.run, LANDINGS[landing2].weight * 0.6).map((layer2) => ({ ...layer2, at: (typeof layer2.at === "number" ? layer2.at : 0) + 0.018 * take.t })),
15802
+ sceneTone({ f: 60 * take.f, f2: 38, d: 0.14 * LANDINGS[landing2].weight * take.t, vol: 0.22 * LANDINGS[landing2].weight ** 0.7 * take.v, delay: 0.02 }),
15803
+ sceneNoise({ type: "bandpass", f: [1800, 2600], q: 0.8, d: 0.18, vol: LANDINGS[landing2].cloth * take.v, delay: 0.04 })
15804
+ ]),
15805
+ FOOTSTEP_FALLOFF
15806
+ );
15807
+ var whoosh = (blow3, from, to, d, vol, q = 1.6) => (take) => [
15808
+ // the air: a band sweeping up as the limb speeds up; it peaks `SWING_LEAD_SECONDS` in, the moment before the contact, then dies away
15809
+ sceneNoise({ type: "bandpass", f: from * take.f, f2: to * take.f, q, a: SWING_LEAD_SECONDS[blow3], d: d * take.t, vol: vol * take.v }),
15810
+ sceneNoise({ type: "lowpass", f: 600 * take.f, a: SWING_LEAD_SECONDS[blow3], d: d * take.t * 0.8, vol: vol * 0.35 * take.v })
15811
+ ];
15812
+ var fleshHit = (o) => (take) => [
15813
+ sceneNoise({ type: "bandpass", f: o.slapF * take.f, q: 1.2, d: 0.03 * take.t, vol: o.slap * take.v }),
15814
+ sceneNoise({ type: "highpass", f: 3500 * take.f, d: 0.012, vol: o.slap * 0.4 * take.v }),
15815
+ sceneTone({ f: o.thudF * take.f, f2: o.thudF * 0.5, d: o.thudD * take.t, vol: o.thud * take.v }),
15816
+ sceneNoise({ type: "lowpass", f: 600 * take.f, d: o.thudD * 1.2 * take.t, vol: o.thud * 0.5 * take.v }),
15817
+ ...o.crack ? [sceneNoise({ type: "highpass", f: 2500, d: 0.015, vol: o.crack * take.v, delay: 6e-3 }), sceneTone({ wave: "square", f: 900 * take.f, f2: 300, d: 0.02, vol: o.crack * 0.15 * take.v, delay: 6e-3 })] : [],
15818
+ ...o.wet ? [sceneNoise({ type: "bandpass", f: 1400 * take.f, f2: 500, q: 2.5, d: 0.12 * take.t, vol: o.wet * take.v, delay: 0.01 })] : []
15819
+ ];
15820
+ var knock = (o) => (take) => [
15821
+ sceneNoise({ type: "bandpass", f: o.clickF * take.f, q: 1.5, d: 0.018, vol: o.click * take.v }),
15822
+ ...o.tones.map(([f, d, vol]) => sceneTone({ f: f * take.f, d: d * take.t, vol: vol * take.v }))
15823
+ ];
15824
+ var voice = (o) => (take) => [
15825
+ sceneVoice({ f0: o.f0 * take.f, f1: o.f1 * take.f, d: o.d * take.t, fA: o.fA * take.f, fB: o.fB * take.f, noise: o.noise, vol: o.vol * take.v })
15826
+ ];
15827
+ var BODY = {
15828
+ jump: (take) => [
15829
+ sceneNoise({ type: "bandpass", f: 900 * take.f, f2: 1700 * take.f, q: 1, a: 0.03, d: 0.12, vol: 0.18 * take.v }),
15830
+ sceneNoise({ type: "bandpass", f: 2200 * take.f, q: 0.8, d: 0.04, vol: 0.12 * take.v }),
15831
+ sceneTone({ f: 85 * take.f, f2: 60, d: 0.04, vol: 0.2 * take.v })
15832
+ ],
15833
+ cloth_rustle: (take) => [
15834
+ { ...sceneNoise({ type: "bandpass", f: 2400 * take.f, q: 0.8, a: 0.06, d: 0.22 * take.t, vol: 0.12 * take.v }), modulators: [{ target: "gain", wave: "triangle", rate: [9, 14], depth: 0.5 }] }
15835
+ ],
15836
+ body_fall: (take) => [
15837
+ sceneTone({ f: 58 * take.f, f2: 35, d: 0.28 * take.t, vol: 0.9 * take.v }),
15838
+ sceneNoise({ type: "lowpass", f: 480 * take.f, d: 0.3 * take.t, vol: 0.55 * take.v }),
15839
+ sceneNoise({ type: "bandpass", f: 1300 * take.f, q: 1, d: 0.05, vol: 0.3 * take.v }),
15840
+ // the limbs a moment later, and the clothes
15841
+ sceneTone({ f: 75 * take.f, f2: 45, d: 0.12, vol: 0.4 * take.v, delay: 0.13 * take.t }),
15842
+ sceneNoise({ type: "bandpass", f: 2200, q: 0.8, d: 0.2, vol: 0.12 * take.v, delay: 0.05 })
15843
+ ],
15844
+ fall_damage: (take) => [
15845
+ ...BODY.body_fall(take),
15846
+ sceneNoise({ type: "highpass", f: 2800, d: 0.02, vol: 0.35 * take.v, delay: 0.01 }),
15847
+ ...voice({ f0: 170, f1: 110, d: 0.32, fA: 650, fB: 1150, noise: 0.25, vol: 0.32 })(take).map((layer2) => ({ ...layer2, at: 0.08 }))
15848
+ ],
15849
+ player_hurt: voice({ f0: 150, f1: 105, d: 0.26, fA: 620, fB: 1100, noise: 0.3, vol: 0.35 }),
15850
+ player_breath_run: (take) => [
15851
+ sceneNoise({ type: "bandpass", f: 1200 * take.f, q: 1, a: 0.08, d: 0.22 * take.t, vol: 0.25 * take.v }),
15852
+ sceneNoise({ type: "bandpass", f: 900 * take.f, q: 1, a: 0.06, d: 0.26 * take.t, vol: 0.3 * take.v, delay: 0.3 * take.t })
15853
+ ],
15854
+ player_death: voice({ f0: 135, f1: 68, d: 1, fA: 560, fB: 980, noise: 0.35, vol: 0.4 }),
15855
+ bite: (take) => [
15856
+ sceneNoise({ type: "highpass", f: 2600, d: 0.015, vol: 0.4 * take.v }),
15857
+ sceneNoise({ type: "bandpass", f: 1600 * take.f, f2: 450, q: 2.2, d: 0.16 * take.t, vol: 0.55 * take.v, delay: 0.01 }),
15858
+ sceneNoise({ type: "bandpass", f: 3200 * take.f, f2: 1800, q: 1.2, d: 0.22 * take.t, vol: 0.25 * take.v, delay: 0.09 }),
15859
+ sceneTone({ f: 120 * take.f, f2: 70, d: 0.06, vol: 0.3 * take.v })
15860
+ ],
15861
+ grab: (take) => [
15862
+ sceneNoise({ type: "bandpass", f: 1900 * take.f, q: 0.9, a: 4e-3, d: 0.13 * take.t, vol: 0.32 * take.v }),
15863
+ sceneTone({ f: 95 * take.f, f2: 60, d: 0.07, vol: 0.3 * take.v }),
15864
+ sceneNoise({ type: "lowpass", f: 700, d: 0.08, vol: 0.2 * take.v })
15865
+ ],
15866
+ punch_swing: whoosh("punch", 450, 1500, 0.16, 0.9),
15867
+ punch_hit_body: fleshHit({ slap: 0.55, slapF: 1500, thud: 0.6, thudF: 115, thudD: 0.09 }),
15868
+ punch_hit_face: fleshHit({ slap: 0.75, slapF: 2300, thud: 0.45, thudF: 150, thudD: 0.07, crack: 0.25 }),
15869
+ punch_hit_object: knock({ clickF: 1800, click: 0.3, tones: [[210, 0.04, 0.25]] }),
15870
+ kick_swing: whoosh("kick", 280, 950, 0.22, 1, 1.3),
15871
+ kick_hit: fleshHit({ slap: 0.45, slapF: 1100, thud: 0.85, thudF: 82, thudD: 0.14 }),
15872
+ kick_hit_object: knock({ clickF: 1e3, click: 0.3, tones: [[92, 0.08, 0.5]] }),
15873
+ shove_two_hands: (take) => [
15874
+ sceneNoise({ type: "bandpass", f: 950 * take.f, q: 0.9, a: 0.01, d: 0.13 * take.t, vol: 0.38 * take.v }),
15875
+ sceneTone({ f: 100 * take.f, f2: 60, d: 0.1 * take.t, vol: 0.45 * take.v }),
15876
+ sceneNoise({ type: "bandpass", f: 2300, q: 0.8, d: 0.16, vol: 0.12 * take.v, delay: 0.03 })
15877
+ ],
15878
+ knife_swing: whoosh("blade", 1900, 4600, 0.12, 1.1, 2.2),
15879
+ knife_stab: fleshHit({ slap: 0.2, slapF: 1200, thud: 0.4, thudF: 160, thudD: 0.06, wet: 0.5 }),
15880
+ knife_slash: (take) => [
15881
+ sceneNoise({ type: "highpass", f: 3200 * take.f, f2: 1800, d: 0.13 * take.t, vol: 0.35 * take.v }),
15882
+ sceneNoise({ type: "bandpass", f: 1300 * take.f, f2: 600, q: 2.5, d: 0.12 * take.t, vol: 0.35 * take.v, delay: 0.02 })
15883
+ ],
15884
+ knife_hit_object: knock({ clickF: 3200, click: 0.25, tones: [[2800, 0.08, 0.07], [4150, 0.06, 0.05]] }),
15885
+ blunt_swing: whoosh("blunt", 220, 760, 0.28, 1.1, 1.5),
15886
+ blunt_hit: fleshHit({ slap: 0.35, slapF: 900, thud: 1, thudF: 70, thudD: 0.18, crack: 0.3 }),
15887
+ blunt_hit_object: knock({ clickF: 1400, click: 0.35, tones: [[300, 0.12, 0.25], [760, 0.08, 0.12]] }),
15888
+ axe_swing: whoosh("chop", 190, 620, 0.3, 1.2, 1.4),
15889
+ axe_chop: fleshHit({ slap: 0.3, slapF: 1400, thud: 0.75, thudF: 140, thudD: 0.1, crack: 0.35, wet: 0.45 }),
15890
+ axe_hit_object: knock({ clickF: 2200, click: 0.4, tones: [[1500, 0.15, 0.1], [2300, 0.1, 0.06], [140, 0.1, 0.35]] })
15891
+ };
15892
+ var BODY_BUS = { player_hurt: "voices", player_death: "voices", player_breath_run: "voices", fall_damage: "voices" };
15893
+ var built = [
15894
+ ...SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstep(surface, gait))),
15895
+ ...SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((l) => landing(surface, l))),
15896
+ ...BODY_SOUND_IDS.map((id) => takes(id, BODY[id], BODY_FALLOFF, id.endsWith("_swing") ? [0.94, 1.06] : [0.96, 1.04], BODY_BUS[id]))
15897
+ ];
15898
+ var BODY_FOLEY_BANK = {
15899
+ id: "body_foley",
15900
+ sounds: built.flatMap((entry) => entry.sounds),
15901
+ sets: built.map((entry) => entry.set)
15902
+ };
15903
+ var BODY_FOLEY_SET_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
15904
+
15905
+ // src/audio/blast-fire.ts
15906
+ var BLAST_FALLOFF = { model: "inverse_square", k: 12e-4, cutoffK: 6e-3, minGain: 2e-3 };
15907
+ var BLAST_FAR_FALLOFF = { model: "inverse_square", k: 8e-5, scale: 0.3, minGain: 1e-3 };
15908
+ var FIRE_FALLOFF = { model: "inverse_square", k: 0.01, cutoffK: 0.01, minGain: 5e-3 };
15909
+ var BLAST_FAR_DISTANCE = 60;
15910
+ var BLAST_KINDS = ["grenade", "big", "fuel", "car", "pipe_bomb", "rocket", "flashbang", "molotov"];
15911
+ var BLAST_KIND_OF = {
15912
+ m67: "grenade",
15913
+ vog_25: "grenade",
15914
+ m79_40mm: "grenade",
15915
+ claymore: "grenade",
15916
+ generic: "grenade",
15917
+ c4: "big",
15918
+ dynamite: "big",
15919
+ fuel_station_tank: "big",
15920
+ propane_p13: "fuel",
15921
+ fuel_barrel: "fuel",
15922
+ fuel_pump: "fuel",
15923
+ car_fuel_tank: "car",
15924
+ pipe_bomb: "pipe_bomb",
15925
+ m72_66mm: "rocket",
15926
+ at4_84mm: "rocket",
15927
+ carl_gustaf_84mm: "rocket",
15928
+ pg7v: "rocket",
15929
+ flashbang: "flashbang",
15930
+ molotov: "molotov"
15931
+ };
15932
+ var BIG_BLAST_RADIUS = 8;
15933
+ var blast = (id, layers, sources, falloff = BLAST_FALLOFF) => ({
15934
+ ...sceneRecipe(id, layers, ""),
15935
+ falloff,
15936
+ sources
15937
+ });
15938
+ var crack = (f, d, vol) => sceneNoise({ type: "highpass", f, a: 1e-3, d, vol });
15939
+ var debris2 = (count, from, to, vol) => [
15940
+ ...sceneMany(count, () => sceneNoise({ type: "bandpass", f: [300, 1400], q: 2.5, d: [0.03, 0.09], vol, delay: [from, to] })),
15941
+ ...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] }))
15942
+ ];
15943
+ 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] }));
15944
+ var glass = (count, from) => [
15945
+ sceneNoise({ type: "highpass", f: [2600, 3e3], d: 0.4, vol: 0.7, wet: 0.5, delay: from }),
15946
+ ...sceneMany(count, () => sceneTone({ f: [3e3, 7500], d: [0.05, 0.13], vol: 0.12, delay: [from, from + 0.7] }))
15947
+ ];
15948
+ var BLAST_SOUNDS = [
15949
+ blast(
15950
+ "explosion_grenade",
15951
+ [
15952
+ crack([2300, 2700], 0.06, 2.4),
15953
+ sceneNoise({ f: [2500, 3e3], f2: 160, d: 0.7, vol: 2, wet: 0.35 }),
15954
+ sceneTone({ f: [80, 92], f2: 32, d: 0.5, vol: 1.6 }),
15955
+ sceneNoise({ type: "bandpass", f: [620, 760], q: 0.8, d: 0.18, vol: 1.1 }),
15956
+ ...shrapnel(14, 0.7),
15957
+ ...debris2(10, 0.35, 1.8, [0.1, 0.2])
15958
+ ],
15959
+ ["sdk: frag grenade (M67, VOG-25, 40 mm): sharp crack, short body, fragments striking, a little debris"]
15960
+ ),
15961
+ blast(
15962
+ "explosion_big",
15963
+ [
15964
+ crack([1900, 2300], 0.08, 2.4),
15965
+ sceneNoise({ f: [2200, 2600], f2: 90, d: 1.6, vol: 2.2, wet: 0.45 }),
15966
+ sceneTone({ f: [52, 60], f2: 22, d: 1.6, vol: 1.9 }),
15967
+ sceneNoise({ f: [280, 330], f2: 60, a: 0.25, d: 2.8, vol: 0.8, wet: 0.6, delay: 0.05 }),
15968
+ ...debris2(24, 0.5, 3.6, [0.12, 0.25])
15969
+ ],
15970
+ ["sdk: big charge (C4, dynamite, a fuel station tank): crack, long deep roar, debris falling for 2 to 4 s"]
15971
+ ),
15972
+ blast(
15973
+ "explosion_fuel",
15974
+ [
15975
+ sceneNoise({ type: "highpass", f: [1700, 2e3], a: 1e-3, d: 0.05, vol: 1 }),
15976
+ sceneTone({ f: [380, 450], f2: 300, d: 0.4, vol: 0.3 }),
15977
+ sceneNoise({ f: [1700, 2e3], f2: 150, a: 0.03, d: 1.4, vol: 2.1, wet: 0.4 }),
15978
+ sceneTone({ f: [48, 56], f2: 25, d: 1.2, vol: 1.7 }),
15979
+ sceneNoise({ type: "bandpass", f: [450, 560], q: 0.6, a: 0.15, d: 2.2, vol: 0.85, wet: 0.5, delay: 0.1 }),
15980
+ ...debris2(10, 0.5, 2.6, [0.08, 0.16])
15981
+ ],
15982
+ ['sdk: a fuel vessel bursting (propane P13, fuel barrel, pump): the shell tearing, a fireball "whoomp" and its roar']
15983
+ ),
15984
+ blast(
15985
+ "explosion_car",
15986
+ [
15987
+ sceneNoise({ type: "highpass", f: [1700, 2e3], a: 1e-3, d: 0.06, vol: 1.1 }),
15988
+ sceneNoise({ f: [1600, 1900], f2: 140, a: 0.03, d: 1.6, vol: 2.1, wet: 0.4 }),
15989
+ sceneTone({ f: [44, 52], f2: 24, d: 1.4, vol: 1.8 }),
15990
+ sceneNoise({ type: "bandpass", f: [420, 520], q: 0.6, a: 0.15, d: 2.6, vol: 0.9, wet: 0.5, delay: 0.1 }),
15991
+ ...glass(10, 0.04),
15992
+ sceneTone({ f: [130, 150], f2: 85, d: 1.1, vol: 0.18, delay: [0.3, 0.5] }),
15993
+ ...debris2(16, 0.6, 3.2, [0.1, 0.2])
15994
+ ],
15995
+ ["sdk: a car going up: the fuel tank fireball, every window breaking, the body groaning, parts falling"]
15996
+ ),
15997
+ blast(
15998
+ "explosion_pipe_bomb",
15999
+ [
16000
+ crack([2e3, 2400], 0.07, 2.2),
16001
+ sceneNoise({ f: [2e3, 2400], f2: 150, d: 0.8, vol: 1.9, wet: 0.35 }),
16002
+ sceneTone({ f: [70, 82], f2: 30, d: 0.6, vol: 1.5 }),
16003
+ ...sceneMany(6, () => sceneTone({ f: [1800, 3200], f2: 1200, d: [0.06, 0.12], vol: 0.06, delay: [0.05, 0.6] })),
16004
+ ...shrapnel(18, 0.9),
16005
+ ...debris2(8, 0.4, 1.8, [0.08, 0.16])
16006
+ ],
16007
+ ["sdk: pipe bomb: crack, body, the steel pipe flying apart in ringing pieces"]
16008
+ ),
16009
+ blast(
16010
+ "explosion_rocket",
16011
+ [
16012
+ crack([2100, 2500], 0.08, 2.4),
16013
+ sceneNoise({ f: [2400, 2800], f2: 120, d: 1.1, vol: 2.1, wet: 0.4 }),
16014
+ sceneTone({ f: [60, 70], f2: 26, d: 0.9, vol: 1.8 }),
16015
+ sceneNoise({ f: [350, 420], f2: 80, a: 0.08, d: 1.8, vol: 0.9, wet: 0.6, delay: 0.05 }),
16016
+ ...shrapnel(10, 0.8),
16017
+ ...debris2(16, 0.4, 2.6, [0.1, 0.2])
16018
+ ],
16019
+ ["sdk: rocket or anti-tank warhead (M72, AT4, RPG): hard crack, heavy body, debris"]
16020
+ ),
16021
+ blast(
16022
+ "explosion_flashbang",
16023
+ [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 })],
16024
+ ["sdk: flashbang: a deafening crack with almost no body and no fragments"]
16025
+ ),
16026
+ blast(
16027
+ "molotov_break_ignite",
16028
+ [
16029
+ ...glass(8, 0),
16030
+ sceneNoise({ f: [280, 340], f2: 2200, a: 0.12, d: 0.9, vol: 1.5, wet: 0.6, delay: 0.05 }),
16031
+ sceneTone({ f: [85, 95], f2: 55, d: 0.4, vol: 0.5, delay: 0.08 })
16032
+ ],
16033
+ ['sdk: molotov: the bottle breaking and the fuel catching with a "fuuush"'],
16034
+ FIRE_FALLOFF
16035
+ ),
16036
+ blast(
16037
+ "explosion_far",
16038
+ [
16039
+ sceneNoise({ f: [550, 650], f2: 80, a: 0.03, d: 2.5, vol: 2, wet: 1.4 }),
16040
+ sceneTone({ f: [42, 48], f2: 25, d: 1.8, vol: 1.2 }),
16041
+ sceneNoise({ f: [350, 420], f2: 70, a: 0.1, d: 1.6, vol: 0.6, wet: 1.5, delay: [0.6, 1] })
16042
+ ],
16043
+ ["sdk: any blast far away: no crack left, a dull thud and the echo rolling back"],
16044
+ BLAST_FAR_FALLOFF
16045
+ ),
16046
+ blast(
16047
+ "explosion_indoor_tail",
16048
+ [
16049
+ ...sceneMany(3, () => sceneNoise({ f: [650, 800], d: 0.12, vol: 0.5, delay: [0.04, 0.2] })),
16050
+ sceneNoise({ f: [850, 950], f2: 120, a: 0.05, d: 2.6, vol: 0.9, wet: 1.6, delay: 0.08 })
16051
+ ],
16052
+ ["sdk: the room answering a blast inside it: slaps off the walls and a boomy tail"]
16053
+ )
16054
+ ];
16055
+ var FIRE_SOUNDS = [
16056
+ {
16057
+ ...blast(
16058
+ "fire_ignite",
16059
+ [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 })],
16060
+ ['sdk: fire catching, a soft "fuuump"'],
16061
+ FIRE_FALLOFF
16062
+ ),
16063
+ group: "fire_ignite"
16064
+ },
16065
+ {
16066
+ ...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),
16067
+ vars: { seconds: 2 }
16068
+ },
16069
+ {
16070
+ ...blast(
16071
+ "fire_extinguish",
16072
+ [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 })],
16073
+ ["sdk: water or foam on a fire: a steam puff and a sizzle"],
16074
+ FIRE_FALLOFF
16075
+ ),
16076
+ vars: { seconds: 1.2 }
16077
+ },
16078
+ { ...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" },
16079
+ { ...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" }
16080
+ ];
16081
+ var FIRE_LOOP = {
16082
+ id: "fire_loop",
16083
+ bus: "world",
16084
+ order: "chain_loop",
16085
+ source: { kind: "noise", loop: true },
16086
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 950 }, q: 0.5 }] },
16087
+ volume: 0,
16088
+ sources: ["sdk: the roar under the crackles of a fire"]
16089
+ };
16090
+ var BLAST_SOUND_OF = {
16091
+ grenade: "explosion_grenade",
16092
+ big: "explosion_big",
16093
+ fuel: "explosion_fuel",
16094
+ car: "explosion_car",
16095
+ pipe_bomb: "explosion_pipe_bomb",
16096
+ rocket: "explosion_rocket",
16097
+ flashbang: "explosion_flashbang",
16098
+ molotov: "molotov_break_ignite"
14267
16099
  };
14268
16100
 
14269
16101
  // src/audio/recipes.ts
@@ -14348,11 +16180,111 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14348
16180
  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 })]),
14349
16181
  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 })]),
14350
16182
  leaf("rattle", sceneMany(5, () => sceneNoise({ type: "bandpass", f: [1600, 2600], q: 3, d: 0.035, vol: 0.32, delay: [0, 0.45] }))),
14351
- 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 })])
16183
+ 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 })]),
16184
+ // 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)
16185
+ 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 })]),
16186
+ leaf("grab_soft", [sceneNoise({ type: "lowpass", f: 1400, a: 0.02, d: 0.16, vol: 0.14 })])
14352
16187
  ];
14353
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
16188
+ 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)"] });
16189
+ var IMPACT_SOUNDS = [
16190
+ // metal
16191
+ 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 })]),
16192
+ 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 })]),
16193
+ 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 })]),
16194
+ 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 })]),
16195
+ // wood (the bullet keeps the art scene's `wood_hit`)
16196
+ 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 })]),
16197
+ 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] }))]),
16198
+ 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 })]),
16199
+ // glass and ceramic
16200
+ 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] }))]),
16201
+ 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 })]),
16202
+ impact2("glass_blade", [sceneNoise({ type: "bandpass", f: 5e3, q: 8, a: 0.02, d: 0.25, vol: 0.3 })]),
16203
+ 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 })]),
16204
+ 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] }))]),
16205
+ // masonry and the ground
16206
+ 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] }))]),
16207
+ 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 })]),
16208
+ impact2("masonry_blade", [sceneNoise({ type: "bandpass", f: [2500, 3500], q: 4, a: 0.01, d: 0.15, vol: 0.4 })]),
16209
+ 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 })]),
16210
+ 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] }))]),
16211
+ 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 })]),
16212
+ 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 })]),
16213
+ 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] }))]),
16214
+ // plastic, rubber, wax
16215
+ 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 })]),
16216
+ 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 })]),
16217
+ 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 })]),
16218
+ 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 })]),
16219
+ // soft things, paper, plants
16220
+ 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 })]),
16221
+ 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] }))]),
16222
+ impact2("paper_hit", [sceneNoise({ type: "bandpass", f: 2200, q: 1, d: 0.05, vol: 0.45 })]),
16223
+ impact2("paper_tear", [sceneNoise({ type: "highpass", f: 2500, a: 0.02, d: 0.3, vol: 0.15 })]),
16224
+ impact2("leaf_rustle", sceneMany(6, () => sceneNoise({ type: "bandpass", f: [2500, 6e3], q: 2, d: 0.05, vol: 0.16, delay: [0, 0.3] }))),
16225
+ 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 })]),
16226
+ 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 })]),
16227
+ // a round glancing off: steel whines down in pitch ("piiuu"), stone and concrete give a short crack with grit
16228
+ 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 })]),
16229
+ 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] }))]),
16230
+ // breaks a material has of its own (the rest keep the art scene's shatter, crumble and splinter)
16231
+ 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] }))]),
16232
+ 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] }))]),
16233
+ 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 })]),
16234
+ 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] }))])
16235
+ ];
16236
+ 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] };
14354
16237
 
14355
16238
  // src/audio/sound.ts
16239
+ var IMPACT_SOUND_KEYS = {
16240
+ impact_metal_ballistic: { sound: "metal_bullet" },
16241
+ impact_metal_blunt: { sound: "metal_blunt" },
16242
+ impact_metal_slash: { sound: "metal_blade" },
16243
+ impact_aluminium_ballistic: { sound: "metal_thin" },
16244
+ impact_aluminium_blunt: { sound: "metal_thin" },
16245
+ impact_copper_ballistic: { sound: "metal_thin" },
16246
+ impact_copper_blunt: { sound: "metal_thin" },
16247
+ impact_wood_ballistic: { sound: "wood_hit" },
16248
+ impact_wood_blunt: { sound: "wood_blunt" },
16249
+ impact_wood_slash: { sound: "wood_blade" },
16250
+ impact_mdf_ballistic: { sound: "mdf_puff" },
16251
+ impact_mdf_blunt: { sound: "mdf_puff", volumeScale: 0.8 },
16252
+ impact_glass_ballistic: { sound: "glass_bullet" },
16253
+ impact_glass_blunt: { sound: "glass_blunt" },
16254
+ impact_glass_slash: { sound: "glass_blade" },
16255
+ impact_ceramic: { sound: "ceramic_hit" },
16256
+ impact_ceramic_ballistic: { sound: "ceramic_bullet" },
16257
+ impact_masonry_ballistic: { sound: "masonry_bullet" },
16258
+ impact_masonry_blunt: { sound: "masonry_blunt" },
16259
+ impact_masonry_slash: { sound: "masonry_blade" },
16260
+ impact_plaster: { sound: "plaster_puff" },
16261
+ impact_soil: { sound: "soil_blunt" },
16262
+ impact_soil_ballistic: { sound: "soil_bullet" },
16263
+ impact_sand: { sound: "sand_hit" },
16264
+ impact_gravel: { sound: "gravel_hit" },
16265
+ impact_plastic: { sound: "plastic_hit" },
16266
+ impact_plastic_ballistic: { sound: "plastic_bullet" },
16267
+ impact_rubber: { sound: "rubber_hit" },
16268
+ impact_wax: { sound: "wax_hit" },
16269
+ impact_soft_ballistic: { sound: "soft_bullet" },
16270
+ impact_soft_slash: { sound: "fabric_rip" },
16271
+ impact_paper: { sound: "paper_hit" },
16272
+ impact_paper_slash: { sound: "paper_tear" },
16273
+ impact_plant: { sound: "leaf_rustle" },
16274
+ impact_plant_slash: { sound: "branch_cut" },
16275
+ impact_flesh_fruit: { sound: "fruit_splat" },
16276
+ ricochet_metal: { sound: "ricochet_metal" },
16277
+ ricochet_masonry: { sound: "ricochet_stone" },
16278
+ ricochet_soil: { sound: "ricochet_stone" },
16279
+ ricochet_ceramic: { sound: "ricochet_stone" },
16280
+ break_ceramic: { sound: "ceramic_break" },
16281
+ break_tempered_glass: { sound: "tempered_burst" },
16282
+ break_plastic: { sound: "plastic_crack" },
16283
+ break_mdf: { sound: "mdf_crumble" },
16284
+ break_soft: { sound: "fabric_rip" },
16285
+ break_paper: { sound: "paper_tear" },
16286
+ break_plant: { sound: "branch_cut" }
16287
+ };
14356
16288
  var DESTRUCTION_SOUND_KEYS = {
14357
16289
  art_scene: {
14358
16290
  impact_metal: { sound: "clang" },
@@ -14360,6 +16292,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14360
16292
  impact_glass: { sound: "ping" },
14361
16293
  impact_masonry: { sound: "chip" },
14362
16294
  impact_soft: { sound: "thud" },
16295
+ // a round strikes: the steel rings high, glass cracks, wood and masonry chip, a soft part takes a dull thunk
16296
+ shot_metal: { sound: "ping" },
16297
+ shot_wood: { sound: "chip" },
16298
+ shot_glass: { sound: "crack" },
16299
+ shot_masonry: { sound: "chip" },
16300
+ shot_soft: { sound: "thunk" },
14363
16301
  drag_wood: { sound: "drag_wood" },
14364
16302
  drag_metal: { sound: "drag_metal" },
14365
16303
  drag_glass: { sound: "drag_glass" },
@@ -14385,22 +16323,38 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14385
16323
  hood_shut: { sound: "shut_metal" },
14386
16324
  trunk_shut: { sound: "shut_metal" },
14387
16325
  door_close: { sound: "shut_wood", volumeScale: 0.7 },
16326
+ // 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
16327
+ vehicle_door_open: { sound: "car_door_open", fallback: "latch" },
16328
+ vehicle_door_shut: { sound: "car_door_close", fallback: "shut_metal" },
16329
+ vehicle_trunk_open: { sound: "car_door_open", fallback: "latch" },
16330
+ vehicle_trunk_shut: { sound: "car_trunk_close", fallback: "shut_metal" },
16331
+ vehicle_hood_open: { sound: "car_door_open", fallback: "latch" },
16332
+ vehicle_hood_shut: { sound: "car_hood_close", fallback: "shut_metal" },
14388
16333
  drawer_open: { sound: "slide" },
14389
16334
  drawer_close: { sound: "slide" },
14390
16335
  lid_open: { sound: "latch" },
14391
16336
  lid_close: { sound: "shut_soft" },
14392
16337
  flap_open: { sound: "latch" },
14393
16338
  flap_close: { sound: "shut_soft" },
16339
+ // an item taken in the hand, by what it is made of (game-audio's KIT_SAMPLE_ALIASES puts recordings on these ids)
16340
+ grab_metal: { sound: "grab_hard" },
16341
+ grab_wood: { sound: "grab_hard" },
16342
+ grab_glass: { sound: "grab_hard", volumeScale: 0.7 },
16343
+ grab_masonry: { sound: "grab_hard" },
16344
+ grab_soft: { sound: "grab_soft" },
14394
16345
  shatter_glass: { sound: "glass_shatter" },
14395
16346
  crumble: { sound: "crumble" },
14396
16347
  splinter: { sound: "crack" },
14397
16348
  ricochet: { sound: "ping" },
14398
16349
  unscrew: { sound: "chip" },
14399
16350
  detach: { sound: "thunk" },
14400
- ignite: { sound: "hiss", params: { seconds: 0.5 } },
14401
- extinguish: { sound: "hiss" },
16351
+ ignite: { sound: "fire_ignite" },
16352
+ primed: { sound: "gas_leak" },
16353
+ extinguish: { sound: "fire_extinguish" },
14402
16354
  melt_sizzle: { sound: "hiss", params: { seconds: 0.6 } },
14403
- explode: { sound: "boom", above: { volume: 0.9, sound: "boom_big" } }
16355
+ // the blast family comes from the explosion's effect (`BLAST_KIND_OF`); this rule answers only an explosion the kit cannot place
16356
+ explode: { sound: "explosion_grenade", above: { volume: 0.9, sound: "explosion_big" } },
16357
+ ...IMPACT_SOUND_KEYS
14404
16358
  },
14405
16359
  game: {
14406
16360
  impact_metal: { sound: "sfx_impact" },
@@ -14414,14 +16368,26 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14414
16368
  explode: { sound: "explosion_boom" }
14415
16369
  }
14416
16370
  };
14417
- var DEFAULT_FIRE = { perFire: 0.05, max: 0.2 };
16371
+ var DEFAULT_FIRE = { perFire: 0.08, max: 0.45 };
16372
+ var DEFAULT_CRACKLES = { base: 2, perSqrtFire: 3, max: 14, volume: 0.5 };
16373
+ var DEFAULT_INDOOR_CONFINEMENT = 1.3;
14418
16374
  function createDestructionSound(engine, options = {}) {
14419
16375
  engine.register(DESTRUCTION_SOUND_BANK);
14420
16376
  engine.register(GAME_SFX_BANK);
14421
- const fire = { ...DEFAULT_FIRE, ...options.fire };
16377
+ engine.register(FOLEY_BANK);
16378
+ const fire2 = { ...DEFAULT_FIRE, ...options.fire };
16379
+ const crackles = options.crackles === false ? null : { ...DEFAULT_CRACKLES, ...options.crackles };
16380
+ const fireLoop = options.fireLoop ?? "fire_loop";
16381
+ const kinds = { ...BLAST_KIND_OF, ...options.blastKinds };
16382
+ const farDistance = options.blastFarDistance ?? BLAST_FAR_DISTANCE;
16383
+ const indoorFrom = options.indoorConfinement ?? DEFAULT_INDOOR_CONFINEMENT;
14422
16384
  const rules = { ...DESTRUCTION_SOUND_KEYS.art_scene, ...options.style === "game" ? DESTRUCTION_SOUND_KEYS.game : {}, ...options.keys };
16385
+ const blastByKind = options.style !== "game" && !(options.keys && "explode" in options.keys);
16386
+ const channel = engine.channel({ seed: options.seed ?? 1 });
14423
16387
  let loop;
14424
16388
  let burning = 0;
16389
+ const fires = [];
16390
+ let nextCrackle = 0;
14425
16391
  const setFire = () => {
14426
16392
  if (options.burnLoop === false) return;
14427
16393
  if (burning <= 0) {
@@ -14429,35 +16395,120 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14429
16395
  loop = void 0;
14430
16396
  return;
14431
16397
  }
14432
- loop ?? (loop = engine.loop("burn_loop"));
14433
- loop.set({ volume: Math.min(fire.max, fire.perFire * burning) });
16398
+ loop ?? (loop = engine.loop(fireLoop));
16399
+ loop.set({ volume: Math.min(fire2.max, fire2.perFire * burning) });
16400
+ };
16401
+ const forget = (point) => {
16402
+ let best = -1;
16403
+ let bestD = Infinity;
16404
+ fires.forEach((f, i) => {
16405
+ const d = (f[0] - point[0]) ** 2 + (f[1] - point[1]) ** 2 + (f[2] - point[2]) ** 2;
16406
+ if (d < bestD) {
16407
+ bestD = d;
16408
+ best = i;
16409
+ }
16410
+ });
16411
+ if (best >= 0) fires.splice(best, 1);
16412
+ };
16413
+ const blastSound = (point, volume, detail) => {
16414
+ const named = detail?.effect === void 0 ? void 0 : kinds[detail.effect];
16415
+ const kind = named ?? ((detail?.radius ?? 0) >= BIG_BLAST_RADIUS || volume >= 0.9 ? "big" : "grenade");
16416
+ const sound = BLAST_SOUND_OF[kind];
16417
+ const distance = engine.plan(sound, { position: point, seed: 1 })?.distance ?? 0;
16418
+ return kind !== "molotov" && distance > farDistance ? "explosion_far" : sound;
14434
16419
  };
14435
16420
  return {
14436
- onSound(key, point, volume) {
16421
+ onSound(key, point, volume, detail) {
14437
16422
  if (key === "burn_loop") {
14438
16423
  burning += 1;
16424
+ fires.push(point);
14439
16425
  setFire();
14440
16426
  return;
14441
16427
  }
14442
16428
  if (key === "burn_end") {
14443
16429
  if (burning > 0) burning -= 1;
16430
+ forget(point);
14444
16431
  setFire();
14445
16432
  return;
14446
16433
  }
14447
- const rule = rules[key];
14448
- if (!rule) return;
14449
- const sound = rule.above && volume >= rule.above.volume ? rule.above.sound : rule.sound;
14450
- engine.play(sound, { position: point, volume: volume * (rule.volumeScale ?? 1), ...rule.params ? { params: rule.params } : {} });
16434
+ if (key === "explode" && blastByKind) {
16435
+ const sound2 = blastSound(point, volume, detail);
16436
+ engine.play(sound2, { position: point, volume });
16437
+ if (sound2 !== "explosion_far" && sound2 !== "molotov_break_ignite" && (detail?.confinement ?? 1) >= indoorFrom) engine.play("explosion_indoor_tail", { position: point, volume });
16438
+ return;
16439
+ }
16440
+ let rule;
16441
+ for (const k of detail?.keys ?? []) {
16442
+ if (k in rules) {
16443
+ rule = rules[k];
16444
+ break;
16445
+ }
16446
+ }
16447
+ if (rule === void 0) rule = key in rules ? rules[key] : rules[key.replace(/^shot_/, "impact_")];
16448
+ if (rule === null) return;
16449
+ if (!rule) {
16450
+ if (engine.registry.sound(key)) engine.play(key, { position: point, volume });
16451
+ return;
16452
+ }
16453
+ const wanted = rule.above && volume >= rule.above.volume ? rule.above.sound : rule.sound;
16454
+ const sound = rule.fallback && !engine.registry.sound(wanted) ? rule.fallback : wanted;
16455
+ const params = detail?.seconds === void 0 ? rule.params : { ...rule.params, seconds: detail.seconds };
16456
+ engine.play(sound, { position: point, volume: volume * (rule.volumeScale ?? 1), ...params ? { params } : {} });
16457
+ },
16458
+ update(dt) {
16459
+ if (!crackles || fires.length === 0 || options.burnLoop === false) return;
16460
+ nextCrackle -= dt;
16461
+ for (let i = 0; i < 4 && nextCrackle <= 0; i++) {
16462
+ const rate = Math.min(crackles.max, crackles.base + crackles.perSqrtFire * Math.sqrt(fires.length));
16463
+ nextCrackle += (0.4 + channel.random() * 1.2) / rate;
16464
+ const at3 = fires[Math.floor(channel.random() * fires.length)];
16465
+ engine.play(channel.random() < 0.18 ? "fire_pop" : "fire_crackle", { position: at3, volume: crackles.volume });
16466
+ }
16467
+ if (nextCrackle < 0) nextCrackle = 0;
16468
+ },
16469
+ blastSound,
16470
+ rule(key, detail) {
16471
+ for (const k of detail?.keys ?? []) if (k in rules) return rules[k] ?? void 0;
16472
+ return rules[key] ?? void 0;
14451
16473
  },
14452
- rule: (key) => rules[key] ?? void 0,
14453
16474
  burning: () => burning,
14454
16475
  dispose() {
14455
16476
  loop?.stop();
14456
16477
  loop = void 0;
14457
16478
  burning = 0;
16479
+ fires.length = 0;
14458
16480
  }
14459
16481
  };
14460
16482
  }
16483
+
16484
+ // src/audio/samples.ts
16485
+ var blast2 = (gain) => ({ bus: "world", gain, falloff: BLAST_FALLOFF, cutoffK: 6e-3, priority: 3 });
16486
+ var fire = (gain, group) => ({ bus: "world", gain, falloff: FIRE_FALLOFF, cutoffK: 0.01, ...group ? { group } : {} });
16487
+ var BLAST_FIRE_SAMPLES = {
16488
+ explosion_grenade: blast2(1),
16489
+ explosion_big: blast2(1),
16490
+ explosion_car: blast2(1),
16491
+ explosion_flashbang: blast2(1),
16492
+ molotov_break_ignite: blast2(0.8),
16493
+ // the far takes are already muffled: no extra distance lowpass
16494
+ explosion_far: { bus: "world", gain: 1, falloff: BLAST_FAR_FALLOFF, cutoffK: 0, priority: 2 },
16495
+ fire_ignite: fire(0.8, "fire_ignite"),
16496
+ fire_crackle: fire(0.8, "fire_crackle"),
16497
+ fire_pop: fire(0.6, "fire_crackle"),
16498
+ fire_extinguish: fire(0.7),
16499
+ gas_leak: fire(0.5),
16500
+ // the roar: recorded fires chained with a crossfade (the level comes from `createDestructionSound`, by how much burns)
16501
+ fire_loop: { bus: "world", gain: 4, loop: { crossfade: 1 } }
16502
+ };
16503
+ function blastFireSampleBank(manifest, options = {}) {
16504
+ const sounds = options.sounds ?? BLAST_FIRE_SAMPLES;
16505
+ const bank = sampleBankFromManifest(manifest, { id: options.id ?? "destruction-samples", ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl }, sounds });
16506
+ const samples = (bank.samples ?? []).map((sample) => {
16507
+ const loop = sounds[sample.id]?.loop;
16508
+ return loop === void 0 ? sample : { ...sample, loop };
16509
+ });
16510
+ return { ...bank, samples };
16511
+ }
14461
16512
  return __toCommonJS(index_exports);
14462
16513
  })();
14463
16514
  //# sourceMappingURL=index.global.js.map