@adatechnology/game-destruction-three 0.14.0 → 0.15.1

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
@@ -2480,7 +2480,6 @@ var Visuals = class {
2480
2480
  var rendererCount = 0;
2481
2481
  var COLLAPSE_CLOUD_KG = 1e3;
2482
2482
  var CHIP_DECAL_EDGE = 0.05;
2483
- var OPAQUE_CRACK = 0.1;
2484
2483
  var WOOD_RIM = 1.5;
2485
2484
  var IMPACT_TAGS = ["metal", "glass", "wood"];
2486
2485
  var GLOW_FROM_C = 430;
@@ -3187,7 +3186,7 @@ var Renderer = class {
3187
3186
  this.startFire(ev);
3188
3187
  this.activate(ev.object, ev.part);
3189
3188
  this.sound("ignite", ev.point, 0.7);
3190
- this.sound("burn_loop", ev.point, 0.5);
3189
+ this.sound("burn_loop", ev.point, 0.5, this.burning(ev.object, ev.part));
3191
3190
  break;
3192
3191
  case "primed":
3193
3192
  if (ev.trigger !== "explosion") {
@@ -3195,7 +3194,7 @@ var Renderer = class {
3195
3194
  this.primedBurning.add(`${ev.object}|${ev.part}`);
3196
3195
  this.activate(ev.object, ev.part);
3197
3196
  this.sound("primed", ev.point, 0.7, { seconds: ev.delay });
3198
- this.sound("burn_loop", ev.point, 0.5);
3197
+ this.sound("burn_loop", ev.point, 0.5, { fuel: "gas" });
3199
3198
  }
3200
3199
  break;
3201
3200
  case "spall":
@@ -3221,15 +3220,15 @@ var Renderer = class {
3221
3220
  this.stopFire(`${ev.object}|${ev.part}`);
3222
3221
  this.vfx.burst("steam", f.point, [0, 1, 0], 14, 1.2);
3223
3222
  }
3224
- if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5);
3223
+ if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5, primed ? { fuel: "gas" } : this.burning(ev.object, ev.part));
3225
3224
  this.activate(ev.object, ev.part);
3226
3225
  this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
3227
3226
  break;
3228
3227
  }
3229
3228
  case "burnout": {
3230
3229
  this.stopFire(`${ev.object}|${ev.part}`);
3231
- this.primedBurning.delete(`${ev.object}|${ev.part}`);
3232
- this.sound("burn_end", ev.point, 0.5);
3230
+ const primed = this.primedBurning.delete(`${ev.object}|${ev.part}`);
3231
+ this.sound("burn_end", ev.point, 0.5, primed ? { fuel: "gas" } : this.burning(ev.object, ev.part));
3233
3232
  this.vfx.burst("ash", ev.point, [0, 1, 0], 16, 1);
3234
3233
  this.vfx.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
3235
3234
  const pv = this.partVis(ev.object, ev.part);
@@ -3297,7 +3296,7 @@ var Renderer = class {
3297
3296
  break;
3298
3297
  }
3299
3298
  case "explode":
3300
- this.primedBurning.delete(`${ev.object}|${ev.part}`);
3299
+ if (this.primedBurning.delete(`${ev.object}|${ev.part}`)) this.sound("burn_end", ev.point, 0.5, { fuel: "gas" });
3301
3300
  this.onExplode(ev);
3302
3301
  break;
3303
3302
  case "destroyed":
@@ -3312,6 +3311,11 @@ var Renderer = class {
3312
3311
  if (detail) this.o.onSound?.(key, point, volume, detail);
3313
3312
  else this.o.onSound?.(key, point, volume);
3314
3313
  }
3314
+ /** What a burning part is made of, for the fire's sound. */
3315
+ burning(objectId, partId) {
3316
+ const material = this.partVis(objectId, partId)?.material || this.world.part(objectId, partId)?.material;
3317
+ return material ? { material } : void 0;
3318
+ }
3315
3319
  partVis(objectId, partId) {
3316
3320
  return this.bounds.get(objectId)?.parts.get(partId) ?? null;
3317
3321
  }
@@ -3411,14 +3415,11 @@ var Renderer = class {
3411
3415
  const pv = b?.parts.get(ev.part);
3412
3416
  if (!b || !pv) return;
3413
3417
  const def = this.world.materials.get(pv.material);
3414
- const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
3415
- const mv = this.nearestMesh(pv, wp);
3416
- if (mv) {
3417
- const glass2 = !!def?.visual?.transparent;
3418
- const own = this.partColor(pv, def) ?? fallbackColor(pv.material, def);
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);
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]));
3418
+ if (def?.visual?.transparent) {
3419
+ const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
3420
+ const mv = this.nearestMesh(pv, wp);
3421
+ const seed = hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
3422
+ if (mv) this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, ev.crack.length, 15267071, seed);
3422
3423
  }
3423
3424
  this.impactSound(pv.material, ev.worldPoint, 0.5, ev.blow);
3424
3425
  }
@@ -4187,6 +4188,47 @@ var FIRE_LOOP = {
4187
4188
  volume: 0,
4188
4189
  sources: ["sdk: the roar under the crackles of a fire"]
4189
4190
  };
4191
+ var FIRE_FUEL_OF = {
4192
+ wood_soft: "wood",
4193
+ wood_hard: "wood",
4194
+ plywood: "wood",
4195
+ mdf: "wood",
4196
+ green_wood: "wood",
4197
+ foliage: "wood",
4198
+ cactus: "wood",
4199
+ paper: "fabric",
4200
+ fabric: "fabric",
4201
+ leather: "fabric",
4202
+ foam: "plastic",
4203
+ plastic: "plastic",
4204
+ rubber: "plastic",
4205
+ wax: "plastic"
4206
+ };
4207
+ var FIRE_LOOP_OF = {
4208
+ wood: "fire_loop",
4209
+ fabric: "fire_loop",
4210
+ plastic: "fire_loop_plastic",
4211
+ gas: "fire_loop_gas",
4212
+ liquid: "fire_loop_liquid"
4213
+ };
4214
+ var FIRE_CRACKLE_OF = { wood: 1, fabric: 0.5, plastic: 0.35, gas: 0, liquid: 0 };
4215
+ var roar = (id, bank, source) => ({
4216
+ id,
4217
+ bus: "world",
4218
+ order: "chain_loop",
4219
+ source: { kind: "noise", loop: true },
4220
+ filters: { mode: "serial", bank },
4221
+ volume: 0,
4222
+ sources: [source]
4223
+ });
4224
+ var FIRE_FUEL_LOOPS = [
4225
+ // a gas jet: a steady, broad roar, brighter than wood and without crackles
4226
+ 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)"),
4227
+ // a fuel pool: a soft, low, rolling roar
4228
+ 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"),
4229
+ // plastic and rubber: a thin sizzle over a low burn
4230
+ roar("fire_loop_plastic", [{ type: "bandpass", frequency: { set: 1800 }, q: 0.6 }], "sdk: plastic and rubber melting in the fire")
4231
+ ];
4190
4232
  var BLAST_SOUND_OF = {
4191
4233
  grenade: "explosion_grenade",
4192
4234
  big: "explosion_big",
@@ -4333,7 +4375,7 @@ var IMPACT_SOUNDS = [
4333
4375
  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
4376
  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] }))])
4335
4377
  ];
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] };
4378
+ 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] };
4337
4379
 
4338
4380
  // src/audio/sound.ts
4339
4381
  import { FOLEY_BANK, GAME_SFX_BANK } from "@adatechnology/game-audio";
@@ -4485,31 +4527,52 @@ function createDestructionSound(engine, options = {}) {
4485
4527
  const rules = { ...DESTRUCTION_SOUND_KEYS.art_scene, ...options.style === "game" ? DESTRUCTION_SOUND_KEYS.game : {}, ...options.keys };
4486
4528
  const blastByKind = options.style !== "game" && !(options.keys && "explode" in options.keys);
4487
4529
  const channel = engine.channel({ seed: options.seed ?? 1 });
4488
- let loop;
4489
- let burning = 0;
4530
+ const fuelOf = { ...FIRE_FUEL_OF, ...options.fuels };
4531
+ const loopOf = { ...FIRE_LOOP_OF, ...options.fireLoop ? { wood: fireLoop, fabric: fireLoop } : {}, ...options.fireLoops };
4532
+ const defaultFuel = options.defaultFuel ?? "wood";
4533
+ const loops = /* @__PURE__ */ new Map();
4490
4534
  const fires = [];
4491
4535
  let nextCrackle = 0;
4536
+ const fuelFor = (detail) => detail?.fuel ?? (detail?.material !== void 0 ? fuelOf[detail.material] : void 0) ?? defaultFuel;
4492
4537
  const setFire = () => {
4493
4538
  if (options.burnLoop === false) return;
4494
- if (burning <= 0) {
4495
- loop?.stop();
4496
- loop = void 0;
4497
- return;
4539
+ const count = /* @__PURE__ */ new Map();
4540
+ for (const f of fires) {
4541
+ const id = loopOf[f.fuel];
4542
+ count.set(id, (count.get(id) ?? 0) + 1);
4543
+ }
4544
+ for (const [id, handle] of loops) {
4545
+ if (count.has(id)) continue;
4546
+ handle.stop();
4547
+ loops.delete(id);
4548
+ }
4549
+ for (const [id, n] of count) {
4550
+ let handle = loops.get(id);
4551
+ if (!handle) {
4552
+ handle = engine.loop(id);
4553
+ loops.set(id, handle);
4554
+ }
4555
+ handle.set({ volume: Math.min(fire2.max, fire2.perFire * n) });
4498
4556
  }
4499
- loop ?? (loop = engine.loop(fireLoop));
4500
- loop.set({ volume: Math.min(fire2.max, fire2.perFire * burning) });
4501
4557
  };
4502
- const forget = (point) => {
4558
+ const forget = (point, fuel) => {
4503
4559
  let best = -1;
4504
4560
  let bestD = Infinity;
4505
4561
  fires.forEach((f, i) => {
4506
- const d = (f[0] - point[0]) ** 2 + (f[1] - point[1]) ** 2 + (f[2] - point[2]) ** 2;
4562
+ if (fuel !== void 0 && f.fuel !== fuel) return;
4563
+ const d = (f.point[0] - point[0]) ** 2 + (f.point[1] - point[1]) ** 2 + (f.point[2] - point[2]) ** 2;
4507
4564
  if (d < bestD) {
4508
4565
  bestD = d;
4509
4566
  best = i;
4510
4567
  }
4511
4568
  });
4512
- if (best >= 0) fires.splice(best, 1);
4569
+ if (best < 0 && fuel !== void 0) forget(point, void 0);
4570
+ else if (best >= 0) fires.splice(best, 1);
4571
+ };
4572
+ const reset = () => {
4573
+ for (const handle of loops.values()) handle.stop();
4574
+ loops.clear();
4575
+ fires.length = 0;
4513
4576
  };
4514
4577
  const blastSound = (point, volume, detail) => {
4515
4578
  const named = detail?.effect === void 0 ? void 0 : kinds[detail.effect];
@@ -4521,14 +4584,12 @@ function createDestructionSound(engine, options = {}) {
4521
4584
  return {
4522
4585
  onSound(key, point, volume, detail) {
4523
4586
  if (key === "burn_loop") {
4524
- burning += 1;
4525
- fires.push(point);
4587
+ fires.push({ point, fuel: fuelFor(detail) });
4526
4588
  setFire();
4527
4589
  return;
4528
4590
  }
4529
4591
  if (key === "burn_end") {
4530
- if (burning > 0) burning -= 1;
4531
- forget(point);
4592
+ forget(point, detail?.fuel ?? (detail?.material !== void 0 ? fuelOf[detail.material] : void 0));
4532
4593
  setFire();
4533
4594
  return;
4534
4595
  }
@@ -4558,12 +4619,15 @@ function createDestructionSound(engine, options = {}) {
4558
4619
  },
4559
4620
  update(dt) {
4560
4621
  if (!crackles || fires.length === 0 || options.burnLoop === false) return;
4622
+ const weight = fires.reduce((sum, f) => sum + FIRE_CRACKLE_OF[f.fuel], 0);
4623
+ if (weight <= 0) return;
4561
4624
  nextCrackle -= dt;
4562
4625
  for (let i = 0; i < 4 && nextCrackle <= 0; i++) {
4563
- const rate = Math.min(crackles.max, crackles.base + crackles.perSqrtFire * Math.sqrt(fires.length));
4626
+ const rate = Math.min(crackles.max, crackles.base * Math.min(1, weight) + crackles.perSqrtFire * Math.sqrt(weight));
4564
4627
  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 });
4628
+ let pick = channel.random() * weight;
4629
+ const at = fires.find((f) => (pick -= FIRE_CRACKLE_OF[f.fuel]) <= 0) ?? fires[fires.length - 1];
4630
+ engine.play(channel.random() < 0.18 ? "fire_pop" : "fire_crackle", { position: at.point, volume: crackles.volume });
4567
4631
  }
4568
4632
  if (nextCrackle < 0) nextCrackle = 0;
4569
4633
  },
@@ -4572,13 +4636,14 @@ function createDestructionSound(engine, options = {}) {
4572
4636
  for (const k of detail?.keys ?? []) if (k in rules) return rules[k] ?? void 0;
4573
4637
  return rules[key] ?? void 0;
4574
4638
  },
4575
- burning: () => burning,
4576
- dispose() {
4577
- loop?.stop();
4578
- loop = void 0;
4579
- burning = 0;
4580
- fires.length = 0;
4581
- }
4639
+ burning: () => fires.length,
4640
+ burningBy: () => {
4641
+ const by = {};
4642
+ for (const f of fires) by[f.fuel] = (by[f.fuel] ?? 0) + 1;
4643
+ return by;
4644
+ },
4645
+ reset,
4646
+ dispose: reset
4582
4647
  };
4583
4648
  }
4584
4649
 
@@ -4600,7 +4665,11 @@ var BLAST_FIRE_SAMPLES = {
4600
4665
  fire_extinguish: fire(0.7),
4601
4666
  gas_leak: fire(0.5),
4602
4667
  // 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 } }
4668
+ fire_loop: { bus: "world", gain: 4, loop: { crossfade: 1 } },
4669
+ // each fuel has its own roar: a gas jet (burners and torches), a pool of fuel, plastic and rubber sizzling under the flames
4670
+ fire_loop_gas: { bus: "world", gain: 6, loop: { crossfade: 1 } },
4671
+ fire_loop_liquid: { bus: "world", gain: 3.5, loop: { crossfade: 1 } },
4672
+ fire_loop_plastic: { bus: "world", gain: 1, loop: { crossfade: 0.6 } }
4604
4673
  };
4605
4674
  function blastFireSampleBank(manifest, options = {}) {
4606
4675
  const sounds = options.sounds ?? BLAST_FIRE_SAMPLES;