@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/README.md CHANGED
@@ -104,7 +104,7 @@ loose meshes. An invalid range (outside the geometry or empty) throws from `bind
104
104
  |---|---|---|
105
105
  | `dent` | vertices within `dent.radius` pushed in by `depth * (1 - (d/r)^2)^2` (skipped on `low` for meshes over 20k vertices); sparks on metal | `impact_*` |
106
106
  | `hole` | dark disc decal on the surface (and on the exit side of a through hole); a crack star on transparent materials; sparks on metal | `impact_*` |
107
- | `crack` | star of jagged lines, white on glass, dark otherwise | `impact_*` |
107
+ | `crack` | star of jagged white lines on glass (transparent materials) only; an opaque body draws no lines, its damage shows as chips, holes and dents | `impact_*` |
108
108
  | `ricochet` | sparks along the reflected direction | `ricochet` |
109
109
  | `debris` | a burst of the requested kind (`sparks`, `dust`, `shards`, `splinters`, `chips`, `fibers`, `leaves`, `drips`, `ash`, `smoke`, `steam`) | |
110
110
  | `fracture` | part hidden; fragments (boxes, thin irregular prisms for shards) with gravity, ground bounce, settling, sleep and shrink-away; debris burst by style | `shatter_glass`, `crumble`, `splinter`, `impact_*` |
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: {
@@ -13021,7 +13021,6 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13021
13021
  var rendererCount = 0;
13022
13022
  var COLLAPSE_CLOUD_KG = 1e3;
13023
13023
  var CHIP_DECAL_EDGE = 0.05;
13024
- var OPAQUE_CRACK = 0.1;
13025
13024
  var WOOD_RIM = 1.5;
13026
13025
  var IMPACT_TAGS = ["metal", "glass", "wood"];
13027
13026
  var GLOW_FROM_C = 430;
@@ -13728,7 +13727,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13728
13727
  this.startFire(ev);
13729
13728
  this.activate(ev.object, ev.part);
13730
13729
  this.sound("ignite", ev.point, 0.7);
13731
- this.sound("burn_loop", ev.point, 0.5);
13730
+ this.sound("burn_loop", ev.point, 0.5, this.burning(ev.object, ev.part));
13732
13731
  break;
13733
13732
  case "primed":
13734
13733
  if (ev.trigger !== "explosion") {
@@ -13736,7 +13735,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13736
13735
  this.primedBurning.add(`${ev.object}|${ev.part}`);
13737
13736
  this.activate(ev.object, ev.part);
13738
13737
  this.sound("primed", ev.point, 0.7, { seconds: ev.delay });
13739
- this.sound("burn_loop", ev.point, 0.5);
13738
+ this.sound("burn_loop", ev.point, 0.5, { fuel: "gas" });
13740
13739
  }
13741
13740
  break;
13742
13741
  case "spall":
@@ -13762,15 +13761,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13762
13761
  this.stopFire(`${ev.object}|${ev.part}`);
13763
13762
  this.vfx.burst("steam", f.point, [0, 1, 0], 14, 1.2);
13764
13763
  }
13765
- if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5);
13764
+ 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
13765
  this.activate(ev.object, ev.part);
13767
13766
  this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
13768
13767
  break;
13769
13768
  }
13770
13769
  case "burnout": {
13771
13770
  this.stopFire(`${ev.object}|${ev.part}`);
13772
- this.primedBurning.delete(`${ev.object}|${ev.part}`);
13773
- this.sound("burn_end", ev.point, 0.5);
13771
+ const primed = this.primedBurning.delete(`${ev.object}|${ev.part}`);
13772
+ this.sound("burn_end", ev.point, 0.5, primed ? { fuel: "gas" } : this.burning(ev.object, ev.part));
13774
13773
  this.vfx.burst("ash", ev.point, [0, 1, 0], 16, 1);
13775
13774
  this.vfx.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
13776
13775
  const pv = this.partVis(ev.object, ev.part);
@@ -13838,7 +13837,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13838
13837
  break;
13839
13838
  }
13840
13839
  case "explode":
13841
- this.primedBurning.delete(`${ev.object}|${ev.part}`);
13840
+ if (this.primedBurning.delete(`${ev.object}|${ev.part}`)) this.sound("burn_end", ev.point, 0.5, { fuel: "gas" });
13842
13841
  this.onExplode(ev);
13843
13842
  break;
13844
13843
  case "destroyed":
@@ -13853,6 +13852,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13853
13852
  if (detail) this.o.onSound?.(key, point, volume, detail);
13854
13853
  else this.o.onSound?.(key, point, volume);
13855
13854
  }
13855
+ /** What a burning part is made of, for the fire's sound. */
13856
+ burning(objectId, partId) {
13857
+ const material = this.partVis(objectId, partId)?.material || this.world.part(objectId, partId)?.material;
13858
+ return material ? { material } : void 0;
13859
+ }
13856
13860
  partVis(objectId, partId) {
13857
13861
  return this.bounds.get(objectId)?.parts.get(partId) ?? null;
13858
13862
  }
@@ -13952,14 +13956,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13952
13956
  const pv = b?.parts.get(ev.part);
13953
13957
  if (!b || !pv) return;
13954
13958
  const def = this.world.materials.get(pv.material);
13955
- const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
13956
- const mv = this.nearestMesh(pv, wp);
13957
- if (mv) {
13958
- const glass2 = !!def?.visual?.transparent;
13959
- const own = this.partColor(pv, def) ?? fallbackColor(pv.material, def);
13960
- const color = glass2 ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
13961
- const length = glass2 ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
13962
- this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, length, color, hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]));
13959
+ if (def?.visual?.transparent) {
13960
+ const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
13961
+ const mv = this.nearestMesh(pv, wp);
13962
+ const seed = hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
13963
+ if (mv) this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, ev.crack.length, 15267071, seed);
13963
13964
  }
13964
13965
  this.impactSound(pv.material, ev.worldPoint, 0.5, ev.blow);
13965
13966
  }
@@ -14539,7 +14540,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14539
14540
  }
14540
14541
  };
14541
14542
 
14542
- // ../game-audio/dist/chunk-7TLFY7VO.js
14543
+ // ../game-audio/dist/chunk-KBTUANNG.js
14543
14544
  var DISTANCE_CURVES = {
14544
14545
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
14545
14546
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -14752,7 +14753,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14752
14753
  ["casing_drop_terra_pistol", 1],
14753
14754
  ["casing_drop_terra_rifle", 1],
14754
14755
  ["casing_drop_terra_shell", 1],
14755
- ["rocket_fire", 2]
14756
+ ["rocket_fire", 2],
14757
+ ["smg_45_auto", 2, "sfx_pistol"],
14758
+ ["weapon_draw", 2],
14759
+ ["suppressor_attach", 3],
14760
+ ["suppressor_detach", 3]
14756
14761
  ]),
14757
14762
  ...entries("explosoes-fogo", [
14758
14763
  ["explosion_grenade", 1, "boom"],
@@ -14766,7 +14771,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14766
14771
  ["fire_loop_large", 1],
14767
14772
  ["fire_extinguish", 2, "hiss"],
14768
14773
  ["gas_leak_hiss", 2],
14769
- ["structure_collapse", 2, "crumble"]
14774
+ ["structure_collapse", 2, "crumble"],
14775
+ ["grenade_bounce", 1],
14776
+ ["c4_plant", 2],
14777
+ ["c4_beep", 2],
14778
+ ["c4_disarm", 2]
14770
14779
  ]),
14771
14780
  ...entries("personagem", [
14772
14781
  ["footstep_concrete_walk", 1, "sfx_step"],
@@ -14782,7 +14791,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14782
14791
  ["blunt_hit", 1],
14783
14792
  ["bite", 1, "sfx_flesh"],
14784
14793
  ["body_fall", 1],
14785
- ["player_hurt", 2]
14794
+ ["player_hurt", 2],
14795
+ ["footstep_tile_walk", 1, "sfx_step"],
14796
+ ["footstep_water_walk", 1, "sfx_step"],
14797
+ ["footstep_grate_walk", 2, "sfx_step"],
14798
+ ["footstep_duct_walk", 3, "sfx_step"],
14799
+ ["ladder_climb", 2],
14800
+ ["swim", 2],
14801
+ ["zombie_groan", 1],
14802
+ ["zombie_alert", 1],
14803
+ ["zombie_attack", 1],
14804
+ ["zombie_pain", 1],
14805
+ ["zombie_claw_hit", 1, "sfx_flesh"],
14806
+ ["zombie_claw_miss", 2]
14786
14807
  ]),
14787
14808
  ...entries("veiculos", [
14788
14809
  ["engine_loop_car", 1],
@@ -14810,7 +14831,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14810
14831
  ["wind_loop", 2],
14811
14832
  ["thunder_near", 2],
14812
14833
  ["thunder_far", 2],
14813
- ["water_splash", 2]
14834
+ ["water_splash", 2],
14835
+ ["helicopter_loop", 3]
14814
14836
  ])
14815
14837
  ];
14816
14838
  var SFX_MATERIALS = [
@@ -14892,6 +14914,60 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14892
14914
  burst: 20,
14893
14915
  burstSpacing: 0.06
14894
14916
  };
14917
+ var SFX_TIERS = { light: 0, medium: 0.34, heavy: 0.67 };
14918
+ function splitTier(id) {
14919
+ const match = /^(.*)_(light|medium|heavy)$/.exec(id);
14920
+ return match ? { base: match[1], tier: match[2] } : { base: id };
14921
+ }
14922
+ var refsOf = (entries2, baseUrl) => (entries2 ?? []).map((entry) => ({ url: `${baseUrl}${entry.file}` }));
14923
+ function sampleBankFromManifest(manifest, options) {
14924
+ const baseUrl = options.baseUrl ?? "";
14925
+ const samples = [];
14926
+ const tiersOf = /* @__PURE__ */ new Map();
14927
+ for (const sound of manifest.sounds) {
14928
+ const { base, tier } = splitTier(sound.id);
14929
+ if (tier) tiersOf.set(base, [...tiersOf.get(base) ?? [], sound]);
14930
+ }
14931
+ const bases = /* @__PURE__ */ new Map();
14932
+ for (const sound of manifest.sounds) {
14933
+ if (!splitTier(sound.id).tier) bases.set(sound.id, sound);
14934
+ }
14935
+ for (const [base, tiers] of tiersOf) {
14936
+ if (!bases.has(base)) bases.set(base, { id: base, category: tiers[0]?.category ?? "", layers: { close: tiers[0]?.layers.close ?? [] } });
14937
+ }
14938
+ for (const sound of bases.values()) {
14939
+ const numbers = options.sounds?.[sound.id] ?? options.defaults?.(sound);
14940
+ const close = refsOf(sound.layers.close, baseUrl);
14941
+ if (!numbers || close.length === 0) continue;
14942
+ const tiers = (tiersOf.get(sound.id) ?? []).map((tier) => {
14943
+ const name = splitTier(tier.id).tier;
14944
+ const tierFar = refsOf(tier.layers.far, baseUrl);
14945
+ return { name, from: SFX_TIERS[name] ?? 0, close: refsOf(tier.layers.close, baseUrl), ...tierFar.length > 0 ? { far: tierFar } : {} };
14946
+ }).sort((a, b) => a.from - b.from);
14947
+ const far = refsOf(sound.layers.far, baseUrl);
14948
+ const indoor = refsOf(sound.layers.tail_in, baseUrl);
14949
+ const outdoor = refsOf(sound.layers.tail_out, baseUrl);
14950
+ const files = [sound, ...tiersOf.get(sound.id) ?? []].flatMap((entry) => Object.values(entry.layers).flatMap((entries2) => entries2 ?? []));
14951
+ samples.push({
14952
+ id: sound.id,
14953
+ bus: numbers.bus,
14954
+ close,
14955
+ ...far.length > 0 ? { far } : {},
14956
+ ...tiers.length > 0 ? { tiers } : {},
14957
+ ...numbers.crossover ? { crossover: numbers.crossover } : {},
14958
+ ...indoor.length > 0 || outdoor.length > 0 ? { tail: { ...indoor.length > 0 ? { indoor } : {}, ...outdoor.length > 0 ? { outdoor } : {}, ...numbers.tailGain === void 0 ? {} : { gain: numbers.tailGain } } } : {},
14959
+ ...numbers.pitch === void 0 ? {} : { pitch: numbers.pitch },
14960
+ ...numbers.gain === void 0 ? {} : { gain: numbers.gain },
14961
+ ...numbers.falloff ? { falloff: numbers.falloff } : {},
14962
+ ...numbers.cutoffK === void 0 ? {} : { cutoffK: numbers.cutoffK },
14963
+ ...numbers.group ? { group: numbers.group } : {},
14964
+ ...numbers.priority === void 0 ? {} : { priority: numbers.priority },
14965
+ ...numbers.fallback ? { fallback: numbers.fallback } : {},
14966
+ sources: files.map((file) => `${file.file}: ${file.source} ${file.author} ${file.license}`)
14967
+ });
14968
+ }
14969
+ return { id: options.id, samples };
14970
+ }
14895
14971
  var PT = {
14896
14972
  group: "Som",
14897
14973
  name: "Painel de som (arquivos e C1\u2013C10)",
@@ -14899,6 +14975,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14899
14975
  category: "1 \xB7 Categoria",
14900
14976
  sound: "1 \xB7 Som",
14901
14977
  play: "1 \xB7 \u25B6 Tocar",
14978
+ play_user: "1 \xB7 \u25B6 Tocar o seu",
14902
14979
  material: "2 \xB7 Material",
14903
14980
  blow: "2 \xB7 Golpe",
14904
14981
  strike: "2 \xB7 \u25B6 Tocar golpe",
@@ -14920,6 +14997,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14920
14997
  approve: "5 \xB7 Aprovo (C10)"
14921
14998
  },
14922
14999
  hints: {
15000
+ play_user: "Toca a sua grava\xE7\xE3o deste som (dos seus pacotes, fora do git), mesmo quando o SDK tem uma melhor e toca na frente.",
14923
15001
  sound: "A = arquivo, S = sintetizado, F = faltando. O que tem arquivo toca o arquivo; o resto toca o som atual.",
14924
15002
  strike: "Toca impact_<material>_<golpe>: o arquivo se existir, sen\xE3o o som atual daquele material.",
14925
15003
  wall: "P\xF5e a parede de casa entre voc\xEA e a fonte: o som fica abafado (C4).",
@@ -14964,9 +15042,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14964
15042
  strengths: { light: "leve", medium: "m\xE9dio", heavy: "forte" },
14965
15043
  states: { file: "A", synth: "S", missing: "F" },
14966
15044
  criteria: { C1: "C1 Sincronia", C2: "C2 Certo p/ a\xE7\xE3o", C3: "C3 Dist\xE2ncia", C4: "C4 Posi\xE7\xE3o", C5: "C5 Varia\xE7\xE3o", C6: "C6 Volume", C7: "C7 Intensidade", C8: "C8 Cauda", C9: "C9 Rajada", C10: "C10 Escuta" },
14967
- metrics: { chosen: "1 \xB7 Escolhido", matrix: "2 \xB7 Matriz", last: "\xDAltimo tocado", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Prefer\xEAncias", state: "5 \xB7 Medi\xE7\xE3o", count: "1 \xB7 Cat\xE1logo" },
15045
+ metrics: { source: "1 \xB7 Fonte do que tocou", chosen: "1 \xB7 Escolhido", matrix: "2 \xB7 Matriz", last: "\xDAltimo tocado", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Prefer\xEAncias", state: "5 \xB7 Medi\xE7\xE3o", count: "1 \xB7 Cat\xE1logo" },
14968
15046
  run: { idle: "parada", measuring: "medindo\u2026", done: "pronta", failed: "falhou", recording: "gravando\u2026", recorded: "grava\xE7\xE3o baixada" },
14969
15047
  words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono" },
15048
+ sources: { sdk: "SDK", user: "seu som", synth: "sintetizado" },
14970
15049
  probe: "Fonte do som",
14971
15050
  wall: "Parede (oclus\xE3o)",
14972
15051
  scenarios: { pipeline: "Arquivo \u2192 decodifica \u2192 toca (round-robin, cauda)", criteria: "Crit\xE9rios C1\u2013C9 no som de calibra\xE7\xE3o", loop: "Loop de arquivo: emenda sem buraco (crossfade)", panel: "Painel: categorias, matriz, A/B e parede" }
@@ -14979,6 +15058,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14979
15058
  category: "1 \xB7 Category",
14980
15059
  sound: "1 \xB7 Sound",
14981
15060
  play: "1 \xB7 \u25B6 Play",
15061
+ play_user: "1 \xB7 \u25B6 Play yours",
14982
15062
  material: "2 \xB7 Material",
14983
15063
  blow: "2 \xB7 Blow",
14984
15064
  strike: "2 \xB7 \u25B6 Play blow",
@@ -15000,6 +15080,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15000
15080
  approve: "5 \xB7 Approve (C10)"
15001
15081
  },
15002
15082
  hints: {
15083
+ play_user: "Plays your own take of this sound (from your packs, kept out of git), even where the SDK has a better one that plays first.",
15003
15084
  sound: "A = file, S = synthesized, F = missing. A file plays the file; the rest plays the current sound.",
15004
15085
  strike: "Plays impact_<material>_<blow>: the file if there is one, else the current sound of that material.",
15005
15086
  wall: "Puts the house wall between you and the source: the sound is muffled (C4).",
@@ -15034,69 +15115,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15034
15115
  environments: { indoor: "indoor", outdoor: "outdoor" },
15035
15116
  strengths: { light: "light", medium: "medium", heavy: "heavy" },
15036
15117
  criteria: { C1: "C1 Sync", C2: "C2 Right sound", C3: "C3 Distance", C4: "C4 Position", C5: "C5 Variation", C6: "C6 Level", C7: "C7 Strength", C8: "C8 Tail", C9: "C9 Burst", C10: "C10 Listening" },
15037
- metrics: { chosen: "1 \xB7 Chosen", matrix: "2 \xB7 Matrix", last: "Last played", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Preferences", state: "5 \xB7 Measurement", count: "1 \xB7 Catalogue" },
15118
+ metrics: { source: "1 \xB7 Source of what played", chosen: "1 \xB7 Chosen", matrix: "2 \xB7 Matrix", last: "Last played", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Preferences", state: "5 \xB7 Measurement", count: "1 \xB7 Catalogue" },
15038
15119
  run: { idle: "idle", measuring: "measuring\u2026", done: "done", failed: "failed", recording: "recording\u2026", recorded: "recording downloaded" },
15039
15120
  words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner" },
15121
+ sources: { sdk: "SDK", user: "your sound", synth: "synthesized" },
15040
15122
  probe: "Sound source",
15041
15123
  wall: "Wall (occlusion)",
15042
15124
  scenarios: { pipeline: "File \u2192 decode \u2192 play (round-robin, tail)", criteria: "Criteria C1\u2013C9 on the calibration sound", loop: "File loop: seamless joins (crossfade)", panel: "Panel: categories, matrix, A/B and wall" }
15043
15125
  };
15044
15126
 
15045
15127
  // ../game-audio/dist/index.js
15046
- var SFX_TIERS = { light: 0, medium: 0.34, heavy: 0.67 };
15047
- function splitTier(id) {
15048
- const match = /^(.*)_(light|medium|heavy)$/.exec(id);
15049
- return match ? { base: match[1], tier: match[2] } : { base: id };
15050
- }
15051
- var refsOf = (entries2, baseUrl) => (entries2 ?? []).map((entry) => ({ url: `${baseUrl}${entry.file}` }));
15052
- function sampleBankFromManifest(manifest, options) {
15053
- const baseUrl = options.baseUrl ?? "";
15054
- const samples = [];
15055
- const tiersOf = /* @__PURE__ */ new Map();
15056
- for (const sound of manifest.sounds) {
15057
- const { base, tier } = splitTier(sound.id);
15058
- if (tier) tiersOf.set(base, [...tiersOf.get(base) ?? [], sound]);
15059
- }
15060
- const bases = /* @__PURE__ */ new Map();
15061
- for (const sound of manifest.sounds) {
15062
- if (!splitTier(sound.id).tier) bases.set(sound.id, sound);
15063
- }
15064
- for (const [base, tiers] of tiersOf) {
15065
- if (!bases.has(base)) bases.set(base, { id: base, category: tiers[0]?.category ?? "", layers: { close: tiers[0]?.layers.close ?? [] } });
15066
- }
15067
- for (const sound of bases.values()) {
15068
- const numbers = options.sounds?.[sound.id] ?? options.defaults?.(sound);
15069
- const close = refsOf(sound.layers.close, baseUrl);
15070
- if (!numbers || close.length === 0) continue;
15071
- const tiers = (tiersOf.get(sound.id) ?? []).map((tier) => {
15072
- const name = splitTier(tier.id).tier;
15073
- const tierFar = refsOf(tier.layers.far, baseUrl);
15074
- return { name, from: SFX_TIERS[name] ?? 0, close: refsOf(tier.layers.close, baseUrl), ...tierFar.length > 0 ? { far: tierFar } : {} };
15075
- }).sort((a, b) => a.from - b.from);
15076
- const far = refsOf(sound.layers.far, baseUrl);
15077
- const indoor = refsOf(sound.layers.tail_in, baseUrl);
15078
- const outdoor = refsOf(sound.layers.tail_out, baseUrl);
15079
- const files = [sound, ...tiersOf.get(sound.id) ?? []].flatMap((entry) => Object.values(entry.layers).flatMap((entries2) => entries2 ?? []));
15080
- samples.push({
15081
- id: sound.id,
15082
- bus: numbers.bus,
15083
- close,
15084
- ...far.length > 0 ? { far } : {},
15085
- ...tiers.length > 0 ? { tiers } : {},
15086
- ...numbers.crossover ? { crossover: numbers.crossover } : {},
15087
- ...indoor.length > 0 || outdoor.length > 0 ? { tail: { ...indoor.length > 0 ? { indoor } : {}, ...outdoor.length > 0 ? { outdoor } : {}, ...numbers.tailGain === void 0 ? {} : { gain: numbers.tailGain } } } : {},
15088
- ...numbers.pitch === void 0 ? {} : { pitch: numbers.pitch },
15089
- ...numbers.gain === void 0 ? {} : { gain: numbers.gain },
15090
- ...numbers.falloff ? { falloff: numbers.falloff } : {},
15091
- ...numbers.cutoffK === void 0 ? {} : { cutoffK: numbers.cutoffK },
15092
- ...numbers.group ? { group: numbers.group } : {},
15093
- ...numbers.priority === void 0 ? {} : { priority: numbers.priority },
15094
- ...numbers.fallback ? { fallback: numbers.fallback } : {},
15095
- sources: files.map((file) => `${file.file}: ${file.source} ${file.author} ${file.license}`)
15096
- });
15097
- }
15098
- return { id: options.id, samples };
15099
- }
15100
15128
  var ATTACK = 2e-3;
15101
15129
  function lengthOf(d, attack) {
15102
15130
  if (typeof d === "number") return attack + d;
@@ -16087,6 +16115,47 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16087
16115
  volume: 0,
16088
16116
  sources: ["sdk: the roar under the crackles of a fire"]
16089
16117
  };
16118
+ var FIRE_FUEL_OF = {
16119
+ wood_soft: "wood",
16120
+ wood_hard: "wood",
16121
+ plywood: "wood",
16122
+ mdf: "wood",
16123
+ green_wood: "wood",
16124
+ foliage: "wood",
16125
+ cactus: "wood",
16126
+ paper: "fabric",
16127
+ fabric: "fabric",
16128
+ leather: "fabric",
16129
+ foam: "plastic",
16130
+ plastic: "plastic",
16131
+ rubber: "plastic",
16132
+ wax: "plastic"
16133
+ };
16134
+ var FIRE_LOOP_OF = {
16135
+ wood: "fire_loop",
16136
+ fabric: "fire_loop",
16137
+ plastic: "fire_loop_plastic",
16138
+ gas: "fire_loop_gas",
16139
+ liquid: "fire_loop_liquid"
16140
+ };
16141
+ var FIRE_CRACKLE_OF = { wood: 1, fabric: 0.5, plastic: 0.35, gas: 0, liquid: 0 };
16142
+ var roar = (id, bank, source) => ({
16143
+ id,
16144
+ bus: "world",
16145
+ order: "chain_loop",
16146
+ source: { kind: "noise", loop: true },
16147
+ filters: { mode: "serial", bank },
16148
+ volume: 0,
16149
+ sources: [source]
16150
+ });
16151
+ var FIRE_FUEL_LOOPS = [
16152
+ // a gas jet: a steady, broad roar, brighter than wood and without crackles
16153
+ 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)"),
16154
+ // a fuel pool: a soft, low, rolling roar
16155
+ 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"),
16156
+ // plastic and rubber: a thin sizzle over a low burn
16157
+ roar("fire_loop_plastic", [{ type: "bandpass", frequency: { set: 1800 }, q: 0.6 }], "sdk: plastic and rubber melting in the fire")
16158
+ ];
16090
16159
  var BLAST_SOUND_OF = {
16091
16160
  grenade: "explosion_grenade",
16092
16161
  big: "explosion_big",
@@ -16233,7 +16302,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16233
16302
  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
16303
  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
16304
  ];
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] };
16305
+ 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
16306
 
16238
16307
  // src/audio/sound.ts
16239
16308
  var IMPACT_SOUND_KEYS = {
@@ -16384,31 +16453,52 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16384
16453
  const rules = { ...DESTRUCTION_SOUND_KEYS.art_scene, ...options.style === "game" ? DESTRUCTION_SOUND_KEYS.game : {}, ...options.keys };
16385
16454
  const blastByKind = options.style !== "game" && !(options.keys && "explode" in options.keys);
16386
16455
  const channel = engine.channel({ seed: options.seed ?? 1 });
16387
- let loop;
16388
- let burning = 0;
16456
+ const fuelOf = { ...FIRE_FUEL_OF, ...options.fuels };
16457
+ const loopOf = { ...FIRE_LOOP_OF, ...options.fireLoop ? { wood: fireLoop, fabric: fireLoop } : {}, ...options.fireLoops };
16458
+ const defaultFuel = options.defaultFuel ?? "wood";
16459
+ const loops = /* @__PURE__ */ new Map();
16389
16460
  const fires = [];
16390
16461
  let nextCrackle = 0;
16462
+ const fuelFor = (detail) => detail?.fuel ?? (detail?.material !== void 0 ? fuelOf[detail.material] : void 0) ?? defaultFuel;
16391
16463
  const setFire = () => {
16392
16464
  if (options.burnLoop === false) return;
16393
- if (burning <= 0) {
16394
- loop?.stop();
16395
- loop = void 0;
16396
- return;
16465
+ const count = /* @__PURE__ */ new Map();
16466
+ for (const f of fires) {
16467
+ const id = loopOf[f.fuel];
16468
+ count.set(id, (count.get(id) ?? 0) + 1);
16469
+ }
16470
+ for (const [id, handle] of loops) {
16471
+ if (count.has(id)) continue;
16472
+ handle.stop();
16473
+ loops.delete(id);
16474
+ }
16475
+ for (const [id, n] of count) {
16476
+ let handle = loops.get(id);
16477
+ if (!handle) {
16478
+ handle = engine.loop(id);
16479
+ loops.set(id, handle);
16480
+ }
16481
+ handle.set({ volume: Math.min(fire2.max, fire2.perFire * n) });
16397
16482
  }
16398
- loop ?? (loop = engine.loop(fireLoop));
16399
- loop.set({ volume: Math.min(fire2.max, fire2.perFire * burning) });
16400
16483
  };
16401
- const forget = (point) => {
16484
+ const forget = (point, fuel) => {
16402
16485
  let best = -1;
16403
16486
  let bestD = Infinity;
16404
16487
  fires.forEach((f, i) => {
16405
- const d = (f[0] - point[0]) ** 2 + (f[1] - point[1]) ** 2 + (f[2] - point[2]) ** 2;
16488
+ if (fuel !== void 0 && f.fuel !== fuel) return;
16489
+ const d = (f.point[0] - point[0]) ** 2 + (f.point[1] - point[1]) ** 2 + (f.point[2] - point[2]) ** 2;
16406
16490
  if (d < bestD) {
16407
16491
  bestD = d;
16408
16492
  best = i;
16409
16493
  }
16410
16494
  });
16411
- if (best >= 0) fires.splice(best, 1);
16495
+ if (best < 0 && fuel !== void 0) forget(point, void 0);
16496
+ else if (best >= 0) fires.splice(best, 1);
16497
+ };
16498
+ const reset = () => {
16499
+ for (const handle of loops.values()) handle.stop();
16500
+ loops.clear();
16501
+ fires.length = 0;
16412
16502
  };
16413
16503
  const blastSound = (point, volume, detail) => {
16414
16504
  const named = detail?.effect === void 0 ? void 0 : kinds[detail.effect];
@@ -16420,14 +16510,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16420
16510
  return {
16421
16511
  onSound(key, point, volume, detail) {
16422
16512
  if (key === "burn_loop") {
16423
- burning += 1;
16424
- fires.push(point);
16513
+ fires.push({ point, fuel: fuelFor(detail) });
16425
16514
  setFire();
16426
16515
  return;
16427
16516
  }
16428
16517
  if (key === "burn_end") {
16429
- if (burning > 0) burning -= 1;
16430
- forget(point);
16518
+ forget(point, detail?.fuel ?? (detail?.material !== void 0 ? fuelOf[detail.material] : void 0));
16431
16519
  setFire();
16432
16520
  return;
16433
16521
  }
@@ -16457,12 +16545,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16457
16545
  },
16458
16546
  update(dt) {
16459
16547
  if (!crackles || fires.length === 0 || options.burnLoop === false) return;
16548
+ const weight = fires.reduce((sum, f) => sum + FIRE_CRACKLE_OF[f.fuel], 0);
16549
+ if (weight <= 0) return;
16460
16550
  nextCrackle -= dt;
16461
16551
  for (let i = 0; i < 4 && nextCrackle <= 0; i++) {
16462
- const rate = Math.min(crackles.max, crackles.base + crackles.perSqrtFire * Math.sqrt(fires.length));
16552
+ const rate = Math.min(crackles.max, crackles.base * Math.min(1, weight) + crackles.perSqrtFire * Math.sqrt(weight));
16463
16553
  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 });
16554
+ let pick3 = channel.random() * weight;
16555
+ const at3 = fires.find((f) => (pick3 -= FIRE_CRACKLE_OF[f.fuel]) <= 0) ?? fires[fires.length - 1];
16556
+ engine.play(channel.random() < 0.18 ? "fire_pop" : "fire_crackle", { position: at3.point, volume: crackles.volume });
16466
16557
  }
16467
16558
  if (nextCrackle < 0) nextCrackle = 0;
16468
16559
  },
@@ -16471,13 +16562,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16471
16562
  for (const k of detail?.keys ?? []) if (k in rules) return rules[k] ?? void 0;
16472
16563
  return rules[key] ?? void 0;
16473
16564
  },
16474
- burning: () => burning,
16475
- dispose() {
16476
- loop?.stop();
16477
- loop = void 0;
16478
- burning = 0;
16479
- fires.length = 0;
16480
- }
16565
+ burning: () => fires.length,
16566
+ burningBy: () => {
16567
+ const by = {};
16568
+ for (const f of fires) by[f.fuel] = (by[f.fuel] ?? 0) + 1;
16569
+ return by;
16570
+ },
16571
+ reset,
16572
+ dispose: reset
16481
16573
  };
16482
16574
  }
16483
16575
 
@@ -16498,7 +16590,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16498
16590
  fire_extinguish: fire(0.7),
16499
16591
  gas_leak: fire(0.5),
16500
16592
  // 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 } }
16593
+ fire_loop: { bus: "world", gain: 4, loop: { crossfade: 1 } },
16594
+ // each fuel has its own roar: a gas jet (burners and torches), a pool of fuel, plastic and rubber sizzling under the flames
16595
+ fire_loop_gas: { bus: "world", gain: 6, loop: { crossfade: 1 } },
16596
+ fire_loop_liquid: { bus: "world", gain: 3.5, loop: { crossfade: 1 } },
16597
+ fire_loop_plastic: { bus: "world", gain: 1, loop: { crossfade: 0.6 } }
16502
16598
  };
16503
16599
  function blastFireSampleBank(manifest, options = {}) {
16504
16600
  const sounds = options.sounds ?? BLAST_FIRE_SAMPLES;