@adatechnology/game-destruction-three 0.13.0 → 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.
package/dist/index.js CHANGED
@@ -2,6 +2,43 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
+ // src/audio/impact-keys.ts
6
+ var IMPACT_FAMILY_TAGS = [
7
+ ["metal", "metal"],
8
+ ["glass", "glass"],
9
+ ["ceramic", "ceramic"],
10
+ ["soil", "soil"],
11
+ ["masonry", "masonry"],
12
+ ["wood", "wood"],
13
+ ["plant", "plant"],
14
+ ["plastic", "plastic"],
15
+ ["rubber", "rubber"],
16
+ ["wax", "wax"],
17
+ ["fabric", "soft"]
18
+ ];
19
+ function impactFamily(def) {
20
+ const tags = def?.tags ?? [];
21
+ for (const [tag, family] of IMPACT_FAMILY_TAGS) if (tags.includes(tag)) return family;
22
+ return void 0;
23
+ }
24
+ var ordered = (prefix, material, family, blow) => {
25
+ const keys = [];
26
+ if (material && blow) keys.push(`${prefix}_${material}_${blow}`);
27
+ if (material) keys.push(`${prefix}_${material}`);
28
+ if (family && blow) keys.push(`${prefix}_${family}_${blow}`);
29
+ if (family) keys.push(`${prefix}_${family}`);
30
+ return keys;
31
+ };
32
+ function impactSoundKeys(material, def, blow) {
33
+ return { keys: ordered("impact", material, impactFamily(def), blow) };
34
+ }
35
+ function breakSoundKeys(material, def, blow) {
36
+ return { keys: ordered("break", material, impactFamily(def), blow) };
37
+ }
38
+ function ricochetSoundKeys(material, def) {
39
+ return { keys: ordered("ricochet", material, impactFamily(def), void 0) };
40
+ }
41
+
5
42
  // src/renderer.ts
6
43
  import { DEFERRED_QUEUE_ID, blastShove, warnOnce } from "@adatechnology/game-contracts";
7
44
  import { createVfx, effectForEvent, EVENT_EFFECTS_MAP } from "@adatechnology/game-vfx";
@@ -174,10 +211,10 @@ var FragmentSystem = class {
174
211
  */
175
212
  warmMeshes() {
176
213
  const opaque = this.materialFor("warm", 8421504, false);
177
- const glass = this.materialFor("warm", 8421504, true);
214
+ const glass2 = this.materialFor("warm", 8421504, true);
178
215
  const geometries = [this.box, ...Array.from({ length: SHARD_SHAPES }, (_, index) => this.shardGeometry(index))];
179
216
  const { geometry, material } = this.markParts();
180
- return [...geometries.flatMap((shared) => [new this.THREE.Mesh(shared, opaque), new this.THREE.Mesh(shared, glass)]), new this.THREE.Mesh(geometry, material)];
217
+ return [...geometries.flatMap((shared) => [new this.THREE.Mesh(shared, opaque), new this.THREE.Mesh(shared, glass2)]), new this.THREE.Mesh(geometry, material)];
181
218
  }
182
219
  /**
183
220
  * 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
@@ -528,6 +565,9 @@ var GroundSpots = class {
528
565
  };
529
566
 
530
567
  // src/pieces.ts
568
+ var SLAM_MIN_SPEED = 2;
569
+ var SLAM_BOUNCE = 0.2;
570
+ var SLAM_FLOOR_NORMAL = 0.7;
531
571
  var SUB_DT = 1 / 120;
532
572
  var MU = 0.7;
533
573
  var SLEEP_AFTER = 0.5;
@@ -762,9 +802,34 @@ var PieceSystem = class {
762
802
  const fixed = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
763
803
  return Math.max(top, fixed);
764
804
  }
805
+ /** 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. */
806
+ sweepOn(b, start, pos) {
807
+ const travel = Math.hypot(pos[0] - start[0], pos[1] - start[1], pos[2] - start[2]);
808
+ if (travel < 1e-3) return;
809
+ const hit = this.o.sweep?.(b, start, pos);
810
+ if (!hit || hit.normal[1] > SLAM_FLOOR_NORMAL) return;
811
+ const v = b.velocity;
812
+ const n = hit.normal;
813
+ const into = -(v[0] * n[0] + v[1] * n[1] + v[2] * n[2]);
814
+ if (into <= 0) return;
815
+ const k = Math.min(1, hit.distance / travel);
816
+ for (let i = 0; i < 3; i++) {
817
+ pos[i] = start[i] + (pos[i] - start[i]) * k;
818
+ v[i] += (1 + SLAM_BOUNCE) * into * n[i];
819
+ }
820
+ const w = b.angularVelocity;
821
+ w[0] *= 0.5;
822
+ w[1] *= 0.5;
823
+ w[2] *= 0.5;
824
+ if (into >= SLAM_MIN_SPEED && this.time - b.lastHit > 0.15) {
825
+ b.lastHit = this.time;
826
+ this.o.onSlam?.(b, hit, into);
827
+ }
828
+ }
765
829
  integrate(b, dt) {
766
830
  const c = this.centerOf(b, this.p);
767
831
  const pos = [c.x, c.y, c.z];
832
+ const start = [c.x, c.y, c.z];
768
833
  const q = this.q.copy(b.root.quaternion);
769
834
  const v = b.velocity;
770
835
  const w = b.angularVelocity;
@@ -874,11 +939,11 @@ var PieceSystem = class {
874
939
  if (contactedLast) touched = true;
875
940
  if (!contactedLast) continue;
876
941
  pos[1] += Math.max(0, maxPen - SLOP * 0.5);
877
- let impact = 0;
878
- for (const r of rs) impact = Math.max(impact, -(v[1] + w[2] * r[0] - w[0] * r[2]));
879
- const restitution = impact > 2 ? 0.25 : 0;
880
- if (impact > landing) {
881
- landing = impact;
942
+ let impact2 = 0;
943
+ for (const r of rs) impact2 = Math.max(impact2, -(v[1] + w[2] * r[0] - w[0] * r[2]));
944
+ const restitution = impact2 > 2 ? 0.25 : 0;
945
+ if (impact2 > landing) {
946
+ landing = impact2;
882
947
  let sx = 0;
883
948
  let sz = 0;
884
949
  for (const r of rs) {
@@ -1009,6 +1074,7 @@ var PieceSystem = class {
1009
1074
  v[0] *= fd;
1010
1075
  v[2] *= fd;
1011
1076
  }
1077
+ if (this.o.sweep && !b.held) this.sweepOn(b, start, pos);
1012
1078
  b.root.quaternion.copy(q);
1013
1079
  const off = this.ax.copy(b.centerV).applyQuaternion(q);
1014
1080
  b.root.position.set(pos[0] - off.x, pos[1] - off.y, pos[2] - off.z);
@@ -2168,10 +2234,10 @@ var Visuals = class {
2168
2234
  const sy = lerp(1, 0.25, melt);
2169
2235
  const sxz = 1 + 0.18 * melt;
2170
2236
  for (let i = range[0] * 3, v = 0; i < range[1] * 3; i += 3, v++) {
2171
- const noise2 = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
2237
+ const noise3 = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
2172
2238
  arr[i] = cx + (work[i] - cx) * sxz;
2173
2239
  arr[i + 2] = cz + (work[i + 2] - cz) * sxz;
2174
- arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise2 * melt * h * 0.04);
2240
+ arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise3 * melt * h * 0.04);
2175
2241
  }
2176
2242
  }
2177
2243
  /** The colour attribute of the host's current geometry (created white when there is none) with vertex colours switched on in its material. */
@@ -2421,12 +2487,14 @@ var GLOW_FROM_C = 430;
2421
2487
  var QUEUE_CAP = 4e3;
2422
2488
  var MIN_FRAGMENTS_PER_SLICE = 8;
2423
2489
  var STATIC_REFRESH = 0.5;
2490
+ var STATIC_REFRESH_SLICE_MS = 1;
2424
2491
  var STATIC_SPAN = 3;
2425
2492
  var FEET_BAND = 0.03;
2426
2493
  var TIP_MIN_HEIGHT = 0.3;
2427
2494
  var TIP_NARROW = 0.3;
2428
2495
  var FIRE_CEILING_SECONDS = 3600;
2429
2496
  var GENERIC_REFERENCE_RADIUS = 15;
2497
+ var SWEEP_CORE = 0.15;
2430
2498
  var PRIMED_FIRE_RADIUS = 0.2;
2431
2499
  var FLUID_COLORS = {
2432
2500
  water: 9226495,
@@ -2469,6 +2537,7 @@ function impactKey(def) {
2469
2537
  if (def.fracture === "tear") return "impact_soft";
2470
2538
  return def.fracture === "none" ? "impact_metal" : "impact_wood";
2471
2539
  }
2540
+ var shotKey = (def) => impactKey(def).replace("impact_", "shot_");
2472
2541
  var dragSoundKey = (def) => impactKey(def).replace("impact_", "drag_");
2473
2542
  function fractureKey(def, style) {
2474
2543
  switch (style) {
@@ -2518,6 +2587,8 @@ var Renderer = class {
2518
2587
  /** Objects to measure again; `staticAll` measures every one. */
2519
2588
  __publicField(this, "staticDirty", /* @__PURE__ */ new Set());
2520
2589
  __publicField(this, "staticAll", true);
2590
+ /** The periodic refresh: the objects still to measure again in this round, a slice per call. */
2591
+ __publicField(this, "staticRound", []);
2521
2592
  /** Boxes of the pieces' meshes (as `staticBoxes`), with the time they were measured. */
2522
2593
  __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
2523
2594
  __publicField(this, "staticAt", 0);
@@ -2609,7 +2680,9 @@ var Renderer = class {
2609
2680
  staticTopAt: (x, z, y, step) => this.staticTopAt(x, z, y, step),
2610
2681
  gravity: this.gravity,
2611
2682
  scene: o.scene,
2612
- onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
2683
+ onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed),
2684
+ sweep: (p, from, to) => this.sweepPiece(p, from, to),
2685
+ onSlam: (p, hit, speed) => this.slam(p, hit, speed)
2613
2686
  });
2614
2687
  this.unsubscribe = this.world.on("*", (ev) => {
2615
2688
  if (this.queue.length < QUEUE_CAP) this.queue.push(ev);
@@ -2799,26 +2872,58 @@ var Renderer = class {
2799
2872
  const p = this.pieceSys.get(objectId);
2800
2873
  if (p) this.pieceSys.impulse(p, impulse, point);
2801
2874
  }
2875
+ /**
2876
+ * 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,
2877
+ * so a crate thrown by a blast stops at the wall instead of passing through it. Null for a world that cannot sweep.
2878
+ */
2879
+ sweepPiece(p, from, to) {
2880
+ if (typeof this.world.sweepBox !== "function") return null;
2881
+ const d = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
2882
+ const travel = Math.hypot(d[0], d[1], d[2]);
2883
+ if (travel < 1e-4) return null;
2884
+ const core = Math.min(SWEEP_CORE, ...p.halfExtents);
2885
+ const box = { min: [from[0] - core, from[1] - core, from[2] - core], max: [from[0] + core, from[1] + core, from[2] + core] };
2886
+ const hit = this.world.sweepBox(box, d, travel, { ignore: p.objectId, spacing: Math.max(0.05, core) });
2887
+ if (!hit) return null;
2888
+ return { object: hit.object, part: hit.part, distance: hit.distance, point: hit.point, normal: hit.normal };
2889
+ }
2890
+ /** 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. */
2891
+ slam(p, hit, speed) {
2892
+ const energy = 0.5 * p.mass * speed * speed;
2893
+ const direction = [-hit.normal[0], -hit.normal[1], -hit.normal[2]];
2894
+ this.landingSound(hit.object, hit.point, clamp(speed / 10, 0.2, 1));
2895
+ this.syncBound(p.objectId);
2896
+ try {
2897
+ this.world.hitPart(hit.object, hit.part, { type: "blunt", point: hit.point, energy, radius: 0.1, direction, normal: hit.normal, source: p.objectId });
2898
+ this.world.hit(p.objectId, { type: "blunt", point: hit.point, energy, radius: 0.08, direction: hit.normal, source: "slam" });
2899
+ } catch {
2900
+ }
2901
+ }
2802
2902
  hardLanding(p, point, speed) {
2803
- const key = this.impactSoundOf(p.objectId);
2804
- this.sound(key, point, clamp(speed / 10, 0.2, 1));
2903
+ this.landingSound(p.objectId, point, clamp(speed / 10, 0.2, 1));
2805
2904
  this.syncBound(p.objectId);
2806
2905
  try {
2807
2906
  this.world.hit(p.objectId, { type: "blunt", point, energy: 0.5 * p.mass * speed * speed, radius: 0.08, direction: [0, -1, 0], source: "fall" });
2808
2907
  } catch {
2809
2908
  }
2810
2909
  }
2811
- impactSoundOf(objectId) {
2910
+ /** The impact of the heaviest part of an object (a piece landing): its material and the blunt blow of the fall. */
2911
+ landingSound(objectId, point, volume) {
2812
2912
  const st = this.world.state(objectId);
2813
- let best;
2913
+ let best = "";
2814
2914
  let mass = -1;
2815
2915
  for (const p of st?.parts ?? []) {
2816
2916
  if (p.mass > mass) {
2817
2917
  mass = p.mass;
2818
- best = this.world.materials.get(p.material);
2918
+ best = p.material;
2819
2919
  }
2820
2920
  }
2821
- return impactKey(best);
2921
+ this.impactSound(best, point, volume, "blunt");
2922
+ }
2923
+ /** The impact on a material by a blow: the legacy family key (`impactKey`) with the more specific keys to try first (`impactSoundKeys`). */
2924
+ impactSound(material, point, volume, blow) {
2925
+ const def = this.world.materials.get(material);
2926
+ this.sound(impactKey(def), point, volume, impactSoundKeys(material, def, blow));
2822
2927
  }
2823
2928
  // ----------------------------------------------------------------------------------------------------------------------------------
2824
2929
  // Update
@@ -2950,9 +3055,9 @@ var Renderer = class {
2950
3055
  staticNow() {
2951
3056
  if (this.time - this.staticAt > STATIC_REFRESH) {
2952
3057
  this.staticAt = this.time;
2953
- this.staticAll = true;
3058
+ if (this.staticRound.length === 0) this.staticRound = [...this.bounds.keys()].reverse();
2954
3059
  }
2955
- if (!this.staticAll && this.staticDirty.size === 0) return this.staticBoxes;
3060
+ if (!this.staticAll && this.staticDirty.size === 0 && this.staticRound.length === 0) return this.staticBoxes;
2956
3061
  const box = new this.THREE.Box3();
2957
3062
  const measure = (b) => {
2958
3063
  if (b.piece || this.pieceSys.get(b.id)) {
@@ -2972,14 +3077,21 @@ var Renderer = class {
2972
3077
  }
2973
3078
  this.staticBoxes.set(b.id, out.length > 6 ? out : []);
2974
3079
  };
3080
+ const measureId = (id) => {
3081
+ const b = this.bounds.get(id);
3082
+ if (b) measure(b);
3083
+ else this.staticBoxes.delete(id);
3084
+ };
2975
3085
  if (this.staticAll) {
2976
3086
  this.staticBoxes.clear();
2977
3087
  for (const b of this.bounds.values()) measure(b);
3088
+ this.staticRound = [];
2978
3089
  } else {
2979
- for (const id of this.staticDirty) {
2980
- const b = this.bounds.get(id);
2981
- if (b) measure(b);
2982
- else this.staticBoxes.delete(id);
3090
+ for (const id of this.staticDirty) measureId(id);
3091
+ const until = performance.now() + STATIC_REFRESH_SLICE_MS;
3092
+ while (this.staticRound.length > 0) {
3093
+ measureId(this.staticRound.pop());
3094
+ if (performance.now() >= until) break;
2983
3095
  }
2984
3096
  }
2985
3097
  this.staticAll = false;
@@ -3039,7 +3151,8 @@ var Renderer = class {
3039
3151
  case "ricochet": {
3040
3152
  const d = ev.direction;
3041
3153
  this.vfx.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
3042
- this.sound("ricochet", ev.point, 0.7);
3154
+ const glanced = this.partVis(ev.object, ev.part)?.material ?? "";
3155
+ this.sound("ricochet", ev.point, 0.7, ricochetSoundKeys(glanced, this.world.materials.get(glanced)));
3043
3156
  break;
3044
3157
  }
3045
3158
  case "debris":
@@ -3081,7 +3194,7 @@ var Renderer = class {
3081
3194
  this.vfx.fire({ point: ev.point, radius: PRIMED_FIRE_RADIUS, life: ev.delay });
3082
3195
  this.primedBurning.add(`${ev.object}|${ev.part}`);
3083
3196
  this.activate(ev.object, ev.part);
3084
- this.sound("ignite", ev.point, 0.7);
3197
+ this.sound("primed", ev.point, 0.7, { seconds: ev.delay });
3085
3198
  this.sound("burn_loop", ev.point, 0.5);
3086
3199
  }
3087
3200
  break;
@@ -3195,8 +3308,9 @@ var Renderer = class {
3195
3308
  const b = this.bounds.get(objectId);
3196
3309
  return b ? [b.root.position.x, b.root.position.y, b.root.position.z] : [0, 0, 0];
3197
3310
  }
3198
- sound(key, point, volume) {
3199
- this.o.onSound?.(key, point, volume);
3311
+ sound(key, point, volume, detail) {
3312
+ if (detail) this.o.onSound?.(key, point, volume, detail);
3313
+ else this.o.onSound?.(key, point, volume);
3200
3314
  }
3201
3315
  partVis(objectId, partId) {
3202
3316
  return this.bounds.get(objectId)?.parts.get(partId) ?? null;
@@ -3261,7 +3375,7 @@ var Renderer = class {
3261
3375
  const reach = shown.reduce((widest, mv) => Math.max(widest, this.visuals.dentReach(mv, wp, ev.dent)), ev.dent.radius);
3262
3376
  this.visuals.applyPartDent(shown, wp, wn, ev.dent, reach);
3263
3377
  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);
3264
- this.sound(impactKey(def), ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1));
3378
+ this.impactSound(pv.material, ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1), ev.blow);
3265
3379
  }
3266
3380
  onHole(ev) {
3267
3381
  const b = this.bounds.get(ev.object);
@@ -3290,7 +3404,7 @@ var Renderer = class {
3290
3404
  this.vfx.burst("dust", ev.worldPoint, [n.x, n.y, n.z], this.low ? 5 : 10, 0.7, puff);
3291
3405
  if (ev.hole.through && ev.exitPoint) this.vfx.burst("dust", ev.exitPoint, [-n.x, -n.y, -n.z], this.low ? 10 : 20, 1.3, puff);
3292
3406
  }
3293
- this.sound(impactKey(def), ev.worldPoint, 0.6);
3407
+ this.sound(shotKey(def), ev.worldPoint, 0.6, impactSoundKeys(pv.material, def, ev.blow));
3294
3408
  }
3295
3409
  onCrack(ev) {
3296
3410
  const b = this.bounds.get(ev.object);
@@ -3300,13 +3414,13 @@ var Renderer = class {
3300
3414
  const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
3301
3415
  const mv = this.nearestMesh(pv, wp);
3302
3416
  if (mv) {
3303
- const glass = !!def?.visual?.transparent;
3417
+ const glass2 = !!def?.visual?.transparent;
3304
3418
  const own = this.partColor(pv, def) ?? fallbackColor(pv.material, def);
3305
- const color = glass ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
3306
- const length = glass ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
3419
+ const color = glass2 ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
3420
+ const length = glass2 ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
3307
3421
  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]));
3308
3422
  }
3309
- this.sound(impactKey(def), ev.worldPoint, 0.5);
3423
+ this.impactSound(pv.material, ev.worldPoint, 0.5, ev.blow);
3310
3424
  }
3311
3425
  /** 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). */
3312
3426
  faceNormal(mv, point) {
@@ -3363,7 +3477,7 @@ var Renderer = class {
3363
3477
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), false), ev.object, maxAlive);
3364
3478
  }
3365
3479
  if (!played) this.vfx.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
3366
- this.sound(impactKey(def), ev.opening.center, 0.8);
3480
+ this.impactSound(pv?.material ?? "", ev.opening.center, 0.8, ev.blow);
3367
3481
  }
3368
3482
  /**
3369
3483
  * 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
@@ -3377,11 +3491,11 @@ var Renderer = class {
3377
3491
  const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
3378
3492
  for (const fr of ev.fragments) {
3379
3493
  const fdef = this.world.materials.get(fr.material);
3380
- const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
3381
- this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone2, false), ev.object, maxAlive);
3494
+ const tone3 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
3495
+ this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone3, false), ev.object, maxAlive);
3382
3496
  }
3383
3497
  if (ev.torn) {
3384
- this.sound(impactKey(def), ev.center, 0.5);
3498
+ this.impactSound(pv?.material ?? "", ev.center, 0.5, ev.blow);
3385
3499
  return;
3386
3500
  }
3387
3501
  const T = this.THREE;
@@ -3391,7 +3505,7 @@ var Renderer = class {
3391
3505
  for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation);
3392
3506
  this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
3393
3507
  this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
3394
- this.sound(impactKey(def), ev.center, 0.7);
3508
+ this.impactSound(pv?.material ?? "", ev.center, 0.7, ev.blow);
3395
3509
  }
3396
3510
  onFracture(ev) {
3397
3511
  const b = this.bounds.get(ev.object);
@@ -3432,7 +3546,7 @@ var Renderer = class {
3432
3546
  default:
3433
3547
  this.vfx.burst("sparks", ev.point, up, 10, 4);
3434
3548
  }
3435
- this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1));
3549
+ this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1), breakSoundKeys(matId, def, ev.blow));
3436
3550
  }
3437
3551
  /** Spawns the fragments of a fracture from index `from`; what does not fit in this frame's budget is spawned by the next frames. */
3438
3552
  spawnFragments(ev, from, look) {
@@ -3445,8 +3559,8 @@ var Renderer = class {
3445
3559
  }
3446
3560
  const fr = ev.fragments[i];
3447
3561
  const fdef = this.world.materials.get(fr.material);
3448
- const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === look.def || !fdef ? look.color : fallbackColor(fr.material, fdef);
3449
- const mat = this.fragments.materialFor(fr.material, tone2, look.transparent || fdef?.visual?.transparent === true, look.opacity);
3562
+ const tone3 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === look.def || !fdef ? look.color : fallbackColor(fr.material, fdef);
3563
+ const mat = this.fragments.materialFor(fr.material, tone3, look.transparent || fdef?.visual?.transparent === true, look.opacity);
3450
3564
  this.fragments.spawn(fr, mat, ev.object, maxAlive);
3451
3565
  }
3452
3566
  }
@@ -3569,10 +3683,11 @@ var Renderer = class {
3569
3683
  const report = options.report;
3570
3684
  const confinement = report?.find((r) => r.blast !== void 0)?.blast?.confinement ?? 1;
3571
3685
  const energy = ev.energy * confinement;
3572
- const reachOf = (objectId) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? 1;
3686
+ const reachOf = (objectId, at) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? this.world.blastReach?.(p, at, { ignore: ev.object ? [ev.object, objectId] : [objectId] }) ?? 1;
3573
3687
  const shove = (pc) => {
3574
3688
  this.pieceSys.centerOf(pc, c);
3575
- 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);
3689
+ if (c.distanceTo(this.v2.set(p[0], p[1], p[2])) > ev.radius) return;
3690
+ 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);
3576
3691
  if (!hit) return;
3577
3692
  const spread = pc.halfExtents[0];
3578
3693
  const momentum = hit.speed * pc.mass;
@@ -3581,7 +3696,12 @@ var Renderer = class {
3581
3696
  for (const pc of [...this.pieceSys.list]) shove(pc);
3582
3697
  if (this.o.shoveable) this.loosen(p, ev.radius, energy, reachOf);
3583
3698
  this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(energy) * 0.08));
3584
- this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1));
3699
+ this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1), {
3700
+ radius: ev.radius,
3701
+ ...ev.effect !== void 0 ? { effect: ev.effect } : {},
3702
+ ...options.effect !== void 0 ? { effect: options.effect } : {},
3703
+ ...report ? { confinement } : {}
3704
+ });
3585
3705
  }
3586
3706
  /** 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. */
3587
3707
  loosen(point, radius, energy, reachOf) {
@@ -3592,7 +3712,8 @@ var Renderer = class {
3592
3712
  const { half, center } = this.measure(b.root, this.meshesOf(b));
3593
3713
  const world = center.clone().applyQuaternion(b.root.quaternion).add(b.root.position);
3594
3714
  const mass = this.massOf(b.id);
3595
- const hit = blastShove({ point, energy, radius, reach: reachOf(b.id) }, { position: [world.x, world.y, world.z], mass }, this.o.blastShove);
3715
+ if (world.distanceTo(this.v2.set(point[0], point[1], point[2])) > radius) continue;
3716
+ 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);
3596
3717
  if (!hit) continue;
3597
3718
  const piece = this.makePiece(b.id, { mass });
3598
3719
  const body = this.pieceSys.get(b.id);
@@ -3616,9 +3737,9 @@ var Renderer = class {
3616
3737
  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] } });
3617
3738
  }
3618
3739
  stopFire(key) {
3619
- const fire = this.fires.get(key);
3620
- if (!fire) return;
3621
- if (fire.handle !== void 0) this.vfx.stop(fire.handle);
3740
+ const fire2 = this.fires.get(key);
3741
+ if (!fire2) return;
3742
+ if (fire2.handle !== void 0) this.vfx.stop(fire2.handle);
3622
3743
  this.fires.delete(key);
3623
3744
  }
3624
3745
  // ----------------------------------------------------------------------------------------------------------------------------------
@@ -3655,9 +3776,9 @@ var Renderer = class {
3655
3776
  if (ps.status === "melted") this.onGone(a.object, pv, "melted", def);
3656
3777
  else if (ps.status === "ashes") this.onGone(a.object, pv, "ashes", def);
3657
3778
  }
3658
- const fire = this.fires.get(key);
3659
- if (fire) {
3660
- if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def);
3779
+ const fire2 = this.fires.get(key);
3780
+ if (fire2) {
3781
+ if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire2, pv, def);
3661
3782
  else this.stopFire(key);
3662
3783
  }
3663
3784
  const hot = def?.visual?.glows === true && ps.temperature > GLOW_FROM_C;
@@ -3787,10 +3908,12 @@ var Renderer = class {
3787
3908
  this.scene.remove(b.root);
3788
3909
  this.pieceSys.remove(b.id);
3789
3910
  this.bounds.delete(b.id);
3911
+ this.staticDirty.add(b.id);
3790
3912
  }
3791
3913
  }
3792
3914
  }
3793
3915
  clear() {
3916
+ this.staticAll = true;
3794
3917
  this.queue.length = 0;
3795
3918
  this.slices.length = 0;
3796
3919
  this.active.clear();
@@ -3817,6 +3940,8 @@ var Renderer = class {
3817
3940
  this.rng = createRng(RNG_SEED);
3818
3941
  this.time = 0;
3819
3942
  this.staticAt = 0;
3943
+ this.staticRound = [];
3944
+ this.staticAll = true;
3820
3945
  this.throttles.clear();
3821
3946
  this.fragments.reset();
3822
3947
  this.pieceSys.reset();
@@ -3874,43 +3999,242 @@ var Renderer = class {
3874
3999
  };
3875
4000
 
3876
4001
  // src/audio/recipes.ts
3877
- import { DISTANCE_CURVES, sceneMany as many, sceneNoise as noise, sceneRecipe, sceneTone as tone } from "@adatechnology/game-audio";
3878
- var scene = (id, layers, line, vars) => sceneRecipe(id, layers, line, vars ? { vars } : {});
4002
+ import { DISTANCE_CURVES, sceneMany as many2, sceneNoise as noise2, sceneRecipe as sceneRecipe2, sceneTone as tone2 } from "@adatechnology/game-audio";
4003
+
4004
+ // src/audio/blast-fire.ts
4005
+ import { sceneMany as many, sceneNoise as noise, sceneRecipe, sceneTone as tone } from "@adatechnology/game-audio";
4006
+ var BLAST_FALLOFF = { model: "inverse_square", k: 12e-4, cutoffK: 6e-3, minGain: 2e-3 };
4007
+ var BLAST_FAR_FALLOFF = { model: "inverse_square", k: 8e-5, scale: 0.3, minGain: 1e-3 };
4008
+ var FIRE_FALLOFF = { model: "inverse_square", k: 0.01, cutoffK: 0.01, minGain: 5e-3 };
4009
+ var BLAST_FAR_DISTANCE = 60;
4010
+ var BLAST_KINDS = ["grenade", "big", "fuel", "car", "pipe_bomb", "rocket", "flashbang", "molotov"];
4011
+ var BLAST_KIND_OF = {
4012
+ m67: "grenade",
4013
+ vog_25: "grenade",
4014
+ m79_40mm: "grenade",
4015
+ claymore: "grenade",
4016
+ generic: "grenade",
4017
+ c4: "big",
4018
+ dynamite: "big",
4019
+ fuel_station_tank: "big",
4020
+ propane_p13: "fuel",
4021
+ fuel_barrel: "fuel",
4022
+ fuel_pump: "fuel",
4023
+ car_fuel_tank: "car",
4024
+ pipe_bomb: "pipe_bomb",
4025
+ m72_66mm: "rocket",
4026
+ at4_84mm: "rocket",
4027
+ carl_gustaf_84mm: "rocket",
4028
+ pg7v: "rocket",
4029
+ flashbang: "flashbang",
4030
+ molotov: "molotov"
4031
+ };
4032
+ var BIG_BLAST_RADIUS = 8;
4033
+ var blast = (id, layers, sources, falloff = BLAST_FALLOFF) => ({
4034
+ ...sceneRecipe(id, layers, ""),
4035
+ falloff,
4036
+ sources
4037
+ });
4038
+ var crack = (f, d, vol) => noise({ type: "highpass", f, a: 1e-3, d, vol });
4039
+ var debris = (count, from, to, vol) => [
4040
+ ...many(count, () => noise({ type: "bandpass", f: [300, 1400], q: 2.5, d: [0.03, 0.09], vol, delay: [from, to] })),
4041
+ ...many(Math.ceil(count / 3), () => noise({ 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] }))
4042
+ ];
4043
+ var shrapnel = (count, to) => many(count, () => noise({ type: "bandpass", f: [2500, 6e3], q: 6, d: [0.015, 0.045], vol: [0.08, 0.16], delay: [0.02, to] }));
4044
+ var glass = (count, from) => [
4045
+ noise({ type: "highpass", f: [2600, 3e3], d: 0.4, vol: 0.7, wet: 0.5, delay: from }),
4046
+ ...many(count, () => tone({ f: [3e3, 7500], d: [0.05, 0.13], vol: 0.12, delay: [from, from + 0.7] }))
4047
+ ];
4048
+ var BLAST_SOUNDS = [
4049
+ blast(
4050
+ "explosion_grenade",
4051
+ [
4052
+ crack([2300, 2700], 0.06, 2.4),
4053
+ noise({ f: [2500, 3e3], f2: 160, d: 0.7, vol: 2, wet: 0.35 }),
4054
+ tone({ f: [80, 92], f2: 32, d: 0.5, vol: 1.6 }),
4055
+ noise({ type: "bandpass", f: [620, 760], q: 0.8, d: 0.18, vol: 1.1 }),
4056
+ ...shrapnel(14, 0.7),
4057
+ ...debris(10, 0.35, 1.8, [0.1, 0.2])
4058
+ ],
4059
+ ["sdk: frag grenade (M67, VOG-25, 40 mm): sharp crack, short body, fragments striking, a little debris"]
4060
+ ),
4061
+ blast(
4062
+ "explosion_big",
4063
+ [
4064
+ crack([1900, 2300], 0.08, 2.4),
4065
+ noise({ f: [2200, 2600], f2: 90, d: 1.6, vol: 2.2, wet: 0.45 }),
4066
+ tone({ f: [52, 60], f2: 22, d: 1.6, vol: 1.9 }),
4067
+ noise({ f: [280, 330], f2: 60, a: 0.25, d: 2.8, vol: 0.8, wet: 0.6, delay: 0.05 }),
4068
+ ...debris(24, 0.5, 3.6, [0.12, 0.25])
4069
+ ],
4070
+ ["sdk: big charge (C4, dynamite, a fuel station tank): crack, long deep roar, debris falling for 2 to 4 s"]
4071
+ ),
4072
+ blast(
4073
+ "explosion_fuel",
4074
+ [
4075
+ noise({ type: "highpass", f: [1700, 2e3], a: 1e-3, d: 0.05, vol: 1 }),
4076
+ tone({ f: [380, 450], f2: 300, d: 0.4, vol: 0.3 }),
4077
+ noise({ f: [1700, 2e3], f2: 150, a: 0.03, d: 1.4, vol: 2.1, wet: 0.4 }),
4078
+ tone({ f: [48, 56], f2: 25, d: 1.2, vol: 1.7 }),
4079
+ noise({ type: "bandpass", f: [450, 560], q: 0.6, a: 0.15, d: 2.2, vol: 0.85, wet: 0.5, delay: 0.1 }),
4080
+ ...debris(10, 0.5, 2.6, [0.08, 0.16])
4081
+ ],
4082
+ ['sdk: a fuel vessel bursting (propane P13, fuel barrel, pump): the shell tearing, a fireball "whoomp" and its roar']
4083
+ ),
4084
+ blast(
4085
+ "explosion_car",
4086
+ [
4087
+ noise({ type: "highpass", f: [1700, 2e3], a: 1e-3, d: 0.06, vol: 1.1 }),
4088
+ noise({ f: [1600, 1900], f2: 140, a: 0.03, d: 1.6, vol: 2.1, wet: 0.4 }),
4089
+ tone({ f: [44, 52], f2: 24, d: 1.4, vol: 1.8 }),
4090
+ noise({ type: "bandpass", f: [420, 520], q: 0.6, a: 0.15, d: 2.6, vol: 0.9, wet: 0.5, delay: 0.1 }),
4091
+ ...glass(10, 0.04),
4092
+ tone({ f: [130, 150], f2: 85, d: 1.1, vol: 0.18, delay: [0.3, 0.5] }),
4093
+ ...debris(16, 0.6, 3.2, [0.1, 0.2])
4094
+ ],
4095
+ ["sdk: a car going up: the fuel tank fireball, every window breaking, the body groaning, parts falling"]
4096
+ ),
4097
+ blast(
4098
+ "explosion_pipe_bomb",
4099
+ [
4100
+ crack([2e3, 2400], 0.07, 2.2),
4101
+ noise({ f: [2e3, 2400], f2: 150, d: 0.8, vol: 1.9, wet: 0.35 }),
4102
+ tone({ f: [70, 82], f2: 30, d: 0.6, vol: 1.5 }),
4103
+ ...many(6, () => tone({ f: [1800, 3200], f2: 1200, d: [0.06, 0.12], vol: 0.06, delay: [0.05, 0.6] })),
4104
+ ...shrapnel(18, 0.9),
4105
+ ...debris(8, 0.4, 1.8, [0.08, 0.16])
4106
+ ],
4107
+ ["sdk: pipe bomb: crack, body, the steel pipe flying apart in ringing pieces"]
4108
+ ),
4109
+ blast(
4110
+ "explosion_rocket",
4111
+ [
4112
+ crack([2100, 2500], 0.08, 2.4),
4113
+ noise({ f: [2400, 2800], f2: 120, d: 1.1, vol: 2.1, wet: 0.4 }),
4114
+ tone({ f: [60, 70], f2: 26, d: 0.9, vol: 1.8 }),
4115
+ noise({ f: [350, 420], f2: 80, a: 0.08, d: 1.8, vol: 0.9, wet: 0.6, delay: 0.05 }),
4116
+ ...shrapnel(10, 0.8),
4117
+ ...debris(16, 0.4, 2.6, [0.1, 0.2])
4118
+ ],
4119
+ ["sdk: rocket or anti-tank warhead (M72, AT4, RPG): hard crack, heavy body, debris"]
4120
+ ),
4121
+ blast(
4122
+ "explosion_flashbang",
4123
+ [crack([1400, 1700], 0.12, 2.8), noise({ f: [1900, 2200], f2: 300, d: 0.35, vol: 1.3, wet: 0.3 }), tone({ f: [110, 130], f2: 60, d: 0.2, vol: 0.8 })],
4124
+ ["sdk: flashbang: a deafening crack with almost no body and no fragments"]
4125
+ ),
4126
+ blast(
4127
+ "molotov_break_ignite",
4128
+ [
4129
+ ...glass(8, 0),
4130
+ noise({ f: [280, 340], f2: 2200, a: 0.12, d: 0.9, vol: 1.5, wet: 0.6, delay: 0.05 }),
4131
+ tone({ f: [85, 95], f2: 55, d: 0.4, vol: 0.5, delay: 0.08 })
4132
+ ],
4133
+ ['sdk: molotov: the bottle breaking and the fuel catching with a "fuuush"'],
4134
+ FIRE_FALLOFF
4135
+ ),
4136
+ blast(
4137
+ "explosion_far",
4138
+ [
4139
+ noise({ f: [550, 650], f2: 80, a: 0.03, d: 2.5, vol: 2, wet: 1.4 }),
4140
+ tone({ f: [42, 48], f2: 25, d: 1.8, vol: 1.2 }),
4141
+ noise({ f: [350, 420], f2: 70, a: 0.1, d: 1.6, vol: 0.6, wet: 1.5, delay: [0.6, 1] })
4142
+ ],
4143
+ ["sdk: any blast far away: no crack left, a dull thud and the echo rolling back"],
4144
+ BLAST_FAR_FALLOFF
4145
+ ),
4146
+ blast(
4147
+ "explosion_indoor_tail",
4148
+ [
4149
+ ...many(3, () => noise({ f: [650, 800], d: 0.12, vol: 0.5, delay: [0.04, 0.2] })),
4150
+ noise({ f: [850, 950], f2: 120, a: 0.05, d: 2.6, vol: 0.9, wet: 1.6, delay: 0.08 })
4151
+ ],
4152
+ ["sdk: the room answering a blast inside it: slaps off the walls and a boomy tail"]
4153
+ )
4154
+ ];
4155
+ var FIRE_SOUNDS = [
4156
+ {
4157
+ ...blast(
4158
+ "fire_ignite",
4159
+ [noise({ f: [230, 270], f2: 1500, a: 0.07, d: 0.5, vol: 1.6 }), tone({ f: [80, 90], f2: 55, d: 0.3, vol: 0.45 }), noise({ type: "highpass", f: 3e3, d: 0.3, vol: 0.1, delay: 0.1 })],
4160
+ ['sdk: fire catching, a soft "fuuump"'],
4161
+ FIRE_FALLOFF
4162
+ ),
4163
+ group: "fire_ignite"
4164
+ },
4165
+ {
4166
+ ...blast("gas_leak", [noise({ 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),
4167
+ vars: { seconds: 2 }
4168
+ },
4169
+ {
4170
+ ...blast(
4171
+ "fire_extinguish",
4172
+ [noise({ type: "highpass", f: [2300, 2700], a: 0.03, d: { param: "seconds" }, vol: 0.3 }), noise({ type: "bandpass", f: [800, 1e3], q: 0.5, d: 0.5, vol: 0.25 })],
4173
+ ["sdk: water or foam on a fire: a steam puff and a sizzle"],
4174
+ FIRE_FALLOFF
4175
+ ),
4176
+ vars: { seconds: 1.2 }
4177
+ },
4178
+ { ...blast("fire_crackle", [noise({ 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" },
4179
+ { ...blast("fire_pop", [noise({ 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" }
4180
+ ];
4181
+ var FIRE_LOOP = {
4182
+ id: "fire_loop",
4183
+ bus: "world",
4184
+ order: "chain_loop",
4185
+ source: { kind: "noise", loop: true },
4186
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 950 }, q: 0.5 }] },
4187
+ volume: 0,
4188
+ sources: ["sdk: the roar under the crackles of a fire"]
4189
+ };
4190
+ var BLAST_SOUND_OF = {
4191
+ grenade: "explosion_grenade",
4192
+ big: "explosion_big",
4193
+ fuel: "explosion_fuel",
4194
+ car: "explosion_car",
4195
+ pipe_bomb: "explosion_pipe_bomb",
4196
+ rocket: "explosion_rocket",
4197
+ flashbang: "explosion_flashbang",
4198
+ molotov: "molotov_break_ignite"
4199
+ };
4200
+
4201
+ // src/audio/recipes.ts
4202
+ var scene = (id, layers, line, vars) => sceneRecipe2(id, layers, line, vars ? { vars } : {});
3879
4203
  var boomLayers = (big) => [
3880
- noise({ f: 1200, f2: 120, d: 1.8, vol: 2, wet: 1.2 }),
3881
- tone({ f: 65, f2: 24, d: 1, vol: 1.8 }),
3882
- noise({ type: "highpass", f: 1500, d: 0.2, vol: 0.8 }),
3883
- ...big ? [noise({ f: 500, f2: 90, a: 0.2, d: 2.4, vol: 1.2, wet: 1, delay: 0.15 })] : [],
3884
- ...many(8, () => tone({ f: [2600, 5600], d: 0.05, vol: 0.08, delay: [0.4, 1.6] }))
4204
+ noise2({ f: 1200, f2: 120, d: 1.8, vol: 2, wet: 1.2 }),
4205
+ tone2({ f: 65, f2: 24, d: 1, vol: 1.8 }),
4206
+ noise2({ type: "highpass", f: 1500, d: 0.2, vol: 0.8 }),
4207
+ ...big ? [noise2({ f: 500, f2: 90, a: 0.2, d: 2.4, vol: 1.2, wet: 1, delay: 0.15 })] : [],
4208
+ ...many2(8, () => tone2({ f: [2600, 5600], d: 0.05, vol: 0.08, delay: [0.4, 1.6] }))
3885
4209
  ];
3886
4210
  var SCENE_SOUNDS = [
3887
4211
  scene(
3888
4212
  "glass_shatter",
3889
- [noise({ type: "highpass", f: 2800, d: 0.45, vol: 0.9, wet: 0.5 }), ...many(12, () => tone({ f: [3e3, 7500], d: [0.06, 0.14], vol: 0.18, delay: [0, 0.55] }))],
4213
+ [noise2({ type: "highpass", f: 2800, d: 0.45, vol: 0.9, wet: 0.5 }), ...many2(12, () => tone2({ f: [3e3, 7500], d: [0.06, 0.14], vol: 0.18, delay: [0, 0.55] }))],
3890
4214
  "L1359 glass"
3891
4215
  ),
3892
4216
  scene(
3893
4217
  "wood_hit",
3894
- [noise({ type: "bandpass", f: 850, q: 1, d: 0.25, vol: 0.9, wet: 0.3 }), tone({ f: 210, f2: 90, d: 0.1, vol: 0.6 }), noise({ type: "highpass", f: 2e3, d: 0.04, vol: 0.5 })],
4218
+ [noise2({ type: "bandpass", f: 850, q: 1, d: 0.25, vol: 0.9, wet: 0.3 }), tone2({ f: 210, f2: 90, d: 0.1, vol: 0.6 }), noise2({ type: "highpass", f: 2e3, d: 0.04, vol: 0.5 })],
3895
4219
  "L1360 wood"
3896
4220
  ),
3897
- scene("thunk", [noise({ type: "bandpass", f: 700, q: 2, d: 0.07, vol: 0.4 })], "L1361 thunk"),
3898
- scene("ping", [tone({ f: [2600, 3500], f2: 1400, d: 0.14, vol: 0.18, wet: 0.3 })], "L1362 ping"),
3899
- scene("clang", [tone({ f: [520, 720], f2: 480, d: 0.5, vol: 0.35, wet: 0.4 }), tone({ f: 1370, f2: 1300, d: 0.3, vol: 0.15 }), noise({ type: "highpass", f: 2e3, d: 0.05, vol: 0.5 })], "L1363 clang"),
3900
- scene("chip", [noise({ type: "bandpass", f: 1600, q: 1.2, d: 0.06, vol: 0.5 })], "L1364 chip"),
4221
+ scene("thunk", [noise2({ type: "bandpass", f: 700, q: 2, d: 0.07, vol: 0.4 })], "L1361 thunk"),
4222
+ scene("ping", [tone2({ f: [2600, 3500], f2: 1400, d: 0.14, vol: 0.18, wet: 0.3 })], "L1362 ping"),
4223
+ scene("clang", [tone2({ f: [520, 720], f2: 480, d: 0.5, vol: 0.35, wet: 0.4 }), tone2({ f: 1370, f2: 1300, d: 0.3, vol: 0.15 }), noise2({ type: "highpass", f: 2e3, d: 0.05, vol: 0.5 })], "L1363 clang"),
4224
+ scene("chip", [noise2({ type: "bandpass", f: 1600, q: 1.2, d: 0.06, vol: 0.5 })], "L1364 chip"),
3901
4225
  scene(
3902
4226
  "crumble",
3903
- [noise({ f: 900, f2: 200, d: 0.6, vol: 0.9, wet: 0.3 }), ...many(5, () => noise({ type: "bandpass", f: [500, 1400], q: 2, d: 0.05, vol: 0.4, delay: [0.1, 0.5] }))],
4227
+ [noise2({ f: 900, f2: 200, d: 0.6, vol: 0.9, wet: 0.3 }), ...many2(5, () => noise2({ type: "bandpass", f: [500, 1400], q: 2, d: 0.05, vol: 0.4, delay: [0.1, 0.5] }))],
3904
4228
  "L1365 crumble"
3905
4229
  ),
3906
4230
  // `thud(x, z, v)`: `v` is the `scale` param (default 1)
3907
- scene("thud", [tone({ f: 75, f2: 38, d: 0.22, vol: { param: "scale", scale: 0.6 } }), noise({ f: 420, d: 0.16, vol: { param: "scale", scale: 0.45 } })], "L1358 thud", { scale: 1 }),
4231
+ scene("thud", [tone2({ f: 75, f2: 38, d: 0.22, vol: { param: "scale", scale: 0.6 } }), noise2({ f: 420, d: 0.16, vol: { param: "scale", scale: 0.45 } })], "L1358 thud", { scale: 1 }),
3908
4232
  // `hiss(x, z, d)`: `seconds` is the param `d` (default 1.2)
3909
- scene("hiss", [noise({ type: "highpass", f: 3500, a: 0.05, d: { param: "seconds" }, vol: 0.35 })], "L1366 hiss", { seconds: 1.2 }),
4233
+ scene("hiss", [noise2({ type: "highpass", f: 3500, a: 0.05, d: { param: "seconds" }, vol: 0.35 })], "L1366 hiss", { seconds: 1.2 }),
3910
4234
  scene("boom", boomLayers(false), "L1367 boom"),
3911
4235
  scene("boom_big", boomLayers(true), "L1367 boom (big)"),
3912
- scene("burst", [noise({ f: 700, f2: 120, d: 0.7, vol: 1.4, wet: 0.5 }), noise({ type: "highpass", f: 3e3, a: 0.05, d: 1.2, vol: 0.4, delay: 0.1 })], "L1357 burst"),
3913
- scene("crack", [noise({ type: "highpass", f: 1800, d: 0.05, vol: 0.8 }), tone({ f: 420, f2: 180, d: 0.06, vol: 0.4 })], "L1354 crack")
4236
+ scene("burst", [noise2({ f: 700, f2: 120, d: 0.7, vol: 1.4, wet: 0.5 }), noise2({ type: "highpass", f: 3e3, a: 0.05, d: 1.2, vol: 0.4, delay: 0.1 })], "L1357 burst"),
4237
+ scene("crack", [noise2({ type: "highpass", f: 1800, d: 0.05, vol: 0.8 }), tone2({ f: 420, f2: 180, d: 0.06, vol: 0.4 })], "L1354 crack")
3914
4238
  ];
3915
4239
  var EXPLOSION_BOOM = {
3916
4240
  id: "explosion_boom",
@@ -3939,29 +4263,129 @@ var BURN_LOOP = {
3939
4263
  volume: 0,
3940
4264
  sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
3941
4265
  };
3942
- var drag = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a base dragged on a floor, one grain per stretch"] });
4266
+ var drag = (id, layers) => ({ ...sceneRecipe2(id, layers, ""), sources: ["sdk: a base dragged on a floor, one grain per stretch"] });
3943
4267
  var DRAG_SOUNDS = [
3944
- drag("drag_wood", [noise({ type: "bandpass", f: 380, q: 1.4, d: 0.28, vol: 0.5 }), noise({ type: "bandpass", f: [900, 1300], q: 3, d: 0.08, vol: 0.14 })]),
3945
- drag("drag_metal", [noise({ type: "bandpass", f: 1500, q: 4, d: 0.26, vol: 0.32 }), tone({ f: [880, 1240], f2: 760, d: 0.18, vol: 0.05 })]),
3946
- drag("drag_glass", [noise({ type: "bandpass", f: 2600, q: 5, d: 0.2, vol: 0.22 })]),
3947
- drag("drag_masonry", [noise({ type: "bandpass", f: 260, q: 0.9, d: 0.3, vol: 0.62 }), ...many(3, () => noise({ type: "bandpass", f: [600, 1200], q: 2, d: 0.04, vol: 0.14, delay: [0, 0.2] }))]),
3948
- drag("drag_soft", [noise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
4268
+ drag("drag_wood", [noise2({ type: "bandpass", f: 380, q: 1.4, d: 0.28, vol: 0.5 }), noise2({ type: "bandpass", f: [900, 1300], q: 3, d: 0.08, vol: 0.14 })]),
4269
+ drag("drag_metal", [noise2({ type: "bandpass", f: 1500, q: 4, d: 0.26, vol: 0.32 }), tone2({ f: [880, 1240], f2: 760, d: 0.18, vol: 0.05 })]),
4270
+ drag("drag_glass", [noise2({ type: "bandpass", f: 2600, q: 5, d: 0.2, vol: 0.22 })]),
4271
+ drag("drag_masonry", [noise2({ type: "bandpass", f: 260, q: 0.9, d: 0.3, vol: 0.62 }), ...many2(3, () => noise2({ type: "bandpass", f: [600, 1200], q: 2, d: 0.04, vol: 0.14, delay: [0, 0.2] }))]),
4272
+ drag("drag_soft", [noise2({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
3949
4273
  ];
3950
- var leaf = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a door, lid or drawer worked by hand"] });
4274
+ var leaf = (id, layers) => ({ ...sceneRecipe2(id, layers, ""), sources: ["sdk: a door, lid or drawer worked by hand"] });
3951
4275
  var LEAF_SOUNDS = [
3952
- leaf("latch", [noise({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.35 }), tone({ f: 1900, f2: 1500, d: 0.025, vol: 0.06, delay: 0.035 })]),
3953
- leaf("creak_wood", [tone({ f: [190, 240], f2: 310, d: 0.5, vol: 0.05 }), noise({ type: "bandpass", f: 520, q: 6, a: 0.05, d: 0.45, vol: 0.12 })]),
3954
- leaf("creak_metal", [tone({ f: [620, 760], f2: 900, d: 0.35, vol: 0.035 }), noise({ type: "bandpass", f: 1400, q: 8, a: 0.04, d: 0.3, vol: 0.08 })]),
3955
- leaf("shut_wood", [tone({ f: 120, f2: 60, d: 0.14, vol: 0.5 }), noise({ type: "bandpass", f: 600, q: 1.2, d: 0.1, vol: 0.55, wet: 0.25 }), noise({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.25, delay: 0.02 })]),
3956
- leaf("shut_metal", [tone({ f: 160, f2: 85, d: 0.18, vol: 0.55 }), noise({ type: "bandpass", f: 900, q: 1.6, d: 0.12, vol: 0.45, wet: 0.3 }), tone({ f: 1250, f2: 1150, d: 0.12, vol: 0.06 })]),
3957
- leaf("shut_soft", [tone({ f: 95, f2: 55, d: 0.12, vol: 0.35 }), noise({ type: "lowpass", f: 500, d: 0.1, vol: 0.3 })]),
3958
- leaf("rattle", many(5, () => noise({ type: "bandpass", f: [1600, 2600], q: 3, d: 0.035, vol: 0.32, delay: [0, 0.45] }))),
3959
- leaf("slide", [noise({ type: "bandpass", f: 700, q: 2, a: 0.04, d: 0.35, vol: 0.22 }), noise({ type: "bandpass", f: 2200, q: 5, d: 0.3, vol: 0.05 })])
4276
+ leaf("latch", [noise2({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.35 }), tone2({ f: 1900, f2: 1500, d: 0.025, vol: 0.06, delay: 0.035 })]),
4277
+ leaf("creak_wood", [tone2({ f: [190, 240], f2: 310, d: 0.5, vol: 0.05 }), noise2({ type: "bandpass", f: 520, q: 6, a: 0.05, d: 0.45, vol: 0.12 })]),
4278
+ leaf("creak_metal", [tone2({ f: [620, 760], f2: 900, d: 0.35, vol: 0.035 }), noise2({ type: "bandpass", f: 1400, q: 8, a: 0.04, d: 0.3, vol: 0.08 })]),
4279
+ leaf("shut_wood", [tone2({ f: 120, f2: 60, d: 0.14, vol: 0.5 }), noise2({ type: "bandpass", f: 600, q: 1.2, d: 0.1, vol: 0.55, wet: 0.25 }), noise2({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.25, delay: 0.02 })]),
4280
+ leaf("shut_metal", [tone2({ f: 160, f2: 85, d: 0.18, vol: 0.55 }), noise2({ type: "bandpass", f: 900, q: 1.6, d: 0.12, vol: 0.45, wet: 0.3 }), tone2({ f: 1250, f2: 1150, d: 0.12, vol: 0.06 })]),
4281
+ leaf("shut_soft", [tone2({ f: 95, f2: 55, d: 0.12, vol: 0.35 }), noise2({ type: "lowpass", f: 500, d: 0.1, vol: 0.3 })]),
4282
+ leaf("rattle", many2(5, () => noise2({ type: "bandpass", f: [1600, 2600], q: 3, d: 0.035, vol: 0.32, delay: [0, 0.45] }))),
4283
+ leaf("slide", [noise2({ type: "bandpass", f: 700, q: 2, a: 0.04, d: 0.35, vol: 0.22 }), noise2({ type: "bandpass", f: 2200, q: 5, d: 0.3, vol: 0.05 })]),
4284
+ // 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)
4285
+ leaf("grab_hard", [noise2({ type: "bandpass", f: 1800, q: 3, d: 0.03, vol: 0.18 }), noise2({ type: "bandpass", f: 900, q: 1.2, a: 0.02, d: 0.14, vol: 0.08 })]),
4286
+ leaf("grab_soft", [noise2({ type: "lowpass", f: 1400, a: 0.02, d: 0.16, vol: 0.14 })])
4287
+ ];
4288
+ var impact = (id, layers) => ({ ...sceneRecipe2(id, layers, ""), sources: ["sdk: a blow on a material, by material and kind of blow (synthesised until the recorded samples arrive)"] });
4289
+ var IMPACT_SOUNDS = [
4290
+ // metal
4291
+ impact("metal_bullet", [noise2({ type: "highpass", f: 3e3, d: 0.03, vol: 0.8 }), tone2({ f: [2600, 3400], f2: 1800, d: 0.14, vol: 0.28, wet: 0.35 }), tone2({ f: [520, 720], f2: 480, d: 0.35, vol: 0.22, wet: 0.35 })]),
4292
+ impact("metal_blunt", [noise2({ type: "bandpass", f: 800, q: 1, d: 0.05, vol: 0.5 }), tone2({ f: [150, 210], f2: 120, d: 0.6, vol: 0.5, wet: 0.4 }), tone2({ f: [420, 520], f2: 400, d: 0.4, vol: 0.12 })]),
4293
+ impact("metal_blade", [noise2({ type: "bandpass", f: [3500, 4500], q: 6, a: 0.01, d: 0.18, vol: 0.45 }), tone2({ f: [2200, 2800], f2: 3e3, d: 0.15, vol: 0.06 })]),
4294
+ impact("metal_thin", [noise2({ type: "highpass", f: 2500, d: 0.04, vol: 0.6 }), tone2({ f: [900, 1300], f2: 700, d: 0.25, vol: 0.22, wet: 0.3 }), noise2({ type: "bandpass", f: 1200, q: 2, d: 0.08, vol: 0.3 })]),
4295
+ // wood (the bullet keeps the art scene's `wood_hit`)
4296
+ impact("wood_blunt", [tone2({ f: [110, 150], f2: 70, d: 0.14, vol: 0.6 }), noise2({ type: "bandpass", f: 500, q: 1.2, d: 0.12, vol: 0.55, wet: 0.2 })]),
4297
+ impact("wood_blade", [noise2({ type: "bandpass", f: [1800, 2400], q: 2, a: 0.01, d: 0.12, vol: 0.5 }), ...many2(3, () => noise2({ type: "bandpass", f: [2500, 4e3], q: 3, d: 0.02, vol: 0.22, delay: [0.02, 0.1] }))]),
4298
+ impact("mdf_puff", [noise2({ type: "lowpass", f: 900, d: 0.08, vol: 1 }), tone2({ f: [180, 230], f2: 110, d: 0.06, vol: 0.3 }), noise2({ type: "highpass", f: 3e3, a: 0.01, d: 0.2, vol: 0.1 })]),
4299
+ // glass and ceramic
4300
+ impact("glass_bullet", [noise2({ type: "highpass", f: 4e3, d: 0.05, vol: 0.7 }), tone2({ f: [3200, 4200], f2: 2600, d: 0.2, vol: 0.14, wet: 0.3 }), ...many2(4, () => tone2({ f: [4e3, 7e3], d: 0.05, vol: 0.07, delay: [0.02, 0.2] }))]),
4301
+ impact("glass_blunt", [noise2({ type: "bandpass", f: 2200, q: 3, d: 0.05, vol: 0.8 }), tone2({ f: [1400, 1800], f2: 1300, d: 0.2, vol: 0.16, wet: 0.3 })]),
4302
+ impact("glass_blade", [noise2({ type: "bandpass", f: 5e3, q: 8, a: 0.02, d: 0.25, vol: 0.3 })]),
4303
+ impact("ceramic_hit", [noise2({ type: "bandpass", f: 3e3, q: 2, d: 0.03, vol: 0.6 }), tone2({ f: [1800, 2400], f2: 1600, d: 0.1, vol: 0.2 })]),
4304
+ impact("ceramic_bullet", [noise2({ type: "highpass", f: 2500, d: 0.06, vol: 0.85 }), ...many2(5, () => tone2({ f: [2500, 5e3], d: 0.04, vol: 0.09, delay: [0, 0.15] }))]),
4305
+ // masonry and the ground
4306
+ impact("masonry_bullet", [noise2({ type: "bandpass", f: [1600, 2e3], q: 1, d: 0.05, vol: 1.8 }), noise2({ type: "lowpass", f: 600, d: 0.12, vol: 0.9, wet: 0.2 }), ...many2(4, () => noise2({ type: "bandpass", f: [2e3, 4e3], q: 3, d: 0.02, vol: 0.28, delay: [0.03, 0.3] }))]),
4307
+ impact("masonry_blunt", [tone2({ f: [70, 95], f2: 45, d: 0.12, vol: 0.55 }), noise2({ type: "lowpass", f: 400, d: 0.1, vol: 0.5 })]),
4308
+ impact("masonry_blade", [noise2({ type: "bandpass", f: [2500, 3500], q: 4, a: 0.01, d: 0.15, vol: 0.4 })]),
4309
+ impact("plaster_puff", [noise2({ type: "lowpass", f: 900, d: 0.09, vol: 0.7 }), noise2({ type: "highpass", f: 3e3, a: 0.01, d: 0.2, vol: 0.14 })]),
4310
+ impact("soil_bullet", [noise2({ type: "lowpass", f: 700, d: 0.08, vol: 2.2 }), ...many2(3, () => noise2({ type: "bandpass", f: [1200, 2200], q: 2, d: 0.03, vol: 0.18, delay: [0.02, 0.2] }))]),
4311
+ impact("soil_blunt", [tone2({ f: [55, 70], f2: 40, d: 0.1, vol: 0.45 }), noise2({ type: "lowpass", f: 300, d: 0.08, vol: 0.4 })]),
4312
+ impact("sand_hit", [noise2({ type: "bandpass", f: 3500, q: 0.8, a: 5e-3, d: 0.15, vol: 0.45 }), noise2({ type: "lowpass", f: 500, d: 0.06, vol: 0.45 })]),
4313
+ impact("gravel_hit", [noise2({ type: "lowpass", f: 500, d: 0.06, vol: 0.45 }), ...many2(6, () => noise2({ type: "bandpass", f: [1500, 3500], q: 3, d: 0.025, vol: 0.28, delay: [0, 0.18] }))]),
4314
+ // plastic, rubber, wax
4315
+ impact("plastic_hit", [noise2({ type: "bandpass", f: 1600, q: 3, d: 0.04, vol: 0.5 }), tone2({ f: [380, 520], f2: 300, d: 0.08, vol: 0.32 })]),
4316
+ impact("plastic_bullet", [noise2({ type: "bandpass", f: 2500, q: 2, d: 0.03, vol: 1.3 }), tone2({ f: [600, 800], f2: 450, d: 0.06, vol: 0.4 })]),
4317
+ impact("rubber_hit", [tone2({ f: [90, 130], f2: 60, d: 0.1, vol: 0.45 }), noise2({ type: "lowpass", f: 350, d: 0.06, vol: 0.28 })]),
4318
+ impact("wax_hit", [noise2({ type: "lowpass", f: 800, d: 0.05, vol: 0.45 }), tone2({ f: [240, 280], f2: 180, d: 0.05, vol: 0.22 })]),
4319
+ // soft things, paper, plants
4320
+ impact("soft_bullet", [noise2({ type: "lowpass", f: 450, a: 3e-3, d: 0.1, vol: 1.8 }), noise2({ type: "bandpass", f: 900, q: 1, d: 0.05, vol: 0.5 })]),
4321
+ impact("fabric_rip", [noise2({ type: "bandpass", f: [2e3, 2800], q: 1.5, a: 0.02, d: 0.25, vol: 0.4 }), ...many2(4, () => noise2({ type: "bandpass", f: [3e3, 5e3], q: 4, d: 0.02, vol: 0.14, delay: [0, 0.22] }))]),
4322
+ impact("paper_hit", [noise2({ type: "bandpass", f: 2200, q: 1, d: 0.05, vol: 0.45 })]),
4323
+ impact("paper_tear", [noise2({ type: "highpass", f: 2500, a: 0.02, d: 0.3, vol: 0.15 })]),
4324
+ impact("leaf_rustle", many2(6, () => noise2({ type: "bandpass", f: [2500, 6e3], q: 2, d: 0.05, vol: 0.16, delay: [0, 0.3] }))),
4325
+ impact("branch_cut", [noise2({ type: "bandpass", f: 1500, q: 2, d: 0.04, vol: 0.55 }), tone2({ f: [260, 340], f2: 150, d: 0.05, vol: 0.22 })]),
4326
+ impact("fruit_splat", [noise2({ type: "lowpass", f: 1200, d: 0.12, vol: 0.55 }), noise2({ type: "bandpass", f: 600, q: 0.7, d: 0.2, vol: 0.32 })]),
4327
+ // a round glancing off: steel whines down in pitch ("piiuu"), stone and concrete give a short crack with grit
4328
+ impact("ricochet_metal", [noise2({ type: "highpass", f: 3e3, d: 0.02, vol: 0.6 }), tone2({ f: [2800, 3600], f2: 900, d: 0.38, vol: 0.2, wet: 0.4 }), tone2({ f: [5200, 6400], f2: 2e3, d: 0.2, vol: 0.05 })]),
4329
+ impact("ricochet_stone", [noise2({ type: "bandpass", f: [2200, 2800], q: 2, d: 0.04, vol: 0.8 }), tone2({ f: [1800, 2400], f2: 1200, d: 0.08, vol: 0.1 }), ...many2(3, () => noise2({ type: "bandpass", f: [2500, 4500], q: 3, d: 0.015, vol: 0.2, delay: [0.02, 0.12] }))]),
4330
+ // breaks a material has of its own (the rest keep the art scene's shatter, crumble and splinter)
4331
+ impact("ceramic_break", [noise2({ type: "highpass", f: 1800, d: 0.3, vol: 0.85, wet: 0.4 }), ...many2(8, () => tone2({ f: [1800, 4500], d: [0.04, 0.1], vol: 0.15, delay: [0, 0.4] }))]),
4332
+ impact("tempered_burst", [noise2({ type: "highpass", f: 3500, d: 0.6, vol: 0.9, wet: 0.4 }), ...many2(16, () => tone2({ f: [4e3, 8e3], d: 0.04, vol: 0.08, delay: [0, 0.8] }))]),
4333
+ impact("plastic_crack", [noise2({ type: "bandpass", f: 1800, q: 2, d: 0.06, vol: 0.75 }), tone2({ f: [450, 550], f2: 250, d: 0.05, vol: 0.22 })]),
4334
+ impact("mdf_crumble", [noise2({ type: "lowpass", f: 700, d: 0.25, vol: 0.75, wet: 0.2 }), ...many2(4, () => noise2({ type: "lowpass", f: 500, d: 0.05, vol: 0.32, delay: [0.05, 0.3] }))])
3960
4335
  ];
3961
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
4336
+ 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] };
3962
4337
 
3963
4338
  // src/audio/sound.ts
3964
- import { GAME_SFX_BANK } from "@adatechnology/game-audio";
4339
+ import { FOLEY_BANK, GAME_SFX_BANK } from "@adatechnology/game-audio";
4340
+ var IMPACT_SOUND_KEYS = {
4341
+ impact_metal_ballistic: { sound: "metal_bullet" },
4342
+ impact_metal_blunt: { sound: "metal_blunt" },
4343
+ impact_metal_slash: { sound: "metal_blade" },
4344
+ impact_aluminium_ballistic: { sound: "metal_thin" },
4345
+ impact_aluminium_blunt: { sound: "metal_thin" },
4346
+ impact_copper_ballistic: { sound: "metal_thin" },
4347
+ impact_copper_blunt: { sound: "metal_thin" },
4348
+ impact_wood_ballistic: { sound: "wood_hit" },
4349
+ impact_wood_blunt: { sound: "wood_blunt" },
4350
+ impact_wood_slash: { sound: "wood_blade" },
4351
+ impact_mdf_ballistic: { sound: "mdf_puff" },
4352
+ impact_mdf_blunt: { sound: "mdf_puff", volumeScale: 0.8 },
4353
+ impact_glass_ballistic: { sound: "glass_bullet" },
4354
+ impact_glass_blunt: { sound: "glass_blunt" },
4355
+ impact_glass_slash: { sound: "glass_blade" },
4356
+ impact_ceramic: { sound: "ceramic_hit" },
4357
+ impact_ceramic_ballistic: { sound: "ceramic_bullet" },
4358
+ impact_masonry_ballistic: { sound: "masonry_bullet" },
4359
+ impact_masonry_blunt: { sound: "masonry_blunt" },
4360
+ impact_masonry_slash: { sound: "masonry_blade" },
4361
+ impact_plaster: { sound: "plaster_puff" },
4362
+ impact_soil: { sound: "soil_blunt" },
4363
+ impact_soil_ballistic: { sound: "soil_bullet" },
4364
+ impact_sand: { sound: "sand_hit" },
4365
+ impact_gravel: { sound: "gravel_hit" },
4366
+ impact_plastic: { sound: "plastic_hit" },
4367
+ impact_plastic_ballistic: { sound: "plastic_bullet" },
4368
+ impact_rubber: { sound: "rubber_hit" },
4369
+ impact_wax: { sound: "wax_hit" },
4370
+ impact_soft_ballistic: { sound: "soft_bullet" },
4371
+ impact_soft_slash: { sound: "fabric_rip" },
4372
+ impact_paper: { sound: "paper_hit" },
4373
+ impact_paper_slash: { sound: "paper_tear" },
4374
+ impact_plant: { sound: "leaf_rustle" },
4375
+ impact_plant_slash: { sound: "branch_cut" },
4376
+ impact_flesh_fruit: { sound: "fruit_splat" },
4377
+ ricochet_metal: { sound: "ricochet_metal" },
4378
+ ricochet_masonry: { sound: "ricochet_stone" },
4379
+ ricochet_soil: { sound: "ricochet_stone" },
4380
+ ricochet_ceramic: { sound: "ricochet_stone" },
4381
+ break_ceramic: { sound: "ceramic_break" },
4382
+ break_tempered_glass: { sound: "tempered_burst" },
4383
+ break_plastic: { sound: "plastic_crack" },
4384
+ break_mdf: { sound: "mdf_crumble" },
4385
+ break_soft: { sound: "fabric_rip" },
4386
+ break_paper: { sound: "paper_tear" },
4387
+ break_plant: { sound: "branch_cut" }
4388
+ };
3965
4389
  var DESTRUCTION_SOUND_KEYS = {
3966
4390
  art_scene: {
3967
4391
  impact_metal: { sound: "clang" },
@@ -3969,6 +4393,12 @@ var DESTRUCTION_SOUND_KEYS = {
3969
4393
  impact_glass: { sound: "ping" },
3970
4394
  impact_masonry: { sound: "chip" },
3971
4395
  impact_soft: { sound: "thud" },
4396
+ // a round strikes: the steel rings high, glass cracks, wood and masonry chip, a soft part takes a dull thunk
4397
+ shot_metal: { sound: "ping" },
4398
+ shot_wood: { sound: "chip" },
4399
+ shot_glass: { sound: "crack" },
4400
+ shot_masonry: { sound: "chip" },
4401
+ shot_soft: { sound: "thunk" },
3972
4402
  drag_wood: { sound: "drag_wood" },
3973
4403
  drag_metal: { sound: "drag_metal" },
3974
4404
  drag_glass: { sound: "drag_glass" },
@@ -3994,22 +4424,38 @@ var DESTRUCTION_SOUND_KEYS = {
3994
4424
  hood_shut: { sound: "shut_metal" },
3995
4425
  trunk_shut: { sound: "shut_metal" },
3996
4426
  door_close: { sound: "shut_wood", volumeScale: 0.7 },
4427
+ // 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
4428
+ vehicle_door_open: { sound: "car_door_open", fallback: "latch" },
4429
+ vehicle_door_shut: { sound: "car_door_close", fallback: "shut_metal" },
4430
+ vehicle_trunk_open: { sound: "car_door_open", fallback: "latch" },
4431
+ vehicle_trunk_shut: { sound: "car_trunk_close", fallback: "shut_metal" },
4432
+ vehicle_hood_open: { sound: "car_door_open", fallback: "latch" },
4433
+ vehicle_hood_shut: { sound: "car_hood_close", fallback: "shut_metal" },
3997
4434
  drawer_open: { sound: "slide" },
3998
4435
  drawer_close: { sound: "slide" },
3999
4436
  lid_open: { sound: "latch" },
4000
4437
  lid_close: { sound: "shut_soft" },
4001
4438
  flap_open: { sound: "latch" },
4002
4439
  flap_close: { sound: "shut_soft" },
4440
+ // an item taken in the hand, by what it is made of (game-audio's KIT_SAMPLE_ALIASES puts recordings on these ids)
4441
+ grab_metal: { sound: "grab_hard" },
4442
+ grab_wood: { sound: "grab_hard" },
4443
+ grab_glass: { sound: "grab_hard", volumeScale: 0.7 },
4444
+ grab_masonry: { sound: "grab_hard" },
4445
+ grab_soft: { sound: "grab_soft" },
4003
4446
  shatter_glass: { sound: "glass_shatter" },
4004
4447
  crumble: { sound: "crumble" },
4005
4448
  splinter: { sound: "crack" },
4006
4449
  ricochet: { sound: "ping" },
4007
4450
  unscrew: { sound: "chip" },
4008
4451
  detach: { sound: "thunk" },
4009
- ignite: { sound: "hiss", params: { seconds: 0.5 } },
4010
- extinguish: { sound: "hiss" },
4452
+ ignite: { sound: "fire_ignite" },
4453
+ primed: { sound: "gas_leak" },
4454
+ extinguish: { sound: "fire_extinguish" },
4011
4455
  melt_sizzle: { sound: "hiss", params: { seconds: 0.6 } },
4012
- explode: { sound: "boom", above: { volume: 0.9, sound: "boom_big" } }
4456
+ // the blast family comes from the explosion's effect (`BLAST_KIND_OF`); this rule answers only an explosion the kit cannot place
4457
+ explode: { sound: "explosion_grenade", above: { volume: 0.9, sound: "explosion_big" } },
4458
+ ...IMPACT_SOUND_KEYS
4013
4459
  },
4014
4460
  game: {
4015
4461
  impact_metal: { sound: "sfx_impact" },
@@ -4023,14 +4469,26 @@ var DESTRUCTION_SOUND_KEYS = {
4023
4469
  explode: { sound: "explosion_boom" }
4024
4470
  }
4025
4471
  };
4026
- var DEFAULT_FIRE = { perFire: 0.05, max: 0.2 };
4472
+ var DEFAULT_FIRE = { perFire: 0.08, max: 0.45 };
4473
+ var DEFAULT_CRACKLES = { base: 2, perSqrtFire: 3, max: 14, volume: 0.5 };
4474
+ var DEFAULT_INDOOR_CONFINEMENT = 1.3;
4027
4475
  function createDestructionSound(engine, options = {}) {
4028
4476
  engine.register(DESTRUCTION_SOUND_BANK);
4029
4477
  engine.register(GAME_SFX_BANK);
4030
- const fire = { ...DEFAULT_FIRE, ...options.fire };
4478
+ engine.register(FOLEY_BANK);
4479
+ const fire2 = { ...DEFAULT_FIRE, ...options.fire };
4480
+ const crackles = options.crackles === false ? null : { ...DEFAULT_CRACKLES, ...options.crackles };
4481
+ const fireLoop = options.fireLoop ?? "fire_loop";
4482
+ const kinds = { ...BLAST_KIND_OF, ...options.blastKinds };
4483
+ const farDistance = options.blastFarDistance ?? BLAST_FAR_DISTANCE;
4484
+ const indoorFrom = options.indoorConfinement ?? DEFAULT_INDOOR_CONFINEMENT;
4031
4485
  const rules = { ...DESTRUCTION_SOUND_KEYS.art_scene, ...options.style === "game" ? DESTRUCTION_SOUND_KEYS.game : {}, ...options.keys };
4486
+ const blastByKind = options.style !== "game" && !(options.keys && "explode" in options.keys);
4487
+ const channel = engine.channel({ seed: options.seed ?? 1 });
4032
4488
  let loop;
4033
4489
  let burning = 0;
4490
+ const fires = [];
4491
+ let nextCrackle = 0;
4034
4492
  const setFire = () => {
4035
4493
  if (options.burnLoop === false) return;
4036
4494
  if (burning <= 0) {
@@ -4038,48 +4496,157 @@ function createDestructionSound(engine, options = {}) {
4038
4496
  loop = void 0;
4039
4497
  return;
4040
4498
  }
4041
- loop ?? (loop = engine.loop("burn_loop"));
4042
- loop.set({ volume: Math.min(fire.max, fire.perFire * burning) });
4499
+ loop ?? (loop = engine.loop(fireLoop));
4500
+ loop.set({ volume: Math.min(fire2.max, fire2.perFire * burning) });
4501
+ };
4502
+ const forget = (point) => {
4503
+ let best = -1;
4504
+ let bestD = Infinity;
4505
+ fires.forEach((f, i) => {
4506
+ const d = (f[0] - point[0]) ** 2 + (f[1] - point[1]) ** 2 + (f[2] - point[2]) ** 2;
4507
+ if (d < bestD) {
4508
+ bestD = d;
4509
+ best = i;
4510
+ }
4511
+ });
4512
+ if (best >= 0) fires.splice(best, 1);
4513
+ };
4514
+ const blastSound = (point, volume, detail) => {
4515
+ const named = detail?.effect === void 0 ? void 0 : kinds[detail.effect];
4516
+ const kind = named ?? ((detail?.radius ?? 0) >= BIG_BLAST_RADIUS || volume >= 0.9 ? "big" : "grenade");
4517
+ const sound = BLAST_SOUND_OF[kind];
4518
+ const distance = engine.plan(sound, { position: point, seed: 1 })?.distance ?? 0;
4519
+ return kind !== "molotov" && distance > farDistance ? "explosion_far" : sound;
4043
4520
  };
4044
4521
  return {
4045
- onSound(key, point, volume) {
4522
+ onSound(key, point, volume, detail) {
4046
4523
  if (key === "burn_loop") {
4047
4524
  burning += 1;
4525
+ fires.push(point);
4048
4526
  setFire();
4049
4527
  return;
4050
4528
  }
4051
4529
  if (key === "burn_end") {
4052
4530
  if (burning > 0) burning -= 1;
4531
+ forget(point);
4053
4532
  setFire();
4054
4533
  return;
4055
4534
  }
4056
- const rule = rules[key];
4057
- if (!rule) return;
4058
- const sound = rule.above && volume >= rule.above.volume ? rule.above.sound : rule.sound;
4059
- engine.play(sound, { position: point, volume: volume * (rule.volumeScale ?? 1), ...rule.params ? { params: rule.params } : {} });
4535
+ if (key === "explode" && blastByKind) {
4536
+ const sound2 = blastSound(point, volume, detail);
4537
+ engine.play(sound2, { position: point, volume });
4538
+ if (sound2 !== "explosion_far" && sound2 !== "molotov_break_ignite" && (detail?.confinement ?? 1) >= indoorFrom) engine.play("explosion_indoor_tail", { position: point, volume });
4539
+ return;
4540
+ }
4541
+ let rule;
4542
+ for (const k of detail?.keys ?? []) {
4543
+ if (k in rules) {
4544
+ rule = rules[k];
4545
+ break;
4546
+ }
4547
+ }
4548
+ if (rule === void 0) rule = key in rules ? rules[key] : rules[key.replace(/^shot_/, "impact_")];
4549
+ if (rule === null) return;
4550
+ if (!rule) {
4551
+ if (engine.registry.sound(key)) engine.play(key, { position: point, volume });
4552
+ return;
4553
+ }
4554
+ const wanted = rule.above && volume >= rule.above.volume ? rule.above.sound : rule.sound;
4555
+ const sound = rule.fallback && !engine.registry.sound(wanted) ? rule.fallback : wanted;
4556
+ const params = detail?.seconds === void 0 ? rule.params : { ...rule.params, seconds: detail.seconds };
4557
+ engine.play(sound, { position: point, volume: volume * (rule.volumeScale ?? 1), ...params ? { params } : {} });
4558
+ },
4559
+ update(dt) {
4560
+ if (!crackles || fires.length === 0 || options.burnLoop === false) return;
4561
+ nextCrackle -= dt;
4562
+ for (let i = 0; i < 4 && nextCrackle <= 0; i++) {
4563
+ const rate = Math.min(crackles.max, crackles.base + crackles.perSqrtFire * Math.sqrt(fires.length));
4564
+ nextCrackle += (0.4 + channel.random() * 1.2) / rate;
4565
+ const at = fires[Math.floor(channel.random() * fires.length)];
4566
+ engine.play(channel.random() < 0.18 ? "fire_pop" : "fire_crackle", { position: at, volume: crackles.volume });
4567
+ }
4568
+ if (nextCrackle < 0) nextCrackle = 0;
4569
+ },
4570
+ blastSound,
4571
+ rule(key, detail) {
4572
+ for (const k of detail?.keys ?? []) if (k in rules) return rules[k] ?? void 0;
4573
+ return rules[key] ?? void 0;
4060
4574
  },
4061
- rule: (key) => rules[key] ?? void 0,
4062
4575
  burning: () => burning,
4063
4576
  dispose() {
4064
4577
  loop?.stop();
4065
4578
  loop = void 0;
4066
4579
  burning = 0;
4580
+ fires.length = 0;
4067
4581
  }
4068
4582
  };
4069
4583
  }
4584
+
4585
+ // src/audio/samples.ts
4586
+ import { sampleBankFromManifest } from "@adatechnology/game-audio";
4587
+ var blast2 = (gain) => ({ bus: "world", gain, falloff: BLAST_FALLOFF, cutoffK: 6e-3, priority: 3 });
4588
+ var fire = (gain, group) => ({ bus: "world", gain, falloff: FIRE_FALLOFF, cutoffK: 0.01, ...group ? { group } : {} });
4589
+ var BLAST_FIRE_SAMPLES = {
4590
+ explosion_grenade: blast2(1),
4591
+ explosion_big: blast2(1),
4592
+ explosion_car: blast2(1),
4593
+ explosion_flashbang: blast2(1),
4594
+ molotov_break_ignite: blast2(0.8),
4595
+ // the far takes are already muffled: no extra distance lowpass
4596
+ explosion_far: { bus: "world", gain: 1, falloff: BLAST_FAR_FALLOFF, cutoffK: 0, priority: 2 },
4597
+ fire_ignite: fire(0.8, "fire_ignite"),
4598
+ fire_crackle: fire(0.8, "fire_crackle"),
4599
+ fire_pop: fire(0.6, "fire_crackle"),
4600
+ fire_extinguish: fire(0.7),
4601
+ gas_leak: fire(0.5),
4602
+ // the roar: recorded fires chained with a crossfade (the level comes from `createDestructionSound`, by how much burns)
4603
+ fire_loop: { bus: "world", gain: 4, loop: { crossfade: 1 } }
4604
+ };
4605
+ function blastFireSampleBank(manifest, options = {}) {
4606
+ const sounds = options.sounds ?? BLAST_FIRE_SAMPLES;
4607
+ const bank = sampleBankFromManifest(manifest, { id: options.id ?? "destruction-samples", ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl }, sounds });
4608
+ const samples = (bank.samples ?? []).map((sample) => {
4609
+ const loop = sounds[sample.id]?.loop;
4610
+ return loop === void 0 ? sample : { ...sample, loop };
4611
+ });
4612
+ return { ...bank, samples };
4613
+ }
4070
4614
  export {
4615
+ BIG_BLAST_RADIUS,
4616
+ BLAST_FALLOFF,
4617
+ BLAST_FAR_DISTANCE,
4618
+ BLAST_FAR_FALLOFF,
4619
+ BLAST_FIRE_SAMPLES,
4620
+ BLAST_KINDS,
4621
+ BLAST_KIND_OF,
4622
+ BLAST_SOUNDS,
4623
+ BLAST_SOUND_OF,
4071
4624
  BURN_LOOP,
4625
+ DEFAULT_CRACKLES,
4072
4626
  DEFAULT_FIRE,
4627
+ DEFAULT_INDOOR_CONFINEMENT,
4073
4628
  DESTRUCTION_SOUND_BANK,
4074
4629
  DESTRUCTION_SOUND_KEYS,
4075
4630
  DRAG_SOUNDS,
4076
4631
  EXPLOSION_BOOM,
4632
+ FIRE_FALLOFF,
4633
+ FIRE_LOOP,
4634
+ FIRE_SOUNDS,
4635
+ IMPACT_FAMILY_TAGS,
4636
+ IMPACT_SOUNDS,
4637
+ IMPACT_SOUND_KEYS,
4077
4638
  SCENE_SOUNDS,
4639
+ blastFireSampleBank,
4640
+ breakSoundKeys,
4078
4641
  createDestructionRenderer,
4079
4642
  createDestructionSound,
4080
4643
  dragSoundKey,
4081
4644
  heatColor,
4645
+ impactFamily,
4082
4646
  impactKey,
4647
+ impactSoundKeys,
4648
+ ricochetSoundKeys,
4649
+ shotKey,
4083
4650
  specFromObject3D
4084
4651
  };
4085
4652
  //# sourceMappingURL=index.js.map