@adatechnology/game-destruction-three 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -32,6 +32,10 @@ interface SoundDetail {
32
32
  readonly confinement?: number;
33
33
  /** `primed`: seconds until the vessel bursts (the gas leak lasts that long). */
34
34
  readonly seconds?: number;
35
+ /** `burn_loop` / `burn_end`: the material burning (`wood_hard`, `rubber`...), which picks the fire's sound. */
36
+ readonly material?: string;
37
+ /** `burn_loop` / `burn_end`: the fuel itself, when it is not a material (a gas jet, a pool of fuel). Wins over `material`. */
38
+ readonly fuel?: 'wood' | 'fabric' | 'plastic' | 'gas' | 'liquid';
35
39
  }
36
40
  interface RendererOptions {
37
41
  /** Scene that receives fragments, loose pieces, decals and particles. */
@@ -352,6 +356,12 @@ declare const BLAST_SOUNDS: readonly SoundRecipe[];
352
356
  declare const FIRE_SOUNDS: readonly SoundRecipe[];
353
357
  /** The roar of the fires: low, soft noise; `createDestructionSound` raises it with every part that burns and sprinkles crackles on it. */
354
358
  declare const FIRE_LOOP: LoopRecipe;
359
+ /**
360
+ * What is burning, for the sound: each fuel has its own roar (a gas jet, a fuel pool, wood, plastic) and its own share of crackles. The rules are the
361
+ * same for all (the level grows with how much burns, fades with distance, never clips); only the timbre comes from the fuel.
362
+ */
363
+ declare const FIRE_FUELS: readonly ["wood", "fabric", "plastic", "gas", "liquid"];
364
+ type FireFuel = (typeof FIRE_FUELS)[number];
355
365
  /** The sound of a blast family. */
356
366
  declare const BLAST_SOUND_OF: Readonly<Record<BlastKind, string>>;
357
367
 
@@ -394,8 +404,14 @@ type DestructionSoundOptions = {
394
404
  };
395
405
  /** Burn loop on or off (default on). */
396
406
  readonly burnLoop?: boolean;
397
- /** The fire loop played while something burns (default `fire_loop`; `burn_loop` is the art scene's bright one). */
407
+ /** The loop of the wood fire (default `fire_loop`; `burn_loop` is the art scene's bright one). */
398
408
  readonly fireLoop?: string;
409
+ /** Adds to or changes `FIRE_FUEL_OF`: which fuel a burning material sounds like. */
410
+ readonly fuels?: Readonly<Record<string, FireFuel>>;
411
+ /** Adds to or changes `FIRE_LOOP_OF`: the loop of each fuel. */
412
+ readonly fireLoops?: Partial<Readonly<Record<FireFuel, string>>>;
413
+ /** The fuel of a fire whose material is not in the table (default `wood`). */
414
+ readonly defaultFuel?: FireFuel;
399
415
  /** Crackles per second over the fire loop: `base + perSqrtFire × √(parts burning)`, at most `max` (default 2, 3, 14). `false` turns them off. */
400
416
  readonly crackles?: false | {
401
417
  readonly base?: number;
@@ -423,6 +439,10 @@ type DestructionSound = {
423
439
  rule(key: string, detail?: SoundDetail): SoundKeyRule | undefined;
424
440
  /** Parts burning now. */
425
441
  readonly burning: () => number;
442
+ /** Parts burning now, by fuel. */
443
+ readonly burningBy: () => Readonly<Partial<Record<FireFuel, number>>>;
444
+ /** Puts every fire sound out (the scene was cleared); the engine and the bank stay. */
445
+ reset(): void;
426
446
  dispose(): void;
427
447
  };
428
448
  declare const DEFAULT_FIRE: {
@@ -13728,7 +13728,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13728
13728
  this.startFire(ev);
13729
13729
  this.activate(ev.object, ev.part);
13730
13730
  this.sound("ignite", ev.point, 0.7);
13731
- this.sound("burn_loop", ev.point, 0.5);
13731
+ this.sound("burn_loop", ev.point, 0.5, this.burning(ev.object, ev.part));
13732
13732
  break;
13733
13733
  case "primed":
13734
13734
  if (ev.trigger !== "explosion") {
@@ -13736,7 +13736,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13736
13736
  this.primedBurning.add(`${ev.object}|${ev.part}`);
13737
13737
  this.activate(ev.object, ev.part);
13738
13738
  this.sound("primed", ev.point, 0.7, { seconds: ev.delay });
13739
- this.sound("burn_loop", ev.point, 0.5);
13739
+ this.sound("burn_loop", ev.point, 0.5, { fuel: "gas" });
13740
13740
  }
13741
13741
  break;
13742
13742
  case "spall":
@@ -13762,15 +13762,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13762
13762
  this.stopFire(`${ev.object}|${ev.part}`);
13763
13763
  this.vfx.burst("steam", f.point, [0, 1, 0], 14, 1.2);
13764
13764
  }
13765
- if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5);
13765
+ if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5, primed ? { fuel: "gas" } : this.burning(ev.object, ev.part));
13766
13766
  this.activate(ev.object, ev.part);
13767
13767
  this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
13768
13768
  break;
13769
13769
  }
13770
13770
  case "burnout": {
13771
13771
  this.stopFire(`${ev.object}|${ev.part}`);
13772
- this.primedBurning.delete(`${ev.object}|${ev.part}`);
13773
- this.sound("burn_end", ev.point, 0.5);
13772
+ const primed = this.primedBurning.delete(`${ev.object}|${ev.part}`);
13773
+ this.sound("burn_end", ev.point, 0.5, primed ? { fuel: "gas" } : this.burning(ev.object, ev.part));
13774
13774
  this.vfx.burst("ash", ev.point, [0, 1, 0], 16, 1);
13775
13775
  this.vfx.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
13776
13776
  const pv = this.partVis(ev.object, ev.part);
@@ -13838,7 +13838,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13838
13838
  break;
13839
13839
  }
13840
13840
  case "explode":
13841
- this.primedBurning.delete(`${ev.object}|${ev.part}`);
13841
+ if (this.primedBurning.delete(`${ev.object}|${ev.part}`)) this.sound("burn_end", ev.point, 0.5, { fuel: "gas" });
13842
13842
  this.onExplode(ev);
13843
13843
  break;
13844
13844
  case "destroyed":
@@ -13853,6 +13853,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13853
13853
  if (detail) this.o.onSound?.(key, point, volume, detail);
13854
13854
  else this.o.onSound?.(key, point, volume);
13855
13855
  }
13856
+ /** What a burning part is made of, for the fire's sound. */
13857
+ burning(objectId, partId) {
13858
+ const material = this.partVis(objectId, partId)?.material || this.world.part(objectId, partId)?.material;
13859
+ return material ? { material } : void 0;
13860
+ }
13856
13861
  partVis(objectId, partId) {
13857
13862
  return this.bounds.get(objectId)?.parts.get(partId) ?? null;
13858
13863
  }
@@ -16087,6 +16092,47 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16087
16092
  volume: 0,
16088
16093
  sources: ["sdk: the roar under the crackles of a fire"]
16089
16094
  };
16095
+ var FIRE_FUEL_OF = {
16096
+ wood_soft: "wood",
16097
+ wood_hard: "wood",
16098
+ plywood: "wood",
16099
+ mdf: "wood",
16100
+ green_wood: "wood",
16101
+ foliage: "wood",
16102
+ cactus: "wood",
16103
+ paper: "fabric",
16104
+ fabric: "fabric",
16105
+ leather: "fabric",
16106
+ foam: "plastic",
16107
+ plastic: "plastic",
16108
+ rubber: "plastic",
16109
+ wax: "plastic"
16110
+ };
16111
+ var FIRE_LOOP_OF = {
16112
+ wood: "fire_loop",
16113
+ fabric: "fire_loop",
16114
+ plastic: "fire_loop_plastic",
16115
+ gas: "fire_loop_gas",
16116
+ liquid: "fire_loop_liquid"
16117
+ };
16118
+ var FIRE_CRACKLE_OF = { wood: 1, fabric: 0.5, plastic: 0.35, gas: 0, liquid: 0 };
16119
+ var roar = (id, bank, source) => ({
16120
+ id,
16121
+ bus: "world",
16122
+ order: "chain_loop",
16123
+ source: { kind: "noise", loop: true },
16124
+ filters: { mode: "serial", bank },
16125
+ volume: 0,
16126
+ sources: [source]
16127
+ });
16128
+ var FIRE_FUEL_LOOPS = [
16129
+ // a gas jet: a steady, broad roar, brighter than wood and without crackles
16130
+ roar("fire_loop_gas", [{ type: "highpass", frequency: { set: 220 }, q: 0.7 }, { type: "lowpass", frequency: { set: 2400 }, q: 0.6 }], "sdk: a gas jet burning (a vessel venting into the flame)"),
16131
+ // a fuel pool: a soft, low, rolling roar
16132
+ roar("fire_loop_liquid", [{ type: "highpass", frequency: { set: 90 }, q: 0.7 }, { type: "lowpass", frequency: { set: 650 }, q: 0.9 }], "sdk: a pool of fuel burning"),
16133
+ // plastic and rubber: a thin sizzle over a low burn
16134
+ roar("fire_loop_plastic", [{ type: "bandpass", frequency: { set: 1800 }, q: 0.6 }], "sdk: plastic and rubber melting in the fire")
16135
+ ];
16090
16136
  var BLAST_SOUND_OF = {
16091
16137
  grenade: "explosion_grenade",
16092
16138
  big: "explosion_big",
@@ -16233,7 +16279,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16233
16279
  impact2("plastic_crack", [sceneNoise({ type: "bandpass", f: 1800, q: 2, d: 0.06, vol: 0.75 }), sceneTone({ f: [450, 550], f2: 250, d: 0.05, vol: 0.22 })]),
16234
16280
  impact2("mdf_crumble", [sceneNoise({ type: "lowpass", f: 700, d: 0.25, vol: 0.75, wet: 0.2 }), ...sceneMany(4, () => sceneNoise({ type: "lowpass", f: 500, d: 0.05, vol: 0.32, delay: [0.05, 0.3] }))])
16235
16281
  ];
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] };
16282
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, ...IMPACT_SOUNDS, EXPLOSION_BOOM, ...BLAST_SOUNDS, ...FIRE_SOUNDS], loops: [BURN_LOOP, FIRE_LOOP, ...FIRE_FUEL_LOOPS] };
16237
16283
 
16238
16284
  // src/audio/sound.ts
16239
16285
  var IMPACT_SOUND_KEYS = {
@@ -16384,31 +16430,52 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16384
16430
  const rules = { ...DESTRUCTION_SOUND_KEYS.art_scene, ...options.style === "game" ? DESTRUCTION_SOUND_KEYS.game : {}, ...options.keys };
16385
16431
  const blastByKind = options.style !== "game" && !(options.keys && "explode" in options.keys);
16386
16432
  const channel = engine.channel({ seed: options.seed ?? 1 });
16387
- let loop;
16388
- let burning = 0;
16433
+ const fuelOf = { ...FIRE_FUEL_OF, ...options.fuels };
16434
+ const loopOf = { ...FIRE_LOOP_OF, ...options.fireLoop ? { wood: fireLoop, fabric: fireLoop } : {}, ...options.fireLoops };
16435
+ const defaultFuel = options.defaultFuel ?? "wood";
16436
+ const loops = /* @__PURE__ */ new Map();
16389
16437
  const fires = [];
16390
16438
  let nextCrackle = 0;
16439
+ const fuelFor = (detail) => detail?.fuel ?? (detail?.material !== void 0 ? fuelOf[detail.material] : void 0) ?? defaultFuel;
16391
16440
  const setFire = () => {
16392
16441
  if (options.burnLoop === false) return;
16393
- if (burning <= 0) {
16394
- loop?.stop();
16395
- loop = void 0;
16396
- return;
16442
+ const count = /* @__PURE__ */ new Map();
16443
+ for (const f of fires) {
16444
+ const id = loopOf[f.fuel];
16445
+ count.set(id, (count.get(id) ?? 0) + 1);
16446
+ }
16447
+ for (const [id, handle] of loops) {
16448
+ if (count.has(id)) continue;
16449
+ handle.stop();
16450
+ loops.delete(id);
16451
+ }
16452
+ for (const [id, n] of count) {
16453
+ let handle = loops.get(id);
16454
+ if (!handle) {
16455
+ handle = engine.loop(id);
16456
+ loops.set(id, handle);
16457
+ }
16458
+ handle.set({ volume: Math.min(fire2.max, fire2.perFire * n) });
16397
16459
  }
16398
- loop ?? (loop = engine.loop(fireLoop));
16399
- loop.set({ volume: Math.min(fire2.max, fire2.perFire * burning) });
16400
16460
  };
16401
- const forget = (point) => {
16461
+ const forget = (point, fuel) => {
16402
16462
  let best = -1;
16403
16463
  let bestD = Infinity;
16404
16464
  fires.forEach((f, i) => {
16405
- const d = (f[0] - point[0]) ** 2 + (f[1] - point[1]) ** 2 + (f[2] - point[2]) ** 2;
16465
+ if (fuel !== void 0 && f.fuel !== fuel) return;
16466
+ const d = (f.point[0] - point[0]) ** 2 + (f.point[1] - point[1]) ** 2 + (f.point[2] - point[2]) ** 2;
16406
16467
  if (d < bestD) {
16407
16468
  bestD = d;
16408
16469
  best = i;
16409
16470
  }
16410
16471
  });
16411
- if (best >= 0) fires.splice(best, 1);
16472
+ if (best < 0 && fuel !== void 0) forget(point, void 0);
16473
+ else if (best >= 0) fires.splice(best, 1);
16474
+ };
16475
+ const reset = () => {
16476
+ for (const handle of loops.values()) handle.stop();
16477
+ loops.clear();
16478
+ fires.length = 0;
16412
16479
  };
16413
16480
  const blastSound = (point, volume, detail) => {
16414
16481
  const named = detail?.effect === void 0 ? void 0 : kinds[detail.effect];
@@ -16420,14 +16487,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16420
16487
  return {
16421
16488
  onSound(key, point, volume, detail) {
16422
16489
  if (key === "burn_loop") {
16423
- burning += 1;
16424
- fires.push(point);
16490
+ fires.push({ point, fuel: fuelFor(detail) });
16425
16491
  setFire();
16426
16492
  return;
16427
16493
  }
16428
16494
  if (key === "burn_end") {
16429
- if (burning > 0) burning -= 1;
16430
- forget(point);
16495
+ forget(point, detail?.fuel ?? (detail?.material !== void 0 ? fuelOf[detail.material] : void 0));
16431
16496
  setFire();
16432
16497
  return;
16433
16498
  }
@@ -16457,12 +16522,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16457
16522
  },
16458
16523
  update(dt) {
16459
16524
  if (!crackles || fires.length === 0 || options.burnLoop === false) return;
16525
+ const weight = fires.reduce((sum, f) => sum + FIRE_CRACKLE_OF[f.fuel], 0);
16526
+ if (weight <= 0) return;
16460
16527
  nextCrackle -= dt;
16461
16528
  for (let i = 0; i < 4 && nextCrackle <= 0; i++) {
16462
- const rate = Math.min(crackles.max, crackles.base + crackles.perSqrtFire * Math.sqrt(fires.length));
16529
+ const rate = Math.min(crackles.max, crackles.base * Math.min(1, weight) + crackles.perSqrtFire * Math.sqrt(weight));
16463
16530
  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 });
16531
+ let pick3 = channel.random() * weight;
16532
+ const at3 = fires.find((f) => (pick3 -= FIRE_CRACKLE_OF[f.fuel]) <= 0) ?? fires[fires.length - 1];
16533
+ engine.play(channel.random() < 0.18 ? "fire_pop" : "fire_crackle", { position: at3.point, volume: crackles.volume });
16466
16534
  }
16467
16535
  if (nextCrackle < 0) nextCrackle = 0;
16468
16536
  },
@@ -16471,13 +16539,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16471
16539
  for (const k of detail?.keys ?? []) if (k in rules) return rules[k] ?? void 0;
16472
16540
  return rules[key] ?? void 0;
16473
16541
  },
16474
- burning: () => burning,
16475
- dispose() {
16476
- loop?.stop();
16477
- loop = void 0;
16478
- burning = 0;
16479
- fires.length = 0;
16480
- }
16542
+ burning: () => fires.length,
16543
+ burningBy: () => {
16544
+ const by = {};
16545
+ for (const f of fires) by[f.fuel] = (by[f.fuel] ?? 0) + 1;
16546
+ return by;
16547
+ },
16548
+ reset,
16549
+ dispose: reset
16481
16550
  };
16482
16551
  }
16483
16552
 
@@ -16498,7 +16567,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16498
16567
  fire_extinguish: fire(0.7),
16499
16568
  gas_leak: fire(0.5),
16500
16569
  // 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 } }
16570
+ fire_loop: { bus: "world", gain: 4, loop: { crossfade: 1 } },
16571
+ // each fuel has its own roar: a gas jet (burners and torches), a pool of fuel, plastic and rubber sizzling under the flames
16572
+ fire_loop_gas: { bus: "world", gain: 6, loop: { crossfade: 1 } },
16573
+ fire_loop_liquid: { bus: "world", gain: 3.5, loop: { crossfade: 1 } },
16574
+ fire_loop_plastic: { bus: "world", gain: 1, loop: { crossfade: 0.6 } }
16502
16575
  };
16503
16576
  function blastFireSampleBank(manifest, options = {}) {
16504
16577
  const sounds = options.sounds ?? BLAST_FIRE_SAMPLES;