@adatechnology/game-weapons-three 0.12.0 → 0.13.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.
@@ -26,6 +26,7 @@ var GameWeaponsThree = (() => {
26
26
  BODY_ACTIONS: () => BODY_ACTIONS,
27
27
  CLASS_REFERENCE_ENERGY: () => CLASS_REFERENCE_ENERGY,
28
28
  COUNTER_ITEM_ID: () => COUNTER_ITEM_ID,
29
+ CUES_IN_SAMPLE: () => CUES_IN_SAMPLE,
29
30
  DEFAULT_ENERGY_CURVE: () => DEFAULT_ENERGY_CURVE,
30
31
  DEFAULT_EXPLOSIVES_SCENE: () => DEFAULT_EXPLOSIVES_SCENE,
31
32
  DEFAULT_THUMBNAIL_BASE: () => DEFAULT_THUMBNAIL_BASE,
@@ -33,12 +34,18 @@ var GameWeaponsThree = (() => {
33
34
  EXPLOSIVES_TEXT: () => EXPLOSIVES_TEXT,
34
35
  GAME_DISTANCE_CURVE: () => GAME_DISTANCE_CURVE,
35
36
  GAME_SOUND_CLASSES: () => GAME_SOUND_CLASSES,
37
+ GUNSHOT_PROFILES: () => GUNSHOT_PROFILES,
38
+ GUNSHOT_SAMPLE: () => GUNSHOT_SAMPLE,
39
+ GUNSHOT_SAMPLE_IDS: () => GUNSHOT_SAMPLE_IDS,
36
40
  MECHANICAL_SOUNDS: () => MECHANICAL_SOUNDS,
37
41
  MODEL_ENTRIES: () => MODEL_ENTRIES,
38
42
  MOUNTED_M134_ITEM_ID: () => MOUNTED_M134_ITEM_ID,
39
43
  P13_VALVE_MESH: () => P13_VALVE_MESH,
44
+ RELOAD_SAMPLE: () => RELOAD_SAMPLE,
45
+ RELOAD_SAMPLE_HITS: () => RELOAD_SAMPLE_HITS,
40
46
  WEAPON_ITEM_PREFIX: () => WEAPON_ITEM_PREFIX,
41
47
  WEAPON_THUMBNAILS: () => WEAPON_THUMBNAILS,
48
+ WEIGHT_INTENSITY: () => WEIGHT_INTENSITY,
42
49
  WORLD_EFFECTS: () => WORLD_EFFECTS,
43
50
  WORLD_EXPLOSIVE_IDS: () => WORLD_EXPLOSIVE_IDS,
44
51
  WORLD_EXPLOSIVE_PREFIX: () => WORLD_EXPLOSIVE_PREFIX,
@@ -54,6 +61,9 @@ var GameWeaponsThree = (() => {
54
61
  buildWeaponModel: () => buildWeaponModel,
55
62
  buildWeaponModelById: () => buildWeaponModelById,
56
63
  cartridgeShape: () => cartridgeShape,
64
+ computeCasingSound: () => computeCasingSound,
65
+ computeFlyby: () => computeFlyby,
66
+ computeGunshot: () => computeGunshot,
57
67
  computeShotSound: () => computeShotSound,
58
68
  createExplosivesTestModule: () => createExplosivesTestModule,
59
69
  createWeaponAudio: () => createWeaponAudio,
@@ -77,6 +87,8 @@ var GameWeaponsThree = (() => {
77
87
  rigStock: () => rigStock,
78
88
  rigUnderbarrel: () => rigUnderbarrel,
79
89
  weaponProp: () => weaponProp,
90
+ weaponSampleBank: () => weaponSampleBank,
91
+ weaponSampleDefaults: () => weaponSampleDefaults,
80
92
  weaponThumbnailCredit: () => weaponThumbnailCredit,
81
93
  weaponThumbnailUrl: () => weaponThumbnailUrl,
82
94
  weaponsLayout: () => weaponsLayout,
@@ -228,6 +240,21 @@ var GameWeaponsThree = (() => {
228
240
  const fraction = to.t > from.t ? (time - from.t) / (to.t - from.t) : 0;
229
241
  return { position: mix(from.position, to.position, fraction), rotation: mix(from.rotation, to.rotation, fraction) };
230
242
  };
243
+ var leadClipEvents = (clip) => {
244
+ const events = clip.events ?? [];
245
+ if (!events.some((event) => (event.lead ?? 0) > 0)) return clip;
246
+ let floor2 = 0;
247
+ const led = events.map((event) => {
248
+ const lead = Math.max(0, Math.min(event.t - floor2, event.lead ?? 0));
249
+ const t = event.t - lead;
250
+ floor2 = Math.max(floor2, t);
251
+ if (event.lead === void 0) return event;
252
+ const moved = { ...event, t, data: lead > 0 ? { ...event.data, lead } : { ...event.data } };
253
+ delete moved.lead;
254
+ return moved;
255
+ });
256
+ return { ...clip, events: led };
257
+ };
231
258
  var createToolClipPlayer = (options) => {
232
259
  const { clips, rig, onEvent } = options;
233
260
  let instances = [];
@@ -270,7 +297,7 @@ var GameWeaponsThree = (() => {
270
297
  }
271
298
  const found = find(step.clip, step.variant);
272
299
  if (!found) return;
273
- const clip = step.duration && step.duration > 0 ? scaleClip(found.clip, step.duration) : found.clip;
300
+ const clip = leadClipEvents(step.duration && step.duration > 0 ? scaleClip(found.clip, step.duration) : found.clip);
274
301
  const interval = options.repeatInterval?.() ?? 0.1;
275
302
  instances = instances.filter((existing) => existing.name !== found.name || existing.delay > 0);
276
303
  for (let round5 = 0; round5 < (step.repeat ?? 1); round5++) {
@@ -343,20 +370,20 @@ var GameWeaponsThree = (() => {
343
370
  return result2;
344
371
  };
345
372
  var mergeById = (shipped, extra) => {
346
- const entries = /* @__PURE__ */ new Map();
347
- for (const entry2 of [...shipped, ...extra ?? []]) entries.set(entry2.id, entry2);
348
- return entries;
373
+ const entries2 = /* @__PURE__ */ new Map();
374
+ for (const entry2 of [...shipped, ...extra ?? []]) entries2.set(entry2.id, entry2);
375
+ return entries2;
349
376
  };
350
- var applyPatches = (entries, patches) => {
377
+ var applyPatches = (entries2, patches) => {
351
378
  for (const [id, patch] of Object.entries(patches ?? {})) {
352
- const entry2 = entries.get(id);
353
- if (entry2) entries.set(id, deepMerge(entry2, patch));
379
+ const entry2 = entries2.get(id);
380
+ if (entry2) entries2.set(id, deepMerge(entry2, patch));
354
381
  else warnOnce(`registry.patch.${id}`, `patch for "${id}" ignored: no entry has that id`);
355
382
  }
356
383
  };
357
- var removeIds = (entries, ids) => {
384
+ var removeIds = (entries2, ids) => {
358
385
  for (const id of ids ?? []) {
359
- if (!entries.delete(id)) warnOnce(`registry.remove.${id}`, `remove of "${id}" ignored: no entry has that id`);
386
+ if (!entries2.delete(id)) warnOnce(`registry.remove.${id}`, `remove of "${id}" ignored: no entry has that id`);
360
387
  }
361
388
  };
362
389
  var KIT_ORIGIN_KEY = "kitOrigin";
@@ -12684,10 +12711,10 @@ var GameWeaponsThree = (() => {
12684
12711
  ]
12685
12712
  }
12686
12713
  ];
12687
- var findDuplicateIds = (kind, entries) => {
12714
+ var findDuplicateIds = (kind, entries2) => {
12688
12715
  const seen = /* @__PURE__ */ new Set();
12689
12716
  const problems = [];
12690
- for (const { id } of entries ?? []) {
12717
+ for (const { id } of entries2 ?? []) {
12691
12718
  if (seen.has(id)) problems.push(`${kind} "${id}" is listed more than once`);
12692
12719
  seen.add(id);
12693
12720
  }
@@ -13567,8 +13594,8 @@ var GameWeaponsThree = (() => {
13567
13594
  var FUSE_RNG_SALT = 3144134277;
13568
13595
  var BREAK_RNG_SALT = 1013904242;
13569
13596
  var UNDERBARREL_SEED_SALT = 2773480762;
13570
- var findById = (entries, id, label) => {
13571
- const found = entries.find((entry2) => entry2.id === id);
13597
+ var findById = (entries2, id, label) => {
13598
+ const found = entries2.find((entry2) => entry2.id === id);
13572
13599
  if (!found) throw new Error(`game-weapons: unknown ${label} "${id}"`);
13573
13600
  return found;
13574
13601
  };
@@ -14712,7 +14739,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
14712
14739
  shade: () => shade
14713
14740
  });
14714
14741
 
14715
- // ../game-shapes/dist/chunk-IFXQIDSA.js
14742
+ // ../game-shapes/dist/chunk-M5N2SHNL.js
14716
14743
  function canonicalJson(value) {
14717
14744
  if (value === null) return "null";
14718
14745
  if (typeof value === "number") return Object.is(value, -0) ? "-0" : Number.isFinite(value) ? String(value) : JSON.stringify(String(value));
@@ -15995,8 +16022,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
15995
16022
  groups: []
15996
16023
  };
15997
16024
  }
15998
- function make(kind, p) {
15999
- switch (kind) {
16025
+ function make(p) {
16026
+ switch (p.kind) {
16000
16027
  case "box": {
16001
16028
  const size = p.size ?? [1, 1, 1];
16002
16029
  const segments = p.segments ?? [1, 1, 1];
@@ -16025,7 +16052,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
16025
16052
  case "mesh":
16026
16053
  return mesh(p);
16027
16054
  default:
16028
- throw new Error(`game-shapes: unknown primitive kind "${String(kind)}"`);
16055
+ throw new Error(`game-shapes: unknown primitive kind "${String(p.kind)}"`);
16029
16056
  }
16030
16057
  }
16031
16058
  var templates = /* @__PURE__ */ new Map();
@@ -16033,7 +16060,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
16033
16060
  var stats = { hits: 0, misses: 0 };
16034
16061
  function buildPrimitive(node, options = {}) {
16035
16062
  const parameters = primitiveParameters(node, options);
16036
- if (node.kind === "mesh" || templateLimit === 0) return make(node.kind, parameters);
16063
+ if (node.kind === "mesh" || templateLimit === 0) return make({ ...node, ...parameters });
16037
16064
  const key2 = `${node.kind}:${canonicalJson(parameters)}`;
16038
16065
  const cached = templates.get(key2);
16039
16066
  if (cached) {
@@ -16043,7 +16070,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
16043
16070
  return cached;
16044
16071
  }
16045
16072
  stats.misses++;
16046
- const built2 = make(node.kind, parameters);
16073
+ const built2 = make({ ...node, ...parameters });
16047
16074
  templates.set(key2, built2);
16048
16075
  if (templates.size > templateLimit) templates.delete(templates.keys().next().value);
16049
16076
  return built2;
@@ -18758,8 +18785,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18758
18785
  // was `maquina`
18759
18786
  });
18760
18787
  var OWN_TYPES = Object.freeze({ lamp: true, gas_piped: true });
18788
+ var sourceOf = (result2) => result2;
18761
18789
  var ADAPTER_VERSION = "0.3.0";
18762
18790
  var r3 = (n) => Math.round(n * 1e3) / 1e3;
18791
+ var at = (it) => ({ x: it.x ?? 0, z: it.z ?? 0, ry: it.ry ?? 0 });
18763
18792
  function mergeRegistry(base, extra) {
18764
18793
  const out = { ...base };
18765
18794
  if (extra) {
@@ -18778,14 +18807,15 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18778
18807
  });
18779
18808
  function create2(options = {}) {
18780
18809
  const opts = options || {};
18781
- const GI = opts.GameInteractables;
18782
- const HH = opts.GameInteractablesHousehold;
18783
18810
  const locale = opts.locale || "pt-BR";
18784
18811
  const msg = createTranslator(locale, opts.messages);
18785
- if (!GI || !HH) throw new Error(msg("error.missingKit"));
18812
+ if (!opts.GameInteractables || !opts.GameInteractablesHousehold) throw new Error(msg("error.missingKit"));
18813
+ const GI = opts.GameInteractables;
18814
+ const HH = opts.GameInteractablesHousehold;
18786
18815
  const region = opts.region || "br";
18787
18816
  const hh = HH.createHousehold({ locale, region });
18788
18817
  const householdTypes = new Set(hh.types.map((t) => t.id));
18818
+ const recorderApi = () => HH.createRecorder().api;
18789
18819
  const worlds = /* @__PURE__ */ new WeakMap();
18790
18820
  const typeMap = mergeRegistry(TYPE_MAP, opts.typeMap);
18791
18821
  const contentMap = mergeRegistry(CONTENT_MAP, opts.contentMap);
@@ -18821,7 +18851,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18821
18851
  function colorOf(res, color2) {
18822
18852
  if (color2 == null) return void 0;
18823
18853
  if (typeof color2 === "number") return color2;
18824
- const v = res.colors && res.colors[color2];
18854
+ const v = res.colors && res.colors[String(color2)];
18825
18855
  return typeof v === "number" ? v : void 0;
18826
18856
  }
18827
18857
  function paletteOf(res) {
@@ -18850,17 +18880,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18850
18880
  drawerItem: (s) => drawerItems.has(s.itemId)
18851
18881
  };
18852
18882
  const itemOfContent = (c) => {
18853
- const id = contentMap[c.itemId] || (hh.items[c.itemId] ? c.itemId : null) || contentByGroup[stackGroup(c)] || contentFallback;
18883
+ const group = stackGroup(c);
18884
+ const id = contentMap[c.itemId] || (hh.items[c.itemId] ? c.itemId : null) || (group != null ? contentByGroup[group] : void 0) || contentFallback;
18854
18885
  return hh.items[id] ? id : contentFallback;
18855
18886
  };
18856
18887
  const volume = (d) => d.size[0] * d.size[1] * d.size[2];
18857
18888
  const candidates = (id, objectType) => {
18858
18889
  const def = hh.items[id];
18859
- const siblings = Object.values(hh.items).filter((i) => i.category === def.category && i.id !== id).sort((a, b) => volume(a) - volume(b) || (a.id < b.id ? -1 : 1));
18890
+ const siblings = Object.values(hh.items).filter((i) => i.category === def?.category && i.id !== id).sort((a, b) => volume(a) - volume(b) || (a.id < b.id ? -1 : 1));
18860
18891
  return [id].concat(siblings.map((i) => i.id), substitutes[objectType] || []);
18861
18892
  };
18862
18893
  function distribute(type, probeItem, stacks) {
18863
- const spec = hh.build(type, probeItem, HH.createRecorder().api);
18894
+ const spec = hh.build(type, probeItem, recorderApi());
18864
18895
  const conts = spec.containers || [];
18865
18896
  const contents = {};
18866
18897
  for (const c of conts) contents[c.id] = c.slots.map(() => null);
@@ -18891,8 +18922,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18891
18922
  }
18892
18923
  return { contents, rejected };
18893
18924
  }
18894
- function plan(res, popts = {}) {
18895
- popts = popts || {};
18925
+ function plan(source, options2 = {}) {
18926
+ const res = sourceOf(source);
18927
+ const popts = options2 || {};
18896
18928
  const o = { ox: popts.originX || 0, oz: popts.originZ || 0, inw: popts.inw || 1, base: popts.base || 0 };
18897
18929
  const K = conv(o);
18898
18930
  const floorSel = popts.floor;
@@ -18956,7 +18988,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18956
18988
  const item = {
18957
18989
  x: K.x(m.x),
18958
18990
  z: K.z(m.z),
18959
- ry: m.frontU != null ? K.ry(m.frontU, m.frontV) : r3(m.ry || 0),
18991
+ ry: m.frontU != null ? K.ry(m.frontU, m.frontV ?? 0) : r3(m.ry || 0),
18960
18992
  w: m.w,
18961
18993
  d: m.d,
18962
18994
  h: m.h,
@@ -18969,7 +19001,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18969
19001
  return item;
18970
19002
  }
18971
19003
  function entry2(id, type, item, origin, floor2, extra) {
18972
- return Object.assign({ id, type, item, origin, floor: floor2, floorY: floorYOf(res, o, floor2), connections: {} }, extra || {});
19004
+ const e = { id, type, item, origin, floor: floor2, floorY: floorYOf(res, o, floor2), connections: {} };
19005
+ return Object.assign(e, extra || {});
18973
19006
  }
18974
19007
  function store(targets, stacks) {
18975
19008
  let rest2 = stacks || [];
@@ -18982,10 +19015,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18982
19015
  }
18983
19016
  const hidden = [];
18984
19017
  function distRect(c, x, z) {
18985
- const dx = x - c.item.x;
18986
- const dz = z - c.item.z;
18987
- const s = Math.sin(c.item.ry);
18988
- const co = Math.cos(c.item.ry);
19018
+ const ci = at(c.item);
19019
+ const dx = x - ci.x;
19020
+ const dz = z - ci.z;
19021
+ const s = Math.sin(ci.ry);
19022
+ const co = Math.cos(ci.ry);
18989
19023
  const lx = dx * co - dz * s;
18990
19024
  const lz = dx * s + dz * co;
18991
19025
  return Math.hypot(Math.max(Math.abs(lx) - (c.item.w || 0) / 2, 0), Math.max(Math.abs(lz) - (c.item.d || 0) / 2, 0));
@@ -18993,9 +19027,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18993
19027
  function hideCylinders() {
18994
19028
  for (const b of hidden) {
18995
19029
  let target = null;
19030
+ const bi = at(b.item);
18996
19031
  for (const c2 of result2.items) {
18997
19032
  if (c2.type !== "kitchen_cabinet_base" || c2.floor !== b.floor) continue;
18998
- const dist2 = distRect(c2, b.item.x, b.item.z);
19033
+ const dist2 = distRect(c2, bi.x, bi.z);
18999
19034
  if (!target || dist2 < target.dist) target = { c: c2, dist: dist2 };
19000
19035
  }
19001
19036
  if (!target) {
@@ -19003,16 +19038,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19003
19038
  continue;
19004
19039
  }
19005
19040
  const c = target.c;
19006
- const spec = hh.build(c.type, c.item, HH.createRecorder().api);
19007
- const sn = Math.sin(c.item.ry);
19008
- const cs = Math.cos(c.item.ry);
19041
+ const spec = hh.build(c.type, c.item, recorderApi());
19042
+ const ci = at(c.item);
19043
+ const sn = Math.sin(ci.ry);
19044
+ const cs = Math.cos(ci.ry);
19009
19045
  let door = null;
19010
19046
  for (const it of spec.interactions || []) {
19011
- const pt = it.action && it.action.do === "toggle_part" ? (spec.parts || []).find((q) => q.id === it.action.part) : null;
19012
- if (!pt || pt.motion.kind !== "hinge") continue;
19013
- const wx = c.item.x + it.pos[0] * cs + it.pos[2] * sn;
19014
- const wz = c.item.z - it.pos[0] * sn + it.pos[2] * cs;
19015
- const dist2 = Math.hypot(wx - b.item.x, wz - b.item.z);
19047
+ const action = it.action;
19048
+ const pt = action.do === "toggle_part" ? (spec.parts || []).find((q) => q.id === action.part) : null;
19049
+ if (!pt || !pt.motion || pt.motion.kind !== "hinge") continue;
19050
+ const wx = ci.x + it.pos[0] * cs + it.pos[2] * sn;
19051
+ const wz = ci.z - it.pos[0] * sn + it.pos[2] * cs;
19052
+ const dist2 = Math.hypot(wx - bi.x, wz - bi.z);
19016
19053
  if (!door || dist2 < door.dist) door = { id: pt.id, dist: dist2 };
19017
19054
  }
19018
19055
  if (!door) {
@@ -19042,9 +19079,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19042
19079
  const campCylinder = firstOfType("gas_canister");
19043
19080
  function planFireplace(m, e, item) {
19044
19081
  const def = hh.types.find((t) => t.id === "fireplace");
19045
- const lack = (def.d - m.d) / 2;
19046
- item.x = r3(item.x + Math.sin(item.ry) * lack);
19047
- item.z = r3(item.z + Math.cos(item.ry) * lack);
19082
+ const lack = ((def ? def.d : m.d) - m.d) / 2;
19083
+ const p = at(item);
19084
+ item.x = r3(p.x + Math.sin(p.ry) * lack);
19085
+ item.z = r3(p.z + Math.cos(p.ry) * lack);
19048
19086
  const room = (res.rooms || []).find((c) => c.id === m.room && c.floor === m.floor);
19049
19087
  const f = (res.floors || []).find((x) => x.index === m.floor);
19050
19088
  item.h = r3(room && room.ceiling || f && f.ceiling || 2.6);
@@ -19072,7 +19110,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19072
19110
  e.connections.gas = m.gas || "cylinder";
19073
19111
  e.connections.cylinder = m.cylinder || null;
19074
19112
  }
19075
- if (map.type === "gas_bottle") result2.cylinders.push({ id: e.id, stove: m.link || null, place: m.placement || null });
19113
+ if (map.type === "gas_bottle") result2.cylinders.push({ id: e.id, stove: typeof m.link === "string" ? m.link : null, place: m.placement || null });
19076
19114
  if (map.type === "gas_canister") {
19077
19115
  result2.canisters.push(e.id);
19078
19116
  if (campCylinder && campCylinder.id === m.id && campStove2) result2.cylinders.push({ id: e.id, stove: campStove2.id, place: null });
@@ -19096,7 +19134,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19096
19134
  push(e);
19097
19135
  }
19098
19136
  function planCompound(m, kind) {
19099
- const ry = m.frontU != null ? K.ry(m.frontU, m.frontV) : r3(m.ry || 0);
19137
+ const ry = m.frontU != null ? K.ry(m.frontU, m.frontV ?? 0) : r3(m.ry || 0);
19100
19138
  const cx = K.x(m.x);
19101
19139
  const cz = K.z(m.z);
19102
19140
  const part = (id, type, w, d, h, y, lx, lz) => {
@@ -19148,11 +19186,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19148
19186
  } else {
19149
19187
  const basin = part(m.id, "sink_bathroom", m.w, m.d, m.h + 0.2, 0, 0, 0);
19150
19188
  list2.push(basin);
19151
- if (contents[m.id]) {
19152
- if (contents[m.id].length) {
19189
+ const own = contents[m.id];
19190
+ if (own) {
19191
+ if (own.length) {
19153
19192
  const cabinet = part(m.id + "#cabinet", "bathroom_cabinet", Math.max(0.45, Math.min(0.6, m.w)), 0.15, 0.75, 1.3, 0, -(m.d / 2) + 0.075);
19154
19193
  list2.push(cabinet);
19155
- store([["bathroom_cabinet", cabinet.item], ["sink_bathroom", basin.item]], contents[m.id]);
19194
+ store([["bathroom_cabinet", cabinet.item], ["sink_bathroom", basin.item]], own);
19156
19195
  } else {
19157
19196
  store([["sink_bathroom", basin.item]], []);
19158
19197
  }
@@ -19192,7 +19231,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19192
19231
  result2.chairs = [];
19193
19232
  if (opts.chairs !== false && popts.chairs !== false) {
19194
19233
  const blocked = blockedOf(res, { originX: o.ox, originZ: o.oz, inw: o.inw });
19195
- const floorKind = (q) => !q.decor && !q.flat && (q.y || 0) < 0.9 && q.aabb;
19234
+ const floorKind = (q) => !q.decor && !q.flat && (q.y || 0) < 0.9 && !!q.aabb;
19196
19235
  const placed = [];
19197
19236
  const free = (m, cu, cv, half) => {
19198
19237
  const a = [cu - half + 0.03, cv - half + 0.03, cu + half - 0.03, cv + half - 0.03];
@@ -19208,8 +19247,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19208
19247
  for (const m of res.items || []) {
19209
19248
  const mode = chairTables[m.type];
19210
19249
  if (!mode || !onFloor(m.floor, m.outdoor) || m.frontU == null) continue;
19211
- const f = [m.frontU, m.frontV];
19212
- const pp = [-m.frontV, m.frontU];
19250
+ const fv = m.frontV ?? 0;
19251
+ const f = [m.frontU, fv];
19252
+ const pp = [-fv, m.frontU];
19213
19253
  const seats = [];
19214
19254
  if (mode === "dining") {
19215
19255
  const n = Math.max(1, Math.min(3, Math.floor((m.w - 0.1) / 0.55)));
@@ -19278,8 +19318,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19278
19318
  const item = { x: K.x(s.u), z: K.z(s.v), ry: K.ry(s.frontU, s.frontV), y: s.y, seed: seedOf2(s.id) };
19279
19319
  item.controls = (s.controls || []).map((id) => id + ":lamp");
19280
19320
  const e = entry2(s.id, "wall_switch", item, s.id, s.floor);
19281
- e.connections.controls = (s.controls || []).slice();
19282
- e.lit = e.connections.controls.some((id) => (result2.items.find((x) => x.id === id) || {}).lit === true);
19321
+ const controls = (s.controls || []).slice();
19322
+ e.connections.controls = controls;
19323
+ e.lit = controls.some((id) => result2.items.find((x) => x.id === id)?.lit === true);
19283
19324
  if (floorSel == null) {
19284
19325
  for (const id of s.controls || []) if (!lights.has(id)) result2.warnings.push(msg("warn.switchUnknownLamp", { id: s.id, lamp: id }));
19285
19326
  }
@@ -19319,7 +19360,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19319
19360
  seed: seedOf2("panel"),
19320
19361
  circuits: circuits.map((c) => ({ id: c.id, label: c.room }))
19321
19362
  };
19322
- if (!item.circuits.length) delete item.circuits;
19363
+ if (!item.circuits?.length) delete item.circuits;
19323
19364
  const e = entry2("panel", "electrical_panel", item, "panel", q.floor);
19324
19365
  push(e);
19325
19366
  result2.panel = e.id;
@@ -19341,10 +19382,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19341
19382
  }
19342
19383
  }
19343
19384
  const piped = result2.items.filter((e) => e.type === "stove" && e.connections.gas === "piped");
19344
- if (piped.length) {
19385
+ const firstPiped = piped[0];
19386
+ if (firstPiped) {
19345
19387
  const med = utilities.gas && utilities.gas.meter;
19346
- const ref = med ? { x: K.x(med.u), z: K.z(med.v), y: med.y || 1 } : { x: piped[0].item.x, z: piped[0].item.z, y: 0.3 };
19347
- const e = entry2("piped_gas", "gas_piped", { x: ref.x, z: ref.z, ry: 0, y: ref.y, seed: seedOf2("piped_gas") }, "piped_gas", piped[0].floor);
19388
+ const ref = med ? { x: K.x(med.u), z: K.z(med.v), y: med.y || 1 } : { x: firstPiped.item.x, z: firstPiped.item.z, y: 0.3 };
19389
+ const e = entry2("piped_gas", "gas_piped", { x: ref.x, z: ref.z, ry: 0, y: ref.y, seed: seedOf2("piped_gas") }, "piped_gas", firstPiped.floor);
19348
19390
  e.floorY = o.base;
19349
19391
  push(e);
19350
19392
  for (const f of piped) f.connections.fuel = "piped_gas:gas";
@@ -19391,9 +19433,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19391
19433
  for (const dv of spec.devices || []) if (L.outlet && dv.socket === "auto") dv.socket = L.outlet + ":socket";
19392
19434
  } else {
19393
19435
  const sh = L.shower || {};
19394
- const heater = (spec.devices || []).find((d) => d.id === "heater");
19436
+ const devices = spec.devices || [];
19437
+ const heater = devices.find((d) => d.id === "heater");
19395
19438
  if (heater && sh.electric === false) {
19396
- spec.devices = spec.devices.filter((d) => d !== heater);
19439
+ spec.devices = devices.filter((d) => d !== heater);
19397
19440
  for (const f of spec.fixtures || []) if (f.heater === "heater") delete f.heater;
19398
19441
  } else if (heater) {
19399
19442
  if (sh.powerW) heater.watts = sh.powerW;
@@ -19404,7 +19447,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19404
19447
  for (const b of spec.burners || []) if (b.fuel === "auto") b.fuel = L.fuel;
19405
19448
  }
19406
19449
  if (entry2.type === "fireplace" && L.chimneyTop) {
19407
- const it = entry2.item;
19450
+ const it = at(entry2.item);
19408
19451
  const dx = L.chimneyTop[0] - it.x;
19409
19452
  const dz = L.chimneyTop[2] - it.z;
19410
19453
  const lx = Math.cos(it.ry) * dx - Math.sin(it.ry) * dz;
@@ -19423,8 +19466,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19423
19466
  }
19424
19467
  return spec;
19425
19468
  }
19426
- function createWorld(res, copts = {}) {
19427
- copts = copts || {};
19469
+ function createWorld(source, options2 = {}) {
19470
+ const res = sourceOf(source);
19471
+ const copts = options2 || {};
19428
19472
  const info = { points: /* @__PURE__ */ new Map(), water: copts.water || null, lightsOn: copts.lightsOn };
19429
19473
  const electrical = res.utilities && res.utilities.electrical;
19430
19474
  const world = GI.createWorld({
@@ -19452,8 +19496,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19452
19496
  const info = worlds.get(world);
19453
19497
  return info ? info.points.get(fixtureId) : void 0;
19454
19498
  }
19455
- function blockedOf(res, bopts = {}) {
19456
- bopts = bopts || {};
19499
+ function blockedOf(source, options2 = {}) {
19500
+ const res = sourceOf(source);
19501
+ const bopts = options2 || {};
19457
19502
  const ox = bopts.originX || 0;
19458
19503
  const oz = bopts.originZ || 0;
19459
19504
  const inw = bopts.inw || 1;
@@ -19488,33 +19533,33 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19488
19533
  return false;
19489
19534
  };
19490
19535
  }
19491
- function add2(world, planned, aopts = {}) {
19492
- aopts = aopts || {};
19536
+ function add2(world, planned, options2 = {}) {
19537
+ const aopts = options2 || {};
19493
19538
  const info = worlds.get(world);
19494
- const build = aopts.build || ((type, item) => hh.build(type, item, HH.createRecorder().api));
19539
+ const build = aopts.build || ((type, item) => hh.build(type, item, recorderApi()));
19495
19540
  const isBlocked2 = aopts.isBlocked || null;
19496
19541
  const out = { added: 0, parts: [], hinges: {}, moved: {}, warnings: [] };
19497
19542
  const override = aopts.lightsOn !== void 0 ? aopts.lightsOn : info && info.lightsOn;
19498
19543
  if (override !== void 0) {
19499
19544
  for (const e of planned.items) if (e.type === "lamp") e.lit = typeof override === "function" ? !!override(e.id, e) : !!override;
19500
19545
  for (const e of planned.items) if (e.type === "lantern_lamp") e.lit = typeof override === "function" ? !!override(e.id, e) : !!override;
19501
- for (const e of planned.items) if (e.type === "wall_switch") e.lit = (e.connections.controls || []).some((id) => (planned.items.find((x) => x.id === id) || {}).lit === true);
19546
+ for (const e of planned.items) if (e.type === "wall_switch") e.lit = (e.connections.controls || []).some((id) => planned.items.find((x) => x.id === id)?.lit === true);
19502
19547
  }
19503
19548
  for (const e of planned.items) if (e.type === "lantern_lamp") e.item.on = !!e.lit;
19504
19549
  let scratch2 = null;
19505
- function probe(e, hinge, setback) {
19550
+ function probe(e, hinge, setback, blockedAt) {
19506
19551
  if (!scratch2) scratch2 = GI.createWorld({ locale, strings: hh.strings, items: hh.items, loot: hh.loot, grid: false });
19507
- const it = e.item;
19552
+ const it = at(e.item);
19508
19553
  const x = it.x + Math.sin(it.ry) * setback;
19509
19554
  const z = it.z + Math.cos(it.ry) * setback;
19510
- const spec = hh.build(e.type, Object.assign({}, it, { hinge }), HH.createRecorder().api);
19555
+ const spec = hh.build(e.type, Object.assign({}, e.item, { hinge }), recorderApi());
19511
19556
  scratch2.add("__probe", spec, { x, y: e.floorY, z, ry: it.ry });
19512
- const blocks = (px, pz) => isBlocked2(px, pz, e.floor);
19557
+ const blocks = (px, pz) => blockedAt(px, pz, e.floor);
19513
19558
  let sum2 = 0;
19514
19559
  let min2 = 1;
19515
19560
  let n = 0;
19516
19561
  for (const p of spec.parts || []) {
19517
- if (p.motion.kind !== "hinge" || !(p.bounds || p.sweep)) continue;
19562
+ if (!p.motion || p.motion.kind !== "hinge" || !(p.bounds || p.sweep)) continue;
19518
19563
  const t = scratch2.fitSweep("__probe", p.id, blocks);
19519
19564
  sum2 += t;
19520
19565
  min2 = Math.min(min2, t);
@@ -19526,12 +19571,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19526
19571
  const better = (a, b) => a.sum > b.sum + 1e-9 || Math.abs(a.sum - b.sum) <= 1e-9 && a.min > b.min + 1e-9;
19527
19572
  function fitDoors(e) {
19528
19573
  if (!isBlocked2 || !householdTypes.has(e.type)) return;
19529
- const standard = probe(e, void 0, 0);
19574
+ const standard = probe(e, void 0, 0, isBlocked2);
19530
19575
  if (!standard.n || standard.min >= 1) return;
19531
19576
  let bestSide;
19532
19577
  let m = standard;
19533
19578
  for (const side of ["left", "right"]) {
19534
- const r = probe(e, side, 0);
19579
+ const r = probe(e, side, 0, isBlocked2);
19535
19580
  if (better(r, m)) {
19536
19581
  m = r;
19537
19582
  bestSide = side;
@@ -19544,13 +19589,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19544
19589
  if (m.min >= 1) return;
19545
19590
  let target = { setback: 0, r: m };
19546
19591
  for (const setback of [0.01, 0.02, 0.03, 0.04]) {
19547
- const r = probe(e, bestSide, setback);
19592
+ const r = probe(e, bestSide, setback, isBlocked2);
19548
19593
  if (better(r, target.r)) target = { setback, r };
19549
19594
  if (r.min >= 1) break;
19550
19595
  }
19551
19596
  if (target.setback > 0 && target.r.sum > m.sum + 0.2) {
19552
- e.item.x = r3(e.item.x + Math.sin(e.item.ry) * target.setback);
19553
- e.item.z = r3(e.item.z + Math.cos(e.item.ry) * target.setback);
19597
+ const p = at(e.item);
19598
+ e.item.x = r3(p.x + Math.sin(p.ry) * target.setback);
19599
+ e.item.z = r3(p.z + Math.cos(p.ry) * target.setback);
19554
19600
  e.moved = target.setback;
19555
19601
  out.moved[e.id] = target.setback;
19556
19602
  }
@@ -19573,7 +19619,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19573
19619
  }
19574
19620
  }
19575
19621
  connect(e, spec);
19576
- world.add(e.id, spec, { x: e.item.x, y: e.floorY, z: e.item.z, ry: e.item.ry });
19622
+ const pose = at(e.item);
19623
+ world.add(e.id, spec, { x: pose.x, y: e.floorY, z: pose.z, ry: pose.ry });
19577
19624
  out.added++;
19578
19625
  if (!isBlocked2) continue;
19579
19626
  for (const p of spec.parts || []) {
@@ -22322,7 +22369,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22322
22369
  const typed = listeners.get(e.type);
22323
22370
  if (typed) for (const fn of [...typed]) fn(e);
22324
22371
  };
22325
- const base = (o, source) => source === void 0 ? { object: o.id, time } : { object: o.id, time, source };
22372
+ let blow3;
22373
+ const base = (o, source) => ({
22374
+ object: o.id,
22375
+ time,
22376
+ ...source === void 0 ? {} : { source },
22377
+ ...blow3 === void 0 ? {} : { blow: blow3 }
22378
+ });
22326
22379
  const pendingCollapse = /* @__PURE__ */ new Map();
22327
22380
  let runDepth = 0;
22328
22381
  const drainBlasts = () => {
@@ -24467,7 +24520,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24467
24520
  res.events = cur.slice(start);
24468
24521
  return res;
24469
24522
  };
24470
- const doHit = (o, hit, only) => {
24523
+ const withBlow = (type, fn) => () => {
24524
+ const outer = blow3;
24525
+ blow3 = type;
24526
+ try {
24527
+ return fn();
24528
+ } finally {
24529
+ blow3 = outer;
24530
+ }
24531
+ };
24532
+ const doHit = (o, hit, only) => withBlow(hit.type, () => resolveHit(o, hit, only))();
24533
+ const resolveHit = (o, hit, only) => {
24471
24534
  let point = hit.point;
24472
24535
  let dir = hit.direction;
24473
24536
  if (hit.space === "local") {
@@ -24906,6 +24969,44 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24906
24969
  blastProbe() {
24907
24970
  return { ...blastProbe };
24908
24971
  },
24972
+ blastReach(point, target, options2 = {}) {
24973
+ const L = vec.distance(point, target);
24974
+ if (L < 1e-4) return 1;
24975
+ const shelter = { ...SHELTER_DEFAULTS, ...options2.shelter };
24976
+ const ignore = new Set(options2.ignore ?? []);
24977
+ const d = vec.scale(vec.sub(target, point), 1 / L);
24978
+ let t = 1;
24979
+ for (const o of objects.values()) {
24980
+ if (ignore.has(o.id) || raySphereMiss(point, d, toWorld(o.st.position, o.st.rotation, o.bc), o.br, L)) continue;
24981
+ for (const p of o.parts) {
24982
+ if (isLost(p) || p.st.stripped) continue;
24983
+ const r = rayPart(o, p, point, d);
24984
+ if (!r) continue;
24985
+ const t0 = Math.max(r.t0, 0);
24986
+ const path = Math.min(r.t1, L) - t0;
24987
+ if (path < shelter.minPath) continue;
24988
+ if (p.st.opening && insideOpening(o, p, toLocal(o.st.position, o.st.rotation, vec.madd(point, d, t0 + path / 2)))) continue;
24989
+ t *= Math.exp(-(p.mat.hpPerKg * p.mat.density * path * (p.mat.damageScale?.shelter ?? 1)) / shelter.k);
24990
+ if (t < 1e-4) return 0;
24991
+ }
24992
+ }
24993
+ for (const im of impls.values()) {
24994
+ if (ignore.has(im.impl.id)) continue;
24995
+ for (const ps of im.impl.describe().parts) {
24996
+ const mat = materials.get(ps.material);
24997
+ if (!mat) continue;
24998
+ const ov = im.poses.get(ps.id);
24999
+ const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), point, d, ov?.pos);
25000
+ if (!r || r.t1 < 0 || r.t0 > L) continue;
25001
+ let path = Math.min(r.t1, L) - Math.max(r.t0, 0);
25002
+ if (ps.thickness !== void 0 && ps.thickness > 0) path = Math.min(path, 2 * ps.thickness);
25003
+ else if (path < shelter.minPath) continue;
25004
+ t *= Math.exp(-(mat.hpPerKg * mat.density * path * (mat.damageScale?.shelter ?? 1)) / shelter.k);
25005
+ if (t < 1e-4) return 0;
25006
+ }
25007
+ }
25008
+ return t;
25009
+ },
24909
25010
  explode(point, energy, radius, source, blast) {
24910
25011
  blastProbe.partsTested = 0;
24911
25012
  blastProbe.partsNear = 0;
@@ -24930,7 +25031,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24930
25031
  },
24931
25032
  shoot(origin, direction, energy, caliber, source, round5) {
24932
25033
  const extras = typeof round5 === "number" ? { piercing: round5 } : round5 ?? {};
24933
- return withCollapses(run(() => {
25034
+ return withCollapses(run(withBlow("ballistic", () => {
24934
25035
  const results = [];
24935
25036
  const d = vec.normalize(direction);
24936
25037
  const ignore = /* @__PURE__ */ new Set();
@@ -24941,10 +25042,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24941
25042
  if (!rh) break;
24942
25043
  const im = impls.get(rh.object);
24943
25044
  if (im) {
24944
- const r = implHit(im, { type: "ballistic", point: rh.point, part: rh.part, normal: rh.normal, direction: d, energy: E, caliber, ...extras, ...source !== void 0 ? { source } : {} });
25045
+ const scaled2 = extras.penetration && E < energy ? { ...extras, penetration: Object.fromEntries(Object.entries(extras.penetration).map(([k, mm]) => [k, mm * (E / energy)])) } : extras;
25046
+ const r = implHit(im, { type: "ballistic", point: rh.point, part: rh.part, normal: rh.normal, direction: d, energy: E, caliber, ...scaled2, ...source !== void 0 ? { source } : {} });
24945
25047
  r.part = rh.part;
24946
25048
  r.point = rh.point;
24947
- r.exitEnergy = r.penetrated ? E * 0.5 : 0;
25049
+ const left = r.events.find((e) => e.type === "hole" && e.energyLeft !== void 0);
25050
+ r.exitEnergy = r.penetrated ? left?.energyLeft ?? E * 0.5 : 0;
24948
25051
  results.push(r);
24949
25052
  ignore.add(`${im.impl.id}\0${rh.part}`);
24950
25053
  if (!r.penetrated) break;
@@ -24963,7 +25066,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24963
25066
  E = out.exitEnergy;
24964
25067
  }
24965
25068
  return results;
24966
- }));
25069
+ })));
24967
25070
  },
24968
25071
  disassemble(objectId, jointId, tool2, dt) {
24969
25072
  const im = impls.get(objectId);
@@ -25772,8 +25875,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
25772
25875
  function linesOf(boxes) {
25773
25876
  return boxes.map(([u0, v0, u1, v1]) => u1 - u0 >= v1 - v0 ? { dir: "u", at: (v0 + v1) / 2, from: u0, to: u1 } : { dir: "v", at: (u0 + u1) / 2, from: v0, to: v1 });
25774
25877
  }
25775
- function coverage(lines, dir, at22, from, to, options) {
25776
- const spans = lines.filter((line) => line.dir === dir && Math.abs(line.at - at22) <= options.lineTolerance).map((line) => [Math.max(from, line.from), Math.min(to, line.to)]).filter(([a, b]) => b > a).sort((x, y) => x[0] - y[0]);
25878
+ function coverage(lines, dir, at23, from, to, options) {
25879
+ const spans = lines.filter((line) => line.dir === dir && Math.abs(line.at - at23) <= options.lineTolerance).map((line) => [Math.max(from, line.from), Math.min(to, line.to)]).filter(([a, b]) => b > a).sort((x, y) => x[0] - y[0]);
25777
25880
  let covered = 0;
25778
25881
  let end3 = from;
25779
25882
  let start = NaN;
@@ -25795,12 +25898,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
25795
25898
  const sorted = lines.filter((line) => line.dir === dir).map((line) => line.at).sort((a, b) => a - b);
25796
25899
  const out = [];
25797
25900
  let group = [];
25798
- for (const at22 of sorted) {
25799
- if (group.length && at22 - group[group.length - 1] > tolerance) {
25901
+ for (const at23 of sorted) {
25902
+ if (group.length && at23 - group[group.length - 1] > tolerance) {
25800
25903
  out.push(group.reduce((s, x) => s + x, 0) / group.length);
25801
25904
  group = [];
25802
25905
  }
25803
- group.push(at22);
25906
+ group.push(at23);
25804
25907
  }
25805
25908
  if (group.length) out.push(group.reduce((s, x) => s + x, 0) / group.length);
25806
25909
  return out;
@@ -25813,13 +25916,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
25813
25916
  const tooWide = width > options.maxSpan + EPS5;
25814
25917
  const tooDeep = depth > options.maxSpan + EPS5;
25815
25918
  const candidates = [];
25816
- for (const at22 of positions(lines, "v", options.lineTolerance)) {
25817
- if (at22 < u0 + options.minPanel - EPS5 || at22 > u1 - options.minPanel + EPS5) continue;
25818
- candidates.push({ dir: "v", at: at22, coverage: coverage(lines, "v", at22, v0, v1, options), skew: Math.abs(at22 - (u0 + u1) / 2) / (width / 2) });
25919
+ for (const at23 of positions(lines, "v", options.lineTolerance)) {
25920
+ if (at23 < u0 + options.minPanel - EPS5 || at23 > u1 - options.minPanel + EPS5) continue;
25921
+ candidates.push({ dir: "v", at: at23, coverage: coverage(lines, "v", at23, v0, v1, options), skew: Math.abs(at23 - (u0 + u1) / 2) / (width / 2) });
25819
25922
  }
25820
- for (const at22 of positions(lines, "u", options.lineTolerance)) {
25821
- if (at22 < v0 + options.minPanel - EPS5 || at22 > v1 - options.minPanel + EPS5) continue;
25822
- candidates.push({ dir: "u", at: at22, coverage: coverage(lines, "u", at22, u0, u1, options), skew: Math.abs(at22 - (v0 + v1) / 2) / (depth / 2) });
25923
+ for (const at23 of positions(lines, "u", options.lineTolerance)) {
25924
+ if (at23 < v0 + options.minPanel - EPS5 || at23 > v1 - options.minPanel + EPS5) continue;
25925
+ candidates.push({ dir: "u", at: at23, coverage: coverage(lines, "u", at23, u0, u1, options), skew: Math.abs(at23 - (v0 + v1) / 2) / (depth / 2) });
25823
25926
  }
25824
25927
  const boundary = candidates.filter((c) => c.coverage >= options.lineCoverage);
25825
25928
  if (tooWide || tooDeep) {
@@ -25905,20 +26008,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
25905
26008
  const held = merged(supports.map(([a, b]) => [Math.max(0, a), Math.min(length2, b)]).filter(([a, b]) => b > a));
25906
26009
  if (!held.length) return { span: Infinity, at: length2 / 2, lo: 0, hi: length2 };
25907
26010
  let span = 0;
25908
- let at22 = length2 / 2;
26011
+ let at23 = length2 / 2;
25909
26012
  let lo = 0;
25910
26013
  let hi = length2;
25911
26014
  const lead = held[0][0];
25912
26015
  if (2 * lead > span) {
25913
26016
  span = 2 * lead;
25914
- at22 = 0;
26017
+ at23 = 0;
25915
26018
  lo = 0;
25916
26019
  hi = lead;
25917
26020
  }
25918
26021
  const trail = length2 - held[held.length - 1][1];
25919
26022
  if (2 * trail > span) {
25920
26023
  span = 2 * trail;
25921
- at22 = length2;
26024
+ at23 = length2;
25922
26025
  lo = length2 - trail;
25923
26026
  hi = length2;
25924
26027
  }
@@ -25926,12 +26029,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
25926
26029
  const gap = held[k][0] - held[k - 1][1];
25927
26030
  if (gap > span) {
25928
26031
  span = gap;
25929
- at22 = (held[k][0] + held[k - 1][1]) / 2;
26032
+ at23 = (held[k][0] + held[k - 1][1]) / 2;
25930
26033
  lo = held[k - 1][1];
25931
26034
  hi = held[k][0];
25932
26035
  }
25933
26036
  }
25934
- return { span, at: at22, lo, hi };
26037
+ return { span, at: at23, lo, hi };
25935
26038
  }
25936
26039
  var lineOf = (aabb, dir) => dir === "u" ? { dir, at: (aabb[1] + aabb[3]) / 2, from: aabb[0], to: aabb[2] } : { dir, at: (aabb[0] + aabb[2]) / 2, from: aabb[1], to: aabb[3] };
25937
26040
  var pointOf = (line, s) => line.dir === "u" ? [line.from + s, line.at] : [line.at, line.from + s];
@@ -25987,37 +26090,37 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
25987
26090
  const side = options.columnSide;
25988
26091
  const all = () => [...props, ...spots.map((spot) => ({ box: boxOfSpot(spot, side), dist: spot.dist }))];
25989
26092
  const allowed = merged(ground.map((box22) => holds2(line, 0.02, box22)).filter((x) => x !== void 0));
25990
- const place = (at22, lo, hi) => {
25991
- if (!allowed.length) return at22;
26093
+ const place = (at23, lo, hi) => {
26094
+ if (!allowed.length) return at23;
25992
26095
  let best;
25993
26096
  let gap = Infinity;
25994
26097
  for (const [a, b] of allowed) {
25995
26098
  const from = Math.max(a + side / 2, lo + 0.2);
25996
26099
  const to = Math.min(b - side / 2, hi - 0.2);
25997
26100
  if (to < from) continue;
25998
- const clamped = Math.min(to, Math.max(from, at22));
25999
- if (Math.abs(clamped - at22) < gap) {
26000
- gap = Math.abs(clamped - at22);
26101
+ const clamped = Math.min(to, Math.max(from, at23));
26102
+ if (Math.abs(clamped - at23) < gap) {
26103
+ gap = Math.abs(clamped - at23);
26001
26104
  best = clamped;
26002
26105
  }
26003
26106
  }
26004
- return best ?? at22;
26107
+ return best ?? at23;
26005
26108
  };
26006
26109
  let held = heldBy(line, width, all()).intervals;
26007
26110
  for (let guard = 0; guard < 64; guard++) {
26008
- let at22;
26111
+ let at23;
26009
26112
  let lo = 0;
26010
26113
  let hi = length2;
26011
- if (ends && !held.some(([a]) => a <= END_REACH)) at22 = 0;
26012
- else if (ends && !held.some(([, b]) => b >= length2 - END_REACH)) at22 = length2;
26114
+ if (ends && !held.some(([a]) => a <= END_REACH)) at23 = 0;
26115
+ else if (ends && !held.some(([, b]) => b >= length2 - END_REACH)) at23 = length2;
26013
26116
  else {
26014
26117
  const found = beamSpan(length2, held);
26015
26118
  if (found.span <= options.maxSpan + 1e-3) break;
26016
- at22 = found.at;
26119
+ at23 = found.at;
26017
26120
  lo = found.lo;
26018
26121
  hi = found.hi;
26019
26122
  }
26020
- const [u, v] = pointOf(line, at22 === 0 || at22 === length2 ? at22 : place(at22, lo, hi));
26123
+ const [u, v] = pointOf(line, at23 === 0 || at23 === length2 ? at23 : place(at23, lo, hi));
26021
26124
  const spot = { u: round2(u), v: round2(v), dist: dist2 };
26022
26125
  if (spots.some((other) => Math.hypot(other.u - spot.u, other.v - spot.v) < 0.15)) break;
26023
26126
  spots.push(spot);
@@ -26322,7 +26425,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26322
26425
  if (!length2) return [[from, to]];
26323
26426
  const offset = spec.bond === "running" && row % 2 === 1 ? length2 / 2 : 0;
26324
26427
  const cuts = [from];
26325
- for (let at22 = from + (offset || length2); at22 < to - MIN_UNIT; at22 += length2) cuts.push(at22);
26428
+ for (let at23 = from + (offset || length2); at23 < to - MIN_UNIT; at23 += length2) cuts.push(at23);
26326
26429
  cuts.push(to);
26327
26430
  return cuts.slice(0, -1).map((start, k) => [start, cuts[k + 1]]).filter(([a, b]) => b - a > MIN_UNIT);
26328
26431
  }
@@ -26733,9 +26836,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26733
26836
  const w = s.length / n;
26734
26837
  for (let k = 0; k < n; k++) {
26735
26838
  const id = `glass_${f}_${s.name}_${k}`;
26736
- const at22 = -s.length / 2 + (k + 0.5) * w;
26839
+ const at23 = -s.length / 2 + (k + 0.5) * w;
26737
26840
  const size = s.axis === 2 ? [w - 0.01, h - 0.01, c.thickness] : [c.thickness, h - 0.01, w - 0.01];
26738
- const position = s.axis === 2 ? [at22, yb + h / 2, s.fixed] : [s.fixed, yb + h / 2, at22];
26841
+ const position = s.axis === 2 ? [at23, yb + h / 2, s.fixed] : [s.fixed, yb + h / 2, at23];
26739
26842
  parts.push({
26740
26843
  id,
26741
26844
  material: c.material,
@@ -26774,10 +26877,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26774
26877
  const clear = length2 - g.columnSize;
26775
26878
  const win = clear * c.window;
26776
26879
  const pier = (clear - win) / 2;
26777
- const at22 = (u) => along === 0 ? [u, 0, fixed2] : [fixed2, 0, u];
26880
+ const at23 = (u) => along === 0 ? [u, 0, fixed2] : [fixed2, 0, u];
26778
26881
  const size = (u, h) => along === 0 ? [u, h, c.thickness] : [c.thickness, h, u];
26779
26882
  const put = (name, u, h, y, centreU, on) => {
26780
- const p = at22(centreU);
26883
+ const p = at23(centreU);
26781
26884
  wall(`infill_${f}_${side}_${k}_${name}`, size(u, h), [p[0], y, p[2]], on);
26782
26885
  };
26783
26886
  const id = (name) => `infill_${f}_${side}_${k}_${name}`;
@@ -26785,7 +26888,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26785
26888
  put("pl", pier, head - sill, yb + sill + (head - sill) / 2, centre - clear / 2 + pier / 2, [c1, id("sill")]);
26786
26889
  put("pr", pier, head - sill, yb + sill + (head - sill) / 2, centre + clear / 2 - pier / 2, [c2, id("sill")]);
26787
26890
  put("head", clear, room - head, yb + head + (room - head) / 2, centre, [c1, c2]);
26788
- const gp = at22(centre);
26891
+ const gp = at23(centre);
26789
26892
  const gid = `glass_${f}_${side}_${k}`;
26790
26893
  parts.push({
26791
26894
  id: gid,
@@ -26847,13 +26950,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26847
26950
  });
26848
26951
  rests(joints, "roof_membrane", top, "glue");
26849
26952
  for (let k = 0; k < count; k++) {
26850
- const at22 = -long / 2 + (k + 0.5) * spacing;
26953
+ const at23 = -long / 2 + (k + 0.5) * spacing;
26851
26954
  const id = `roof_drain_${k}`;
26852
26955
  parts.push({
26853
26956
  id,
26854
26957
  material: "plastic",
26855
26958
  shape: { kind: "cylinder", radius: 0.05, height: 0.1 },
26856
- position: longX ? [slab.position[0] + at22, my, slab.position[2]] : [slab.position[0], my, slab.position[2] + at22],
26959
+ position: longX ? [slab.position[0] + at23, my, slab.position[2]] : [slab.position[0], my, slab.position[2] + at23],
26857
26960
  fill: 0.2,
26858
26961
  tags: ["drain"]
26859
26962
  });
@@ -26874,11 +26977,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26874
26977
  const floor2 = `slab_${level - 1}`;
26875
26978
  const y = storeyBase(g, level);
26876
26979
  const shafts = options.shafts ?? [];
26877
- const put = (id, material, size, at22, mass, extra = {}) => {
26878
- parts.push({ id: id + sfx, material, shape: box2(size), position: [at22[0], y + size[1] / 2, at22[1]], mass, fill: 0.2, structure: { role: "partition" }, tags: ["mechanical"], ...extra });
26980
+ const put = (id, material, size, at23, mass, extra = {}) => {
26981
+ parts.push({ id: id + sfx, material, shape: box2(size), position: [at23[0], y + size[1] / 2, at23[1]], mass, fill: 0.2, structure: { role: "partition" }, tags: ["mechanical"], ...extra });
26879
26982
  rests(joints, id + sfx, floor2, "bolt");
26880
26983
  };
26881
- shafts.forEach((at22, k) => put(`machine_${k}`, "steel", [1.2, 1, 0.8], at22, c.machineMass + c.suspendedMass, { tags: ["mechanical", "elevator_machine"] }));
26984
+ shafts.forEach((at23, k) => put(`machine_${k}`, "steel", [1.2, 1, 0.8], at23, c.machineMass + c.suspendedMass, { tags: ["mechanical", "elevator_machine"] }));
26882
26985
  const liters = options.waterLiters ?? towerWater(level, options);
26883
26986
  const depth = 2;
26884
26987
  const side = Math.sqrt(liters / 1e3 / depth);
@@ -26919,14 +27022,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26919
27022
  return n === void 0 ? want : Math.round((want + span / 2) / (span / k)) * (span / k) - span / 2;
26920
27023
  };
26921
27024
  const colZ = column(g.spanZ, g.columnsZ, g.spanZ * 0.35);
26922
- const at22 = options.at ?? [column(g.spanX, g.columnsX, 0), colZ - Math.sign(colZ || 1) * (g.columnSize / 2 + 0.1)];
27025
+ const at23 = options.at ?? [column(g.spanX, g.columnsX, 0), colZ - Math.sign(colZ || 1) * (g.columnSize / 2 + 0.1)];
26923
27026
  const y = storeyBase(g, g.floors - 1);
26924
27027
  const floor2 = `slab_${g.floors - 2}`;
26925
27028
  const part = {
26926
27029
  id: "damper",
26927
27030
  material: c.material,
26928
27031
  shape: box2([side, height, side]),
26929
- position: [at22[0], y + height / 2, at22[1]],
27032
+ position: [at23[0], y + height / 2, at23[1]],
26930
27033
  mass,
26931
27034
  structure: { role: "partition" },
26932
27035
  tags: ["mechanical", "tuned_mass_damper"],
@@ -35428,8 +35531,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
35428
35531
  facadeExtra: (f, rng2, w, d, op) => {
35429
35532
  f.roof = "slab";
35430
35533
  f.finish = rng2() < 0.55 ? "exposed_block" : "partial_plaster";
35431
- f.slab = { exposedRebar: true, waterTank: { u: r22(0.5 + rng2() * (w - 1.5)), v: r22(1 + rng2() * (d - 2)) }, annex: rng2() < 0.55 ? { rect: [0, 0, r22(w * (0.5 + rng2() * 0.3)), r22(d * 0.4)], state: "exposed_brick" } : null };
35432
- if (floorsOf(op) > 1) f.slab.exposedRebar = rng2() < 0.6;
35534
+ const slab = { exposedRebar: true, waterTank: { u: r22(0.5 + rng2() * (w - 1.5)), v: r22(1 + rng2() * (d - 2)) }, annex: rng2() < 0.55 ? { rect: [0, 0, r22(w * (0.5 + rng2() * 0.3)), r22(d * 0.4)], state: "exposed_brick" } : null };
35535
+ f.slab = slab;
35536
+ if (floorsOf(op) > 1) slab.exposedRebar = rng2() < 0.6;
35433
35537
  }
35434
35538
  };
35435
35539
  }
@@ -36789,7 +36893,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
36789
36893
  };
36790
36894
  var accessoryTextOf = (locale) => ACCESSORY_TEXT[locale] ?? ACCESSORY_TEXT.en;
36791
36895
 
36792
- // ../game-audio/dist/chunk-DZITVFMJ.js
36896
+ // ../game-audio/dist/chunk-7TLFY7VO.js
36793
36897
  var DISTANCE_CURVES = {
36794
36898
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
36795
36899
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -36869,15 +36973,31 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
36869
36973
  {
36870
36974
  id: "game",
36871
36975
  master: 0.5,
36872
- noise: { seconds: 1.5, create: "lazy" },
36873
- sources: ["jogo:L62077-62081 master 0.5 to the output", "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"]
36976
+ noise: {
36977
+ seconds: 1.5,
36978
+ create: "lazy"
36979
+ },
36980
+ sources: [
36981
+ "jogo:L62077-62081 master 0.5 to the output",
36982
+ "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"
36983
+ ]
36874
36984
  },
36875
36985
  {
36876
36986
  id: "art_scene",
36877
36987
  master: 0.85,
36878
- compressor: { threshold: -12, ratio: 5 },
36879
- reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
36880
- noise: { seconds: 2, create: "eager" },
36988
+ compressor: {
36989
+ threshold: -12,
36990
+ ratio: 5
36991
+ },
36992
+ reverb: {
36993
+ seconds: 2.2,
36994
+ decay: 3.4,
36995
+ wet: 0.4
36996
+ },
36997
+ noise: {
36998
+ seconds: 2,
36999
+ create: "eager"
37000
+ },
36881
37001
  sources: [
36882
37002
  "cena:L1308 compressor -12 dB, ratio 5",
36883
37003
  "cena:L1309 master 0.85",
@@ -36885,12 +37005,36 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
36885
37005
  "cena:L1311 wet 0.4",
36886
37006
  "cena:L1312 noise 2 s made with the chain"
36887
37007
  ]
37008
+ },
37009
+ {
37010
+ id: "kit",
37011
+ master: 0.85,
37012
+ compressor: {
37013
+ threshold: -12,
37014
+ ratio: 5
37015
+ },
37016
+ reverb: {
37017
+ seconds: 2.2,
37018
+ decay: 3.4,
37019
+ wet: 0.4
37020
+ },
37021
+ noise: {
37022
+ seconds: 2,
37023
+ create: "eager"
37024
+ },
37025
+ sources: [
37026
+ "sdk: the art scene chain (cena L1308-L1312) with a limiter after the compressor: the page has none, and a blast with its debris and a fire catching reached +7 dBFS"
37027
+ ],
37028
+ limiter: {
37029
+ ceiling: -1,
37030
+ knee: -6
37031
+ }
36888
37032
  }
36889
37033
  ];
36890
37034
  var MIX_PROFILES = Object.fromEntries(
36891
37035
  mix_profiles_default.map((profile) => [profile.id, profile])
36892
37036
  );
36893
- var DEFAULT_MIX = "art_scene";
37037
+ var DEFAULT_MIX = "kit";
36894
37038
  function resolveMix(mix3, profiles = MIX_PROFILES) {
36895
37039
  if (typeof mix3 !== "string") return mix3;
36896
37040
  const profile = profiles[mix3];
@@ -36911,6 +37055,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
36911
37055
  "jogo:L66986 one groan every 0.25 s (`groanBudget`), only nearer than 40 m",
36912
37056
  "jogo:L70472 the budget counts down by the frame time"
36913
37057
  ]
37058
+ },
37059
+ fire_ignite: {
37060
+ maxConcurrent: 2,
37061
+ cooldown: 0.15,
37062
+ sources: [
37063
+ "sdk: many parts catching at once (a blast sets a shelf alight) are one or two ignitions, not forty"
37064
+ ]
37065
+ },
37066
+ fire_crackle: {
37067
+ maxConcurrent: 4,
37068
+ sources: [
37069
+ "sdk: the crackles of every fire share four voices"
37070
+ ]
36914
37071
  }
36915
37072
  };
36916
37073
  var VOICE_GROUPS = voice_groups_default;
@@ -37002,6 +37159,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37002
37159
  const sounds = /* @__PURE__ */ new Map();
37003
37160
  const loops = /* @__PURE__ */ new Map();
37004
37161
  const sets = /* @__PURE__ */ new Map();
37162
+ const samples = /* @__PURE__ */ new Map();
37005
37163
  const owners = /* @__PURE__ */ new Map();
37006
37164
  const registry = {
37007
37165
  register(bank) {
@@ -37018,25 +37176,34 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37018
37176
  sets.set(set2.id, set2);
37019
37177
  owners.set(set2.id, bank.id);
37020
37178
  }
37179
+ for (const sample of bank.samples ?? []) {
37180
+ samples.set(sample.id, sample);
37181
+ owners.set(sample.id, bank.id);
37182
+ }
37021
37183
  },
37022
37184
  patch(id, patch) {
37023
37185
  const sound3 = sounds.get(id);
37024
37186
  if (sound3) sounds.set(id, mergeRecords(sound3, patch));
37025
37187
  const loop = loops.get(id);
37026
37188
  if (loop) loops.set(id, mergeRecords(loop, patch));
37189
+ const sample = samples.get(id);
37190
+ if (sample) samples.set(id, mergeRecords(sample, patch));
37027
37191
  },
37028
37192
  remove(id) {
37029
37193
  sounds.delete(id);
37030
37194
  loops.delete(id);
37031
37195
  sets.delete(id);
37196
+ samples.delete(id);
37032
37197
  owners.delete(id);
37033
37198
  },
37034
37199
  sound: (id) => sounds.get(id),
37035
37200
  loop: (id) => loops.get(id),
37036
37201
  set: (id) => sets.get(id),
37202
+ sample: (id) => samples.get(id),
37037
37203
  soundIds: () => [...sounds.keys()],
37038
37204
  setIds: () => [...sets.keys()],
37039
37205
  loopIds: () => [...loops.keys()],
37206
+ sampleIds: () => [...samples.keys()],
37040
37207
  bankOf: (id) => owners.get(id)
37041
37208
  };
37042
37209
  for (const bank of banks) registry.register(bank);
@@ -37291,7 +37458,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37291
37458
  channels.forEach((samples, channel) => buffer.getChannelData(channel).set(samples));
37292
37459
  return buffer;
37293
37460
  }
37461
+ var LIMITER_POINTS = 4097;
37462
+ function limiterCurve(ceilingDb, kneeDb, range3, points = LIMITER_POINTS) {
37463
+ const ceiling = 10 ** (ceilingDb / 20);
37464
+ const knee = Math.min(10 ** (kneeDb / 20), ceiling);
37465
+ const curve = new Float32Array(new ArrayBuffer(points * 4));
37466
+ for (let i = 0; i < points; i++) {
37467
+ const x = (i / (points - 1) * 2 - 1) * range3;
37468
+ const a = Math.abs(x);
37469
+ const y = a <= knee || ceiling === knee ? Math.min(a, ceiling) : knee + (ceiling - knee) * Math.tanh((a - knee) / (ceiling - knee));
37470
+ curve[i] = Math.sign(x) * y;
37471
+ }
37472
+ return curve;
37473
+ }
37294
37474
  function buildChain(context, profile, random) {
37475
+ let limiter;
37476
+ if (profile.limiter) {
37477
+ const range3 = profile.limiter.range ?? 4;
37478
+ const input = context.createGain();
37479
+ input.gain.value = 1 / range3;
37480
+ const shaper = context.createWaveShaper();
37481
+ shaper.curve = limiterCurve(profile.limiter.ceiling, profile.limiter.knee, range3);
37482
+ input.connect(shaper);
37483
+ shaper.connect(context.destination);
37484
+ limiter = { input, shaper };
37485
+ }
37486
+ const output = limiter?.input ?? context.destination;
37295
37487
  let compressor;
37296
37488
  if (profile.compressor) {
37297
37489
  compressor = context.createDynamicsCompressor();
@@ -37300,17 +37492,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37300
37492
  if (profile.compressor.knee !== void 0) compressor.knee.value = profile.compressor.knee;
37301
37493
  if (profile.compressor.attack !== void 0) compressor.attack.value = profile.compressor.attack;
37302
37494
  if (profile.compressor.release !== void 0) compressor.release.value = profile.compressor.release;
37303
- compressor.connect(context.destination);
37495
+ compressor.connect(output);
37304
37496
  }
37305
37497
  const master = context.createGain();
37306
37498
  master.gain.value = profile.master;
37307
- master.connect(compressor ?? context.destination);
37499
+ master.connect(compressor ?? output);
37308
37500
  if (!profile.reverb) {
37309
37501
  return {
37310
37502
  master,
37311
37503
  disconnect: () => {
37312
37504
  master.disconnect();
37313
37505
  compressor?.disconnect();
37506
+ limiter?.input.disconnect();
37507
+ limiter?.shaper.disconnect();
37314
37508
  }
37315
37509
  };
37316
37510
  }
@@ -37329,6 +37523,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37329
37523
  convolver.disconnect();
37330
37524
  master.disconnect();
37331
37525
  compressor?.disconnect();
37526
+ limiter?.input.disconnect();
37527
+ limiter?.shaper.disconnect();
37332
37528
  }
37333
37529
  };
37334
37530
  }
@@ -37338,7 +37534,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37338
37534
  return Constructor ? new Constructor() : null;
37339
37535
  }
37340
37536
  function unlockContext(context) {
37341
- if (context.state === "suspended") void context.resume();
37537
+ if (context.state === "suspended" && typeof context.close === "function") void context.resume();
37342
37538
  }
37343
37539
  var LOOP_START_ABOVE = 2e-3;
37344
37540
  var LOOP_SMOOTHING = { volume: 0.15, frequency: 0.08 };
@@ -37410,9 +37606,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37410
37606
  }
37411
37607
  };
37412
37608
  }
37609
+ function outputStage(recipe2, target, gain) {
37610
+ if (!recipe2.pan) {
37611
+ gain.connect(target.output);
37612
+ return { out: gain };
37613
+ }
37614
+ const panner = target.context.createStereoPanner();
37615
+ gain.connect(panner);
37616
+ panner.connect(target.output);
37617
+ return { out: panner, panner };
37618
+ }
37413
37619
  var fixed = (range3) => typeof range3 === "number" ? range3 : 0;
37414
37620
  var fixedFrequency = (automation) => fixed("set" in automation ? automation.set : automation.from);
37415
- function sourceOf(recipe2, target) {
37621
+ function sourceOf2(recipe2, target) {
37416
37622
  if (recipe2.source.kind === "noise") return { source: noiseSource(target, true) };
37417
37623
  const oscillator = target.context.createOscillator();
37418
37624
  oscillator.type = recipe2.source.wave;
@@ -37436,8 +37642,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37436
37642
  gain.gain.value = 0;
37437
37643
  const last = filters.reduce((previous, filter) => previous ? previous.connect(filter) : filter, void 0);
37438
37644
  last?.connect(gain);
37439
- gain.connect(target.output);
37440
- const { source, frequency } = sourceOf(recipe2, target);
37645
+ const output = outputStage(recipe2, target, gain);
37646
+ const { source, frequency } = sourceOf2(recipe2, target);
37441
37647
  recipe2.filters?.bank.forEach((spec, index) => {
37442
37648
  const filter = filters[index];
37443
37649
  filter.type = spec.type;
@@ -37463,11 +37669,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37463
37669
  }
37464
37670
  source.connect(filters[0] ?? gain);
37465
37671
  source.start();
37466
- return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll(sources) };
37672
+ return { gain, ...output, source, ...frequency ? { frequency } : {}, ...filters[0] ? { cutoff: filters[0].frequency } : {}, stop: stopAll(sources) };
37467
37673
  }
37468
37674
  function realizeChainLoop(recipe2, target, volume) {
37469
37675
  const { context } = target;
37470
- const { source, frequency } = sourceOf(recipe2, target);
37676
+ const { source, frequency } = sourceOf2(recipe2, target);
37471
37677
  const filters = (recipe2.filters?.bank ?? []).map(
37472
37678
  (spec) => filterNode(context, { type: spec.type, frequency: { value: fixedFrequency(spec.frequency) }, ...spec.q === void 0 ? {} : { q: fixed(spec.q) } }, target.time)
37473
37679
  );
@@ -37484,10 +37690,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37484
37690
  let tail = source;
37485
37691
  for (const filter of filters) tail = tail.connect(filter);
37486
37692
  tail.connect(gain);
37487
- gain.connect(target.output);
37693
+ const output = outputStage(recipe2, target, gain);
37488
37694
  source.start();
37489
37695
  for (const lfo of lfos) lfo.start();
37490
- return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll([source, ...lfos]) };
37696
+ return { gain, ...output, source, ...frequency ? { frequency } : {}, ...filters[0] ? { cutoff: filters[0].frequency } : {}, stop: stopAll([source, ...lfos]) };
37491
37697
  }
37492
37698
  var INERT = { set() {
37493
37699
  }, stop() {
@@ -37505,12 +37711,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37505
37711
  return nodes !== void 0;
37506
37712
  },
37507
37713
  rewire() {
37508
- nodes?.gain.disconnect();
37509
- nodes?.gain.connect(host.output(loopRecipe.bus));
37714
+ nodes?.out.disconnect();
37715
+ nodes?.out.connect(host.output(loopRecipe.bus));
37510
37716
  },
37511
37717
  stop() {
37512
37718
  nodes?.stop();
37513
- nodes?.gain.disconnect();
37719
+ nodes?.out.disconnect();
37514
37720
  nodes = void 0;
37515
37721
  state = createLoopState(loopRecipe);
37516
37722
  host.unregister(entry2);
@@ -37530,16 +37736,250 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37530
37736
  state = createLoopState({ ...loopRecipe, volume: opening }, true);
37531
37737
  };
37532
37738
  if (isChain) open();
37739
+ let position;
37740
+ let cutoff;
37741
+ const placed = loopRecipe.falloff !== void 0 || loopRecipe.pan === true;
37533
37742
  return {
37534
37743
  set(next) {
37535
37744
  if (isChain && !nodes) open();
37536
37745
  const context = host.ready();
37537
37746
  if (!context) return;
37747
+ position = next.position ?? position;
37748
+ cutoff = next.cutoff ?? cutoff;
37538
37749
  const action = state.set(next);
37539
37750
  if (!action) return;
37540
37751
  nodes ?? (nodes = create4(context, 0));
37541
- nodes.gain.gain.setTargetAtTime(action.volume, context.currentTime, smoothing.volume);
37752
+ const listener = placed && position ? host.listener?.() : void 0;
37753
+ const distance3 = listener && position ? listenerDistance(listener, position, host.distance) : 0;
37754
+ const level = listener ? falloffGain(loopRecipe.falloff, action.volume, distance3) : action.volume;
37755
+ nodes.gain.gain.setTargetAtTime(level, context.currentTime, smoothing.volume);
37542
37756
  if (action.frequency !== void 0 && nodes.frequency) nodes.frequency.setTargetAtTime(action.frequency, context.currentTime, smoothing.frequency);
37757
+ if (cutoff !== void 0 && nodes.cutoff) nodes.cutoff.setTargetAtTime(cutoff / cutoffDivisor(loopRecipe.falloff, distance3), context.currentTime, smoothing.frequency);
37758
+ if (listener && position && nodes.panner) nodes.panner.pan.setTargetAtTime(panFor(listener, position), context.currentTime, smoothing.frequency);
37759
+ },
37760
+ stop: entry2.stop
37761
+ };
37762
+ }
37763
+ var DEFAULT_SAMPLE_FALLOFF = { model: "inverse_square", k: 2e-3, minGain: 2e-3 };
37764
+ var DEFAULT_CROSSOVER = { from: 30, to: 80 };
37765
+ var DEFAULT_SAMPLE_PITCH = 0.04;
37766
+ var DEFAULT_TAIL_GAIN = 0.6;
37767
+ var DEFAULT_SAMPLE_CUTOFF_K = 0.01;
37768
+ var OPEN_CUTOFF = 2e4;
37769
+ var LAYER_FLOOR = 1e-3;
37770
+ function pickTake(count, last, random) {
37771
+ if (count <= 1) return 0;
37772
+ if (last === void 0 || last < 0 || last >= count) return Math.min(count - 1, Math.floor(random() * count));
37773
+ const drawn = Math.min(count - 2, Math.floor(random() * (count - 1)));
37774
+ return drawn >= last ? drawn + 1 : drawn;
37775
+ }
37776
+ function farMixAt(crossover, distance3) {
37777
+ if (crossover.to <= crossover.from) return distance3 >= crossover.to ? 1 : 0;
37778
+ const t = (distance3 - crossover.from) / (crossover.to - crossover.from);
37779
+ return t <= 0 ? 0 : t >= 1 ? 1 : t;
37780
+ }
37781
+ function crossGains(farMix) {
37782
+ return { close: Math.cos(farMix * Math.PI / 2), far: Math.sin(farMix * Math.PI / 2) };
37783
+ }
37784
+ function sampleCutoff(cutoffK, distance3, occlusion = 0) {
37785
+ const byDistance = cutoffK > 0 ? OPEN_CUTOFF / (1 + distance3 * cutoffK) : OPEN_CUTOFF;
37786
+ const occluded = occlusion > 0 ? byDistance / (1 + occlusion * 9) : byDistance;
37787
+ return Math.max(80, Math.min(OPEN_CUTOFF, occluded));
37788
+ }
37789
+ function sampleUrls(sound3) {
37790
+ const tiers = (sound3.tiers ?? []).flatMap((tier) => [...tier.close, ...tier.far ?? []]);
37791
+ const all = [...sound3.close, ...sound3.far ?? [], ...tiers, ...sound3.tail?.indoor ?? [], ...sound3.tail?.outdoor ?? []].map((ref) => ref.url);
37792
+ return [...new Set(all)];
37793
+ }
37794
+ function tierFor(sound3, intensity) {
37795
+ if (intensity === void 0 || !sound3.tiers) return void 0;
37796
+ let chosen;
37797
+ for (const tier of sound3.tiers) if (tier.from <= intensity) chosen = tier;
37798
+ return chosen ?? sound3.tiers[0];
37799
+ }
37800
+ function planSample(sound3, input) {
37801
+ if (sound3.close.length === 0) throw new Error(`sample sound ${sound3.id} has no close take`);
37802
+ const listener = input.listener ?? DEFAULT_LISTENER;
37803
+ const distance3 = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0);
37804
+ const falloff = sound3.falloff ?? DEFAULT_SAMPLE_FALLOFF;
37805
+ const occlusion = Math.max(0, Math.min(1, input.occlusion ?? 0));
37806
+ const level = falloffGain(falloff, (input.volume ?? 1) * (sound3.gain ?? 1), distance3) * (1 - 0.45 * occlusion);
37807
+ if (isSilent(falloff, level) || level <= 0) return null;
37808
+ const pitch = sound3.pitch ?? DEFAULT_SAMPLE_PITCH;
37809
+ const rate = pitch > 0 ? 1 + (input.random() * 2 - 1) * pitch : 1;
37810
+ const environment = input.environment ?? "outdoor";
37811
+ const tier = tierFor(sound3, input.intensity);
37812
+ const close = tier?.close.length ? tier.close : sound3.close;
37813
+ const far = tier?.far?.length ? tier.far : sound3.far ?? [];
37814
+ const farMix = far.length > 0 ? farMixAt(sound3.crossover ?? DEFAULT_CROSSOVER, distance3) : 0;
37815
+ const gains = crossGains(farMix);
37816
+ const layers = [];
37817
+ const take = (role, refs, gain, at3) => {
37818
+ if (refs.length === 0 || gain < LAYER_FLOOR) return;
37819
+ const key2 = `${sound3.id}/${tier && role !== "tail" ? `${tier.name}/` : ""}${role}`;
37820
+ const index = pickTake(refs.length, input.memory?.get(key2), input.random);
37821
+ input.memory?.set(key2, index);
37822
+ const ref = refs[index];
37823
+ layers.push({ role, url: ref.url, take: index, gain: gain * (ref.gain ?? 1) * level, rate, at: at3 });
37824
+ };
37825
+ take("close", close, gains.close, 0);
37826
+ take("far", far, gains.far, 0);
37827
+ const tails = sound3.tail?.[environment] ?? [];
37828
+ take("tail", tails, sound3.tail?.gain ?? DEFAULT_TAIL_GAIN, sound3.tail?.at ?? 0);
37829
+ const pan = (sound3.pan ?? true) && input.position ? panFor(listener, input.position) : void 0;
37830
+ return {
37831
+ kind: "sample",
37832
+ id: sound3.id,
37833
+ bus: sound3.bus,
37834
+ level,
37835
+ distance: distance3,
37836
+ ...pan === void 0 ? {} : { pan },
37837
+ cutoff: sampleCutoff(sound3.cutoffK ?? DEFAULT_SAMPLE_CUTOFF_K, distance3, occlusion),
37838
+ rate,
37839
+ environment,
37840
+ ...tier ? { tier: tier.name } : {},
37841
+ farMix,
37842
+ layers,
37843
+ priority: sound3.priority ?? input.priority ?? busPriority(sound3.bus),
37844
+ ...sound3.group ? { group: sound3.group } : {}
37845
+ };
37846
+ }
37847
+ var DEFAULT_LOOP_CROSSFADE = 0.25;
37848
+ var LOOKAHEAD = 1.5;
37849
+ var VOLUME_SMOOTHING = 0.05;
37850
+ function loopCrossfade(sound3) {
37851
+ if (!sound3.loop) return void 0;
37852
+ return sound3.loop === true ? DEFAULT_LOOP_CROSSFADE : Math.max(0, sound3.loop.crossfade ?? DEFAULT_LOOP_CROSSFADE);
37853
+ }
37854
+ var CROSSFADE_STEPS = 8;
37855
+ var equalPowerIn = (t) => Math.sin(Math.max(0, Math.min(1, t)) * Math.PI / 2);
37856
+ var ramp2 = (param2, direction, at3, seconds) => {
37857
+ const value = (t) => direction === "in" ? equalPowerIn(t) : equalPowerIn(1 - t);
37858
+ param2.setValueAtTime(value(0), at3);
37859
+ for (let step = 1; step <= CROSSFADE_STEPS; step++) param2.linearRampToValueAtTime(value(step / CROSSFADE_STEPS), at3 + seconds * step / CROSSFADE_STEPS);
37860
+ };
37861
+ function createSampleLoopHandle(sound3, host, volume = 0) {
37862
+ const crossfade = loopCrossfade(sound3) ?? DEFAULT_LOOP_CROSSFADE;
37863
+ let level = volume;
37864
+ let stopped = false;
37865
+ let fallback2;
37866
+ let master;
37867
+ let segments = [];
37868
+ let last;
37869
+ let timer;
37870
+ const entry2 = {
37871
+ get created() {
37872
+ return master !== void 0;
37873
+ },
37874
+ rewire() {
37875
+ master?.disconnect();
37876
+ master?.connect(host.output(sound3.bus));
37877
+ },
37878
+ stop() {
37879
+ stopped = true;
37880
+ if (timer !== void 0) clearTimeout(timer);
37881
+ timer = void 0;
37882
+ for (const segment of segments) {
37883
+ try {
37884
+ segment.source.stop();
37885
+ } catch {
37886
+ }
37887
+ segment.source.disconnect();
37888
+ segment.gain.disconnect();
37889
+ }
37890
+ segments = [];
37891
+ master?.disconnect();
37892
+ master = void 0;
37893
+ fallback2?.stop();
37894
+ fallback2 = void 0;
37895
+ host.unregister(entry2);
37896
+ }
37897
+ };
37898
+ const nextBuffer = () => {
37899
+ const takes2 = sound3.close;
37900
+ const take = pickTake(takes2.length, last, host.random);
37901
+ last = take;
37902
+ const ref = takes2[take];
37903
+ return ref ? host.buffer(ref.url) : void 0;
37904
+ };
37905
+ const schedule = (context, output, at3, first) => {
37906
+ const buffer = nextBuffer();
37907
+ if (!buffer) return void 0;
37908
+ const source = context.createBufferSource();
37909
+ source.buffer = buffer;
37910
+ const gain = context.createGain();
37911
+ source.connect(gain);
37912
+ gain.connect(output);
37913
+ const fade = Math.min(crossfade, buffer.duration / 2);
37914
+ if (buffer.duration < 2 * crossfade || sound3.close.length === 1) {
37915
+ source.loop = true;
37916
+ if (!first) ramp2(gain.gain, "in", at3, Math.max(1e-3, fade));
37917
+ source.start(at3);
37918
+ segments.push({ source, gain, end: Number.POSITIVE_INFINITY });
37919
+ return void 0;
37920
+ }
37921
+ if (first) gain.gain.setValueAtTime(1, at3);
37922
+ else ramp2(gain.gain, "in", at3, fade);
37923
+ const end3 = at3 + buffer.duration;
37924
+ ramp2(gain.gain, "out", end3 - fade, fade);
37925
+ source.start(at3);
37926
+ source.stop(end3);
37927
+ segments.push({ source, gain, end: end3 });
37928
+ source.onended = () => {
37929
+ segments = segments.filter((segment) => segment.source !== source);
37930
+ source.disconnect();
37931
+ gain.disconnect();
37932
+ };
37933
+ return end3 - fade;
37934
+ };
37935
+ const pump = (context, output, at3, first) => {
37936
+ if (stopped) return;
37937
+ let next = schedule(context, output, at3, first);
37938
+ while (next !== void 0 && next < context.currentTime + LOOKAHEAD) next = schedule(context, output, Math.max(next, context.currentTime), false);
37939
+ if (next === void 0) return;
37940
+ const wait = Math.max(0, (next - context.currentTime - LOOKAHEAD / 2) * 1e3);
37941
+ timer = setTimeout(() => {
37942
+ timer = void 0;
37943
+ pump(context, output, next, false);
37944
+ }, wait);
37945
+ };
37946
+ const fileLevel = () => level * (sound3.gain ?? 1);
37947
+ const startFiles = () => {
37948
+ if (stopped || master || !host.isReady(sound3)) return master !== void 0;
37949
+ const context = host.ready();
37950
+ if (!context) return false;
37951
+ master = context.createGain();
37952
+ master.gain.setValueAtTime(fileLevel(), context.currentTime);
37953
+ master.connect(host.output(sound3.bus));
37954
+ host.register(entry2);
37955
+ pump(context, master, context.currentTime, true);
37956
+ if (fallback2) {
37957
+ fallback2.set({ volume: 0 });
37958
+ const old = fallback2;
37959
+ fallback2 = void 0;
37960
+ setTimeout(() => old.stop(), VOLUME_SMOOTHING * 6e3);
37961
+ }
37962
+ return true;
37963
+ };
37964
+ const ensure = () => {
37965
+ if (stopped || master) return;
37966
+ if (startFiles()) return;
37967
+ if (!fallback2) {
37968
+ fallback2 = host.fallback(level);
37969
+ void host.load(sound3).then(() => {
37970
+ if (level > 0 || master) startFiles();
37971
+ }).catch(() => void 0);
37972
+ }
37973
+ };
37974
+ if (volume > 0) ensure();
37975
+ return {
37976
+ set(next) {
37977
+ if (stopped) return;
37978
+ if (next.volume !== void 0) level = host.muted(sound3.bus) ? 0 : Math.max(0, next.volume);
37979
+ if (level > 0) ensure();
37980
+ const context = host.ready();
37981
+ if (master && context) master.gain.setTargetAtTime(fileLevel(), context.currentTime, VOLUME_SMOOTHING);
37982
+ fallback2?.set(next.volume === void 0 ? next : { ...next, volume: level });
37543
37983
  },
37544
37984
  stop: entry2.stop
37545
37985
  };
@@ -37818,7 +38258,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37818
38258
  return voiceOf(oscillators);
37819
38259
  }
37820
38260
  var end2 = (layer2, start) => start + layer2.duration + layer2.tail;
37821
- function outputStage(plan, layer2, target) {
38261
+ function outputStage2(plan, layer2, target) {
37822
38262
  const { context } = target;
37823
38263
  const level = gainNode(context, layer2.level);
37824
38264
  let node = level;
@@ -37850,7 +38290,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37850
38290
  let tail = source;
37851
38291
  for (const filter of filters) tail = tail.connect(filter);
37852
38292
  tail.connect(envelope2);
37853
- envelope2.connect(outputStage(plan, layer2, target));
38293
+ envelope2.connect(outputStage2(plan, layer2, target));
37854
38294
  source.start(time, layer2.source.kind === "noise" ? layer2.source.offset : 0);
37855
38295
  source.stop(end2(layer2, time));
37856
38296
  return [source];
@@ -37867,7 +38307,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37867
38307
  const envelope2 = context.createGain();
37868
38308
  applyEnvelope(envelope2.gain, layer2.envelope, time);
37869
38309
  oscillator.connect(envelope2);
37870
- envelope2.connect(outputStage(plan, layer2, target));
38310
+ envelope2.connect(outputStage2(plan, layer2, target));
37871
38311
  oscillator.start(time);
37872
38312
  oscillator.stop(end2(layer2, time));
37873
38313
  return [oscillator];
@@ -37899,7 +38339,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37899
38339
  const envelope2 = context.createGain();
37900
38340
  applyEnvelope(envelope2.gain, layer2.envelope, time);
37901
38341
  mix3.connect(envelope2);
37902
- envelope2.connect(outputStage(plan, layer2, target));
38342
+ envelope2.connect(outputStage2(plan, layer2, target));
37903
38343
  const stop = end2(layer2, time);
37904
38344
  oscillator.start(time);
37905
38345
  vibrato.start(time);
@@ -38071,6 +38511,135 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38071
38511
  reset: () => last.clear()
38072
38512
  };
38073
38513
  }
38514
+ function fetchLoader(baseUrl) {
38515
+ return async (url2) => {
38516
+ const target = baseUrl ? new URL(url2, baseUrl).toString() : url2;
38517
+ const response = await fetch(target);
38518
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText} for ${target}`);
38519
+ return response.arrayBuffer();
38520
+ };
38521
+ }
38522
+ var decode = (context, bytes) => new Promise((resolve2, reject) => {
38523
+ const result2 = context.decodeAudioData(bytes, resolve2, reject);
38524
+ result2?.catch?.(reject);
38525
+ });
38526
+ function createSampleStore(loader = fetchLoader()) {
38527
+ const buffers = /* @__PURE__ */ new Map();
38528
+ const pending = /* @__PURE__ */ new Map();
38529
+ const failed = /* @__PURE__ */ new Map();
38530
+ let generation = 0;
38531
+ const loadOne = (url2, context) => {
38532
+ const existing = buffers.get(url2);
38533
+ if (existing) return Promise.resolve(existing);
38534
+ const inFlight = pending.get(url2);
38535
+ if (inFlight) return inFlight;
38536
+ const started = generation;
38537
+ failed.delete(url2);
38538
+ const promise = loader(url2).then((bytes) => decode(context, bytes)).then((buffer) => {
38539
+ if (started === generation) buffers.set(url2, buffer);
38540
+ return buffer;
38541
+ }).catch((error) => {
38542
+ if (started === generation) failed.set(url2, error instanceof Error ? error.message : String(error));
38543
+ throw error;
38544
+ }).finally(() => {
38545
+ if (pending.get(url2) === promise) pending.delete(url2);
38546
+ });
38547
+ pending.set(url2, promise);
38548
+ return promise;
38549
+ };
38550
+ return {
38551
+ status(url2) {
38552
+ if (buffers.has(url2)) return "ready";
38553
+ if (pending.has(url2)) return "loading";
38554
+ if (failed.has(url2)) return "failed";
38555
+ return "none";
38556
+ },
38557
+ buffer: (url2) => buffers.get(url2),
38558
+ ready: (urls) => urls.every((url2) => buffers.has(url2)),
38559
+ seconds: (url2) => buffers.get(url2)?.duration,
38560
+ async load(urls, context) {
38561
+ const results = await Promise.allSettled(urls.map((url2) => loadOne(url2, context)));
38562
+ const loaded = [];
38563
+ const errors = [];
38564
+ results.forEach((result2, index) => {
38565
+ const url2 = urls[index];
38566
+ if (result2.status === "fulfilled") loaded.push(url2);
38567
+ else errors.push({ url: url2, error: result2.reason instanceof Error ? result2.reason.message : String(result2.reason) });
38568
+ });
38569
+ return { loaded, failed: errors };
38570
+ },
38571
+ clear() {
38572
+ generation++;
38573
+ buffers.clear();
38574
+ pending.clear();
38575
+ failed.clear();
38576
+ }
38577
+ };
38578
+ }
38579
+ function realizeSample(plan, target) {
38580
+ const context = target.context;
38581
+ let head = target.output;
38582
+ if (plan.pan !== void 0 && typeof context.createStereoPanner === "function") {
38583
+ const panner = context.createStereoPanner();
38584
+ panner.pan.value = plan.pan;
38585
+ panner.connect(head);
38586
+ head = panner;
38587
+ }
38588
+ if (plan.cutoff < OPEN_CUTOFF) {
38589
+ const lowpass = context.createBiquadFilter();
38590
+ lowpass.type = "lowpass";
38591
+ lowpass.frequency.value = plan.cutoff;
38592
+ lowpass.Q.value = 0.7;
38593
+ lowpass.connect(head);
38594
+ head = lowpass;
38595
+ }
38596
+ const voice2 = context.createGain();
38597
+ voice2.connect(head);
38598
+ head = voice2;
38599
+ const sources = [];
38600
+ for (const layer2 of plan.layers) {
38601
+ const buffer = target.buffer(layer2.url);
38602
+ if (!buffer) continue;
38603
+ const source = context.createBufferSource();
38604
+ source.buffer = buffer;
38605
+ source.playbackRate.value = layer2.rate;
38606
+ const gain = gainNode(context, layer2.gain);
38607
+ source.connect(gain);
38608
+ gain.connect(head);
38609
+ source.start(target.time + layer2.at);
38610
+ sources.push(source);
38611
+ }
38612
+ const realized = voiceOf(sources);
38613
+ return {
38614
+ ...realized,
38615
+ fade(at3, seconds) {
38616
+ voice2.gain.setValueAtTime(1, at3);
38617
+ voice2.gain.linearRampToValueAtTime(0, at3 + seconds);
38618
+ for (const source of sources) {
38619
+ try {
38620
+ source.stop(at3 + seconds);
38621
+ } catch {
38622
+ }
38623
+ }
38624
+ }
38625
+ };
38626
+ }
38627
+ function sampleLength(plan, seconds) {
38628
+ let length2 = 0;
38629
+ for (const layer2 of plan.layers) length2 = Math.max(length2, layer2.at + (seconds(layer2.url) ?? 0) / layer2.rate);
38630
+ return length2;
38631
+ }
38632
+ function limiterCurve2(knee, ceiling, range3, points = 2049) {
38633
+ const curve = new Float32Array(points);
38634
+ const room = Math.max(1e-6, ceiling - knee);
38635
+ for (let index = 0; index < points; index++) {
38636
+ const value = (index / (points - 1) * 2 - 1) * range3;
38637
+ const size = Math.abs(value);
38638
+ const bent = size <= knee ? size : knee + room * Math.tanh((size - knee) / room);
38639
+ curve[index] = Math.sign(value) * bent;
38640
+ }
38641
+ return curve;
38642
+ }
38074
38643
  var usesNoise = (recipe2) => recipe2.layers.some((layer2) => layer2.source.kind !== "osc");
38075
38644
  var planUsesNoise = (plan) => plan.layers.some((layer2) => layer2.source.kind !== "osc");
38076
38645
  function createAudioEngine(options = {}) {
@@ -38090,6 +38659,43 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38090
38659
  let chain = null;
38091
38660
  let listener = DEFAULT_LISTENER;
38092
38661
  let lastTime = 0;
38662
+ let environment = options.environment ?? "outdoor";
38663
+ const sampleLimiters = /* @__PURE__ */ new Map();
38664
+ const sampleOutput = (bus) => {
38665
+ const setting = options.samples?.limiter;
38666
+ if (setting === false || !context) return outputFor(bus);
38667
+ const existing = sampleLimiters.get(bus);
38668
+ if (existing) return existing;
38669
+ const range3 = 4;
38670
+ const input = context.createGain();
38671
+ input.gain.value = 1 / range3;
38672
+ const shaper = context.createWaveShaper();
38673
+ shaper.curve = limiterCurve2(setting?.knee ?? 0.5, setting?.ceiling ?? 0.891, range3);
38674
+ ensureBus(bus);
38675
+ input.connect(shaper);
38676
+ shaper.connect(outputFor(bus));
38677
+ sampleLimiters.set(bus, input);
38678
+ return input;
38679
+ };
38680
+ const chokes = /* @__PURE__ */ new Map();
38681
+ const choked = (id) => {
38682
+ const list2 = (chokes.get(id) ?? []).filter((entry2) => active.has(entry2.id));
38683
+ chokes.set(id, list2);
38684
+ return list2;
38685
+ };
38686
+ const watchers = /* @__PURE__ */ new Set();
38687
+ const announce = (event) => {
38688
+ options.onVoiceStart?.(event);
38689
+ for (const watcher of watchers) watcher(event);
38690
+ };
38691
+ const sources = [];
38692
+ const baseLoader = options.samples?.loader ?? fetchLoader(options.samples?.baseUrl);
38693
+ const sampleStore = createSampleStore((url2) => (sources.find((source) => source.accepts(url2))?.loader ?? baseLoader)(url2));
38694
+ const memory = /* @__PURE__ */ new Map();
38695
+ const latencyOf = (audio) => {
38696
+ const live = audio;
38697
+ return (live.baseLatency ?? 0) + (live.outputLatency ?? 0);
38698
+ };
38093
38699
  const outputFor = (bus) => busNodes.get(bus) ?? chain?.master;
38094
38700
  const watched = /* @__PURE__ */ new Set();
38095
38701
  const meterOf = (bus) => {
@@ -38146,17 +38752,31 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38146
38752
  ...options.occlusion && playOptions.position && recipe2.falloff?.model !== "none" ? { occlusion: options.occlusion(playOptions.position) } : {}
38147
38753
  });
38148
38754
  const startQueue = options.deferredStart ? createStartQueue(options.deferredStart) : null;
38149
- const build = (plan, store, delay) => {
38755
+ const build = (plan, store, delay, requestedAt, fallbackOf) => {
38150
38756
  const audio = context;
38151
- return realize(plan, {
38757
+ const time = audio.currentTime + delay;
38758
+ const realized = realize(plan, {
38152
38759
  context: audio,
38153
38760
  output: outputFor(plan.bus),
38154
38761
  ...chain?.wet ? { wet: chain.wet } : {},
38155
38762
  noise: () => noiseBuffer(store, audio, mix3.noise.seconds),
38156
- time: audio.currentTime + delay
38763
+ time
38157
38764
  });
38765
+ announce({
38766
+ id: plan.id,
38767
+ kind: "recipe",
38768
+ ...fallbackOf ? { fallbackOf } : {},
38769
+ requestedAt,
38770
+ startAt: time,
38771
+ outputLatency: latencyOf(audio),
38772
+ level: plan.level,
38773
+ distance: plan.distance,
38774
+ ...plan.pan === void 0 ? {} : { pan: plan.pan }
38775
+ });
38776
+ return realized;
38158
38777
  };
38159
- const deferVoice = (plan, store, voiceId, delay) => {
38778
+ const deferVoice = (plan, store, voiceId, delay, fallbackOf) => {
38779
+ const requestedAt = context?.currentTime ?? 0;
38160
38780
  let built2 = null;
38161
38781
  let cancelled = false;
38162
38782
  const pending = {
@@ -38170,7 +38790,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38170
38790
  };
38171
38791
  startQueue.push({
38172
38792
  run() {
38173
- if (!cancelled && context) built2 = build(plan, store, delay);
38793
+ if (!cancelled && context) built2 = build(plan, store, delay, requestedAt, fallbackOf);
38174
38794
  },
38175
38795
  drop() {
38176
38796
  if (voiceId === void 0) return;
@@ -38180,13 +38800,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38180
38800
  });
38181
38801
  return pending;
38182
38802
  };
38183
- const start = (plan, store, voiceId, delay = 0) => {
38184
- const realized = startQueue ? deferVoice(plan, store, voiceId, delay) : build(plan, store, delay);
38803
+ const start = (plan, store, voiceId, delay = 0, fallbackOf, urgent = false) => {
38804
+ const realized = startQueue && !urgent ? deferVoice(plan, store, voiceId, delay, fallbackOf) : build(plan, store, delay, context?.currentTime ?? 0, fallbackOf);
38185
38805
  if (voiceId === void 0) return null;
38186
38806
  voices.setLength(voiceId, plan.length + delay);
38187
38807
  active.set(voiceId, realized);
38188
38808
  return {
38189
38809
  id: voiceId,
38810
+ kind: "recipe",
38190
38811
  plan,
38191
38812
  stop() {
38192
38813
  realized.stop();
@@ -38210,7 +38831,83 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38210
38831
  const set2 = registry.set(id);
38211
38832
  return set2 ? chooser.pick(set2, random) : { id, rate: 1 };
38212
38833
  };
38213
- const playWith = (asked, playOptions, streamRandom, store) => {
38834
+ const loadSounds = (sounds) => {
38835
+ const audio = ready();
38836
+ if (!audio) return Promise.resolve({ loaded: [], failed: sounds.flatMap(sampleUrls).map((url2) => ({ url: url2, error: "no audio context" })) });
38837
+ return sampleStore.load([...new Set(sounds.flatMap(sampleUrls))], audio);
38838
+ };
38839
+ const sampleInput = (playOptions, random, withMemory) => ({
38840
+ random,
38841
+ listener,
38842
+ environment: playOptions.environment ?? environment,
38843
+ ...withMemory ? { memory } : {},
38844
+ ...options.distance ? { distanceMode: options.distance } : {},
38845
+ ...playOptions.volume === void 0 ? {} : { volume: playOptions.volume },
38846
+ ...playOptions.intensity === void 0 ? {} : { intensity: playOptions.intensity },
38847
+ ...playOptions.distance === void 0 ? {} : { distance: playOptions.distance },
38848
+ ...playOptions.position ? { position: playOptions.position } : {},
38849
+ ...playOptions.occlusion !== void 0 ? { occlusion: playOptions.occlusion } : options.occlusion && playOptions.position ? { occlusion: options.occlusion(playOptions.position) } : {}
38850
+ });
38851
+ const playSample = (sample, playOptions, random, store) => {
38852
+ if (busSettings.get(sample.bus)?.muted) return null;
38853
+ if (!sampleStore.ready(sampleUrls(sample))) {
38854
+ if (options.samples?.autoLoad !== false && ready()) void loadSounds([sample]).catch(() => void 0);
38855
+ if (sample.fallback && sample.fallback !== sample.id) return playWith(sample.fallback, playOptions, random, store, sample.id);
38856
+ return registry.sound(sample.id) ? void 0 : null;
38857
+ }
38858
+ const plan = planSample(sample, sampleInput(playOptions, playOptions.seed === void 0 ? random : createRng3(playOptions.seed), true));
38859
+ if (!plan) return null;
38860
+ const audio = ready();
38861
+ if (!audio) return null;
38862
+ unlockContext(audio);
38863
+ advance();
38864
+ const voiceId = admit(plan, plan.level, plan.distance);
38865
+ if (voiceId === null) return null;
38866
+ const requestedAt = audio.currentTime;
38867
+ const time = requestedAt + (playOptions.delay ?? 0);
38868
+ const chokeList = sample.choke ? choked(sample.id) : void 0;
38869
+ const realized = realizeSample(plan, { context: audio, output: sampleOutput(plan.bus), buffer: (url2) => sampleStore.buffer(url2), time });
38870
+ announce({
38871
+ id: plan.id,
38872
+ kind: "sample",
38873
+ requestedAt,
38874
+ startAt: time,
38875
+ outputLatency: latencyOf(audio),
38876
+ level: plan.level,
38877
+ distance: plan.distance,
38878
+ ...plan.pan === void 0 ? {} : { pan: plan.pan },
38879
+ layers: plan.layers.map((layer2) => ({ role: layer2.role, url: layer2.url, take: layer2.take, rate: layer2.rate })),
38880
+ ...plan.tier ? { tier: plan.tier } : {}
38881
+ });
38882
+ voices.setLength(voiceId, sampleLength(plan, (url2) => sampleStore.seconds(url2)) + (playOptions.delay ?? 0));
38883
+ active.set(voiceId, realized);
38884
+ if (chokeList && sample.choke) {
38885
+ chokeList.push({ id: voiceId, voice: realized });
38886
+ while (chokeList.length > Math.max(1, sample.choke.voices)) {
38887
+ const oldest = chokeList.shift();
38888
+ if (!oldest) break;
38889
+ oldest.voice.fade(time, sample.choke.fade ?? 0.03);
38890
+ voices.release(oldest.id);
38891
+ active.delete(oldest.id);
38892
+ }
38893
+ }
38894
+ return {
38895
+ id: voiceId,
38896
+ kind: "sample",
38897
+ plan,
38898
+ stop() {
38899
+ realized.stop();
38900
+ voices.release(voiceId);
38901
+ active.delete(voiceId);
38902
+ }
38903
+ };
38904
+ };
38905
+ const playWith = (asked, playOptions, streamRandom, store, fallbackOf) => {
38906
+ const sample = fallbackOf === void 0 ? registry.sample(asked) : void 0;
38907
+ if (sample) {
38908
+ const played = playSample(sample, playOptions, streamRandom, store);
38909
+ if (played !== void 0) return played;
38910
+ }
38214
38911
  const seeded = playOptions.seed === void 0 ? null : createRng3(playOptions.seed);
38215
38912
  const { id, rate } = resolve2(asked, seeded ?? streamRandom);
38216
38913
  const random = seeded ?? streamRandom;
@@ -38228,7 +38925,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38228
38925
  if (voiceId === null) return null;
38229
38926
  if (!silent && usesNoise(recipe2)) prepareNoise(store, audio, mix3.noise.seconds);
38230
38927
  const plan = planSound(recipe2, inputFor(recipe2, playOptions, random));
38231
- return plan ? start(retunePlan(plan, rate), store, voiceId, playOptions.delay) : null;
38928
+ return plan ? start(retunePlan(plan, rate), store, voiceId, playOptions.delay, fallbackOf, playOptions.urgent) : null;
38232
38929
  };
38233
38930
  const randomFor = (playOptions) => {
38234
38931
  if (playOptions.channel === void 0) return engineRandom;
@@ -38250,12 +38947,42 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38250
38947
  prepareNoise: () => context && prepareNoise(shared2, context, mix3.noise.seconds),
38251
38948
  noise: () => noiseBuffer(shared2, context, mix3.noise.seconds),
38252
38949
  register: (loop) => void loops.add(loop),
38253
- unregister: (loop) => void loops.delete(loop)
38950
+ unregister: (loop) => void loops.delete(loop),
38951
+ listener: () => listener,
38952
+ ...options.distance ? { distance: options.distance } : {}
38254
38953
  };
38255
38954
  return {
38256
38955
  registry,
38257
38956
  register: (bank) => registry.register(bank),
38258
38957
  plan: (id, playOptions = {}) => planWith(id, playOptions, createRng3(playOptions.seed ?? seed)),
38958
+ planSample(id, playOptions = {}) {
38959
+ const sample = registry.sample(id);
38960
+ return sample ? planSample(sample, sampleInput(playOptions, createRng3(playOptions.seed ?? seed), false)) : null;
38961
+ },
38962
+ loadSamples(ids) {
38963
+ const chosen = (ids ?? registry.sampleIds()).map((id) => registry.sample(id)).filter((sample) => sample !== void 0);
38964
+ return loadSounds(chosen);
38965
+ },
38966
+ sampleStatus(id) {
38967
+ const sample = registry.sample(id);
38968
+ if (!sample) return "none";
38969
+ const statuses = sampleUrls(sample).map((url2) => sampleStore.status(url2));
38970
+ if (statuses.every((status) => status === "ready")) return "ready";
38971
+ if (statuses.includes("failed")) return "failed";
38972
+ if (statuses.includes("loading")) return "loading";
38973
+ return statuses.includes("ready") ? "loading" : "none";
38974
+ },
38975
+ setEnvironment(next) {
38976
+ environment = next;
38977
+ },
38978
+ output: () => chain?.master ?? null,
38979
+ addSampleSource(accepts, loader) {
38980
+ sources.push({ accepts, loader });
38981
+ },
38982
+ watchVoices(listener2) {
38983
+ watchers.add(listener2);
38984
+ return () => void watchers.delete(listener2);
38985
+ },
38259
38986
  play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared2),
38260
38987
  playPlan(plan, playOptions = {}) {
38261
38988
  const audio = ready();
@@ -38267,7 +38994,26 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38267
38994
  if (planUsesNoise(plan)) prepareNoise(shared2, audio, mix3.noise.seconds);
38268
38995
  return start(plan, shared2, voiceId, playOptions.delay);
38269
38996
  },
38270
- loop: (id, loopOptions = {}) => createLoopHandle(registry.loop(id), loopHost, loopOptions.volume),
38997
+ loop(id, loopOptions = {}) {
38998
+ const sample = registry.sample(id);
38999
+ if (!sample?.loop) return createLoopHandle(registry.loop(id), loopHost, loopOptions.volume);
39000
+ return createSampleLoopHandle(
39001
+ sample,
39002
+ {
39003
+ ready,
39004
+ output: sampleOutput,
39005
+ buffer: (url2) => sampleStore.buffer(url2),
39006
+ isReady: (sound3) => sampleStore.ready(sampleUrls(sound3)),
39007
+ load: (sound3) => options.samples?.autoLoad === false ? Promise.resolve() : loadSounds([sound3]),
39008
+ fallback: (volume) => createLoopHandle(registry.loop(sample.fallback ?? sample.id) ?? registry.loop(sample.id), loopHost, volume),
39009
+ random: engineRandom,
39010
+ register: loopHost.register,
39011
+ unregister: loopHost.unregister,
39012
+ muted: (bus) => busSettings.get(bus)?.muted === true
39013
+ },
39014
+ loopOptions.volume
39015
+ );
39016
+ },
38271
39017
  channel(channelOptions) {
38272
39018
  const random = channelOptions.random ?? (channelOptions.seed === void 0 ? engineRandom : createRng3(channelOptions.seed));
38273
39019
  const store = channelOptions.ownNoise ? createNoiseStore(random) : shared2;
@@ -38348,6 +39094,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38348
39094
  busNodes.clear();
38349
39095
  meters.clear();
38350
39096
  shared2.reset();
39097
+ sampleStore.clear();
39098
+ memory.clear();
39099
+ sampleLimiters.clear();
39100
+ chokes.clear();
38351
39101
  }
38352
39102
  };
38353
39103
  }
@@ -38396,8 +39146,395 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38396
39146
  }
38397
39147
  ]
38398
39148
  };
39149
+ var entries = (category, list2) => list2.map(([id, priority, current]) => ({ id, category, priority, ...current ? { current } : {} }));
39150
+ var SFX_CATALOG = [
39151
+ ...entries("armas", [
39152
+ ["pistol_22lr", 2, "sfx_pistol"],
39153
+ ["pistol_9mm", 1, "sfx_pistol"],
39154
+ ["pistol_45", 1, "sfx_pistol"],
39155
+ ["revolver_38_357", 2, "sfx_pistol"],
39156
+ ["revolver_44_50ae", 3, "sfx_pistol"],
39157
+ ["smg_9mm_auto", 1, "sfx_pistol"],
39158
+ ["shotgun_12", 1, "sfx_shotgun"],
39159
+ ["rifle_5_56", 1, "sfx_rifle"],
39160
+ ["rifle_7_62x39", 1, "sfx_rifle"],
39161
+ ["rifle_auto_burst", 1, "sfx_rifle"],
39162
+ ["rifle_308_3006", 2, "sfx_rifle"],
39163
+ ["sniper_338_300wm", 2, "sfx_rifle"],
39164
+ ["antimateriel_50bmg", 3, "sfx_rifle"],
39165
+ ["suppressed_pistol", 2],
39166
+ ["suppressed_rifle", 2],
39167
+ ["gunshot_far_pistol", 1],
39168
+ ["gunshot_far_rifle", 1],
39169
+ ["gunshot_far_shotgun", 1],
39170
+ ["gunshot_far_heavy", 2],
39171
+ ["gunshot_tail_indoor", 1],
39172
+ ["gunshot_tail_outdoor", 1],
39173
+ ["bullet_crack_by", 2],
39174
+ ["bullet_whiz", 2],
39175
+ ["ricochet_metal", 2, "ping"],
39176
+ ["dry_fire", 1],
39177
+ ["mag_out", 1],
39178
+ ["mag_in", 1],
39179
+ ["slide_pull_release", 1],
39180
+ ["pump_cycle", 1],
39181
+ ["casing_drop_concreto_pistol", 1],
39182
+ ["casing_drop_concreto_rifle", 1],
39183
+ ["casing_drop_concreto_shell", 1],
39184
+ ["casing_drop_madeira_pistol", 1],
39185
+ ["casing_drop_madeira_rifle", 1],
39186
+ ["casing_drop_madeira_shell", 1],
39187
+ ["casing_drop_metal_pistol", 1],
39188
+ ["casing_drop_metal_rifle", 1],
39189
+ ["casing_drop_metal_shell", 1],
39190
+ ["casing_drop_terra_pistol", 1],
39191
+ ["casing_drop_terra_rifle", 1],
39192
+ ["casing_drop_terra_shell", 1],
39193
+ ["rocket_fire", 2]
39194
+ ]),
39195
+ ...entries("explosoes-fogo", [
39196
+ ["explosion_grenade", 1, "boom"],
39197
+ ["explosion_big", 1, "boom_big"],
39198
+ ["explosion_car", 2, "boom_big"],
39199
+ ["explosion_far", 1],
39200
+ ["debris_rain_stone", 1],
39201
+ ["molotov_break_ignite", 1, "glass_shatter"],
39202
+ ["fire_ignite", 1, "hiss"],
39203
+ ["fire_loop_small", 1],
39204
+ ["fire_loop_large", 1],
39205
+ ["fire_extinguish", 2, "hiss"],
39206
+ ["gas_leak_hiss", 2],
39207
+ ["structure_collapse", 2, "crumble"]
39208
+ ]),
39209
+ ...entries("personagem", [
39210
+ ["footstep_concrete_walk", 1, "sfx_step"],
39211
+ ["footstep_wood_walk", 1, "sfx_step"],
39212
+ ["footstep_dirt_walk", 1, "sfx_step"],
39213
+ ["footstep_grass_walk", 1, "sfx_step"],
39214
+ ["footstep_gravel_walk", 1, "sfx_step"],
39215
+ ["land_concrete", 1, "sfx_step"],
39216
+ ["punch_swing", 1],
39217
+ ["punch_hit_body", 1, "sfx_flesh"],
39218
+ ["kick_hit", 1, "sfx_flesh"],
39219
+ ["knife_stab", 1],
39220
+ ["blunt_hit", 1],
39221
+ ["bite", 1, "sfx_flesh"],
39222
+ ["body_fall", 1],
39223
+ ["player_hurt", 2]
39224
+ ]),
39225
+ ...entries("veiculos", [
39226
+ ["engine_loop_car", 1],
39227
+ ["engine_start", 1],
39228
+ ["crash_light", 1, "crash"],
39229
+ ["crash_heavy", 1, "crash"],
39230
+ ["car_door_open", 1, "latch"],
39231
+ ["car_door_close", 1, "shut_metal"],
39232
+ ["horn_car", 2, "horn"],
39233
+ ["tire_skid_asphalt", 2]
39234
+ ]),
39235
+ ...entries("itens-portas", [
39236
+ ["item_pickup_metal", 1],
39237
+ ["can_open", 1],
39238
+ ["can_crush", 1],
39239
+ ["door_wood_open", 1, "latch"],
39240
+ ["door_wood_close", 1, "shut_wood"],
39241
+ ["door_creak", 1, "creak_wood"],
39242
+ ["door_locked_rattle", 1, "rattle"],
39243
+ ["drawer_open", 2, "slide"],
39244
+ ["fridge_close", 2, "shut_metal"]
39245
+ ]),
39246
+ ...entries("ambiente", [
39247
+ ["rain_loop", 2],
39248
+ ["wind_loop", 2],
39249
+ ["thunder_near", 2],
39250
+ ["thunder_far", 2],
39251
+ ["water_splash", 2]
39252
+ ])
39253
+ ];
39254
+ var SFX_MATERIALS = [
39255
+ ["steel", "metal"],
39256
+ ["aluminium", "metal"],
39257
+ ["wood", "wood"],
39258
+ ["plywood", "wood"],
39259
+ ["mdf", "wood"],
39260
+ ["glass", "glass"],
39261
+ ["tempered_glass", "glass"],
39262
+ ["ceramic", "glass"],
39263
+ ["concrete", "masonry"],
39264
+ ["brick", "masonry"],
39265
+ ["plaster", "masonry"],
39266
+ ["plastic", "soft"],
39267
+ ["fabric", "soft"],
39268
+ ["paper", "soft"],
39269
+ ["foliage", "soft"],
39270
+ ["soil", "soft"],
39271
+ ["flesh", "soft"]
39272
+ ];
39273
+ var SFX_BLOWS = ["bullet", "blunt", "blade", "break", "drag"];
39274
+ var CURRENT_BY_GROUP = {
39275
+ metal: { bullet: "clang", blunt: "clang", blade: "clang", break: "clang", drag: "drag_metal" },
39276
+ wood: { bullet: "wood_hit", blunt: "wood_hit", blade: "wood_hit", break: "crack", drag: "drag_wood" },
39277
+ glass: { bullet: "ping", blunt: "ping", break: "glass_shatter", drag: "drag_glass" },
39278
+ masonry: { bullet: "chip", blunt: "chip", blade: "chip", break: "crumble", drag: "drag_masonry" },
39279
+ soft: { bullet: "thud", blunt: "thud", blade: "thud", break: "thud", drag: "drag_soft" }
39280
+ };
39281
+ var impactSoundId = (material, blow3) => `impact_${material}_${blow3}`;
39282
+ var SFX_IMPACTS = SFX_MATERIALS.flatMap(
39283
+ ([material, group]) => SFX_BLOWS.map((blow3) => {
39284
+ const current = CURRENT_BY_GROUP[group]?.[blow3];
39285
+ return { id: impactSoundId(material, blow3), category: "impactos", priority: ["steel", "wood", "glass", "concrete", "brick", "soil", "flesh"].includes(material) && blow3 === "bullet" ? 1 : 2, ...current ? { current } : {} };
39286
+ })
39287
+ );
39288
+ var CALIBRATION_ID = "calibration_shot";
39289
+ var PREFIX = "calibration/";
39290
+ var url = (name) => `${PREFIX}${name}`;
39291
+ var CALIBRATION_SOUND = {
39292
+ id: CALIBRATION_ID,
39293
+ bus: "weapons",
39294
+ close: ["close_1.wav", "close_2.wav", "close_3.wav", "close_4.wav"].map((name) => ({ url: url(name) })),
39295
+ far: ["far_1.wav", "far_2.wav"].map((name) => ({ url: url(name) })),
39296
+ tiers: [
39297
+ { name: "light", from: 0, close: ["light_1.wav", "light_2.wav"].map((name) => ({ url: url(name) })) },
39298
+ { name: "medium", from: 0.34, close: ["close_1.wav", "close_2.wav", "close_3.wav", "close_4.wav"].map((name) => ({ url: url(name) })) },
39299
+ { name: "heavy", from: 0.67, close: ["heavy_1.wav", "heavy_2.wav"].map((name) => ({ url: url(name) })) }
39300
+ ],
39301
+ tail: { indoor: [{ url: url("tail_in.wav") }], outdoor: [{ url: url("tail_out.wav") }] },
39302
+ pitch: 0.04,
39303
+ choke: { voices: 2 },
39304
+ fallback: "sfx_pistol",
39305
+ sources: ["sdk: calibration takes synthesized by the harness (measure/calibration.ts), no file, no licence"]
39306
+ };
39307
+ var CALIBRATION_LOOP_ID = "calibration_loop";
39308
+ var CALIBRATION_LOOP_SOUND = {
39309
+ id: CALIBRATION_LOOP_ID,
39310
+ bus: "ambience",
39311
+ close: ["loop_1.wav", "loop_2.wav", "loop_3.wav"].map((name) => ({ url: url(name) })),
39312
+ loop: { crossfade: 0.15 },
39313
+ sources: ["sdk: calibration takes synthesized by the harness (measure/calibration.ts), no file, no licence"]
39314
+ };
39315
+ var CRITERIA_TARGETS = {
39316
+ /** C1: event to the first sample, and onset to the peak (one 60 fps frame). */
39317
+ syncMs: 20,
39318
+ peakAfterOnsetMs: 1e3 / 60,
39319
+ /** C3: distances measured, metres. */
39320
+ distances: [1, 10, 50, 200],
39321
+ /** C5: plays, distinct takes and the pitch range (fraction). */
39322
+ plays: 10,
39323
+ minTakes: 4,
39324
+ pitch: [0.03, 0.05],
39325
+ /** C6: the highest peak, dBFS. */
39326
+ peakDb: -1,
39327
+ /** C8: the end of each file against its peak, dB. */
39328
+ endDropDb: -40,
39329
+ /** C9: a burst of rapid fire. */
39330
+ burst: 20,
39331
+ burstSpacing: 0.06
39332
+ };
39333
+ var PT = {
39334
+ group: "Som",
39335
+ name: "Painel de som (arquivos e C1\u2013C10)",
39336
+ controls: {
39337
+ category: "1 \xB7 Categoria",
39338
+ sound: "1 \xB7 Som",
39339
+ play: "1 \xB7 \u25B6 Tocar",
39340
+ material: "2 \xB7 Material",
39341
+ blow: "2 \xB7 Golpe",
39342
+ strike: "2 \xB7 \u25B6 Tocar golpe",
39343
+ distance: "3 \xB7 Dist\xE2ncia",
39344
+ bearing: "3 \xB7 Dire\xE7\xE3o",
39345
+ environment: "3 \xB7 Lugar",
39346
+ strength: "3 \xB7 For\xE7a",
39347
+ wall: "3 \xB7 Atr\xE1s da parede",
39348
+ burst: "3 \xB7 Rajada 20\xD7",
39349
+ play_a: "4 \xB7 \u25B6 A (atual)",
39350
+ play_b: "4 \xB7 \u25B6 B (novo)",
39351
+ ab_cycle: "4 \xB7 Alternar A/B",
39352
+ blind: "4 \xB7 \xC0s cegas (X)",
39353
+ reveal: "4 \xB7 Revelar X",
39354
+ prefer_a: "4 \xB7 Prefiro A",
39355
+ prefer_b: "4 \xB7 Prefiro B",
39356
+ measure: "5 \xB7 Medir C1\u2013C10",
39357
+ record: "5 \xB7 Gravar 3 s",
39358
+ approve: "5 \xB7 Aprovo (C10)"
39359
+ },
39360
+ hints: {
39361
+ sound: "A = arquivo, S = sintetizado, F = faltando. O que tem arquivo toca o arquivo; o resto toca o som atual.",
39362
+ strike: "Toca impact_<material>_<golpe>: o arquivo se existir, sen\xE3o o som atual daquele material.",
39363
+ wall: "P\xF5e a parede de casa entre voc\xEA e a fonte: o som fica abafado (C4).",
39364
+ burst: "Vinte vezes a cada 60 ms, como uma rajada (C9).",
39365
+ ab_cycle: "A, B, A, B, um por segundo.",
39366
+ blind: "Toca A ou B sem dizer qual; depois revele.",
39367
+ measure: "Mede o som escolhido no motor real (contexto offline): sincronia, som certo, dist\xE2ncia, lado, varia\xE7\xE3o, volume, for\xE7a, cauda e rajada.",
39368
+ record: "Grava 3 s da sa\xEDda com o som tocando e baixa o arquivo (C10).",
39369
+ approve: "Marca o C10 do som escolhido como aprovado pelo seu ouvido."
39370
+ },
39371
+ categories: {
39372
+ armas: "Armas",
39373
+ impactos: "Impactos",
39374
+ "explosoes-fogo": "Explos\xF5es e fogo",
39375
+ personagem: "Personagem",
39376
+ veiculos: "Ve\xEDculos",
39377
+ "itens-portas": "Itens e portas",
39378
+ ambiente: "Ambiente",
39379
+ calibracao: "Calibra\xE7\xE3o (SDK)"
39380
+ },
39381
+ blows: { bullet: "tiro", blunt: "soco/chute", blade: "faca", break: "quebra", drag: "arrasto/queda" },
39382
+ materials: {
39383
+ steel: "a\xE7o",
39384
+ aluminium: "alum\xEDnio",
39385
+ wood: "madeira",
39386
+ plywood: "compensado",
39387
+ mdf: "MDF",
39388
+ glass: "vidro",
39389
+ tempered_glass: "vidro temperado",
39390
+ ceramic: "cer\xE2mica",
39391
+ concrete: "concreto",
39392
+ brick: "tijolo",
39393
+ plaster: "gesso",
39394
+ plastic: "pl\xE1stico",
39395
+ fabric: "tecido",
39396
+ paper: "papel\xE3o",
39397
+ foliage: "folhagem",
39398
+ soil: "terra",
39399
+ flesh: "carne"
39400
+ },
39401
+ environments: { indoor: "interior", outdoor: "exterior" },
39402
+ strengths: { light: "leve", medium: "m\xE9dio", heavy: "forte" },
39403
+ states: { file: "A", synth: "S", missing: "F" },
39404
+ 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" },
39405
+ 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" },
39406
+ run: { idle: "parada", measuring: "medindo\u2026", done: "pronta", failed: "falhou", recording: "gravando\u2026", recorded: "grava\xE7\xE3o baixada" },
39407
+ words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono" },
39408
+ probe: "Fonte do som",
39409
+ wall: "Parede (oclus\xE3o)",
39410
+ 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" }
39411
+ };
39412
+ var EN2 = {
39413
+ ...PT,
39414
+ group: "Sound",
39415
+ name: "Sound panel (files and C1\u2013C10)",
39416
+ controls: {
39417
+ category: "1 \xB7 Category",
39418
+ sound: "1 \xB7 Sound",
39419
+ play: "1 \xB7 \u25B6 Play",
39420
+ material: "2 \xB7 Material",
39421
+ blow: "2 \xB7 Blow",
39422
+ strike: "2 \xB7 \u25B6 Play blow",
39423
+ distance: "3 \xB7 Distance",
39424
+ bearing: "3 \xB7 Bearing",
39425
+ environment: "3 \xB7 Place",
39426
+ strength: "3 \xB7 Strength",
39427
+ wall: "3 \xB7 Behind the wall",
39428
+ burst: "3 \xB7 Burst 20\xD7",
39429
+ play_a: "4 \xB7 \u25B6 A (current)",
39430
+ play_b: "4 \xB7 \u25B6 B (new)",
39431
+ ab_cycle: "4 \xB7 Alternate A/B",
39432
+ blind: "4 \xB7 Blind (X)",
39433
+ reveal: "4 \xB7 Reveal X",
39434
+ prefer_a: "4 \xB7 Prefer A",
39435
+ prefer_b: "4 \xB7 Prefer B",
39436
+ measure: "5 \xB7 Measure C1\u2013C10",
39437
+ record: "5 \xB7 Record 3 s",
39438
+ approve: "5 \xB7 Approve (C10)"
39439
+ },
39440
+ hints: {
39441
+ sound: "A = file, S = synthesized, F = missing. A file plays the file; the rest plays the current sound.",
39442
+ strike: "Plays impact_<material>_<blow>: the file if there is one, else the current sound of that material.",
39443
+ wall: "Puts the house wall between you and the source: the sound is muffled (C4).",
39444
+ burst: "Twenty times every 60 ms, like a burst (C9).",
39445
+ ab_cycle: "A, B, A, B, one a second.",
39446
+ blind: "Plays A or B without saying which; reveal it after.",
39447
+ measure: "Measures the chosen sound through the real engine (offline context): sync, right sound, distance, side, variation, level, strength, tail and burst.",
39448
+ record: "Records 3 s of the output with the sound playing and downloads it (C10).",
39449
+ approve: "Marks C10 of the chosen sound as approved by your ear."
39450
+ },
39451
+ categories: { armas: "Weapons", impactos: "Impacts", "explosoes-fogo": "Explosions and fire", personagem: "Character", veiculos: "Vehicles", "itens-portas": "Items and doors", ambiente: "Ambience", calibracao: "Calibration (SDK)" },
39452
+ blows: { bullet: "bullet", blunt: "punch/kick", blade: "knife", break: "break", drag: "drag/fall" },
39453
+ materials: {
39454
+ steel: "steel",
39455
+ aluminium: "aluminium",
39456
+ wood: "wood",
39457
+ plywood: "plywood",
39458
+ mdf: "MDF",
39459
+ glass: "glass",
39460
+ tempered_glass: "tempered glass",
39461
+ ceramic: "ceramic",
39462
+ concrete: "concrete",
39463
+ brick: "brick",
39464
+ plaster: "plaster",
39465
+ plastic: "plastic",
39466
+ fabric: "fabric",
39467
+ paper: "cardboard",
39468
+ foliage: "foliage",
39469
+ soil: "soil",
39470
+ flesh: "flesh"
39471
+ },
39472
+ environments: { indoor: "indoor", outdoor: "outdoor" },
39473
+ strengths: { light: "light", medium: "medium", heavy: "heavy" },
39474
+ 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" },
39475
+ 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" },
39476
+ run: { idle: "idle", measuring: "measuring\u2026", done: "done", failed: "failed", recording: "recording\u2026", recorded: "recording downloaded" },
39477
+ words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner" },
39478
+ probe: "Sound source",
39479
+ wall: "Wall (occlusion)",
39480
+ 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" }
39481
+ };
38399
39482
 
38400
39483
  // ../game-audio/dist/index.js
39484
+ var SFX_TIERS = { light: 0, medium: 0.34, heavy: 0.67 };
39485
+ function splitTier(id) {
39486
+ const match = /^(.*)_(light|medium|heavy)$/.exec(id);
39487
+ return match ? { base: match[1], tier: match[2] } : { base: id };
39488
+ }
39489
+ var refsOf = (entries2, baseUrl) => (entries2 ?? []).map((entry2) => ({ url: `${baseUrl}${entry2.file}` }));
39490
+ function sampleBankFromManifest(manifest, options) {
39491
+ const baseUrl = options.baseUrl ?? "";
39492
+ const samples = [];
39493
+ const tiersOf = /* @__PURE__ */ new Map();
39494
+ for (const sound3 of manifest.sounds) {
39495
+ const { base, tier } = splitTier(sound3.id);
39496
+ if (tier) tiersOf.set(base, [...tiersOf.get(base) ?? [], sound3]);
39497
+ }
39498
+ const bases = /* @__PURE__ */ new Map();
39499
+ for (const sound3 of manifest.sounds) {
39500
+ if (!splitTier(sound3.id).tier) bases.set(sound3.id, sound3);
39501
+ }
39502
+ for (const [base, tiers] of tiersOf) {
39503
+ if (!bases.has(base)) bases.set(base, { id: base, category: tiers[0]?.category ?? "", layers: { close: tiers[0]?.layers.close ?? [] } });
39504
+ }
39505
+ for (const sound3 of bases.values()) {
39506
+ const numbers = options.sounds?.[sound3.id] ?? options.defaults?.(sound3);
39507
+ const close = refsOf(sound3.layers.close, baseUrl);
39508
+ if (!numbers || close.length === 0) continue;
39509
+ const tiers = (tiersOf.get(sound3.id) ?? []).map((tier) => {
39510
+ const name = splitTier(tier.id).tier;
39511
+ const tierFar = refsOf(tier.layers.far, baseUrl);
39512
+ return { name, from: SFX_TIERS[name] ?? 0, close: refsOf(tier.layers.close, baseUrl), ...tierFar.length > 0 ? { far: tierFar } : {} };
39513
+ }).sort((a, b) => a.from - b.from);
39514
+ const far = refsOf(sound3.layers.far, baseUrl);
39515
+ const indoor = refsOf(sound3.layers.tail_in, baseUrl);
39516
+ const outdoor = refsOf(sound3.layers.tail_out, baseUrl);
39517
+ const files = [sound3, ...tiersOf.get(sound3.id) ?? []].flatMap((entry2) => Object.values(entry2.layers).flatMap((entries2) => entries2 ?? []));
39518
+ samples.push({
39519
+ id: sound3.id,
39520
+ bus: numbers.bus,
39521
+ close,
39522
+ ...far.length > 0 ? { far } : {},
39523
+ ...tiers.length > 0 ? { tiers } : {},
39524
+ ...numbers.crossover ? { crossover: numbers.crossover } : {},
39525
+ ...indoor.length > 0 || outdoor.length > 0 ? { tail: { ...indoor.length > 0 ? { indoor } : {}, ...outdoor.length > 0 ? { outdoor } : {}, ...numbers.tailGain === void 0 ? {} : { gain: numbers.tailGain } } } : {},
39526
+ ...numbers.pitch === void 0 ? {} : { pitch: numbers.pitch },
39527
+ ...numbers.gain === void 0 ? {} : { gain: numbers.gain },
39528
+ ...numbers.falloff ? { falloff: numbers.falloff } : {},
39529
+ ...numbers.cutoffK === void 0 ? {} : { cutoffK: numbers.cutoffK },
39530
+ ...numbers.group ? { group: numbers.group } : {},
39531
+ ...numbers.priority === void 0 ? {} : { priority: numbers.priority },
39532
+ ...numbers.fallback ? { fallback: numbers.fallback } : {},
39533
+ sources: files.map((file) => `${file.file}: ${file.source} ${file.author} ${file.license}`)
39534
+ });
39535
+ }
39536
+ return { id: options.id, samples };
39537
+ }
38401
39538
  var ATTACK = 2e-3;
38402
39539
  function lengthOf(d, attack) {
38403
39540
  if (typeof d === "number") return attack + d;
@@ -38460,6 +39597,570 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38460
39597
  ]
38461
39598
  };
38462
39599
  }
39600
+ var sceneMany = (count, make3) => Array.from({ length: count }, make3);
39601
+ var FOLEY_FALLOFF = {
39602
+ model: "inverse_square",
39603
+ k: 0.02,
39604
+ cutoffK: 0.04,
39605
+ minGain: 4e-3
39606
+ };
39607
+ var vary = (f, pct = 0.04) => [Math.round(f * (1 - pct)), Math.round(f * (1 + pct))];
39608
+ var foley = (id, layers, note2) => ({
39609
+ id,
39610
+ bus: "world",
39611
+ pan: true,
39612
+ layers,
39613
+ falloff: FOLEY_FALLOFF,
39614
+ sources: [`sdk: ${note2}`]
39615
+ });
39616
+ var click = (f, vol, delay = 0) => [
39617
+ sceneNoise({
39618
+ type: "bandpass",
39619
+ f: vary(f, 0.08),
39620
+ q: 4,
39621
+ d: [0.018, 0.03],
39622
+ vol: [vol * 0.85, vol * 1.1],
39623
+ delay
39624
+ }),
39625
+ sceneTone({
39626
+ f: vary(f * 0.8, 0.06),
39627
+ f2: Math.round(f * 0.62),
39628
+ d: [0.015, 0.025],
39629
+ vol: vol * 0.18,
39630
+ delay
39631
+ })
39632
+ ];
39633
+ var meet = (body, panel, vol, latch) => [
39634
+ sceneTone({
39635
+ f: vary(body),
39636
+ f2: Math.round(body * 0.5),
39637
+ d: [0.12, 0.16],
39638
+ vol: [vol * 0.9, vol * 1.1]
39639
+ }),
39640
+ sceneNoise({
39641
+ type: "bandpass",
39642
+ f: vary(panel, 0.06),
39643
+ q: 1.2,
39644
+ d: [0.08, 0.12],
39645
+ vol: [vol * 0.9, vol * 1.1],
39646
+ wet: 0.25
39647
+ }),
39648
+ ...click(latch, vol * 0.45, [0.012, 0.028])
39649
+ ];
39650
+ var LEAF_FOLEY = [
39651
+ // a wooden house door or wardrobe door: latch, a short creak, the leaf on the frame (the hard slam adds the frame rattling)
39652
+ foley("door_wood_open", click(2400, 0.4), "the latch of a wooden door giving"),
39653
+ foley(
39654
+ "door_creak",
39655
+ [
39656
+ sceneTone({ f: [190, 240], f2: 310, d: [0.4, 0.55], vol: [0.04, 0.06] }),
39657
+ sceneNoise({
39658
+ type: "bandpass",
39659
+ f: [480, 560],
39660
+ q: 6,
39661
+ a: 0.05,
39662
+ d: [0.35, 0.5],
39663
+ vol: [0.1, 0.14]
39664
+ })
39665
+ ],
39666
+ "a wooden hinge turning"
39667
+ ),
39668
+ foley("door_wood_close", meet(120, 600, 0.55, 2400), "a wooden leaf meeting its frame"),
39669
+ foley(
39670
+ "door_wood_slam",
39671
+ [
39672
+ ...meet(110, 560, 0.85, 2300),
39673
+ ...sceneMany(
39674
+ 3,
39675
+ () => sceneNoise({
39676
+ type: "bandpass",
39677
+ f: [900, 1500],
39678
+ q: 3,
39679
+ d: 0.03,
39680
+ vol: [0.08, 0.14],
39681
+ delay: [0.05, 0.18]
39682
+ })
39683
+ )
39684
+ ],
39685
+ "a wooden door slammed: the frame and the hinges rattle after the hit"
39686
+ ),
39687
+ // a metal door or gate: a hollow panel that rings a little
39688
+ foley("door_metal_open_close", [...meet(160, 900, 0.55, 2e3), sceneTone({ f: vary(1250), f2: 1150, d: [0.1, 0.16], vol: 0.06, wet: 0.3 })], "a metal door or gate meeting its frame"),
39689
+ foley(
39690
+ "door_creak_metal",
39691
+ [
39692
+ sceneTone({ f: [620, 760], f2: 900, d: [0.3, 0.4], vol: [0.03, 0.045] }),
39693
+ sceneNoise({
39694
+ type: "bandpass",
39695
+ f: [1300, 1500],
39696
+ q: 8,
39697
+ a: 0.04,
39698
+ d: [0.25, 0.35],
39699
+ vol: [0.07, 0.09]
39700
+ })
39701
+ ],
39702
+ "a metal hinge turning"
39703
+ ),
39704
+ // the handle of a locked door shaken: the latch knocking in its keeper, five to seven times in half a second
39705
+ foley(
39706
+ "door_locked_rattle",
39707
+ [
39708
+ ...sceneMany(
39709
+ 6,
39710
+ () => sceneNoise({
39711
+ type: "bandpass",
39712
+ f: [1600, 2600],
39713
+ q: 3,
39714
+ d: [0.025, 0.04],
39715
+ vol: [0.22, 0.34],
39716
+ delay: [0, 0.45]
39717
+ })
39718
+ ),
39719
+ ...sceneMany(
39720
+ 3,
39721
+ () => sceneTone({
39722
+ f: [170, 210],
39723
+ f2: 120,
39724
+ d: 0.05,
39725
+ vol: [0.12, 0.18],
39726
+ delay: [0.02, 0.4]
39727
+ })
39728
+ )
39729
+ ],
39730
+ "the handle of a locked door shaken: latch knocks and the leaf thumping in its frame"
39731
+ ),
39732
+ // a fridge: the rubber seal lets go with a soft suck, the bottles in the door clink; closing it is a soft thump and the seal pulling shut
39733
+ foley(
39734
+ "fridge_open",
39735
+ [
39736
+ sceneNoise({
39737
+ type: "lowpass",
39738
+ f: [320, 380],
39739
+ a: 0.03,
39740
+ d: [0.14, 0.2],
39741
+ vol: [0.32, 0.4]
39742
+ }),
39743
+ sceneNoise({
39744
+ type: "bandpass",
39745
+ f: [800, 950],
39746
+ f2: 1400,
39747
+ q: 2,
39748
+ a: 0.02,
39749
+ d: 0.12,
39750
+ vol: [0.08, 0.12]
39751
+ }),
39752
+ ...sceneMany(
39753
+ 2,
39754
+ () => sceneTone({
39755
+ f: [2600, 4200],
39756
+ d: [0.04, 0.08],
39757
+ vol: [0.015, 0.03],
39758
+ delay: [0.08, 0.22]
39759
+ })
39760
+ )
39761
+ ],
39762
+ "a fridge door: the seal letting go and the bottles in the door"
39763
+ ),
39764
+ foley(
39765
+ "fridge_close",
39766
+ [
39767
+ sceneTone({ f: vary(85), f2: 45, d: [0.1, 0.13], vol: [0.38, 0.46] }),
39768
+ sceneNoise({
39769
+ type: "lowpass",
39770
+ f: [520, 620],
39771
+ d: [0.08, 0.12],
39772
+ vol: [0.3, 0.36]
39773
+ }),
39774
+ sceneNoise({
39775
+ type: "lowpass",
39776
+ f: [260, 320],
39777
+ a: 0.02,
39778
+ d: 0.1,
39779
+ vol: [0.12, 0.18],
39780
+ delay: [0.04, 0.07]
39781
+ }),
39782
+ ...sceneMany(
39783
+ 2,
39784
+ () => sceneTone({
39785
+ f: [2600, 4200],
39786
+ d: [0.04, 0.07],
39787
+ vol: [0.015, 0.03],
39788
+ delay: [0.03, 0.12]
39789
+ })
39790
+ )
39791
+ ],
39792
+ "a fridge door shut: a soft thump, the seal pulling and the bottles"
39793
+ ),
39794
+ // a microwave: the spring catch of the door, plastic on plastic
39795
+ foley(
39796
+ "microwave_open",
39797
+ [
39798
+ ...click(1800, 0.34),
39799
+ sceneTone({
39800
+ f: [3e3, 3400],
39801
+ f2: 2800,
39802
+ d: 0.05,
39803
+ vol: 0.025,
39804
+ delay: [0.01, 0.02]
39805
+ })
39806
+ ],
39807
+ "the spring catch of a microwave door"
39808
+ ),
39809
+ foley(
39810
+ "microwave_close",
39811
+ [
39812
+ sceneNoise({
39813
+ type: "bandpass",
39814
+ f: [1200, 1450],
39815
+ q: 2.5,
39816
+ d: [0.04, 0.06],
39817
+ vol: [0.36, 0.44]
39818
+ }),
39819
+ sceneTone({ f: vary(180), f2: 120, d: 0.05, vol: [0.16, 0.22] }),
39820
+ ...click(2200, 0.2, [8e-3, 0.016])
39821
+ ],
39822
+ "a microwave door clacking shut"
39823
+ ),
39824
+ foley("microwave_beep", [sceneTone({ wave: "square", f: 2e3, d: 0.12, vol: 0.05 })], "the beep of a microwave"),
39825
+ // drawers and sliding leaves: the runners, then the stop
39826
+ foley(
39827
+ "drawer_open",
39828
+ [
39829
+ sceneNoise({
39830
+ type: "bandpass",
39831
+ f: [620, 780],
39832
+ q: 2,
39833
+ a: 0.04,
39834
+ d: [0.28, 0.36],
39835
+ vol: [0.18, 0.24]
39836
+ }),
39837
+ sceneNoise({ type: "bandpass", f: [2e3, 2400], q: 5, d: 0.28, vol: 0.04 }),
39838
+ ...click(1400, 0.16, [0.28, 0.34])
39839
+ ],
39840
+ "a drawer on its runners and its stop"
39841
+ ),
39842
+ foley(
39843
+ "drawer_close",
39844
+ [
39845
+ sceneNoise({
39846
+ type: "bandpass",
39847
+ f: [620, 780],
39848
+ q: 2,
39849
+ a: 0.03,
39850
+ d: [0.2, 0.28],
39851
+ vol: [0.18, 0.24]
39852
+ }),
39853
+ sceneTone({
39854
+ f: vary(150),
39855
+ f2: 90,
39856
+ d: 0.08,
39857
+ vol: [0.22, 0.3],
39858
+ delay: [0.2, 0.26]
39859
+ })
39860
+ ],
39861
+ "a drawer pushed home"
39862
+ )
39863
+ ];
39864
+ var ITEM_FOLEY = [
39865
+ // the hand closing on the item: a touch of skin and cloth, then the item's own small sound
39866
+ foley(
39867
+ "item_pickup_metal",
39868
+ [
39869
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
39870
+ sceneTone({
39871
+ f: [1900, 2600],
39872
+ f2: 1700,
39873
+ d: [0.05, 0.08],
39874
+ vol: [0.06, 0.09],
39875
+ delay: [0.01, 0.03]
39876
+ })
39877
+ ],
39878
+ "picking up a metal thing"
39879
+ ),
39880
+ foley(
39881
+ "item_pickup_plastic",
39882
+ [
39883
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
39884
+ sceneNoise({
39885
+ type: "bandpass",
39886
+ f: [1500, 1900],
39887
+ q: 3,
39888
+ d: 0.03,
39889
+ vol: [0.15, 0.21],
39890
+ delay: [0.01, 0.03]
39891
+ })
39892
+ ],
39893
+ "picking up a plastic thing"
39894
+ ),
39895
+ foley(
39896
+ "item_pickup_glass",
39897
+ [
39898
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.15 }),
39899
+ sceneTone({
39900
+ f: [3200, 4200],
39901
+ d: [0.05, 0.08],
39902
+ vol: [0.038, 0.06],
39903
+ delay: [0.01, 0.03]
39904
+ })
39905
+ ],
39906
+ "picking up a glass thing"
39907
+ ),
39908
+ foley(
39909
+ "item_pickup_paper",
39910
+ [
39911
+ sceneNoise({
39912
+ type: "highpass",
39913
+ f: [2600, 3400],
39914
+ a: 0.01,
39915
+ d: [0.1, 0.14],
39916
+ vol: [0.12, 0.18]
39917
+ }),
39918
+ sceneNoise({ type: "bandpass", f: [800, 1e3], q: 1, d: 0.05, vol: 0.12 })
39919
+ ],
39920
+ "picking up paper or cardboard"
39921
+ ),
39922
+ foley(
39923
+ "item_pickup_fabric",
39924
+ [
39925
+ sceneNoise({
39926
+ type: "bandpass",
39927
+ f: [900, 1300],
39928
+ q: 0.8,
39929
+ a: 0.02,
39930
+ d: [0.12, 0.16],
39931
+ vol: [0.15, 0.21]
39932
+ })
39933
+ ],
39934
+ "picking up cloth"
39935
+ ),
39936
+ foley(
39937
+ "item_pickup_wood",
39938
+ [
39939
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
39940
+ sceneNoise({
39941
+ type: "bandpass",
39942
+ f: [900, 1100],
39943
+ q: 2,
39944
+ d: 0.04,
39945
+ vol: [0.15, 0.21],
39946
+ delay: [0.01, 0.03]
39947
+ })
39948
+ ],
39949
+ "picking up a wooden thing"
39950
+ ),
39951
+ foley(
39952
+ "item_pickup_food",
39953
+ [
39954
+ sceneNoise({ type: "bandpass", f: [700, 900], q: 1, d: 0.06, vol: 0.18 }),
39955
+ sceneNoise({
39956
+ type: "lowpass",
39957
+ f: [500, 650],
39958
+ d: 0.05,
39959
+ vol: 0.12,
39960
+ delay: [0.01, 0.03]
39961
+ })
39962
+ ],
39963
+ "picking up food"
39964
+ ),
39965
+ foley(
39966
+ "item_throw",
39967
+ [
39968
+ sceneNoise({
39969
+ type: "bandpass",
39970
+ f: [450, 650],
39971
+ f2: 1400,
39972
+ q: 1.5,
39973
+ a: 0.05,
39974
+ d: 0.18,
39975
+ vol: [0.1, 0.14]
39976
+ })
39977
+ ],
39978
+ "the arm throwing something: air"
39979
+ ),
39980
+ // a can: the tab cracks, the gas hisses out; drinking is gulps; crushing it crumples the sheet
39981
+ foley(
39982
+ "can_open",
39983
+ [
39984
+ ...click(3200, 0.36),
39985
+ sceneNoise({
39986
+ type: "highpass",
39987
+ f: [3600, 4400],
39988
+ a: 5e-3,
39989
+ d: [0.22, 0.3],
39990
+ vol: [0.16, 0.22],
39991
+ delay: [0.012, 0.02]
39992
+ }),
39993
+ ...sceneMany(5, () => sceneTone({ f: [4e3, 7e3], d: 0.02, vol: 0.01, delay: [0.05, 0.4] }))
39994
+ ],
39995
+ "the tab of a can and the gas hissing out"
39996
+ ),
39997
+ foley(
39998
+ "can_crush",
39999
+ [
40000
+ sceneNoise({
40001
+ type: "bandpass",
40002
+ f: [1600, 2200],
40003
+ q: 1.2,
40004
+ d: [0.18, 0.25],
40005
+ vol: [0.3, 0.38]
40006
+ }),
40007
+ ...sceneMany(
40008
+ 6,
40009
+ () => sceneTone({
40010
+ f: [1200, 3400],
40011
+ f2: 900,
40012
+ d: 0.03,
40013
+ vol: [0.03, 0.06],
40014
+ delay: [0, 0.2]
40015
+ })
40016
+ )
40017
+ ],
40018
+ "a can crushed in the hand"
40019
+ ),
40020
+ foley(
40021
+ "can_puncture_spray",
40022
+ [
40023
+ sceneNoise({
40024
+ type: "highpass",
40025
+ f: [2400, 3e3],
40026
+ a: 5e-3,
40027
+ d: [0.5, 0.7],
40028
+ vol: [0.2, 0.26]
40029
+ }),
40030
+ sceneNoise({ type: "bandpass", f: [600, 800], q: 2, d: 0.4, vol: 0.06 })
40031
+ ],
40032
+ "a can holed: the jet hissing out"
40033
+ ),
40034
+ foley(
40035
+ "bottle_open",
40036
+ [
40037
+ ...sceneMany(
40038
+ 5,
40039
+ () => sceneNoise({
40040
+ type: "bandpass",
40041
+ f: [2200, 3e3],
40042
+ q: 4,
40043
+ d: 8e-3,
40044
+ vol: [0.1, 0.16],
40045
+ delay: [0, 0.18]
40046
+ })
40047
+ ),
40048
+ sceneNoise({
40049
+ type: "highpass",
40050
+ f: 3800,
40051
+ a: 5e-3,
40052
+ d: 0.12,
40053
+ vol: [0.06, 0.1],
40054
+ delay: [0.18, 0.22]
40055
+ })
40056
+ ],
40057
+ "the screw cap of a bottle turning off"
40058
+ ),
40059
+ foley(
40060
+ "drink_gulp",
40061
+ [
40062
+ sceneNoise({
40063
+ type: "lowpass",
40064
+ f: [280, 360],
40065
+ a: 0.02,
40066
+ d: [0.1, 0.14],
40067
+ vol: [0.24, 0.3]
40068
+ }),
40069
+ sceneTone({ f: [140, 170], f2: 85, d: [0.08, 0.11], vol: [0.12, 0.16] })
40070
+ ],
40071
+ "a gulp"
40072
+ ),
40073
+ foley(
40074
+ "eat_crunch",
40075
+ [
40076
+ ...sceneMany(
40077
+ 4,
40078
+ () => sceneNoise({
40079
+ type: "bandpass",
40080
+ f: [1800, 3200],
40081
+ q: 1.5,
40082
+ d: [0.02, 0.04],
40083
+ vol: [0.12, 0.2],
40084
+ delay: [0, 0.12]
40085
+ })
40086
+ )
40087
+ ],
40088
+ "a bite of something crunchy"
40089
+ ),
40090
+ foley(
40091
+ "eat_chew",
40092
+ [
40093
+ sceneNoise({
40094
+ type: "lowpass",
40095
+ f: [450, 600],
40096
+ a: 0.03,
40097
+ d: [0.1, 0.14],
40098
+ vol: [0.12, 0.16]
40099
+ }),
40100
+ sceneNoise({
40101
+ type: "bandpass",
40102
+ f: [1100, 1400],
40103
+ q: 2,
40104
+ d: 0.04,
40105
+ vol: 0.04,
40106
+ delay: [0.03, 0.06]
40107
+ })
40108
+ ],
40109
+ "chewing"
40110
+ ),
40111
+ foley(
40112
+ "bandage_wrap",
40113
+ [
40114
+ sceneNoise({
40115
+ type: "bandpass",
40116
+ f: [1400, 1800],
40117
+ f2: 2400,
40118
+ q: 1,
40119
+ a: 0.04,
40120
+ d: [0.3, 0.4],
40121
+ vol: [0.12, 0.16]
40122
+ })
40123
+ ],
40124
+ "a bandage unrolled round a limb"
40125
+ ),
40126
+ foley(
40127
+ "pills_shake",
40128
+ [
40129
+ ...sceneMany(
40130
+ 8,
40131
+ () => sceneTone({
40132
+ f: [2400, 3600],
40133
+ d: 0.015,
40134
+ vol: [0.03, 0.05],
40135
+ delay: [0, 0.25]
40136
+ })
40137
+ ),
40138
+ sceneNoise({ type: "bandpass", f: [1500, 1900], q: 2, d: 0.25, vol: 0.05 })
40139
+ ],
40140
+ "pills shaken in their bottle"
40141
+ ),
40142
+ foley(
40143
+ "medkit_use",
40144
+ [
40145
+ ...click(1700, 0.24),
40146
+ sceneNoise({
40147
+ type: "bandpass",
40148
+ f: [1e3, 1400],
40149
+ q: 1,
40150
+ a: 0.03,
40151
+ d: [0.25, 0.32],
40152
+ vol: [0.08, 0.12],
40153
+ delay: [0.05, 0.08]
40154
+ })
40155
+ ],
40156
+ "a first aid kit opened and rummaged"
40157
+ ),
40158
+ foley("flashlight_click", click(2600, 0.3), "the switch of a flashlight")
40159
+ ];
40160
+ var FOLEY_BANK = {
40161
+ id: "foley",
40162
+ sounds: [...LEAF_FOLEY, ...ITEM_FOLEY]
40163
+ };
38463
40164
  var TAKES = [
38464
40165
  { f: 1, t: 1, v: 1 },
38465
40166
  { f: 1.08, t: 0.9, v: 0.92 },
@@ -39548,7 +41249,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39548
41249
  if (energy < 5e3) return "rifle_308_3006";
39549
41250
  return energy < 12e3 ? "sniper_338_300wm" : "antimateriel_50bmg";
39550
41251
  };
39551
- var pickTake = (last, random, takes2 = GUNSHOT_TAKES) => {
41252
+ var pickTake2 = (last, random, takes2 = GUNSHOT_TAKES) => {
39552
41253
  if (takes2 <= 1) return 0;
39553
41254
  if (last === void 0) return Math.floor(random() * takes2) % takes2;
39554
41255
  const next = Math.floor(random() * (takes2 - 1)) % (takes2 - 1);
@@ -39690,11 +41391,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39690
41391
  if (surface !== "soft") voices.push({ kind: "tone", frequency: 2900 * pitch, to: 2800 * pitch, gain: 0.025 * level, seconds: surface === "metal" ? 0.06 : 0.025 });
39691
41392
  } else {
39692
41393
  voices.push({ kind: "noise", filter: surface === "soft" || surface === "wood" ? "lowpass" : "highpass", frequency: knock2.frequency * pitch, q: 0.7, gain: knock2.gain * level, seconds: knock2.seconds });
39693
- const ringSeconds = RING_SECONDS[surface] * (kind === "rifle" ? 1.3 : 1);
39694
- if (ringSeconds > 0) {
41394
+ const ringSeconds2 = RING_SECONDS[surface] * (kind === "rifle" ? 1.3 : 1);
41395
+ if (ringSeconds2 > 0) {
39695
41396
  const f = RING[kind] * pitch;
39696
- voices.push({ kind: "tone", frequency: f, to: f * 0.985, gain: 0.07 * level, seconds: ringSeconds });
39697
- voices.push({ kind: "tone", frequency: f * PARTIAL, to: f * PARTIAL * 0.99, gain: 0.03 * level, seconds: ringSeconds * 0.6 });
41397
+ voices.push({ kind: "tone", frequency: f, to: f * 0.985, gain: 0.07 * level, seconds: ringSeconds2 });
41398
+ voices.push({ kind: "tone", frequency: f * PARTIAL, to: f * PARTIAL * 0.99, gain: 0.03 * level, seconds: ringSeconds2 * 0.6 });
39698
41399
  }
39699
41400
  }
39700
41401
  const surfaceName = surface === "hard" ? "concreto" : surface === "soft" ? "terra" : surface === "wood" ? "madeira" : "metal";
@@ -40455,6 +42156,72 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
40455
42156
  });
40456
42157
  };
40457
42158
 
42159
+ // src/data/reload-sample-hits.json
42160
+ var reload_sample_hits_default = {
42161
+ $comment: "Seconds from the start of each recorded reload step to its hit (median of its takes), measured on the converted files by tools/sfx-sources/hits.mjs. The reload clip starts the sound that much early, so the hit lands on the frame the part reaches its stop.",
42162
+ hits: {
42163
+ bolt_lift: 0.072,
42164
+ bolt_lock: 0.078,
42165
+ bolt_pull: 0.128,
42166
+ bolt_push: 0.054,
42167
+ charging_handle_pull: 0.074,
42168
+ charging_handle_release: 0.048,
42169
+ cylinder_close: 0.024,
42170
+ cylinder_eject: 0.394,
42171
+ cylinder_open: 0.07,
42172
+ lever_close: 0.147,
42173
+ lever_open: 0.095,
42174
+ mag_out: 0.012,
42175
+ mag_seat: 0.094,
42176
+ pump_back: 0.094,
42177
+ pump_forward: 0.118,
42178
+ shell_insert: 0.21,
42179
+ slide_release: 0.031,
42180
+ speedloader: 0.089
42181
+ }
42182
+ };
42183
+
42184
+ // src/audio/samples.ts
42185
+ var GUNSHOT_SAMPLE_IDS = [...Object.keys(gunshot_profiles_default.profiles), "suppressed_pistol", "suppressed_rifle"];
42186
+ var GUNSHOT_SAMPLE = {
42187
+ bus: "weapons",
42188
+ gain: 1,
42189
+ pitch: 0.04,
42190
+ crossover: { from: 20, to: 90 },
42191
+ falloff: { model: "inverse_square", k: 5e-4, minGain: 6e-4 },
42192
+ cutoffK: 0.04,
42193
+ priority: 3
42194
+ };
42195
+ var SUPPRESSED_SAMPLE = { ...GUNSHOT_SAMPLE, gain: 0.55, crossover: { from: 10, to: 50 }, falloff: { model: "inverse_square", k: 4e-3, minGain: 8e-4 } };
42196
+ var RELOAD_SAMPLE = { bus: "weapons", gain: 0.32, pitch: 0.05, falloff: { model: "inverse_square", k: 0.05, minGain: 2e-3 }, cutoffK: 0.02, priority: 2 };
42197
+ var CASING_SAMPLE = { bus: "weapons", gain: 0.22, pitch: 0.06, falloff: { model: "inverse_square", k: 0.08, minGain: 3e-3 }, cutoffK: 0.03, priority: 1 };
42198
+ var casingSampleId = (sample) => sample.replace(/_(pistol|rifle|shell)$/, "");
42199
+ var RELOAD_SAMPLE_HITS = reload_sample_hits_default.hits;
42200
+ var WEIGHT_INTENSITY = { light: 0.1, medium: 0.5, heavy: 0.9 };
42201
+ var CUES_IN_SAMPLE = { mag_release: "mag_out", mag_insert: "mag_seat" };
42202
+ var isGunshot = (id) => GUNSHOT_SAMPLE_IDS.includes(id);
42203
+ function weaponSampleDefaults(sound3, overrides = {}) {
42204
+ if (sound3.category !== "armas") return void 0;
42205
+ const base = sound3.id.startsWith("suppressed_") ? SUPPRESSED_SAMPLE : isGunshot(sound3.id) ? GUNSHOT_SAMPLE : sound3.id.startsWith("casing_drop_") ? CASING_SAMPLE : RELOAD_SAMPLE;
42206
+ return { ...base, ...overrides[sound3.id] };
42207
+ }
42208
+ function weaponSampleBank(options) {
42209
+ const bank = sampleBankFromManifest(options.manifest, { id: "weapons_three_samples", ...options.url ? {} : { baseUrl: options.baseUrl ?? "" }, defaults: (sound3) => weaponSampleDefaults(sound3, options.sounds) });
42210
+ const url2 = options.url;
42211
+ const remap = (refs) => refs && url2 ? refs.map((ref) => ({ ...ref, url: url2(ref.url) })) : refs ? [...refs] : void 0;
42212
+ const samples = (bank.samples ?? []).map((sample) => {
42213
+ const tiers = sample.tiers?.map((tier) => ({ ...tier, close: remap(tier.close) ?? [], ...tier.far ? { far: remap(tier.far) ?? [] } : {} }));
42214
+ return {
42215
+ ...sample,
42216
+ close: remap(sample.close) ?? [],
42217
+ ...sample.far ? { far: remap(sample.far) ?? [] } : {},
42218
+ ...tiers ? { tiers } : {},
42219
+ ...isGunshot(sample.id) ? { choke: { voices: 3, fade: 0.06 } } : {}
42220
+ };
42221
+ });
42222
+ return { id: bank.id, samples };
42223
+ }
42224
+
40458
42225
  // src/audio.ts
40459
42226
  var NOISE_SECONDS = 1.5;
40460
42227
  var DEFAULT_SEED = 1;
@@ -40475,6 +42242,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
40475
42242
  const channel = shared2 && options.seed !== void 0 ? shared2.channel({ seed }) : engine.channel({});
40476
42243
  engine.register({ id: `weapons_three@${classBurstId(distanceCurve)}`, sounds: weaponRecipes(distanceCurve) });
40477
42244
  const classBurst = classBurstId(distanceCurve);
42245
+ const sampleBank = options.samples ? weaponSampleBank(options.samples) : null;
42246
+ if (sampleBank) engine.register(sampleBank);
42247
+ const sampleIds = new Set((sampleBank?.samples ?? []).map((sample) => sample.id));
42248
+ let sampleLoad = null;
42249
+ const loadSamples = () => sampleLoad ?? (sampleLoad = sampleIds.size > 0 ? engine.loadSamples([...sampleIds]).then(() => void 0, () => void 0) : Promise.resolve());
42250
+ const sampleReady = (id) => {
42251
+ if (!sampleIds.has(id)) return false;
42252
+ if (engine.sampleStatus(id) === "ready") return true;
42253
+ void loadSamples();
42254
+ return false;
42255
+ };
40478
42256
  const burst2 = (config) => {
40479
42257
  const id = config.filter === "highpass" ? BURST_IDS.highpass : BURST_IDS.lowpass;
40480
42258
  const params = { frequency: config.frequency, q: config.q ?? 0.8, gain: Math.max(config.gain, 2e-4), seconds: config.seconds };
@@ -40523,8 +42301,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
40523
42301
  };
40524
42302
  const casing2 = (input) => {
40525
42303
  const sound3 = computeCasingSound({ ...input, pitch: input.pitch ?? 1 + (channel.random() * 2 - 1) * 0.06 });
42304
+ const recorded = sound3.kind === "shell" ? "" : casingSampleId(sound3.sample);
42305
+ if (recorded && sampleReady(recorded)) {
42306
+ if ((input.bounce ?? 0) === 0) channel.play(recorded, { volume: Math.min(1, Math.max(0.2, (input.speed ?? 1.5) / 2.5)) });
42307
+ options.onCasing?.(sound3, "sample");
42308
+ return;
42309
+ }
40526
42310
  for (const voice2 of sound3.voices) playCasingVoice(voice2);
40527
- options.onCasing?.(sound3);
42311
+ options.onCasing?.(sound3, "synth");
40528
42312
  };
40529
42313
  const lastTake = /* @__PURE__ */ new Map();
40530
42314
  const pitchSpread = options.pitchSpread ?? 0.04;
@@ -40542,12 +42326,29 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
40542
42326
  if (options.shotModel !== "game") {
40543
42327
  const base = { energyJ, category, ...extras, space: extras.space ?? (extras.indoors ? "indoor" : "outdoor") };
40544
42328
  const profile = computeGunshot(base, options.gunshot).profile;
40545
- const take = pickTake(lastTake.get(profile), channel.random);
42329
+ const take = pickTake2(lastTake.get(profile), channel.random);
40546
42330
  lastTake.set(profile, take);
40547
42331
  const pitch = 1 + (channel.random() * 2 - 1) * pitchSpread;
40548
42332
  const shot = computeGunshot({ ...base, take, pitch }, options.gunshot);
42333
+ const recorded = shot.samples.close;
42334
+ if (shot.audible && sampleReady(recorded)) {
42335
+ const occlusion = Math.min(1, Math.max(0, extras.occlusion ?? 0));
42336
+ const handle = channel.play(recorded, {
42337
+ volume: (extras.volume ?? 1) * (1 - 0.5 * occlusion),
42338
+ environment: base.space === "indoor" ? "indoor" : "outdoor",
42339
+ ...extras.position ? { position: [extras.position[0], extras.position[1], extras.position[2]] } : {},
42340
+ ...extras.distance === void 0 ? {} : { distance: extras.distance },
42341
+ ...shot.delay > 0 ? { delay: shot.delay } : {}
42342
+ });
42343
+ if (base.space === "indoor") {
42344
+ for (const voice2 of shot.voices) if (voice2.group === "tail") playGunshotVoice(voice2, extras);
42345
+ }
42346
+ const layers = handle?.kind === "sample" ? handle.plan.layers.map((layer2) => ({ role: layer2.role, take: layer2.take, rate: layer2.rate })) : [];
42347
+ options.onGunshot?.(shot, { ...base, take, pitch }, { kind: "sample", id: recorded, layers });
42348
+ return;
42349
+ }
40549
42350
  for (const voice2 of shot.voices) playGunshotVoice(voice2, extras);
40550
- if (shot.audible) options.onGunshot?.(shot, { ...base, take, pitch });
42351
+ if (shot.audible) options.onGunshot?.(shot, { ...base, take, pitch }, { kind: "synth" });
40551
42352
  return;
40552
42353
  }
40553
42354
  const sound3 = shotSound({ ...extras, energyJ, category });
@@ -40562,9 +42363,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
40562
42363
  mechanical("dry_fire");
40563
42364
  },
40564
42365
  mechanical,
40565
- cue(id, profile) {
40566
- for (const entry2 of reloadCueSteps(id, profile, cueTable)) step(entry2);
42366
+ cue(id, profile, lead = 0) {
42367
+ const carriedBy = CUES_IN_SAMPLE[id];
42368
+ if (carriedBy && sampleReady(carriedBy)) return "carried";
42369
+ if (sampleReady(id)) {
42370
+ channel.play(id, { intensity: WEIGHT_INTENSITY[(profile ?? DEFAULT_SOUND_PROFILE).weight] });
42371
+ return "sample";
42372
+ }
42373
+ for (const entry2 of reloadCueSteps(id, profile, cueTable)) step(lead > 0 ? { ...entry2, at: entry2.at + lead } : entry2);
42374
+ return "synth";
40567
42375
  },
42376
+ loadSamples,
40568
42377
  event(event) {
40569
42378
  const id = mechanicalSoundOf(event);
40570
42379
  if (id) mechanical(id);
@@ -47576,17 +49385,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
47576
49385
  if (!builder) throw new Error(`game-items-three: no builder for the model "${entry2.id}"`);
47577
49386
  return { entry: entry2, builder };
47578
49387
  });
47579
- var registryOver = (entries, builders) => ({
49388
+ var registryOver = (entries2, builders) => ({
47580
49389
  has: (id) => builders.has(id),
47581
- entry: (id) => entries.get(id),
49390
+ entry: (id) => entries2.get(id),
47582
49391
  builder: (id) => builders.get(id),
47583
- list: () => [...entries.keys()],
49392
+ list: () => [...entries2.keys()],
47584
49393
  register(entry2, builder) {
47585
- entries.set(entry2.id, entry2);
49394
+ entries2.set(entry2.id, entry2);
47586
49395
  builders.set(entry2.id, builder);
47587
49396
  },
47588
49397
  unregister(id) {
47589
- entries.delete(id);
49398
+ entries2.delete(id);
47590
49399
  builders.delete(id);
47591
49400
  }
47592
49401
  });
@@ -51275,6 +53084,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
51275
53084
  }
51276
53085
 
51277
53086
  // src/animation/clip-builder.ts
53087
+ var RELOAD_SAMPLE_HITS2 = reload_sample_hits_default.hits;
51278
53088
  var ROOT = "root";
51279
53089
  var AXIS_INDEX = { x: 0, y: 1, z: 2 };
51280
53090
  var EASE_STEPS = 10;
@@ -51282,7 +53092,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
51282
53092
  var move = (part, axis2, start, duration, from, to) => ({ part, channel: "position", axis: axis2, start, duration, from, to });
51283
53093
  var turn = (part, axis2, start, duration, from, to) => ({ part, channel: "rotation", axis: axis2, start, duration, from, to });
51284
53094
  var cue = (t, type, data) => ({ t, type, ...data ? { data } : {} });
51285
- var sound2 = (t, id) => ({ t, type: "sound", data: { id } });
53095
+ var sound2 = (t, id) => {
53096
+ const lead = RELOAD_SAMPLE_HITS2[id];
53097
+ return lead ? { t, type: "sound", data: { id }, lead } : { t, type: "sound", data: { id } };
53098
+ };
51286
53099
  var stage = (id, t) => ({ t, type: STAGE_EVENT, data: { stage: id } });
51287
53100
  var valueAt = (tweens, t, includeInstants) => {
51288
53101
  let value = 0;
@@ -54613,7 +56426,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
54613
56426
  var ARC_HEIGHT = 2.2;
54614
56427
  var GLASS_GREEN = 3828282;
54615
56428
  var OBJECT_ID_DRAWS = 12;
54616
- var at = (offset) => vadd(TARGET, offset);
56429
+ var at2 = (offset) => vadd(TARGET, offset);
54617
56430
  var MOLOTOV = {
54618
56431
  id: "molotov",
54619
56432
  pageId: "molotov",
@@ -54635,17 +56448,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
54635
56448
  when(not(variable("landed")), [
54636
56449
  set("landed", true),
54637
56450
  cue2("bottle_visible", { visible: false }),
54638
- repeat(scaled(12), debris({ p: at([0, 0.1, 0]), dir: UP6, cone: 1.3, sp: [1, 3], size: [0.04, 0.01, 0.04], c: GLASS_GREEN, life: 12 })),
54639
- burst("add", 45, { p: at([0, 0.15, 0]), dir: UP6, cone: 1.35, sp: [2, 4], drag: 2, life: [0.3, 0.6], size: [0.3, 0.5], c0: 16765562, c1: 10493960, fi: 0.05 }),
54640
- flash(at([0, 0.6, 0]), 16747056, 6, 0.3, 12, true),
56451
+ repeat(scaled(12), debris({ p: at2([0, 0.1, 0]), dir: UP6, cone: 1.3, sp: [1, 3], size: [0.04, 0.01, 0.04], c: GLASS_GREEN, life: 12 })),
56452
+ burst("add", 45, { p: at2([0, 0.15, 0]), dir: UP6, cone: 1.35, sp: [2, 4], drag: 2, life: [0.3, 0.6], size: [0.3, 0.5], c0: 16765562, c1: 10493960, fi: 0.05 }),
56453
+ flash(at2([0, 0.6, 0]), 16747056, 6, 0.3, 12, true),
54641
56454
  decal(TARGET[0], 0, TARGET[2], 4.2, 854793, { grow: 0.8, life: 60 })
54642
56455
  ])
54643
56456
  ]),
54644
56457
  when(and(gt(TIME, FLIGHT), lt(TIME, 8.2)), [
54645
56458
  set("poolRadius", min(2.2, add(0.5, mul(sub(TIME, 1), 2.1)))),
54646
56459
  set("poolFade", ite(gt(TIME, 7), max(0.1, sub(1, div(sub(TIME, 7), 1.2))), 1)),
54647
- call2("fire_emit", "mol", at([0, 0.05, 0]), variable("poolRadius"), mul(1.6, variable("poolFade")), { smoke: 16 }),
54648
- fireLight(0, at([0, 1, 0]), mul(3.2, variable("poolFade")), TIME)
56460
+ call2("fire_emit", "mol", at2([0, 0.05, 0]), variable("poolRadius"), mul(1.6, variable("poolFade")), { smoke: 16 }),
56461
+ fireLight(0, at2([0, 1, 0]), mul(3.2, variable("poolFade")), TIME)
54649
56462
  ], [
54650
56463
  when(ge(TIME, 8.2), [
54651
56464
  fireLightOff(0),
@@ -55143,11 +56956,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
55143
56956
  ])
55144
56957
  ]
55145
56958
  };
55146
- var at2 = (x, y, z) => [SPOT[0] + x, SPOT[1] + y, SPOT[2] + z];
55147
- var FRONT_LEG = at2(-0.52, 0.2, 0.32);
55148
- var TOP_CORNER = at2(0.3, 0.78, 0.2);
55149
- var TOP_PIECE = at2(-0.3, 0.78, -0.2);
55150
- var FAR_CORNER = at2(0.3, 0.78, -0.2);
56959
+ var at22 = (x, y, z) => [SPOT[0] + x, SPOT[1] + y, SPOT[2] + z];
56960
+ var FRONT_LEG = at22(-0.52, 0.2, 0.32);
56961
+ var TOP_CORNER = at22(0.3, 0.78, 0.2);
56962
+ var TOP_PIECE = at22(-0.3, 0.78, -0.2);
56963
+ var FAR_CORNER = at22(0.3, 0.78, -0.2);
55151
56964
  var HOLE_OBJECT_ID_DRAWS = 12;
55152
56965
  var HOLES = 9;
55153
56966
  var GLOCK_FROM = [-0.4, 1, 6];
@@ -56647,35 +58460,35 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
56647
58460
  var NOOP = () => {
56648
58461
  };
56649
58462
  function createSchedule() {
56650
- const entries = [];
58463
+ const entries2 = [];
56651
58464
  const spare = [];
56652
58465
  return {
56653
58466
  at(time, action) {
56654
58467
  const entry2 = spare.pop() ?? { time: 0, action: NOOP };
56655
58468
  entry2.time = time;
56656
58469
  entry2.action = action;
56657
- entries.push(entry2);
58470
+ entries2.push(entry2);
56658
58471
  },
56659
58472
  update(now) {
56660
- for (let index = entries.length - 1; index >= 0; index--) {
56661
- const entry2 = entries[index];
58473
+ for (let index = entries2.length - 1; index >= 0; index--) {
58474
+ const entry2 = entries2[index];
56662
58475
  if (!entry2 || entry2.time > now) continue;
56663
58476
  const action = entry2.action;
56664
- entries.splice(index, 1);
58477
+ entries2.splice(index, 1);
56665
58478
  entry2.action = NOOP;
56666
58479
  spare.push(entry2);
56667
58480
  action();
56668
58481
  }
56669
58482
  },
56670
58483
  clear() {
56671
- for (const entry2 of entries) {
58484
+ for (const entry2 of entries2) {
56672
58485
  entry2.action = NOOP;
56673
58486
  spare.push(entry2);
56674
58487
  }
56675
- entries.length = 0;
58488
+ entries2.length = 0;
56676
58489
  },
56677
58490
  get pending() {
56678
- return entries.length;
58491
+ return entries2.length;
56679
58492
  }
56680
58493
  };
56681
58494
  }
@@ -57430,12 +59243,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
57430
59243
  var SCORCH_VARIANTS = 3;
57431
59244
  var RIM_COLORS = [2829359, 3815478];
57432
59245
  function createMarkStore() {
57433
- const entries = [];
59246
+ const entries2 = [];
57434
59247
  let version = 0;
57435
59248
  let bloodCount = 0;
57436
59249
  const listeners = /* @__PURE__ */ new Set();
57437
59250
  return {
57438
- entries,
59251
+ entries: entries2,
57439
59252
  get version() {
57440
59253
  return version;
57441
59254
  },
@@ -57444,13 +59257,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
57444
59257
  },
57445
59258
  lift: 0,
57446
59259
  add(mark) {
57447
- entries.push(mark);
59260
+ entries2.push(mark);
57448
59261
  if (mark.kind === "blood") bloodCount++;
57449
59262
  version++;
57450
59263
  for (const listener of listeners) listener({ type: "add", mark });
57451
59264
  },
57452
59265
  clear() {
57453
- entries.length = 0;
59266
+ entries2.length = 0;
57454
59267
  bloodCount = 0;
57455
59268
  this.lift = 0;
57456
59269
  version++;
@@ -61741,7 +63554,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61741
63554
  breechRound();
61742
63555
  break;
61743
63556
  case "sound":
61744
- world.sound(session.id, String(data.id), session.soundProfile);
63557
+ world.sound(session.id, String(data.id), session.soundProfile, typeof data.lead === "number" ? data.lead : void 0);
61745
63558
  break;
61746
63559
  default:
61747
63560
  break;
@@ -62314,9 +64127,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
62314
64127
  audio,
62315
64128
  layers,
62316
64129
  groundAt,
62317
- sound: (weapon, cue3, profile) => {
62318
- audio?.cue(cue3, profile);
62319
- options.onSound?.({ weapon, cue: cue3, profile });
64130
+ sound: (weapon, cue3, profile, lead) => {
64131
+ const played = audio?.cue(cue3, profile, lead);
64132
+ options.onSound?.({ weapon, cue: cue3, profile, ...played ? { played } : {} });
62320
64133
  },
62321
64134
  schedule: (delay, seconds, step, done) => void tasks.push({ delay, seconds, elapsed: 0, step: (progress) => step(ease(progress)), done })
62322
64135
  };
@@ -63711,7 +65524,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
63711
65524
  reloadStart: "Reload started: {weapon} ({ammo}/{capacity})",
63712
65525
  reloadClip: "Reload of {weapon}: {kind}, clip {clip} ({seconds} s)",
63713
65526
  soundCue: "Sound: {cue} ({weapon})",
65527
+ cueRecorded: " \xB7 recorded",
65528
+ cueCarried: " \xB7 inside the next recording",
63714
65529
  gunshotSound: "Shot sound: {profile}, take {take}/4, pitch {pitch}, {space}, {distance} m{far}",
65530
+ gunshotRecorded: " \xB7 recorded take {takes}",
63715
65531
  casingSound: "Case lands: {case} on {surface}",
63716
65532
  spaces: { indoor: "indoors", outdoor: "outdoors" },
63717
65533
  surfaces: { hard: "concrete", wood: "wood", metal: "metal", soft: "dirt" },
@@ -63743,7 +65559,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
63743
65559
  reloadStart: "Recarga iniciada: {weapon} ({ammo}/{capacity})",
63744
65560
  reloadClip: "Recarga de {weapon}: {kind}, clipe {clip} ({seconds} s)",
63745
65561
  soundCue: "Som: {cue} ({weapon})",
65562
+ cueRecorded: " \xB7 grava\xE7\xE3o",
65563
+ cueCarried: " \xB7 dentro da grava\xE7\xE3o seguinte",
63746
65564
  gunshotSound: "Som do tiro: {profile}, tomada {take}/4, tom {pitch}, {space}, {distance} m{far}",
65565
+ gunshotRecorded: " \xB7 grava\xE7\xE3o, tomada {takes}",
63747
65566
  casingSound: "C\xE1psula caiu: {case} em {surface}",
63748
65567
  spaces: { indoor: "ambiente fechado", outdoor: "ao ar livre" },
63749
65568
  surfaces: { hard: "concreto", wood: "madeira", metal: "metal", soft: "terra" },
@@ -63865,7 +65684,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
63865
65684
  const accessoryText = accessoryTextOf(locale);
63866
65685
  const seedOption = options.seed === void 0 ? {} : { seed: options.seed };
63867
65686
  let audio;
63868
- const ownAudio = () => audio === void 0 ? audio = options.sound === false ? null : createWeaponAudio(void 0, seedOption) : audio;
65687
+ const ownAudio = () => audio === void 0 ? audio = options.sound === false ? null : createWeaponAudio(void 0, { ...seedOption, ...options.samples ? { samples: options.samples } : {} }) : audio;
63869
65688
  const weaponDefs = [...arsenal.weapons.values()].filter((def) => !BODY_ACTIONS.has(def.id));
63870
65689
  const thumbnailBase = options.thumbnailBase ?? DEFAULT_THUMBNAIL_BASE;
63871
65690
  const withThumbnail = (item, key2) => {
@@ -63952,18 +65771,22 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
63952
65771
  audio = createWeaponAudio(void 0, {
63953
65772
  ...seedOption,
63954
65773
  engine: lentEngine,
63955
- onGunshot: (shot, input) => lent.log(
63956
- fill(text.gunshotSound, { profile: shot.profile, take: String(shot.take + 1), pitch: `${shot.pitch >= 1 ? "+" : ""}${((shot.pitch - 1) * 100).toFixed(1)} %`, space: text.spaces[input.space ?? "outdoor"], distance: String(Math.round(input.distance ?? 0)), far: shot.closeMix < 1 ? ` (${Math.round((1 - shot.closeMix) * 100)} % far)` : "" }),
63957
- { profile: shot.profile, take: shot.take, pitch: shot.pitch, space: input.space ?? "outdoor", distance: input.distance ?? 0, closeMix: shot.closeMix, delay: shot.delay, samples: shot.samples },
63958
- "sound"
63959
- ),
63960
- onCasing: (sound3) => lent.log(fill(text.casingSound, { case: sound3.kind, surface: text.surfaces[sound3.surface] }), { case: sound3.kind, surface: sound3.surface, sample: sound3.sample }, "sound")
65774
+ ...options.samples ? { samples: options.samples } : {},
65775
+ onGunshot: (shot, input, played) => {
65776
+ const recorded = played.kind === "sample" && played.layers.length > 0 ? fill(text.gunshotRecorded, { takes: played.layers.map((layer2) => `${layer2.role} ${layer2.take + 1}`).join(", ") }) : "";
65777
+ lent.log(
65778
+ fill(text.gunshotSound, { profile: shot.profile, take: String(shot.take + 1), pitch: `${shot.pitch >= 1 ? "+" : ""}${((shot.pitch - 1) * 100).toFixed(1)} %`, space: text.spaces[input.space ?? "outdoor"], distance: String(Math.round(input.distance ?? 0)), far: shot.closeMix < 1 ? ` (${Math.round((1 - shot.closeMix) * 100)} % far)` : "" }) + recorded,
65779
+ { profile: shot.profile, take: shot.take, pitch: shot.pitch, space: input.space ?? "outdoor", distance: input.distance ?? 0, closeMix: shot.closeMix, delay: shot.delay, samples: shot.samples, played },
65780
+ "sound"
65781
+ );
65782
+ },
65783
+ onCasing: (sound3, played) => lent.log(fill(text.casingSound, { case: sound3.kind, surface: text.surfaces[sound3.surface] }) + (played === "sample" ? text.cueRecorded : ""), { case: sound3.kind, surface: sound3.surface, sample: sound3.sample, played }, "sound")
63961
65784
  });
63962
65785
  }
63963
65786
  let simulatedMs = 0;
63964
65787
  const offClock = lent.onFrame((dt) => simulatedMs += dt * 1e3);
63965
65788
  const created = createWeaponFx({
63966
- onSound: ({ weapon, cue: cue3, profile }) => lent.log(fill(text.soundCue, { weapon: arsenal.name(weapon), cue: text.soundCues[cue3] ?? cue3 }), { weapon, cue: cue3, weight: profile.weight, material: profile.material }, "sound"),
65789
+ onSound: ({ weapon, cue: cue3, profile, played }) => lent.log(fill(text.soundCue, { weapon: arsenal.name(weapon), cue: text.soundCues[cue3] ?? cue3 }) + (played === "sample" ? text.cueRecorded : played === "carried" ? text.cueCarried : ""), { weapon, cue: cue3, weight: profile.weight, material: profile.material, ...played ? { played } : {} }, "sound"),
63967
65790
  THREE: lent.THREE,
63968
65791
  scene: lent.scene,
63969
65792
  camera: lent.camera,
@@ -64130,6 +65953,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
64130
65953
  push_outward: "Every body inside the reach moved away from the blast ({moved} of {n}, {least} m at the least)",
64131
65954
  push_falloff: "{what}: the nearer one moved more than the farther one ({near} m at {nearDistance} m, {far} m at {farDistance} m)",
64132
65955
  push_person: "{who} at {distance} m was pushed {moved} m away",
65956
+ push_sheltered: "{what} at {distance} m stood behind cover ({cover}% of the blast got through) and moved only {moved} m",
64133
65957
  push_heavy: "{what} (too heavy to grab, not fixed) was moved {moved} m by the blast"
64134
65958
  }
64135
65959
  },
@@ -64215,6 +66039,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
64215
66039
  push_outward: "Todo corpo dentro do alcance se afastou da explos\xE3o ({moved} de {n}, no m\xEDnimo {least} m)",
64216
66040
  push_falloff: "{what}: o mais perto andou mais que o mais longe ({near} m a {nearDistance} m, {far} m a {farDistance} m)",
64217
66041
  push_person: "{who} a {distance} m foi empurrado {moved} m para longe",
66042
+ push_sheltered: "{what} a {distance} m estava atr\xE1s de cobertura ({cover}% da explos\xE3o passou) e andou s\xF3 {moved} m",
64218
66043
  push_heavy: "{what} (pesado demais para pegar, mas solto) foi movido {moved} m pela explos\xE3o"
64219
66044
  }
64220
66045
  }
@@ -64760,6 +66585,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
64760
66585
  ];
64761
66586
  var PUSH_READ_AT = 0.8;
64762
66587
  var MOVED = 0.15;
66588
+ var SHELTERED = 0.5;
66589
+ var SHELTERED_MOST = 1;
66590
+ var CHEST = 1.1;
66591
+ var MIDDLE = 0.3;
64763
66592
  var HEAVY_ITEMS = ["camp_table"];
64764
66593
  var flat = (a, b) => Math.hypot(a[0] - b[0], a[2] - b[2]);
64765
66594
  var bearing = (centre, distance3, degrees) => [centre[0] + Math.sin(degrees * Math.PI / 180) * distance3, 0, centre[2] + Math.cos(degrees * Math.PI / 180) * distance3];
@@ -64776,9 +66605,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
64776
66605
  name: mode === "p13" ? text.push_p13 : text.push_grenade,
64777
66606
  run: ({ room, step, expect }) => {
64778
66607
  let centre;
66608
+ let source;
64779
66609
  if (mode === "p13") {
64780
66610
  room.openArea(EXPLOSIVES_AREA_ID);
64781
66611
  const cylinder2 = room.spawned().find((spawned) => spawned.itemId === CYLINDER_ITEM);
66612
+ source = cylinder2?.objectId;
64782
66613
  const at3 = cylinder2?.root?.position;
64783
66614
  centre = [at3?.x ?? 0, 0, at3?.z ?? -10.66];
64784
66615
  } else {
@@ -64829,6 +66660,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
64829
66660
  const blast = bench.lastBlast();
64830
66661
  expect(!!blast, text.push_told, { waited });
64831
66662
  if (!blast) return;
66663
+ const coverOf = new Map(
66664
+ placed.map((body) => {
66665
+ const from = origin.get(body.objectId) ?? body.from;
66666
+ const person = body.kind === "zombie" || body.kind === "survivor";
66667
+ const ignore = source ? [source, body.objectId] : [body.objectId];
66668
+ return [body.objectId, room.world.blastReach?.(blast.point, [from[0], from[1] + (person ? CHEST : MIDDLE), from[2]], { ignore }) ?? 1];
66669
+ })
66670
+ );
64832
66671
  step(PUSH_READ_AT);
64833
66672
  const rows = placed.map((body) => {
64834
66673
  const from = origin.get(body.objectId) ?? body.from;
@@ -64836,10 +66675,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
64836
66675
  const distance3 = flat(from, blast.point);
64837
66676
  const away = Math.max(1e-6, distance3);
64838
66677
  const outward = ((now[0] - from[0]) * (from[0] - blast.point[0]) + (now[2] - from[2]) * (from[2] - blast.point[2])) / away;
64839
- return { ...body, distance: distance3, outward, moved: flat(now, from) };
66678
+ return { ...body, distance: distance3, outward, moved: flat(now, from), cover: coverOf.get(body.objectId) ?? 1 };
64840
66679
  });
64841
66680
  const torn = (row) => (PUSH_KINDS.some((entry2) => entry2.kind === row.kind) || HEAVY_ITEMS.includes(row.kind)) && partsOf(row.objectId) < (partsBefore.get(row.objectId) ?? 0);
64842
- const reached = rows.filter((row) => row.distance < blast.radius * 0.85 && !torn(row));
66681
+ const inReach = rows.filter((row) => row.distance < blast.radius * 0.85 && !torn(row));
66682
+ const sheltered = inReach.filter((row) => row.cover < SHELTERED);
66683
+ for (const row of sheltered) {
66684
+ expect(row.outward < SHELTERED_MOST, fillText2(text.push_sheltered, { what: things[row.kind] ?? row.kind, distance: row.distance.toFixed(1), cover: (row.cover * 100).toFixed(0), moved: Math.max(0, row.outward).toFixed(2) }), { row: { kind: row.kind, distance: row.distance, outward: row.outward, cover: row.cover } });
66685
+ }
66686
+ const reached = inReach.filter((row) => row.cover >= SHELTERED);
64843
66687
  const movedAway = reached.filter((row) => row.outward >= MOVED);
64844
66688
  const least = reached.length === 0 ? 0 : Math.min(...reached.map((row) => row.outward));
64845
66689
  expect(reached.length > 0 && movedAway.length === reached.length, fillText2(text.push_outward, { moved: movedAway.length, n: reached.length, least: least.toFixed(2) }), { rows: reached.map((row) => ({ kind: row.kind, distance: row.distance, outward: row.outward })), torn: rows.filter(torn).map((row) => row.objectId) });
@@ -65013,12 +66857,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
65013
66857
  // src/weapon-props.ts
65014
66858
  function createWeaponProps(options = {}) {
65015
66859
  const entryOf = ([id, model]) => ({ id, model: model ?? { item: "" } });
65016
- const entries = mergeById(SHIPPED_WEAPON_ITEM_MODELS.map(entryOf), (options.packs ?? []).flatMap((pack2) => Object.entries(pack2).map(entryOf)));
66860
+ const entries2 = mergeById(SHIPPED_WEAPON_ITEM_MODELS.map(entryOf), (options.packs ?? []).flatMap((pack2) => Object.entries(pack2).map(entryOf)));
65017
66861
  const patches = {};
65018
66862
  for (const [id, model] of Object.entries(options.patch ?? {})) patches[id] = { model };
65019
- applyPatches(entries, patches);
65020
- removeIds(entries, options.remove);
65021
- return weaponPropsOver(new Map([...entries].map(([id, entry2]) => [id, entry2.model])));
66863
+ applyPatches(entries2, patches);
66864
+ removeIds(entries2, options.remove);
66865
+ return weaponPropsOver(new Map([...entries2].map(([id, entry2]) => [id, entry2.model])));
65022
66866
  }
65023
66867
  return __toCommonJS(index_exports);
65024
66868
  })();