@adatechnology/game-test-room 0.7.0 → 0.9.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.
@@ -341,6 +341,37 @@ var GameTestRoom = (() => {
341
341
  }
342
342
  return { ok: problems.length === 0, problems };
343
343
  }
344
+ var DEFAULT_ENVIRONMENT = {
345
+ rain: 0,
346
+ windX: 0,
347
+ windZ: 0,
348
+ windSpeed: 0,
349
+ gust: 0,
350
+ fogFar: 220,
351
+ visibility: 220,
352
+ nightFactor: 0,
353
+ temperatureC: 20,
354
+ wetness: 0
355
+ };
356
+ function createEnvironmentBus(initial = {}) {
357
+ let state = { ...DEFAULT_ENVIRONMENT, ...initial };
358
+ const listeners = /* @__PURE__ */ new Set();
359
+ return {
360
+ get: () => state,
361
+ set(patch) {
362
+ const changed = Object.keys(patch).filter((key) => patch[key] !== void 0 && patch[key] !== state[key]);
363
+ if (changed.length === 0) return;
364
+ const next = { ...state };
365
+ for (const key of changed) next[key] = patch[key];
366
+ state = next;
367
+ for (const listener of [...listeners]) listener(state, changed);
368
+ },
369
+ on(listener) {
370
+ listeners.add(listener);
371
+ return () => void listeners.delete(listener);
372
+ }
373
+ };
374
+ }
344
375
 
345
376
  // ../game-destruction/dist/index.js
346
377
  var FIRE_PRIME_C = 300;
@@ -836,9 +867,9 @@ var GameTestRoom = (() => {
836
867
  const step = count > 1 && room / (count - 1) < spacing ? room / (count - 1) : spacing;
837
868
  return Array.from({ length: count }, (_, i) => (i - (count - 1) / 2) * step);
838
869
  };
839
- var bar = (axis2, along, fixed) => {
870
+ var bar = (axis2, along, fixed2) => {
840
871
  const a = [0, 0, 0], b = [0, 0, 0];
841
- for (const [index, value] of fixed) {
872
+ for (const [index, value] of fixed2) {
842
873
  a[index] = value;
843
874
  b[index] = value;
844
875
  }
@@ -1159,7 +1190,8 @@ var GameTestRoom = (() => {
1159
1190
  0.25
1160
1191
  ],
1161
1192
  damageScale: {
1162
- blunt: 1.5
1193
+ blunt: 1.5,
1194
+ shelter: 3
1163
1195
  },
1164
1196
  hitDebris: "shards",
1165
1197
  breakDebris: "shards",
@@ -1210,7 +1242,10 @@ var GameTestRoom = (() => {
1210
1242
  "glass",
1211
1243
  "brittle"
1212
1244
  ],
1213
- compressive: 2e5
1245
+ compressive: 2e5,
1246
+ damageScale: {
1247
+ shelter: 3
1248
+ }
1214
1249
  },
1215
1250
  {
1216
1251
  id: "ceramic",
@@ -1890,10 +1925,14 @@ var GameTestRoom = (() => {
1890
1925
  };
1891
1926
  var DMG_PER_J = 0.02;
1892
1927
  var BLAST_COUPLING = 0.1;
1928
+ var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
1929
+ var FRAGMENT_HIT_DEFAULTS = { max: 24, energyFactor: 0.5, hitType: "blunt", range: 6, flightTime: 0.4, spread: 10, aim: 40 };
1930
+ var CONFINEMENT_DEFAULTS = { rays: 26, reach: 1.5, gain: 1.5, power: 2, max: 3 };
1893
1931
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
1894
1932
  var BREACH_RADIUS = 0.55;
1895
1933
  var BREACH_BLAST_FACTOR = 6;
1896
1934
  var BREACH_AREA = 0.25;
1935
+ var BREACH_BLAST_REACH = Infinity;
1897
1936
  var RESIDUAL_CAPACITY = 0.15;
1898
1937
  var UNIT_FRAGMENT_FACTOR = 4;
1899
1938
  var BREACH_GRID_CAP = 4096;
@@ -1928,6 +1967,7 @@ var GameTestRoom = (() => {
1928
1967
  const fireSpread = options.fireSpread ?? 0.6;
1929
1968
  const breachRadius = options.breach?.radius ?? BREACH_RADIUS;
1930
1969
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
1970
+ const breachBlastReach = options.breach?.blastReach ?? BREACH_BLAST_REACH;
1931
1971
  const rng = createRng(options.seed ?? 1);
1932
1972
  const rnd2 = () => rng.next();
1933
1973
  let time = 0;
@@ -2283,7 +2323,7 @@ var GameTestRoom = (() => {
2283
2323
  if (disc < 0) return true;
2284
2324
  return -b - Math.sqrt(disc) > maxDist;
2285
2325
  };
2286
- const raycastInternal = (origin, dir, maxDist, ignore) => {
2326
+ const raycastInternal = (origin, dir, maxDist, ignore, worldOnly = false) => {
2287
2327
  const d = vec.normalize(dir);
2288
2328
  let best = null;
2289
2329
  for (const o of objects.values()) {
@@ -2296,7 +2336,7 @@ var GameTestRoom = (() => {
2296
2336
  if (!best || r.t0 < best.distance) best = { object: o.id, part: p.spec.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
2297
2337
  }
2298
2338
  }
2299
- for (const im of impls.values()) {
2339
+ for (const im of worldOnly ? [] : impls.values()) {
2300
2340
  for (const ps of im.impl.describe().parts) {
2301
2341
  if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
2302
2342
  const ov = im.poses.get(ps.id);
@@ -2709,7 +2749,7 @@ var GameTestRoom = (() => {
2709
2749
  else if (chained && (trigger === "explosion" || 1 - p.st.hp / p.st.maxHp >= shotDamageFractionOf(p.spec))) prime(o, p, trigger, src);
2710
2750
  return done;
2711
2751
  };
2712
- const ballistic2 = (o, p, entry, n, d, E, caliber, pathLen, src) => {
2752
+ const ballistic2 = (o, p, entry, n, d, E, caliber, pathLen, src, piercing = 1) => {
2713
2753
  const mat = p.mat;
2714
2754
  if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
2715
2755
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
@@ -2722,7 +2762,9 @@ var GameTestRoom = (() => {
2722
2762
  const exitLocal = objLocal(o, vec.madd(entry, d, Math.min(pathLen, effM)));
2723
2763
  const coats = p.layers.length > 0 ? layersEnergy(p.layers, p.rt.spalls, face, objLocal(o, entry)) + layersEnergy(p.layers, p.rt.spalls, -face, exitLocal) : 0;
2724
2764
  const thr = coats > 0 ? (mat.penetration * p.thickness * 1e3 + coats) / slant : mat.penetration * effM * 1e3;
2725
- const penetrated = E > thr;
2765
+ const hard = mat.tags?.includes("metal") || mat.tags?.includes("masonry");
2766
+ const Ep = piercing === 1 || !hard ? E : E * piercing;
2767
+ const penetrated = Ep > thr;
2726
2768
  const scale2 = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2727
2769
  const id = p.spec.id;
2728
2770
  if (!penetrated && mat.ricochetDeg && angle < mat.ricochetDeg) {
@@ -2734,7 +2776,7 @@ var GameTestRoom = (() => {
2734
2776
  }
2735
2777
  if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
2736
2778
  const brittle = mat.ductility < 0.3;
2737
- const ratio = E / Math.max(thr, 1e-6);
2779
+ const ratio = Ep / Math.max(thr, 1e-6);
2738
2780
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
2739
2781
  let exitPoint;
2740
2782
  if (penetrated) {
@@ -2764,7 +2806,7 @@ var GameTestRoom = (() => {
2764
2806
  debris2(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
2765
2807
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
2766
2808
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale2, "ballistic", entry, d, E * 0.1, 12, src);
2767
- return { penetrated, exitEnergy: penetrated ? E - thr : 0, exitPoint, damage: dmg };
2809
+ return { penetrated, exitEnergy: penetrated ? E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
2768
2810
  };
2769
2811
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
2770
2812
  const mat = p.mat;
@@ -2792,54 +2834,110 @@ var GameTestRoom = (() => {
2792
2834
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
2793
2835
  return done;
2794
2836
  };
2837
+ const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
2838
+ if (isLost(p)) return null;
2839
+ const { c, q } = partXf(o, p);
2840
+ const lp = toLocal(c, q, point);
2841
+ const dist = distanceToShape(p.spec.shape, lp);
2842
+ if (dist >= radius) return null;
2843
+ const sp = surfacePoint(p.spec.shape, lp);
2844
+ const wp = toWorld(c, q, sp.point);
2845
+ const reach = reachAt ? reachAt(wp, p) : 1;
2846
+ const f = (1 - dist / radius) ** 2 * reach;
2847
+ const Ein = energy * f * BLAST_COUPLING;
2848
+ const wn = rotateVec(q, sp.normal);
2849
+ if (!res.part) {
2850
+ res.part = p.spec.id;
2851
+ res.point = wp;
2852
+ }
2853
+ const mat = p.mat;
2854
+ if (Ein > 20) {
2855
+ if (mat.ductility >= 0.5) addDent(o, p, wp, wn, clamp2(0.05 + Math.sqrt(Ein) * 2e-3, 0.05, 0.4), clamp2(4e-3 + Ein * 2e-5, 4e-3, 0.08), src);
2856
+ else if (mat.ductility < 0.3 && Ein > 60) addCrack(o, p, wp, wn, clamp2(0.05 + Ein * 5e-4, 0.05, 1), src);
2857
+ debris2(o, p, mat.hitDebris, wp, wn, clamp2(Math.round(Ein / 80) + 1, 1, 12), clamp2(Math.sqrt(Ein) / 4, 2, 15), src);
2858
+ }
2859
+ const th = mat.thermal;
2860
+ if (th.ignitionC !== void 0 && p.st.fuel > 0) {
2861
+ p.st.temperature += 300 * f;
2862
+ o.active = true;
2863
+ if (f >= 0.25 && !p.st.burning) {
2864
+ p.st.temperature = Math.max(p.st.temperature, th.ignitionC + 1);
2865
+ checkIgnite(o, p, src);
2866
+ }
2867
+ }
2868
+ wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2869
+ const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
2870
+ if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
2871
+ frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
2872
+ return reach;
2873
+ }
2874
+ if (Ein > 20 && p.layers.length > 0) spallOuter(o, p, wp, faceOf(o, p, wn), wn, clamp2(0.05 + Math.sqrt(Ein) * 2e-3, 0.05, 0.4), src);
2875
+ const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
2876
+ res.damage += dealt;
2877
+ if (dist <= breachBlastReach * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2878
+ return reach;
2879
+ };
2795
2880
  const blastObject = (o, point, energy, radius, src) => {
2796
2881
  const start = cur.length;
2797
2882
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
2798
- for (const p of o.parts) {
2799
- if (isLost(p)) continue;
2800
- const { c, q } = partXf(o, p);
2801
- const lp = toLocal(c, q, point);
2802
- const dist = distanceToShape(p.spec.shape, lp);
2803
- if (dist >= radius) continue;
2804
- const f = (1 - dist / radius) ** 2;
2805
- const Ein = energy * f * BLAST_COUPLING;
2806
- const sp = surfacePoint(p.spec.shape, lp);
2807
- const wp = toWorld(c, q, sp.point);
2808
- const wn = rotateVec(q, sp.normal);
2809
- if (!res.part) {
2810
- res.part = p.spec.id;
2811
- res.point = wp;
2812
- }
2813
- const mat = p.mat;
2814
- if (Ein > 20) {
2815
- if (mat.ductility >= 0.5) addDent(o, p, wp, wn, clamp2(0.05 + Math.sqrt(Ein) * 2e-3, 0.05, 0.4), clamp2(4e-3 + Ein * 2e-5, 4e-3, 0.08), src);
2816
- else if (mat.ductility < 0.3 && Ein > 60) addCrack(o, p, wp, wn, clamp2(0.05 + Ein * 5e-4, 0.05, 1), src);
2817
- debris2(o, p, mat.hitDebris, wp, wn, clamp2(Math.round(Ein / 80) + 1, 1, 12), clamp2(Math.sqrt(Ein) / 4, 2, 15), src);
2818
- }
2819
- const th = mat.thermal;
2820
- if (th.ignitionC !== void 0 && p.st.fuel > 0) {
2821
- p.st.temperature += 300 * f;
2822
- o.active = true;
2823
- if (f >= 0.25 && !p.st.burning) {
2824
- p.st.temperature = Math.max(p.st.temperature, th.ignitionC + 1);
2825
- checkIgnite(o, p, src);
2826
- }
2883
+ for (const p of o.parts) blastPart(o, p, point, energy, radius, src, res);
2884
+ res.events = cur.slice(start);
2885
+ return res;
2886
+ };
2887
+ const blastCfg = (call3) => {
2888
+ const pick3 = (key) => call3 && call3[key] !== void 0 ? call3[key] : options.blast?.[key];
2889
+ const merge = (v, defaults) => {
2890
+ if (!v) return null;
2891
+ const out = { ...defaults };
2892
+ if (v !== true) {
2893
+ for (const [key, value] of Object.entries(v)) if (value !== void 0) out[key] = value;
2827
2894
  }
2828
- wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2829
- const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
2830
- if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
2831
- frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
2832
- continue;
2895
+ return out;
2896
+ };
2897
+ const conf = pick3("confinement");
2898
+ return {
2899
+ shelter: merge(pick3("shelter"), SHELTER_DEFAULTS),
2900
+ frag: merge(pick3("fragmentHits"), FRAGMENT_HIT_DEFAULTS),
2901
+ conf: typeof conf === "number" ? Number.isFinite(conf) ? Math.max(0, conf) : 1 : merge(conf, CONFINEMENT_DEFAULTS)
2902
+ };
2903
+ };
2904
+ const confinementRays = /* @__PURE__ */ new Map();
2905
+ const raysOf = (n) => {
2906
+ let rays = confinementRays.get(n);
2907
+ if (rays) return rays;
2908
+ const r = createRng((options.seed ?? 1) ^ 1540483477);
2909
+ const off = r.next();
2910
+ const phi0 = r.next() * Math.PI * 2;
2911
+ rays = [];
2912
+ for (let i = 0; i < n; i++) {
2913
+ const y = 1 - 2 * (i + off) / n;
2914
+ const s = Math.sqrt(Math.max(0, 1 - y * y));
2915
+ const phi = phi0 + i * 2.399963229728653;
2916
+ rays.push([Math.cos(phi) * s, y, Math.sin(phi) * s]);
2917
+ }
2918
+ confinementRays.set(n, rays);
2919
+ return rays;
2920
+ };
2921
+ const confinementOf = (conf, point, radius) => {
2922
+ if (conf === null) return 1;
2923
+ if (typeof conf === "number") return conf;
2924
+ let hits = 0;
2925
+ let sum2 = 0;
2926
+ for (const d of raysOf(Math.max(1, Math.round(conf.rays)))) {
2927
+ const h = raycastInternal(point, d, radius * conf.reach, void 0, true);
2928
+ if (h) {
2929
+ hits++;
2930
+ sum2 += h.distance;
2833
2931
  }
2834
- if (Ein > 20 && p.layers.length > 0) spallOuter(o, p, wp, faceOf(o, p, wn), wn, clamp2(0.05 + Math.sqrt(Ein) * 2e-3, 0.05, 0.4), src);
2835
- const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
2836
- res.damage += dealt;
2837
- addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2838
2932
  }
2839
- res.events = cur.slice(start);
2840
- return res;
2933
+ if (hits === 0) return 1;
2934
+ const closed = hits / Math.max(1, Math.round(conf.rays));
2935
+ const ratio = clamp2(radius / Math.max(sum2 / hits, 1e-3), 0, 2);
2936
+ return Math.min(Math.max(1, conf.max), 1 + conf.gain * closed ** conf.power * ratio);
2841
2937
  };
2842
- const blastWorld = (point, energy, radius, src) => {
2938
+ const FLAG_LOST = 1;
2939
+ const FLAG_OPEN = 2;
2940
+ const blastPlain = (point, energy, radius, src) => {
2843
2941
  const out = [];
2844
2942
  for (const o of [...objects.values()]) {
2845
2943
  if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), point) - o.br >= radius) continue;
@@ -2847,16 +2945,225 @@ var GameTestRoom = (() => {
2847
2945
  if (r.part) out.push(r);
2848
2946
  }
2849
2947
  for (const im of [...impls.values()]) {
2850
- const inRange = im.impl.describe().parts.some((ps) => {
2851
- const ov = im.poses.get(ps.id);
2852
- const c = toWorld(im.position, im.rotation, ov?.pos ?? ps.position);
2853
- const q = quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]));
2854
- return distanceToShape(ps.shape, toLocal(c, q, point)) < radius;
2855
- });
2856
- if (inRange) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
2948
+ if (implInRange(im, point, radius)) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
2949
+ }
2950
+ return out;
2951
+ };
2952
+ const implParts = (im) => im.impl.describe().parts.map((ps) => {
2953
+ const ov = im.poses.get(ps.id);
2954
+ return { ps, c: toWorld(im.position, im.rotation, ov?.pos ?? ps.position), q: quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0])) };
2955
+ });
2956
+ const implInRange = (im, point, radius) => implParts(im).some(({ ps, c, q }) => distanceToShape(ps.shape, toLocal(c, q, point)) < radius);
2957
+ const blastScene = (point, energy, radius, src, cfg, only) => {
2958
+ const mult = confinementOf(cfg.conf, point, radius);
2959
+ const E = energy * mult;
2960
+ const shelter = cfg.shelter;
2961
+ const flags = /* @__PURE__ */ new Map();
2962
+ const inRange = [];
2963
+ for (const o of objects.values()) {
2964
+ if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), point) - o.br >= radius) continue;
2965
+ inRange.push(o);
2966
+ if (shelter) for (const p of o.parts) flags.set(p, (isLost(p) ? FLAG_LOST : 0) | (p.st.opening || p.st.stripped ? FLAG_OPEN : 0));
2967
+ }
2968
+ const shields = /* @__PURE__ */ new Map();
2969
+ const reachAt = (tp, self) => {
2970
+ if (!shelter) return 1;
2971
+ const L = vec.distance(point, tp);
2972
+ if (L < 1e-4) return 1;
2973
+ const d = vec.scale(vec.sub(tp, point), 1 / L);
2974
+ let t = 1;
2975
+ for (const o of inRange) {
2976
+ if (raySphereMiss(point, d, toWorld(o.st.position, o.st.rotation, o.bc), o.br, L)) continue;
2977
+ for (const p of o.parts) {
2978
+ if (p === self) continue;
2979
+ const was = flags.get(p) ?? 0;
2980
+ if (was & FLAG_LOST) continue;
2981
+ const r = rayPart(o, p, point, d);
2982
+ if (!r) continue;
2983
+ const t0 = Math.max(r.t0, 0);
2984
+ const path = Math.min(r.t1, L) - t0;
2985
+ if (path < shelter.minPath) continue;
2986
+ const mid = vec.madd(point, d, t0 + path / 2);
2987
+ const opening = p.st.opening && insideOpening(o, p, mid);
2988
+ if (opening && was & FLAG_OPEN) continue;
2989
+ if (p.st.stripped && was & FLAG_OPEN) continue;
2990
+ const resistance = p.mat.hpPerKg * p.mat.density * path * (p.mat.damageScale?.shelter ?? 1);
2991
+ let ti = Math.exp(-resistance / shelter.k);
2992
+ if (isLost(p) || opening || p.st.stripped && !(was & FLAG_OPEN)) ti += (1 - ti) * shelter.brokenTransmission;
2993
+ t *= ti;
2994
+ if (cfg.frag) {
2995
+ let list = shields.get(p);
2996
+ if (!list) shields.set(p, list = []);
2997
+ list.push(tp);
2998
+ }
2999
+ }
3000
+ }
3001
+ return t;
3002
+ };
3003
+ const items = [];
3004
+ let order = 0;
3005
+ for (const o of inRange) {
3006
+ if (only && only !== o) continue;
3007
+ for (const p of o.parts) {
3008
+ if (isLost(p)) continue;
3009
+ const dist = distToPart(o, p, point);
3010
+ if (dist < radius) items.push({ dist, order: order++, o, p });
3011
+ }
3012
+ }
3013
+ for (const im of impls.values()) {
3014
+ if (only && only !== im) continue;
3015
+ let dist = Infinity;
3016
+ for (const { ps, c, q } of implParts(im)) dist = Math.min(dist, distanceToShape(ps.shape, toLocal(c, q, point)));
3017
+ if (dist < radius) items.push({ dist, order: order++, im });
3018
+ }
3019
+ items.sort((a, b) => a.dist - b.dist || a.order - b.order);
3020
+ const results = /* @__PURE__ */ new Map();
3021
+ const reports = /* @__PURE__ */ new Map();
3022
+ const report = (key) => {
3023
+ let r = reports.get(key);
3024
+ if (!r) reports.set(key, r = { shelter: 0, confinement: mult, fragmentHits: 0 });
3025
+ return r;
3026
+ };
3027
+ const thrown = [];
3028
+ const sweep = (from) => {
3029
+ if (!cfg.frag) return;
3030
+ for (let i = from; i < cur.length; i++) {
3031
+ const e = cur[i];
3032
+ if (e.type !== "fracture" && e.type !== "stage") continue;
3033
+ for (const frag of e.fragments ?? []) thrown.push({ frag, object: e.object, part: objects.get(e.object)?.byId.get(e.part) ?? null });
3034
+ }
3035
+ };
3036
+ for (const it of items) {
3037
+ if (it.o && it.p) {
3038
+ const o = it.o;
3039
+ let res = results.get(o);
3040
+ if (!res) results.set(o, res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] });
3041
+ const start = cur.length;
3042
+ const k = blastPart(o, it.p, point, E, radius, src, res, shelter ? reachAt : void 0);
3043
+ res.events.push(...cur.slice(start));
3044
+ sweep(start);
3045
+ if (k !== null) {
3046
+ const rep = report(o);
3047
+ rep.shelter = Math.max(rep.shelter, k);
3048
+ if (shelter) (rep.parts ?? (rep.parts = {}))[it.p.spec.id] = k;
3049
+ }
3050
+ } else if (it.im) {
3051
+ const im = it.im;
3052
+ let num = 0;
3053
+ let den = 0;
3054
+ for (const { ps, c, q } of implParts(im)) {
3055
+ const lp = toLocal(c, q, point);
3056
+ const dist = distanceToShape(ps.shape, lp);
3057
+ if (dist >= radius) continue;
3058
+ const f = (1 - dist / radius) ** 2;
3059
+ const sp = surfacePoint(ps.shape, lp);
3060
+ num += f * reachAt(toWorld(c, q, sp.point), null);
3061
+ den += f;
3062
+ }
3063
+ const k = den > 0 ? num / den : 1;
3064
+ const res = implHit(im, { type: "explosive", point, energy: E * k, radius, ...shelter ? { shelter: k } : {}, ...src !== void 0 ? { source: src } : {} });
3065
+ results.set(im, res);
3066
+ report(im).shelter = k;
3067
+ }
3068
+ }
3069
+ if (cfg.frag && thrown.length > 0) {
3070
+ const g = cfg.frag;
3071
+ const energyOf2 = (f) => 0.5 * f.mass * (f.velocity[0] ** 2 + f.velocity[1] ** 2 + f.velocity[2] ** 2);
3072
+ const list = thrown.map((t, i) => ({ ...t, i, e: energyOf2(t.frag) })).sort((a, b) => b.e - a.e || a.i - b.i).slice(0, Math.max(0, Math.floor(g.max)));
3073
+ list.forEach((t, i) => throwFragment(t.frag, t.object, t.part, g, point, i, src, shields, results, reports, mult));
2857
3074
  }
3075
+ const out = [];
3076
+ const reportShelter = (key, res) => {
3077
+ res.blast = reports.get(key) ?? { shelter: 1, confinement: mult, fragmentHits: 0 };
3078
+ };
3079
+ const summary = [];
3080
+ let fragmentHits = 0;
3081
+ for (const o of objects.values()) {
3082
+ const r = results.get(o);
3083
+ if (!r?.part) continue;
3084
+ reportShelter(o, r);
3085
+ out.push(r);
3086
+ for (const [part, factor] of Object.entries(r.blast?.parts ?? {})) summary.push({ object: o.id, part, factor });
3087
+ fragmentHits += r.blast?.fragmentHits ?? 0;
3088
+ }
3089
+ for (const im of impls.values()) {
3090
+ const r = results.get(im);
3091
+ if (!r) continue;
3092
+ reportShelter(im, r);
3093
+ out.push(r);
3094
+ if (shelter) summary.push({ object: im.impl.id, factor: r.blast?.shelter ?? 1 });
3095
+ fragmentHits += r.blast?.fragmentHits ?? 0;
3096
+ }
3097
+ emit2({ type: "blast", object: "", time, ...src !== void 0 ? { source: src } : {}, point, energy: E, radius, confinement: mult, shelter: summary, fragmentHits });
2858
3098
  return out;
2859
3099
  };
3100
+ const seedOf = (...nums) => {
3101
+ let h = 2166136261;
3102
+ for (const n of nums) {
3103
+ h = Math.imul(h ^ (Math.round(n * 1e3) | 0), 16777619);
3104
+ h ^= h >>> 15;
3105
+ }
3106
+ return h | 0;
3107
+ };
3108
+ const throwFragment = (frag, objectId, from, g, centre, index, src, shields, results, reports, mult) => {
3109
+ const v = frag.velocity;
3110
+ const speed = Math.hypot(v[0], v[1], v[2]);
3111
+ const energy = g.energyFactor * 0.5 * frag.mass * speed * speed;
3112
+ if (speed < 0.5 || energy < 1) return;
3113
+ const r = createRng(seedOf(options.seed ?? 1, centre[0], centre[1], centre[2], index, frag.center[0], frag.center[1], frag.center[2]));
3114
+ let dir = vec.scale(v, 1 / speed);
3115
+ const behind = from ? shields.get(from) : void 0;
3116
+ if (behind) {
3117
+ let best = Math.cos(g.aim * Math.PI / 180);
3118
+ for (const tp of behind) {
3119
+ const to = vec.sub(tp, frag.center);
3120
+ const len = vec.length(to);
3121
+ if (len < 1e-4) continue;
3122
+ const cos2 = vec.dot(dir, to) / len;
3123
+ if (cos2 >= best) {
3124
+ best = cos2;
3125
+ dir = vec.scale(to, 1 / len);
3126
+ }
3127
+ }
3128
+ }
3129
+ const spread3 = Math.tan(g.spread * Math.PI / 180);
3130
+ dir = vec.normalize([dir[0] + (r.next() - 0.5) * 2 * spread3, dir[1] + (r.next() - 0.5) * 2 * spread3, dir[2] + (r.next() - 0.5) * 2 * spread3], dir);
3131
+ const range2 = Math.min(g.range, speed * g.flightTime);
3132
+ const origin = vec.madd(frag.center, dir, 0.02);
3133
+ const rh = raycastInternal(origin, dir, range2, from ? /* @__PURE__ */ new Set([`${objectId}\0${from.spec.id}`]) : void 0);
3134
+ if (!rh) return;
3135
+ const hit = {
3136
+ type: g.hitType,
3137
+ point: rh.point,
3138
+ direction: dir,
3139
+ energy,
3140
+ radius: 0.05,
3141
+ part: rh.part,
3142
+ ...g.hitType === "ballistic" ? { caliber: clamp2(Math.max(...frag.size) * 1e3, 3, 30), mass: frag.mass * 1e3 } : {},
3143
+ ...src !== void 0 ? { source: src } : {}
3144
+ };
3145
+ emit2({ type: "fragment_hit", object: objectId, time, ...src !== void 0 ? { source: src } : {}, from: { object: objectId, part: frag.part }, target: { object: rh.object, part: rh.part }, point: rh.point, direction: dir, energy, mass: frag.mass, speed });
3146
+ const im = impls.get(rh.object);
3147
+ const o = im ? void 0 : objects.get(rh.object);
3148
+ const key = im ?? o;
3149
+ if (!key) return;
3150
+ const res = im ? implHit(im, hit) : doHit(o, hit, o.byId.get(rh.part));
3151
+ const have = results.get(key);
3152
+ if (have) {
3153
+ have.damage += res.damage;
3154
+ have.events.push(...res.events);
3155
+ if (!have.part) have.part = res.part;
3156
+ if (res.penetrated) have.penetrated = true;
3157
+ } else results.set(key, { ...res, events: [...res.events] });
3158
+ let rep = reports.get(key);
3159
+ if (!rep) reports.set(key, rep = { shelter: 1, confinement: mult, fragmentHits: 0 });
3160
+ rep.fragmentHits++;
3161
+ };
3162
+ const blastWorld = (point, energy, radius, src, call3, only) => {
3163
+ const cfg = blastCfg(call3);
3164
+ if (!cfg.shelter && !cfg.frag && cfg.conf === null) return blastPlain(point, energy, radius, src);
3165
+ return blastScene(point, energy, radius, src, cfg, only);
3166
+ };
2860
3167
  const heatObject = (o, point, T, radius, duration, src) => {
2861
3168
  const start = cur.length;
2862
3169
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
@@ -2893,7 +3200,7 @@ var GameTestRoom = (() => {
2893
3200
  if (hit.type === "ballistic") {
2894
3201
  const loc = locate(o, point, dir, 0.05, only);
2895
3202
  if (loc) {
2896
- const out = ballistic2(o, loc.p, loc.point, loc.normal, loc.dir, E, hit.caliber ?? 9, loc.pathLen, src);
3203
+ const out = ballistic2(o, loc.p, loc.point, loc.normal, loc.dir, E, hit.caliber ?? 9, loc.pathLen, src, hit.piercing ?? 1);
2897
3204
  res = { ...res, part: loc.p.spec.id, point: loc.point, damage: out.damage, penetrated: out.penetrated, exitEnergy: out.exitEnergy };
2898
3205
  }
2899
3206
  } else if (hit.type === "blunt" || hit.type === "slash") {
@@ -2915,7 +3222,13 @@ var GameTestRoom = (() => {
2915
3222
  }
2916
3223
  }
2917
3224
  } else if (hit.type === "explosive") {
2918
- res = blastObject(o, point, E, hit.radius ?? 2, src);
3225
+ const call3 = hit.blast || hit.confinement !== void 0 ? { ...hit.blast, ...hit.confinement !== void 0 ? { confinement: hit.confinement } : {} } : void 0;
3226
+ const cfg = blastCfg(call3);
3227
+ if (!cfg.shelter && !cfg.frag && cfg.conf === null) res = blastObject(o, point, E, hit.radius ?? 2, src);
3228
+ else {
3229
+ res = blastWorld(point, E, hit.radius ?? 2, src, call3, o)[0] ?? res;
3230
+ res.events = cur.slice(start);
3231
+ }
2919
3232
  } else {
2920
3233
  res = heatObject(o, point, hit.temperature ?? 800, hit.radius ?? 0.25, hit.duration ?? 1, src);
2921
3234
  }
@@ -3147,15 +3460,15 @@ var GameTestRoom = (() => {
3147
3460
  return best;
3148
3461
  };
3149
3462
  const detonatePrimed = (until) => {
3150
- const end = time;
3463
+ const end3 = time;
3151
3464
  for (let next = nextPrimed(until); next; next = nextPrimed(until)) {
3152
3465
  primed.delete(next.p);
3153
- time = Math.max(0, Math.min(end, next.p.rt.chainDue));
3466
+ time = Math.max(0, Math.min(end3, next.p.rt.chainDue));
3154
3467
  next.p.rt.chainDue = void 0;
3155
3468
  failPart(next.o, next.p, null, null, 0, 40);
3156
3469
  drainBlasts();
3157
3470
  }
3158
- time = end;
3471
+ time = end3;
3159
3472
  };
3160
3473
  const releaseFront = () => {
3161
3474
  if (frontQueue.length === 0) return;
@@ -3172,6 +3485,14 @@ var GameTestRoom = (() => {
3172
3485
  for (const e of r.events) if (e.type === "collapse" && !r.value.some((h) => h.events.includes(e))) last.events.push(e);
3173
3486
  return r.value;
3174
3487
  };
3488
+ const blastImpl = (im, h) => {
3489
+ const call3 = h.blast || h.confinement !== void 0 ? { ...h.blast, ...h.confinement !== void 0 ? { confinement: h.confinement } : {} } : void 0;
3490
+ const cfg = blastCfg(call3);
3491
+ if (!cfg.shelter && !cfg.frag && cfg.conf === null) return implHit(im, h);
3492
+ const world2 = h.space === "local" ? toWorld(im.position, im.rotation, h.point) : h.point;
3493
+ const r = blastWorld(world2, h.energy ?? 0, h.radius ?? 2, h.source, call3, im);
3494
+ return r.find((x) => x.object === im.impl.id) ?? implHit(im, h);
3495
+ };
3175
3496
  const whole = (fn) => {
3176
3497
  const r = run(fn);
3177
3498
  r.value.events = r.events;
@@ -3259,7 +3580,7 @@ var GameTestRoom = (() => {
3259
3580
  },
3260
3581
  hit(objectId, h) {
3261
3582
  const im = impls.get(objectId);
3262
- if (im) return whole(() => implHit(im, h));
3583
+ if (im) return whole(() => h.type === "explosive" ? blastImpl(im, h) : implHit(im, h));
3263
3584
  const o = getObj(objectId);
3264
3585
  return whole(() => doHit(o, h));
3265
3586
  },
@@ -3270,14 +3591,14 @@ var GameTestRoom = (() => {
3270
3591
  const p = getPart(o, partId);
3271
3592
  return whole(() => doHit(o, h, p));
3272
3593
  },
3273
- explode(point, energy, radius, source) {
3274
- return withCollapses(run(() => blastWorld(point, energy, radius, source)));
3594
+ explode(point, energy, radius, source, blast) {
3595
+ return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
3275
3596
  },
3276
3597
  raycast(origin, direction, maxDistance = Infinity) {
3277
3598
  const r = raycastInternal(origin, direction, maxDistance);
3278
3599
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
3279
3600
  },
3280
- shoot(origin, direction, energy, caliber, source) {
3601
+ shoot(origin, direction, energy, caliber, source, piercing) {
3281
3602
  return withCollapses(run(() => {
3282
3603
  const results = [];
3283
3604
  const d = vec.normalize(direction);
@@ -3289,7 +3610,7 @@ var GameTestRoom = (() => {
3289
3610
  if (!rh) break;
3290
3611
  const im = impls.get(rh.object);
3291
3612
  if (im) {
3292
- const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...source !== void 0 ? { source } : {} });
3613
+ const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...piercing !== void 0 ? { piercing } : {}, ...source !== void 0 ? { source } : {} });
3293
3614
  r.part = rh.part;
3294
3615
  r.point = rh.point;
3295
3616
  r.exitEnergy = r.penetrated ? E * 0.5 : 0;
@@ -3303,7 +3624,7 @@ var GameTestRoom = (() => {
3303
3624
  const o = getObj(rh.object);
3304
3625
  const p = getPart(o, rh.part);
3305
3626
  const start = cur.length;
3306
- const out = ballistic2(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source);
3627
+ const out = ballistic2(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source, piercing ?? 1);
3307
3628
  results.push({ object: o.id, part: p.spec.id, point: rh.point, damage: out.damage, penetrated: out.penetrated, exitEnergy: out.exitEnergy, events: cur.slice(start) });
3308
3629
  ignore.add(`${o.id}\0${p.spec.id}`);
3309
3630
  if (!out.penetrated || !out.exitPoint) break;
@@ -3673,6 +3994,8 @@ var GameTestRoom = (() => {
3673
3994
  stage: "damage stage",
3674
3995
  expose: "rebar bare",
3675
3996
  body: "body",
3997
+ blast: "blast",
3998
+ fragment_hit: "fragment hit",
3676
3999
  destroyed: "destroyed"
3677
4000
  },
3678
4001
  fractureStyles: { shatter: "shatter", crumble: "crumble", splinter: "splinter", tear: "tear", snap: "snap", none: "none" },
@@ -3759,6 +4082,8 @@ var GameTestRoom = (() => {
3759
4082
  stage: "est\xE1gio de dano",
3760
4083
  expose: "vergalh\xE3o \xE0 mostra",
3761
4084
  body: "corpo",
4085
+ blast: "explos\xE3o",
4086
+ fragment_hit: "estilha\xE7o atingiu",
3762
4087
  destroyed: "destru\xEDdo"
3763
4088
  },
3764
4089
  fractureStyles: { shatter: "estilha\xE7ar", crumble: "esfarelar", splinter: "lascar", tear: "rasgar", snap: "partir", none: "nenhuma" },
@@ -3767,6 +4092,1447 @@ var GameTestRoom = (() => {
3767
4092
  };
3768
4093
  var resolveStrings = (locale) => locales[locale ?? "en"] ?? locales.en;
3769
4094
 
4095
+ // ../game-audio/dist/index.js
4096
+ var FNV_OFFSET = 2166136261;
4097
+ var FNV_PRIME = 16777619;
4098
+ var MIX = 2654435761;
4099
+ function createRng2(seed) {
4100
+ let state = seed >>> 0;
4101
+ return () => {
4102
+ state = state + 1831565813 >>> 0;
4103
+ let mixed = Math.imul(state ^ state >>> 15, state | 1);
4104
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
4105
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
4106
+ };
4107
+ }
4108
+ function streamSeed(seed, name) {
4109
+ const label = String(name);
4110
+ let hash = FNV_OFFSET;
4111
+ for (let index = 0; index < label.length; index++) hash = Math.imul(hash ^ label.charCodeAt(index), FNV_PRIME) >>> 0;
4112
+ return Math.imul(seed >>> 0 ^ hash, MIX) >>> 0;
4113
+ }
4114
+ function streamFor(seed, name) {
4115
+ return createRng2(streamSeed(seed, name));
4116
+ }
4117
+ var DISTANCE_CURVES = {
4118
+ /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
4119
+ game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
4120
+ /** The game's `grito`: `1 / (1 + d² × 0.006)`, silent below 0.02 (jogo L62129–62130). */
4121
+ game_scream: { model: "inverse_square", k: 6e-3, minGain: 0.02 },
4122
+ /** The game's `voz`: `1 / (1 + d² × 0.01)`, silent below 0.02 (jogo L62146–62147). */
4123
+ game_voice: { model: "inverse_square", k: 0.01, minGain: 0.02 },
4124
+ /** The game's `boom`: `1.4 / (1 + d² × 0.002)`, never silent (jogo L68280). */
4125
+ game_boom: { model: "inverse_square", k: 2e-3, scale: 1.4 },
4126
+ /** The art scene's `outAt`: `vol / (1 + d² × 0.008)` (cena L1320). */
4127
+ art_scene: { model: "inverse_square", k: 8e-3 },
4128
+ /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
4129
+ animals: { model: "linear", range: 70, scale: 0.25, minGain: 5e-3 }
4130
+ };
4131
+ function falloffGain(falloff, volume, distance4) {
4132
+ if (!falloff || falloff.model === "none") return volume;
4133
+ if (falloff.model === "linear") {
4134
+ const level2 = Math.max(0, 1 - distance4 / falloff.range) * volume;
4135
+ return falloff.scale === void 0 ? level2 : level2 * falloff.scale;
4136
+ }
4137
+ const level = volume / (1 + distance4 * distance4 * falloff.k);
4138
+ return falloff.scale === void 0 ? level : falloff.scale * level;
4139
+ }
4140
+ function isSilent(falloff, gain) {
4141
+ if (!falloff || falloff.model === "none" || falloff.minGain === void 0) return false;
4142
+ return falloff.model === "linear" ? gain <= falloff.minGain : gain < falloff.minGain;
4143
+ }
4144
+ function cutoffDivisor(falloff, distance4) {
4145
+ if (!falloff || falloff.model !== "inverse_square" || falloff.cutoffK === void 0) return 1;
4146
+ return 1 + distance4 * falloff.cutoffK;
4147
+ }
4148
+ var DEFAULT_LISTENER = { position: [0, 0, 0], yaw: 0 };
4149
+ var PAN_LIMIT = 0.85;
4150
+ var PAN_MIN_DISTANCE = 2;
4151
+ function listenerDistance(listener, position, mode = "xz") {
4152
+ const dx = position[0] - listener.position[0];
4153
+ const dz = position[2] - listener.position[2];
4154
+ if (mode === "xz") return Math.hypot(dx, dz);
4155
+ return Math.hypot(dx, position[1] - listener.position[1], dz);
4156
+ }
4157
+ function panFor(listener, position) {
4158
+ const rightX = Math.cos(listener.yaw);
4159
+ const rightZ = -Math.sin(listener.yaw);
4160
+ const dx = position[0] - listener.position[0];
4161
+ const dz = position[2] - listener.position[2];
4162
+ const distance4 = Math.hypot(dx, dz) || 1;
4163
+ const pan = (dx * rightX + dz * rightZ) / Math.max(distance4, PAN_MIN_DISTANCE);
4164
+ return pan < -PAN_LIMIT ? -PAN_LIMIT : pan > PAN_LIMIT ? PAN_LIMIT : pan;
4165
+ }
4166
+ var voice_groups_default = {
4167
+ undead_voice: {
4168
+ maxConcurrent: 5,
4169
+ cooldown: 0.25,
4170
+ maxDistance: 40,
4171
+ sources: [
4172
+ "cena:L1342 `AU.groans > 4` refuses, so five play at once",
4173
+ "jogo:L66986 one groan every 0.25 s (`groanBudget`), only nearer than 40 m",
4174
+ "jogo:L70472 the budget counts down by the frame time"
4175
+ ]
4176
+ }
4177
+ };
4178
+ var VOICE_GROUPS = voice_groups_default;
4179
+ var BUS_PRIORITY = { ui: 5, weapons: 4, voices: 3, world: 2, weather: 1, ambience: 1 };
4180
+ var DEFAULT_MAX_VOICES = 32;
4181
+ function busPriority(bus) {
4182
+ return BUS_PRIORITY[bus];
4183
+ }
4184
+ function createVoiceManager(options = {}) {
4185
+ const groups = new Map(Object.entries(options.groups ?? VOICE_GROUPS));
4186
+ const maxVoices = options.maxVoices ?? DEFAULT_MAX_VOICES;
4187
+ const budgets = /* @__PURE__ */ new Map();
4188
+ let voices = [];
4189
+ let nextId = 1;
4190
+ let peak = 0;
4191
+ let dropped = 0;
4192
+ const refuse = (reason) => {
4193
+ dropped += 1;
4194
+ return { accepted: false, reason };
4195
+ };
4196
+ const groupCheck = (request, evicted) => {
4197
+ const group = request.group === void 0 ? void 0 : groups.get(request.group);
4198
+ if (!group) return void 0;
4199
+ if (group.maxDistance !== void 0 && !((request.distance ?? 0) < group.maxDistance)) return "distance";
4200
+ if (group.cooldown !== void 0 && (budgets.get(request.group) ?? 0) > 0) return "cooldown";
4201
+ const members = voices.filter((voice) => voice.group === request.group);
4202
+ if (group.maxConcurrent === void 0 || members.length < group.maxConcurrent) return void 0;
4203
+ if (!group.steal) return "group_full";
4204
+ evicted.push(members[0].id);
4205
+ return void 0;
4206
+ };
4207
+ return {
4208
+ request(request) {
4209
+ const evicted = [];
4210
+ const refusal = groupCheck(request, evicted);
4211
+ if (refusal) return refuse(refusal);
4212
+ const score = request.priority * request.gain;
4213
+ const remaining = voices.filter((voice) => !evicted.includes(voice.id));
4214
+ if (remaining.length >= maxVoices) {
4215
+ const lowest = remaining.reduce((low, voice) => voice.score < low.score ? voice : low);
4216
+ if (score <= lowest.score) return refuse("global_full");
4217
+ evicted.push(lowest.id);
4218
+ dropped += 1;
4219
+ }
4220
+ const group = request.group === void 0 ? void 0 : groups.get(request.group);
4221
+ if (group?.cooldown !== void 0) budgets.set(request.group, group.cooldown);
4222
+ const id = nextId++;
4223
+ voices = [...voices.filter((voice) => !evicted.includes(voice.id)), { id, ...request.group === void 0 ? {} : { group: request.group }, score, remaining: request.length }];
4224
+ peak = Math.max(peak, voices.length);
4225
+ return { accepted: true, id, evicted };
4226
+ },
4227
+ release(id) {
4228
+ voices = voices.filter((voice) => voice.id !== id);
4229
+ },
4230
+ setLength(id, seconds) {
4231
+ const voice = voices.find((entry) => entry.id === id);
4232
+ if (voice) voice.remaining = seconds;
4233
+ },
4234
+ advance(dt) {
4235
+ for (const [group, budget] of budgets) budgets.set(group, budget - dt);
4236
+ const ended = [];
4237
+ for (const voice of voices) {
4238
+ voice.remaining -= dt;
4239
+ if (voice.remaining <= 0) ended.push(voice.id);
4240
+ }
4241
+ if (ended.length > 0) voices = voices.filter((voice) => voice.remaining > 0);
4242
+ return ended;
4243
+ },
4244
+ setGroup(id, group) {
4245
+ if (group) groups.set(id, group);
4246
+ else groups.delete(id);
4247
+ },
4248
+ stats: () => ({ active: voices.length, peak, dropped, maxVoices })
4249
+ };
4250
+ }
4251
+ var DEFAULT_TAIL = 0.05;
4252
+ var DEFAULT_NOISE_SECONDS = 1.5;
4253
+ var OCCLUSION_GAIN = 0.5;
4254
+ var OCCLUSION_CUTOFF = 0.6;
4255
+ function isParamRef(range2) {
4256
+ return typeof range2 === "object" && !Array.isArray(range2);
4257
+ }
4258
+ function draw(range2, scope) {
4259
+ if (typeof range2 === "number") return range2;
4260
+ if (isParamRef(range2)) {
4261
+ const value = scope.params[range2.param];
4262
+ if (value === void 0) throw new Error(`game-audio: the recipe reads the param "${range2.param}", which was not given`);
4263
+ return value * (range2.scale ?? 1) + (range2.add ?? 0);
4264
+ }
4265
+ const [min2, max2] = range2;
4266
+ return min2 + scope.random() * (max2 - min2);
4267
+ }
4268
+ function drawTime(time, scope) {
4269
+ if (time === "duration" || typeof time === "string" && time.endsWith("d")) return time;
4270
+ return draw(time, scope);
4271
+ }
4272
+ function timeOf(time, duration) {
4273
+ if (typeof time === "number") return time;
4274
+ if (time === "duration") return duration;
4275
+ return Number(time.slice(0, -1)) * duration;
4276
+ }
4277
+ function drawAutomation(automation, scope) {
4278
+ const pending = {};
4279
+ for (const key of Object.keys(automation)) {
4280
+ if (key === "set" && "set" in automation) pending.value = draw(automation.set, scope);
4281
+ else if (key === "from" && "from" in automation) pending.from = draw(automation.from, scope);
4282
+ else if (key === "to" && automation.to !== void 0) pending.to = automation.to === "from" ? pending.from ?? pending.value ?? 0 : draw(automation.to, scope);
4283
+ else if (key === "curve") pending.curve = automation.curve;
4284
+ else if (key === "over" && automation.over !== void 0) pending.over = drawTime(automation.over, scope);
4285
+ }
4286
+ return pending;
4287
+ }
4288
+ function finishAutomation(pending, duration) {
4289
+ if (pending.from !== void 0 && pending.to === void 0) return { from: pending.from };
4290
+ if (pending.from !== void 0) return { from: pending.from, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over ?? "duration", duration) };
4291
+ if (pending.to === void 0 || pending.over === void 0) return { value: pending.value };
4292
+ return { value: pending.value, to: pending.to, curve: pending.curve ?? "linear", at: timeOf(pending.over, duration) };
4293
+ }
4294
+ var startFrequency = (automation) => automation.from ?? automation.value ?? 0;
4295
+ function drawFilter(spec, scope) {
4296
+ const pending = { spec, frequency: {} };
4297
+ for (const key of Object.keys(spec)) {
4298
+ if (key === "frequency") pending.frequency = drawAutomation(spec.frequency, scope);
4299
+ else if (key === "q" && spec.q !== void 0) pending.q = draw(spec.q, scope);
4300
+ else if (key === "gain" && spec.gain !== void 0) pending.gain = draw(spec.gain, scope);
4301
+ else if (key === "level" && spec.level !== void 0) pending.level = draw(spec.level, scope);
4302
+ }
4303
+ return pending;
4304
+ }
4305
+ function drawModulator(modulator, scope) {
4306
+ const pending = { modulator, rate: 0 };
4307
+ for (const key of Object.keys(modulator)) {
4308
+ if (key === "rate") pending.rate = draw(modulator.rate, scope);
4309
+ else if (key === "depth" && !(typeof modulator.depth === "object" && "scaleOf" in modulator.depth)) pending.depth = draw(modulator.depth, scope);
4310
+ }
4311
+ return pending;
4312
+ }
4313
+ function drawVoice(source, scope) {
4314
+ const pending = { frequency: {}, vibratoRate: 0, formants: [], breath: { frequency: 0, q: source.breath.q, gain: 0 } };
4315
+ for (const key of Object.keys(source)) {
4316
+ if (key === "frequency") pending.frequency = drawAutomation(source.frequency, scope);
4317
+ else if (key === "vibrato") pending.vibratoRate = draw(source.vibrato.rate, scope);
4318
+ else if (key === "formants") pending.formants = source.formants.map((formant) => ({ frequency: draw(formant.frequency, scope), q: formant.q }));
4319
+ else if (key === "breath") pending.breath = { frequency: draw(source.breath.frequency, scope), q: source.breath.q, gain: draw(source.breath.gain, scope) };
4320
+ }
4321
+ return pending;
4322
+ }
4323
+ function drawLayer(layer2, scope) {
4324
+ const pending = { at: 0, gain: 1, filters: [], modulators: [] };
4325
+ for (const key of Object.keys(layer2)) {
4326
+ if (key === "at" && layer2.at !== void 0) pending.at = draw(layer2.at, scope);
4327
+ else if (key === "duration" && layer2.duration !== void 0) pending.duration = draw(layer2.duration, scope);
4328
+ else if (key === "gain" && layer2.gain !== void 0) pending.gain = draw(layer2.gain, scope);
4329
+ else if (key === "source" && layer2.source.kind === "osc") pending.automation = drawAutomation(layer2.source.frequency, scope);
4330
+ else if (key === "source" && layer2.source.kind === "formant_voice") pending.voice = drawVoice(layer2.source, scope);
4331
+ else if (key === "filters" && layer2.filters) pending.filters = layer2.filters.bank.map((spec) => drawFilter(spec, scope));
4332
+ else if (key === "modulators" && layer2.modulators) pending.modulators = layer2.modulators.map((modulator) => drawModulator(modulator, scope));
4333
+ }
4334
+ return pending;
4335
+ }
4336
+ function drawOffset(source, duration, scope, noiseSeconds) {
4337
+ if (source.kind === "formant_voice") return scope.random();
4338
+ if (source.kind !== "noise" || source.offset === void 0) return 0;
4339
+ if (typeof source.offset === "number") return source.offset;
4340
+ if (source.offset === "fit") return scope.random() * Math.max(0, noiseSeconds - duration);
4341
+ return scope.random() * (source.offsetScale ?? 1);
4342
+ }
4343
+ function finishSource(layer2, pending, duration, offset) {
4344
+ const source = layer2.source;
4345
+ if (source.kind === "noise") return { kind: "noise", offset, loop: source.loop === true };
4346
+ if (source.kind === "osc") return { kind: "osc", wave: source.wave, frequency: finishAutomation(pending.automation ?? {}, duration) };
4347
+ const voice = pending.voice;
4348
+ return {
4349
+ kind: "formant_voice",
4350
+ wave: source.wave,
4351
+ frequency: finishAutomation(voice.frequency, duration),
4352
+ vibrato: { rate: voice.vibratoRate, depth: startFrequency(voice.frequency) * source.vibrato.depth },
4353
+ formants: voice.formants,
4354
+ breath: { ...voice.breath, offset },
4355
+ mix: source.mix
4356
+ };
4357
+ }
4358
+ function finishModulator(pending, sourceFrequency) {
4359
+ const { modulator } = pending;
4360
+ const depth = typeof modulator.depth === "object" && "scaleOf" in modulator.depth ? sourceFrequency * modulator.depth.factor : pending.depth ?? 0;
4361
+ return { target: modulator.target, ...modulator.wave ? { wave: modulator.wave } : {}, rate: pending.rate, depth, ...modulator.base === void 0 ? {} : { base: modulator.base } };
4362
+ }
4363
+ function finishEnvelope(envelope2, duration, level) {
4364
+ return envelope2.map(([time, value, curve, fixed2]) => ({ time: timeOf(time, duration), value: fixed2 === "fixed" ? value : value * level, curve }));
4365
+ }
4366
+ function planLayer(layer2, scope, defaults, levelInEnvelope, soundLevel) {
4367
+ const pending = drawLayer(layer2, scope);
4368
+ const duration = pending.duration ?? defaults.duration;
4369
+ if (duration === void 0) throw new Error("game-audio: a layer needs a duration (its own or the recipe\u2019s)");
4370
+ const offset = drawOffset(layer2.source, duration, scope, defaults.noiseSeconds);
4371
+ const level = pending.gain * soundLevel;
4372
+ const sourceFrequency = layer2.source.kind === "noise" ? 0 : startFrequency(pending.automation ?? pending.voice?.frequency ?? {});
4373
+ const planned = {
4374
+ at: pending.at,
4375
+ duration,
4376
+ tail: layer2.tail ?? defaults.tail ?? DEFAULT_TAIL,
4377
+ level,
4378
+ ...layer2.wet === void 0 ? {} : { wet: layer2.wet },
4379
+ source: finishSource(layer2, pending, duration, offset),
4380
+ ...layer2.filters ? {
4381
+ filters: {
4382
+ mode: layer2.filters.mode,
4383
+ bank: pending.filters.map(({ spec, frequency, q, gain, level: filterLevel }) => ({
4384
+ type: spec.type,
4385
+ frequency: finishAutomation(frequency, duration),
4386
+ ...q === void 0 ? {} : { q },
4387
+ ...gain === void 0 ? {} : { gain },
4388
+ ...filterLevel === void 0 ? {} : { level: filterLevel }
4389
+ }))
4390
+ }
4391
+ } : {},
4392
+ modulators: pending.modulators.map((modulator) => finishModulator(modulator, sourceFrequency)),
4393
+ envelope: finishEnvelope(layer2.envelope, duration, levelInEnvelope ? level : 1)
4394
+ };
4395
+ if (!layer2.repeat) return [planned];
4396
+ return Array.from({ length: layer2.repeat.count }, (_, index) => ({ ...planned, at: planned.at + index * (layer2.repeat?.spacing ?? 0) }));
4397
+ }
4398
+ function scaleAutomation(automation, divisor) {
4399
+ const scale2 = (value) => value === void 0 ? void 0 : value / divisor;
4400
+ const scaled2 = { ...automation };
4401
+ for (const key of ["value", "from", "to"]) if (automation[key] !== void 0) scaled2[key] = scale2(automation[key]);
4402
+ return scaled2;
4403
+ }
4404
+ function muffle(layer2, divisor) {
4405
+ if (divisor === 1 || !layer2.filters) return layer2;
4406
+ const bank = layer2.filters.bank.map((filter) => filter.type === "lowpass" ? { ...filter, frequency: scaleAutomation(filter.frequency, divisor) } : filter);
4407
+ return { ...layer2, filters: { ...layer2.filters, bank } };
4408
+ }
4409
+ var layerEnd = (layer2) => layer2.at + layer2.duration + layer2.tail;
4410
+ function planSound(recipe, input = {}) {
4411
+ const listener = input.listener ?? DEFAULT_LISTENER;
4412
+ const distance4 = input.distance ?? (input.position ? listenerDistance(listener, input.position, input.distanceMode) : 0);
4413
+ const occlusion = Math.min(1, Math.max(0, input.occlusion ?? 0));
4414
+ const order = recipe.order ?? "layered";
4415
+ const falloffLevel = falloffGain(recipe.falloff, input.volume ?? 1, distance4);
4416
+ const level = occlusion > 0 ? falloffLevel * (1 - OCCLUSION_GAIN * occlusion) : falloffLevel;
4417
+ const silent = isSilent(recipe.falloff, falloffLevel);
4418
+ const base = {
4419
+ id: recipe.id,
4420
+ bus: recipe.bus,
4421
+ order,
4422
+ ...recipe.group === void 0 ? {} : { group: recipe.group },
4423
+ priority: recipe.priority ?? busPriority(recipe.bus),
4424
+ level,
4425
+ distance: distance4,
4426
+ ...recipe.pan ? { pan: input.position ? panFor(listener, input.position) : 0 } : {}
4427
+ };
4428
+ if (silent) return recipe.keepSilent ? { ...base, silent: true, length: 0, layers: [] } : null;
4429
+ const scope = { random: input.random ?? createRng2(1), params: { ...input.params } };
4430
+ for (const [name, range2] of Object.entries(recipe.vars ?? {})) if (scope.params[name] === void 0) scope.params[name] = draw(range2, scope);
4431
+ const defaults = {
4432
+ ...recipe.duration === void 0 ? {} : { duration: draw(recipe.duration, scope) },
4433
+ ...recipe.tail === void 0 ? {} : { tail: recipe.tail },
4434
+ noiseSeconds: input.noiseSeconds ?? DEFAULT_NOISE_SECONDS
4435
+ };
4436
+ const divisor = cutoffDivisor(recipe.falloff, distance4) / (1 - OCCLUSION_CUTOFF * occlusion);
4437
+ const layers = recipe.layers.flatMap((layer2) => planLayer(layer2, scope, defaults, order !== "layered", level)).map((layer2) => muffle(layer2, divisor));
4438
+ return { ...base, silent: false, length: Math.max(0, ...layers.map(layerEnd)), layers };
4439
+ }
4440
+ var LOOP_START_ABOVE = 2e-3;
4441
+ var LOOP_SMOOTHING = { volume: 0.15, frequency: 0.08 };
4442
+ function createLoopState(recipe, open = false) {
4443
+ const startAbove = recipe.startAbove ?? LOOP_START_ABOVE;
4444
+ let created = open;
4445
+ let volume = recipe.volume ?? 0;
4446
+ return {
4447
+ set(state) {
4448
+ volume = state.volume ?? volume;
4449
+ const create = !created;
4450
+ if (create && !(volume >= startAbove)) return null;
4451
+ created = true;
4452
+ const frequency = recipe.source.kind === "osc" && state.frequency ? state.frequency : void 0;
4453
+ return { create, volume, ...frequency === void 0 ? {} : { frequency } };
4454
+ },
4455
+ get created() {
4456
+ return created;
4457
+ },
4458
+ reset() {
4459
+ created = false;
4460
+ }
4461
+ };
4462
+ }
4463
+ var mix_profiles_default = [
4464
+ {
4465
+ id: "game",
4466
+ master: 0.5,
4467
+ noise: { seconds: 1.5, create: "lazy" },
4468
+ 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"]
4469
+ },
4470
+ {
4471
+ id: "art_scene",
4472
+ master: 0.85,
4473
+ compressor: { threshold: -12, ratio: 5 },
4474
+ reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
4475
+ noise: { seconds: 2, create: "eager" },
4476
+ sources: [
4477
+ "cena:L1308 compressor -12 dB, ratio 5",
4478
+ "cena:L1309 master 0.85",
4479
+ "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
4480
+ "cena:L1311 wet 0.4",
4481
+ "cena:L1312 noise 2 s made with the chain"
4482
+ ]
4483
+ }
4484
+ ];
4485
+ var MIX_PROFILES = Object.fromEntries(
4486
+ mix_profiles_default.map((profile) => [profile.id, profile])
4487
+ );
4488
+ var DEFAULT_MIX = "art_scene";
4489
+ function resolveMix(mix, profiles = MIX_PROFILES) {
4490
+ if (typeof mix !== "string") return mix;
4491
+ const profile = profiles[mix];
4492
+ if (!profile) throw new Error(`game-audio: unknown mix profile "${mix}" (known: ${Object.keys(profiles).join(", ")})`);
4493
+ return profile;
4494
+ }
4495
+ function busGain(setting) {
4496
+ if (!setting) return 1;
4497
+ return setting.muted ? 0 : setting.gain ?? 1;
4498
+ }
4499
+ function isPlainObject2(value) {
4500
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4501
+ }
4502
+ function mergeRecords(base, patch) {
4503
+ const merged = { ...base };
4504
+ for (const [key, value] of Object.entries(patch)) {
4505
+ if (value === void 0) continue;
4506
+ const current = merged[key];
4507
+ merged[key] = isPlainObject2(current) && isPlainObject2(value) ? mergeRecords(current, value) : value;
4508
+ }
4509
+ return merged;
4510
+ }
4511
+ function createSoundRegistry(banks = []) {
4512
+ const sounds = /* @__PURE__ */ new Map();
4513
+ const loops = /* @__PURE__ */ new Map();
4514
+ const owners = /* @__PURE__ */ new Map();
4515
+ const registry = {
4516
+ register(bank) {
4517
+ for (const sound of bank.sounds ?? []) {
4518
+ sounds.set(sound.id, sound);
4519
+ owners.set(sound.id, bank.id);
4520
+ }
4521
+ for (const loop of bank.loops ?? []) {
4522
+ loops.set(loop.id, loop);
4523
+ owners.set(loop.id, bank.id);
4524
+ }
4525
+ },
4526
+ patch(id, patch) {
4527
+ const sound = sounds.get(id);
4528
+ if (sound) sounds.set(id, mergeRecords(sound, patch));
4529
+ const loop = loops.get(id);
4530
+ if (loop) loops.set(id, mergeRecords(loop, patch));
4531
+ },
4532
+ remove(id) {
4533
+ sounds.delete(id);
4534
+ loops.delete(id);
4535
+ owners.delete(id);
4536
+ },
4537
+ sound: (id) => sounds.get(id),
4538
+ loop: (id) => loops.get(id),
4539
+ soundIds: () => [...sounds.keys()],
4540
+ loopIds: () => [...loops.keys()],
4541
+ bankOf: (id) => owners.get(id)
4542
+ };
4543
+ for (const bank of banks) registry.register(bank);
4544
+ return registry;
4545
+ }
4546
+ var game_sfx_default = {
4547
+ sources: ["jogo:L62089-62107"],
4548
+ note: "The game's sfx table P: [lowpass cutoff Hz, peak gain, seconds]. Played as a noise burst with the game's distance curve (falloff game_sfx) and a 0.05 s tail. Team names in `team`.",
4549
+ classes: [
4550
+ { id: "pistol", team: "pistola", bus: "weapons", cutoff: 1800, gain: 0.9, seconds: 0.18 },
4551
+ { id: "shotgun", team: "escopeta", bus: "weapons", cutoff: 900, gain: 1.2, seconds: 0.35 },
4552
+ { id: "rifle", team: "fuzil", bus: "weapons", cutoff: 1400, gain: 1.1, seconds: 0.28 },
4553
+ { id: "long_rifle", team: "rifle", bus: "weapons", cutoff: 1100, gain: 1.3, seconds: 0.45 },
4554
+ { id: "impact", team: "impacto", bus: "world", cutoff: 2500, gain: 0.25, seconds: 0.05 },
4555
+ { id: "flesh", team: "carne", bus: "world", cutoff: 600, gain: 0.35, seconds: 0.08 },
4556
+ { id: "step", team: "passo", bus: "world", cutoff: 400, gain: 0.08, seconds: 0.05 },
4557
+ { id: "glass", team: "vidro", bus: "world", cutoff: 5e3, gain: 0.5, seconds: 0.3 },
4558
+ { id: "wood", team: "madeira", bus: "world", cutoff: 700, gain: 0.5, seconds: 0.2 },
4559
+ { id: "reload", team: "recarga", bus: "weapons", cutoff: 3e3, gain: 0.15, seconds: 0.06 },
4560
+ { id: "empty", team: "vazio", bus: "weapons", cutoff: 4e3, gain: 0.12, seconds: 0.03 },
4561
+ { id: "explosion", team: "explosao", bus: "world", cutoff: 200, gain: 1.6, seconds: 1.4 }
4562
+ ],
4563
+ fallback: { id: "unknown", team: "(any other name)", bus: "world", cutoff: 1e3, gain: 0.5, seconds: 0.1 }
4564
+ };
4565
+ var GAME_SFX = game_sfx_default;
4566
+ var GAME_SFX_CLASSES = GAME_SFX.classes;
4567
+ var GAME_SFX_FALLBACK = GAME_SFX.fallback;
4568
+ function noiseClassRecipe(row, id = `sfx_${row.id}`) {
4569
+ return {
4570
+ id,
4571
+ bus: row.bus,
4572
+ order: "burst",
4573
+ duration: row.seconds,
4574
+ tail: 0.05,
4575
+ layers: [
4576
+ {
4577
+ source: { kind: "noise", offset: "random" },
4578
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: row.cutoff } }] },
4579
+ envelope: [
4580
+ [0, row.gain, "set"],
4581
+ ["1d", 1e-3, "exp", "fixed"]
4582
+ ]
4583
+ }
4584
+ ],
4585
+ falloff: DISTANCE_CURVES.game_sfx,
4586
+ sources: [...GAME_SFX.sources.map((source) => `${source} ${row.team}`)]
4587
+ };
4588
+ }
4589
+ var GAME_SFX_BANK = { id: "game_sfx", sounds: [...GAME_SFX_CLASSES, GAME_SFX_FALLBACK].map((row) => noiseClassRecipe(row)) };
4590
+ function fillNoise(random, length3) {
4591
+ const samples = new Float32Array(length3);
4592
+ for (let index = 0; index < length3; index++) samples[index] = random() * 2 - 1;
4593
+ return samples;
4594
+ }
4595
+ function fillImpulse(random, options) {
4596
+ const size = Math.floor(options.length);
4597
+ return Array.from({ length: options.channels }, () => {
4598
+ const samples = new Float32Array(size);
4599
+ for (let index = 0; index < options.length; index++) {
4600
+ const value = (random() * 2 - 1) * Math.exp(options.decay * Math.log(1 - index / options.length));
4601
+ if (index < size) samples[index] = value;
4602
+ }
4603
+ return samples;
4604
+ });
4605
+ }
4606
+ function bufferOf(context, channels, requestedLength) {
4607
+ const buffer = context.createBuffer(channels.length, requestedLength, context.sampleRate);
4608
+ channels.forEach((samples, channel) => buffer.getChannelData(channel).set(samples));
4609
+ return buffer;
4610
+ }
4611
+ function buildChain(context, profile, random) {
4612
+ let compressor;
4613
+ if (profile.compressor) {
4614
+ compressor = context.createDynamicsCompressor();
4615
+ compressor.threshold.value = profile.compressor.threshold;
4616
+ compressor.ratio.value = profile.compressor.ratio;
4617
+ if (profile.compressor.knee !== void 0) compressor.knee.value = profile.compressor.knee;
4618
+ if (profile.compressor.attack !== void 0) compressor.attack.value = profile.compressor.attack;
4619
+ if (profile.compressor.release !== void 0) compressor.release.value = profile.compressor.release;
4620
+ compressor.connect(context.destination);
4621
+ }
4622
+ const master = context.createGain();
4623
+ master.gain.value = profile.master;
4624
+ master.connect(compressor ?? context.destination);
4625
+ if (!profile.reverb) {
4626
+ return {
4627
+ master,
4628
+ disconnect: () => {
4629
+ master.disconnect();
4630
+ compressor?.disconnect();
4631
+ }
4632
+ };
4633
+ }
4634
+ const convolver = context.createConvolver();
4635
+ const length3 = context.sampleRate * profile.reverb.seconds;
4636
+ convolver.buffer = bufferOf(context, fillImpulse(random, { channels: 2, length: length3, decay: profile.reverb.decay }), length3);
4637
+ const wet = context.createGain();
4638
+ wet.gain.value = profile.reverb.wet;
4639
+ wet.connect(convolver);
4640
+ convolver.connect(master);
4641
+ return {
4642
+ master,
4643
+ wet,
4644
+ disconnect: () => {
4645
+ wet.disconnect();
4646
+ convolver.disconnect();
4647
+ master.disconnect();
4648
+ compressor?.disconnect();
4649
+ }
4650
+ };
4651
+ }
4652
+ function globalContext() {
4653
+ const scope = globalThis;
4654
+ const Constructor = scope.AudioContext ?? scope.webkitAudioContext;
4655
+ return Constructor ? new Constructor() : null;
4656
+ }
4657
+ function unlockContext(context) {
4658
+ if (context.state === "suspended") void context.resume();
4659
+ }
4660
+ function assign(param2, automation, time) {
4661
+ if (automation.value !== void 0) param2.value = automation.value;
4662
+ if (automation.from !== void 0) param2.setValueAtTime(automation.from, time);
4663
+ }
4664
+ function ramp(param2, curve, value, at3) {
4665
+ if (curve === "set") param2.setValueAtTime(value, at3);
4666
+ else if (curve === "exp") param2.exponentialRampToValueAtTime(value, at3);
4667
+ else param2.linearRampToValueAtTime(value, at3);
4668
+ }
4669
+ function rampTo(param2, automation, time) {
4670
+ if (automation.to !== void 0 && automation.at !== void 0) ramp(param2, automation.curve ?? "linear", automation.to, time + automation.at);
4671
+ }
4672
+ function applyEnvelope(param2, points, time) {
4673
+ for (const point of points) ramp(param2, point.curve, point.value, time + point.time);
4674
+ }
4675
+ function noiseSource(target, loop) {
4676
+ const source = target.context.createBufferSource();
4677
+ source.buffer = target.noise();
4678
+ if (loop) source.loop = true;
4679
+ return source;
4680
+ }
4681
+ function filterNode(context, filter, time) {
4682
+ const node = context.createBiquadFilter();
4683
+ node.type = filter.type;
4684
+ assign(node.frequency, filter.frequency, time);
4685
+ if (filter.q !== void 0) node.Q.value = filter.q;
4686
+ if (filter.gain !== void 0) node.gain.value = filter.gain;
4687
+ return node;
4688
+ }
4689
+ function gainNode(context, value) {
4690
+ const node = context.createGain();
4691
+ node.gain.value = value;
4692
+ return node;
4693
+ }
4694
+ function voiceOf(sources) {
4695
+ return {
4696
+ sources,
4697
+ stop() {
4698
+ for (const source of sources) {
4699
+ try {
4700
+ source.stop();
4701
+ } catch {
4702
+ }
4703
+ }
4704
+ }
4705
+ };
4706
+ }
4707
+ var fixed = (range2) => typeof range2 === "number" ? range2 : 0;
4708
+ var fixedFrequency = (automation) => fixed("set" in automation ? automation.set : automation.from);
4709
+ function sourceOf(recipe, target) {
4710
+ if (recipe.source.kind === "noise") return { source: noiseSource(target, true) };
4711
+ const oscillator = target.context.createOscillator();
4712
+ oscillator.type = recipe.source.wave;
4713
+ if (recipe.source.frequency) oscillator.frequency.value = fixedFrequency(recipe.source.frequency);
4714
+ return { source: oscillator, frequency: oscillator.frequency };
4715
+ }
4716
+ function stopAll(sources) {
4717
+ return () => {
4718
+ for (const source of sources) {
4719
+ try {
4720
+ source.stop();
4721
+ } catch {
4722
+ }
4723
+ }
4724
+ };
4725
+ }
4726
+ function realizeGameLoop(recipe, target) {
4727
+ const { context } = target;
4728
+ const gain = context.createGain();
4729
+ const filters = (recipe.filters?.bank ?? []).map(() => context.createBiquadFilter());
4730
+ gain.gain.value = 0;
4731
+ const last = filters.reduce((previous, filter) => previous ? previous.connect(filter) : filter, void 0);
4732
+ last?.connect(gain);
4733
+ gain.connect(target.output);
4734
+ const { source, frequency } = sourceOf(recipe, target);
4735
+ recipe.filters?.bank.forEach((spec, index) => {
4736
+ const filter = filters[index];
4737
+ filter.type = spec.type;
4738
+ filter.frequency.value = fixedFrequency(spec.frequency);
4739
+ if (spec.q !== void 0) filter.Q.value = fixed(spec.q);
4740
+ });
4741
+ const sources = [source];
4742
+ for (const modulator of recipe.modulators ?? []) {
4743
+ const lfo = context.createOscillator();
4744
+ const depth = context.createGain();
4745
+ const stage = context.createGain();
4746
+ if (modulator.wave) lfo.type = modulator.wave;
4747
+ lfo.frequency.value = fixed(modulator.rate);
4748
+ depth.gain.value = fixed(modulator.depth);
4749
+ stage.gain.value = modulator.base ?? 0;
4750
+ lfo.connect(depth);
4751
+ depth.connect(stage.gain);
4752
+ last?.disconnect();
4753
+ last?.connect(stage);
4754
+ stage.connect(gain);
4755
+ lfo.start();
4756
+ sources.push(lfo);
4757
+ }
4758
+ source.connect(filters[0] ?? gain);
4759
+ source.start();
4760
+ return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll(sources) };
4761
+ }
4762
+ function realizeChainLoop(recipe, target, volume) {
4763
+ const { context } = target;
4764
+ const { source, frequency } = sourceOf(recipe, target);
4765
+ const filters = (recipe.filters?.bank ?? []).map(
4766
+ (spec) => filterNode(context, { type: spec.type, frequency: { value: fixedFrequency(spec.frequency) }, ...spec.q === void 0 ? {} : { q: fixed(spec.q) } }, target.time)
4767
+ );
4768
+ const gain = gainNode(context, volume);
4769
+ const lfos = (recipe.modulators ?? []).map((modulator) => {
4770
+ const lfo = context.createOscillator();
4771
+ if (modulator.wave) lfo.type = modulator.wave;
4772
+ lfo.frequency.value = fixed(modulator.rate);
4773
+ const depth = gainNode(context, fixed(modulator.depth));
4774
+ lfo.connect(depth);
4775
+ depth.connect(gain.gain);
4776
+ return lfo;
4777
+ });
4778
+ let tail = source;
4779
+ for (const filter of filters) tail = tail.connect(filter);
4780
+ tail.connect(gain);
4781
+ gain.connect(target.output);
4782
+ source.start();
4783
+ for (const lfo of lfos) lfo.start();
4784
+ return { gain, source, ...frequency ? { frequency } : {}, stop: stopAll([source, ...lfos]) };
4785
+ }
4786
+ var INERT = { set() {
4787
+ }, stop() {
4788
+ } };
4789
+ function createLoopHandle(recipe, host, volume) {
4790
+ if (!recipe) return INERT;
4791
+ const loopRecipe = recipe;
4792
+ const isChain = loopRecipe.order === "chain_loop";
4793
+ const opening = volume ?? loopRecipe.volume ?? 0;
4794
+ const smoothing = loopRecipe.smoothing ?? LOOP_SMOOTHING;
4795
+ let state = createLoopState(loopRecipe);
4796
+ let nodes;
4797
+ const entry = {
4798
+ get created() {
4799
+ return nodes !== void 0;
4800
+ },
4801
+ rewire() {
4802
+ nodes?.gain.disconnect();
4803
+ nodes?.gain.connect(host.output(loopRecipe.bus));
4804
+ },
4805
+ stop() {
4806
+ nodes?.stop();
4807
+ nodes?.gain.disconnect();
4808
+ nodes = void 0;
4809
+ state = createLoopState(loopRecipe);
4810
+ host.unregister(entry);
4811
+ }
4812
+ };
4813
+ const create = (context, startVolume) => {
4814
+ if (loopRecipe.source.kind === "noise") host.prepareNoise();
4815
+ const target = { context, output: host.output(loopRecipe.bus), noise: host.noise, time: context.currentTime };
4816
+ const made = isChain ? realizeChainLoop(loopRecipe, target, startVolume) : realizeGameLoop(loopRecipe, target);
4817
+ host.register(entry);
4818
+ return made;
4819
+ };
4820
+ const open = () => {
4821
+ const context = host.ready();
4822
+ if (!context) return;
4823
+ nodes = create(context, opening);
4824
+ state = createLoopState({ ...loopRecipe, volume: opening }, true);
4825
+ };
4826
+ if (isChain) open();
4827
+ return {
4828
+ set(next) {
4829
+ if (isChain && !nodes) open();
4830
+ const context = host.ready();
4831
+ if (!context) return;
4832
+ const action = state.set(next);
4833
+ if (!action) return;
4834
+ nodes ?? (nodes = create(context, 0));
4835
+ nodes.gain.gain.setTargetAtTime(action.volume, context.currentTime, smoothing.volume);
4836
+ if (action.frequency !== void 0 && nodes.frequency) nodes.frequency.setTargetAtTime(action.frequency, context.currentTime, smoothing.frequency);
4837
+ },
4838
+ stop: entry.stop
4839
+ };
4840
+ }
4841
+ function createNoiseStore(random) {
4842
+ const store = {
4843
+ random,
4844
+ reset() {
4845
+ store.samples = void 0;
4846
+ store.buffer = void 0;
4847
+ }
4848
+ };
4849
+ return store;
4850
+ }
4851
+ function prepareNoise(store, context, seconds) {
4852
+ store.samples ?? (store.samples = fillNoise(store.random, Math.floor(context.sampleRate * seconds)));
4853
+ return store.samples;
4854
+ }
4855
+ function noiseBuffer(store, context, seconds) {
4856
+ const samples = prepareNoise(store, context, seconds);
4857
+ store.buffer ?? (store.buffer = bufferOf(context, [samples], context.sampleRate * seconds));
4858
+ return store.buffer;
4859
+ }
4860
+ var end = (layer2, start) => start + layer2.duration + layer2.tail;
4861
+ var firstFilter = (layer2) => layer2.filters?.bank[0];
4862
+ var offsetOf = (layer2) => layer2.source.kind === "noise" ? layer2.source.offset : 0;
4863
+ function setFilter(node, filter, time) {
4864
+ if (!filter) return;
4865
+ node.type = filter.type;
4866
+ assign(node.frequency, filter.frequency, time);
4867
+ if (filter.q !== void 0) node.Q.value = filter.q;
4868
+ }
4869
+ function realizeBurst(plan, target) {
4870
+ const { context } = target;
4871
+ const layer2 = plan.layers[0];
4872
+ const time = target.time + layer2.at;
4873
+ const gain = context.createGain();
4874
+ gain.connect(target.output);
4875
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
4876
+ const filter = context.createBiquadFilter();
4877
+ source.connect(filter);
4878
+ filter.connect(gain);
4879
+ setFilter(filter, firstFilter(layer2), time);
4880
+ applyEnvelope(gain.gain, layer2.envelope, time);
4881
+ source.start(time, offsetOf(layer2));
4882
+ source.stop(end(layer2, time));
4883
+ return voiceOf([source]);
4884
+ }
4885
+ function realizeScream(plan, target) {
4886
+ const { context } = target;
4887
+ const layer2 = plan.layers[0];
4888
+ const time = target.time + layer2.at;
4889
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
4890
+ const gain = context.createGain();
4891
+ gain.connect(target.output);
4892
+ for (const band of layer2.filters?.bank ?? []) {
4893
+ const filter = context.createBiquadFilter();
4894
+ filter.type = band.type;
4895
+ if (band.q !== void 0) filter.Q.value = band.q;
4896
+ assign(filter.frequency, band.frequency, time);
4897
+ rampTo(filter.frequency, band.frequency, time);
4898
+ const level = context.createGain();
4899
+ level.gain.value = band.level ?? 1;
4900
+ source.connect(filter);
4901
+ filter.connect(level);
4902
+ level.connect(gain);
4903
+ }
4904
+ const sources = [source];
4905
+ const lfos = layer2.modulators.map((modulator) => {
4906
+ const lfo = context.createOscillator();
4907
+ const depth = context.createGain();
4908
+ if (modulator.wave) lfo.type = modulator.wave;
4909
+ lfo.frequency.value = modulator.rate;
4910
+ depth.gain.value = modulator.depth;
4911
+ lfo.connect(depth);
4912
+ depth.connect(gain.gain);
4913
+ return lfo;
4914
+ });
4915
+ applyEnvelope(gain.gain, layer2.envelope, time);
4916
+ source.start(time, offsetOf(layer2));
4917
+ source.stop(end(layer2, time));
4918
+ for (const lfo of lfos) {
4919
+ lfo.start(time);
4920
+ lfo.stop(end(layer2, time));
4921
+ sources.push(lfo);
4922
+ }
4923
+ return voiceOf(sources);
4924
+ }
4925
+ function realizeGroan(plan, target) {
4926
+ const { context } = target;
4927
+ const layer2 = plan.layers[0];
4928
+ const time = target.time + layer2.at;
4929
+ const oscillator = context.createOscillator();
4930
+ const gain = context.createGain();
4931
+ const filter = context.createBiquadFilter();
4932
+ const band = firstFilter(layer2);
4933
+ if (layer2.source.kind === "osc") oscillator.type = layer2.source.wave;
4934
+ if (band) {
4935
+ filter.type = band.type;
4936
+ if (band.q !== void 0) filter.Q.value = band.q;
4937
+ }
4938
+ if (layer2.source.kind === "osc") {
4939
+ assign(oscillator.frequency, layer2.source.frequency, time);
4940
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
4941
+ }
4942
+ if (band) assign(filter.frequency, band.frequency, time);
4943
+ applyEnvelope(gain.gain, layer2.envelope, time);
4944
+ oscillator.connect(filter);
4945
+ filter.connect(gain);
4946
+ gain.connect(target.output);
4947
+ oscillator.start(time);
4948
+ oscillator.stop(end(layer2, time));
4949
+ return voiceOf([oscillator]);
4950
+ }
4951
+ function realizeBoom(plan, target) {
4952
+ const { context } = target;
4953
+ const layer2 = plan.layers[0];
4954
+ const time = target.time + layer2.at;
4955
+ const oscillator = context.createOscillator();
4956
+ const gain = context.createGain();
4957
+ if (layer2.source.kind === "osc") {
4958
+ oscillator.type = layer2.source.wave;
4959
+ assign(oscillator.frequency, layer2.source.frequency, time);
4960
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
4961
+ }
4962
+ applyEnvelope(gain.gain, layer2.envelope, time);
4963
+ oscillator.connect(gain);
4964
+ gain.connect(target.output);
4965
+ oscillator.start(time);
4966
+ oscillator.stop(end(layer2, time));
4967
+ return voiceOf([oscillator]);
4968
+ }
4969
+ function realizeStatic(plan, target) {
4970
+ const { context } = target;
4971
+ const layer2 = plan.layers[0];
4972
+ const time = target.time + layer2.at;
4973
+ const gain = context.createGain();
4974
+ const source = context.createBufferSource();
4975
+ const filter = context.createBiquadFilter();
4976
+ source.buffer = target.noise();
4977
+ setFilter(filter, firstFilter(layer2), time);
4978
+ applyEnvelope(gain.gain, layer2.envelope, time);
4979
+ source.connect(filter);
4980
+ filter.connect(gain);
4981
+ gain.connect(target.output);
4982
+ source.start(time, offsetOf(layer2));
4983
+ source.stop(end(layer2, time));
4984
+ return voiceOf([source]);
4985
+ }
4986
+ function realizeChord(plan, target) {
4987
+ const { context } = target;
4988
+ const layers = plan.layers;
4989
+ const oscillators = layers.map(() => context.createOscillator());
4990
+ const gain = context.createGain();
4991
+ for (let index = layers.length - 1; index >= 0; index--) {
4992
+ const source = layers[index].source;
4993
+ if (source.kind === "osc") oscillators[index].type = source.wave;
4994
+ }
4995
+ layers.forEach((layer2, index) => {
4996
+ if (layer2.source.kind === "osc") assign(oscillators[index].frequency, layer2.source.frequency, target.time + layer2.at);
4997
+ });
4998
+ applyEnvelope(gain.gain, layers[0].envelope, target.time + layers[0].at);
4999
+ for (const oscillator of oscillators) oscillator.connect(gain);
5000
+ gain.connect(target.output);
5001
+ layers.forEach((layer2, index) => oscillators[index].start(target.time + layer2.at));
5002
+ layers.forEach((layer2, index) => oscillators[index].stop(end(layer2, target.time + layer2.at)));
5003
+ return voiceOf(oscillators);
5004
+ }
5005
+ function realizeCalls(plan, target) {
5006
+ const { context } = target;
5007
+ const gain = context.createGain();
5008
+ const filter = context.createBiquadFilter();
5009
+ if (plan.silent) return voiceOf([]);
5010
+ const band = firstFilter(plan.layers[0]);
5011
+ if (band) filter.type = band.type;
5012
+ filter.connect(gain);
5013
+ gain.connect(target.output);
5014
+ const oscillators = plan.layers.map((layer2) => {
5015
+ const time = target.time + layer2.at;
5016
+ const oscillator = context.createOscillator();
5017
+ if (layer2.source.kind === "osc") {
5018
+ oscillator.type = layer2.source.wave;
5019
+ assign(oscillator.frequency, layer2.source.frequency, time);
5020
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
5021
+ }
5022
+ if (band) {
5023
+ assign(filter.frequency, band.frequency, time);
5024
+ if (band.q !== void 0) filter.Q.value = band.q;
5025
+ }
5026
+ const envelope2 = context.createGain();
5027
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
5028
+ oscillator.connect(envelope2);
5029
+ envelope2.connect(filter);
5030
+ oscillator.start(time);
5031
+ oscillator.stop(end(layer2, time));
5032
+ return oscillator;
5033
+ });
5034
+ return voiceOf(oscillators);
5035
+ }
5036
+ var end2 = (layer2, start) => start + layer2.duration + layer2.tail;
5037
+ function outputStage(plan, layer2, target) {
5038
+ const { context } = target;
5039
+ const level = gainNode(context, layer2.level);
5040
+ let node = level;
5041
+ if (plan.pan !== void 0 && typeof context.createStereoPanner === "function") {
5042
+ const panner = context.createStereoPanner();
5043
+ panner.pan.value = plan.pan;
5044
+ level.connect(panner);
5045
+ node = panner;
5046
+ }
5047
+ node.connect(target.output);
5048
+ if (layer2.wet && target.wet) {
5049
+ const send = gainNode(context, layer2.wet);
5050
+ node.connect(send);
5051
+ send.connect(target.wet);
5052
+ }
5053
+ return level;
5054
+ }
5055
+ function realizeNoiseLayer(plan, layer2, target) {
5056
+ const { context } = target;
5057
+ const time = target.time + layer2.at;
5058
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
5059
+ const filters = (layer2.filters?.bank ?? []).map((filter) => {
5060
+ const node = filterNode(context, filter, time);
5061
+ rampTo(node.frequency, filter.frequency, time);
5062
+ return node;
5063
+ });
5064
+ const envelope2 = context.createGain();
5065
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
5066
+ let tail = source;
5067
+ for (const filter of filters) tail = tail.connect(filter);
5068
+ tail.connect(envelope2);
5069
+ envelope2.connect(outputStage(plan, layer2, target));
5070
+ source.start(time, layer2.source.kind === "noise" ? layer2.source.offset : 0);
5071
+ source.stop(end2(layer2, time));
5072
+ return [source];
5073
+ }
5074
+ function realizeToneLayer(plan, layer2, target) {
5075
+ const { context } = target;
5076
+ const time = target.time + layer2.at;
5077
+ const oscillator = context.createOscillator();
5078
+ if (layer2.source.kind === "osc") {
5079
+ oscillator.type = layer2.source.wave;
5080
+ assign(oscillator.frequency, layer2.source.frequency, time);
5081
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
5082
+ }
5083
+ const envelope2 = context.createGain();
5084
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
5085
+ oscillator.connect(envelope2);
5086
+ envelope2.connect(outputStage(plan, layer2, target));
5087
+ oscillator.start(time);
5088
+ oscillator.stop(end2(layer2, time));
5089
+ return [oscillator];
5090
+ }
5091
+ function realizeVoiceLayer(plan, layer2, target) {
5092
+ const source = layer2.source;
5093
+ if (source.kind !== "formant_voice") return [];
5094
+ const { context } = target;
5095
+ const time = target.time + layer2.at;
5096
+ const oscillator = context.createOscillator();
5097
+ oscillator.type = source.wave;
5098
+ assign(oscillator.frequency, source.frequency, time);
5099
+ rampTo(oscillator.frequency, source.frequency, time);
5100
+ const vibrato = context.createOscillator();
5101
+ vibrato.frequency.value = source.vibrato.rate;
5102
+ const vibratoDepth = gainNode(context, source.vibrato.depth);
5103
+ vibrato.connect(vibratoDepth);
5104
+ vibratoDepth.connect(oscillator.frequency);
5105
+ const formants = source.formants.map((formant) => filterNode(context, { type: "bandpass", frequency: { value: formant.frequency }, q: formant.q }, time));
5106
+ const breath = noiseSource(target, true);
5107
+ const breathBand = filterNode(context, { type: "bandpass", frequency: { value: source.breath.frequency }, q: source.breath.q }, time);
5108
+ const breathLevel = gainNode(context, source.breath.gain);
5109
+ const mix = gainNode(context, source.mix);
5110
+ for (const formant of formants) oscillator.connect(formant);
5111
+ breath.connect(breathBand);
5112
+ breathBand.connect(breathLevel);
5113
+ for (const formant of formants) formant.connect(mix);
5114
+ breathLevel.connect(mix);
5115
+ const envelope2 = context.createGain();
5116
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
5117
+ mix.connect(envelope2);
5118
+ envelope2.connect(outputStage(plan, layer2, target));
5119
+ const stop = end2(layer2, time);
5120
+ oscillator.start(time);
5121
+ vibrato.start(time);
5122
+ breath.start(time, source.breath.offset);
5123
+ oscillator.stop(stop);
5124
+ vibrato.stop(stop);
5125
+ breath.stop(stop);
5126
+ return [oscillator, vibrato, breath];
5127
+ }
5128
+ function realizeLayered(plan, target) {
5129
+ const sources = plan.layers.flatMap((layer2) => {
5130
+ if (layer2.source.kind === "noise") return realizeNoiseLayer(plan, layer2, target);
5131
+ if (layer2.source.kind === "osc") return realizeToneLayer(plan, layer2, target);
5132
+ return realizeVoiceLayer(plan, layer2, target);
5133
+ });
5134
+ return voiceOf(sources);
5135
+ }
5136
+ function realizeSiren(plan, target) {
5137
+ const { context } = target;
5138
+ const layer2 = plan.layers[0];
5139
+ const time = target.time + layer2.at;
5140
+ const oscillator = context.createOscillator();
5141
+ if (layer2.source.kind === "osc") {
5142
+ oscillator.type = layer2.source.wave;
5143
+ assign(oscillator.frequency, layer2.source.frequency, time);
5144
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
5145
+ }
5146
+ const depths = layer2.modulators.map((modulator) => {
5147
+ const lfo = context.createOscillator();
5148
+ if (modulator.wave) lfo.type = modulator.wave;
5149
+ lfo.frequency.value = modulator.rate;
5150
+ const depth = gainNode(context, modulator.depth);
5151
+ lfo.connect(depth);
5152
+ if (modulator.target === "frequency") depth.connect(oscillator.frequency);
5153
+ return { lfo, depth, target: modulator.target };
5154
+ });
5155
+ const lfos = depths.map(({ lfo }) => lfo);
5156
+ const envelope2 = context.createGain();
5157
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
5158
+ for (const { depth, target: modulated } of depths) if (modulated === "gain") depth.connect(envelope2.gain);
5159
+ const filters = (layer2.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
5160
+ let tail = oscillator;
5161
+ for (const filter of filters) tail = tail.connect(filter);
5162
+ tail.connect(envelope2);
5163
+ envelope2.connect(target.output);
5164
+ if (layer2.wet && target.wet) {
5165
+ const send = gainNode(context, layer2.wet);
5166
+ envelope2.connect(send);
5167
+ send.connect(target.wet);
5168
+ }
5169
+ const stop = end2(layer2, time);
5170
+ oscillator.start(time);
5171
+ for (const lfo of lfos) lfo.start(time);
5172
+ oscillator.stop(stop);
5173
+ for (const lfo of lfos) lfo.stop(stop);
5174
+ return voiceOf([oscillator, ...lfos]);
5175
+ }
5176
+ function noiseLayer(layer2, target, time) {
5177
+ const { context } = target;
5178
+ const source = noiseSource(target, layer2.source.kind === "noise" && layer2.source.loop);
5179
+ const filters = (layer2.filters?.bank ?? []).map((filter) => filterNode(context, filter, time));
5180
+ const envelope2 = context.createGain();
5181
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
5182
+ let tail = source;
5183
+ for (const filter of filters) tail = tail.connect(filter);
5184
+ tail.connect(envelope2).connect(target.output);
5185
+ source.start(time, layer2.source.kind === "noise" ? layer2.source.offset : 0);
5186
+ source.stop(time + layer2.duration + layer2.tail);
5187
+ return source;
5188
+ }
5189
+ function toneLayer(layer2, target, time) {
5190
+ const { context } = target;
5191
+ const oscillator = context.createOscillator();
5192
+ if (layer2.source.kind === "osc") {
5193
+ oscillator.type = layer2.source.wave;
5194
+ assign(oscillator.frequency, layer2.source.frequency, time);
5195
+ rampTo(oscillator.frequency, layer2.source.frequency, time);
5196
+ }
5197
+ const envelope2 = context.createGain();
5198
+ applyEnvelope(envelope2.gain, layer2.envelope, time);
5199
+ oscillator.connect(envelope2).connect(target.output);
5200
+ oscillator.start(time);
5201
+ oscillator.stop(time + layer2.duration + layer2.tail);
5202
+ return oscillator;
5203
+ }
5204
+ function realizeWeapon(plan, target) {
5205
+ const sources = plan.layers.map((layer2) => {
5206
+ const time = target.time + layer2.at;
5207
+ return layer2.source.kind === "noise" ? noiseLayer(layer2, target, time) : toneLayer(layer2, target, time);
5208
+ });
5209
+ return voiceOf(sources);
5210
+ }
5211
+ var BUILDERS = {
5212
+ layered: realizeLayered,
5213
+ burst: realizeBurst,
5214
+ scream: realizeScream,
5215
+ groan: realizeGroan,
5216
+ boom: realizeBoom,
5217
+ static: realizeStatic,
5218
+ chord: realizeChord,
5219
+ calls: realizeCalls,
5220
+ siren: realizeSiren,
5221
+ weapon: realizeWeapon
5222
+ };
5223
+ function realize(plan, target) {
5224
+ return BUILDERS[plan.order](plan, target);
5225
+ }
5226
+ var usesNoise = (recipe) => recipe.layers.some((layer2) => layer2.source.kind !== "osc");
5227
+ var planUsesNoise = (plan) => plan.layers.some((layer2) => layer2.source.kind !== "osc");
5228
+ function createAudioEngine(options = {}) {
5229
+ const seed = options.seed ?? 1;
5230
+ const registry = createSoundRegistry(options.banks);
5231
+ const voices = createVoiceManager({ ...options.groups ? { groups: options.groups } : {}, ...options.maxVoices === void 0 ? {} : { maxVoices: options.maxVoices } });
5232
+ const engineRandom = createRng2(seed);
5233
+ const shared = createNoiseStore(engineRandom);
5234
+ const busSettings = /* @__PURE__ */ new Map();
5235
+ const busNodes = /* @__PURE__ */ new Map();
5236
+ const meters = /* @__PURE__ */ new Map();
5237
+ const active = /* @__PURE__ */ new Map();
5238
+ const loops = /* @__PURE__ */ new Set();
5239
+ const named = /* @__PURE__ */ new Map();
5240
+ let mix = resolveMix(options.mix ?? DEFAULT_MIX, options.profiles);
5241
+ let context = null;
5242
+ let chain = null;
5243
+ let listener = DEFAULT_LISTENER;
5244
+ let lastTime = 0;
5245
+ const outputFor = (bus) => busNodes.get(bus) ?? chain?.master;
5246
+ const watched = /* @__PURE__ */ new Set();
5247
+ const meterOf = (bus) => {
5248
+ const existing = meters.get(bus);
5249
+ if (existing) return existing;
5250
+ const node = busNodes.get(bus);
5251
+ if (!context || !node || typeof context.createAnalyser !== "function") return void 0;
5252
+ const analyser = context.createAnalyser();
5253
+ analyser.fftSize = 1024;
5254
+ node.connect(analyser);
5255
+ const made = { analyser, data: new Float32Array(analyser.fftSize) };
5256
+ meters.set(bus, made);
5257
+ return made;
5258
+ };
5259
+ const ensureBus = (bus) => {
5260
+ if (!context || !chain || busNodes.has(bus)) return;
5261
+ const node = context.createGain();
5262
+ node.connect(chain.master);
5263
+ busNodes.set(bus, node);
5264
+ if (watched.has(bus)) meterOf(bus);
5265
+ };
5266
+ const ready = () => {
5267
+ if (context) return context;
5268
+ const created = (options.createContext ?? globalContext)();
5269
+ if (!created) return null;
5270
+ context = created;
5271
+ chain = buildChain(created, mix, engineRandom);
5272
+ if (mix.noise.create === "eager") noiseBuffer(shared, created, mix.noise.seconds);
5273
+ for (const [bus, setting] of Object.entries(mix.buses ?? {})) {
5274
+ busSettings.set(bus, { ...setting, ...busSettings.get(bus) });
5275
+ }
5276
+ for (const [bus, setting] of busSettings) {
5277
+ ensureBus(bus);
5278
+ busNodes.get(bus).gain.value = busGain(setting);
5279
+ }
5280
+ lastTime = created.currentTime;
5281
+ return created;
5282
+ };
5283
+ const advance = () => {
5284
+ if (!context) return;
5285
+ const elapsed = context.currentTime - lastTime;
5286
+ lastTime = context.currentTime;
5287
+ if (elapsed > 0) for (const id of voices.advance(elapsed)) active.delete(id);
5288
+ };
5289
+ const inputFor = (recipe, playOptions, random) => ({
5290
+ random,
5291
+ listener,
5292
+ noiseSeconds: mix.noise.seconds,
5293
+ ...options.distance ? { distanceMode: options.distance } : {},
5294
+ ...playOptions.volume === void 0 ? {} : { volume: playOptions.volume },
5295
+ ...playOptions.distance === void 0 ? {} : { distance: playOptions.distance },
5296
+ ...playOptions.position ? { position: playOptions.position } : {},
5297
+ ...playOptions.params ? { params: playOptions.params } : {},
5298
+ ...options.occlusion && playOptions.position && recipe.falloff?.model !== "none" ? { occlusion: options.occlusion(playOptions.position) } : {}
5299
+ });
5300
+ const start = (plan, store, voiceId, delay = 0) => {
5301
+ const audio = context;
5302
+ const realized = realize(plan, {
5303
+ context: audio,
5304
+ output: outputFor(plan.bus),
5305
+ ...chain?.wet ? { wet: chain.wet } : {},
5306
+ noise: () => noiseBuffer(store, audio, mix.noise.seconds),
5307
+ time: audio.currentTime + delay
5308
+ });
5309
+ if (voiceId === void 0) return null;
5310
+ voices.setLength(voiceId, plan.length + delay);
5311
+ active.set(voiceId, realized);
5312
+ return {
5313
+ id: voiceId,
5314
+ plan,
5315
+ stop() {
5316
+ realized.stop();
5317
+ voices.release(voiceId);
5318
+ active.delete(voiceId);
5319
+ }
5320
+ };
5321
+ };
5322
+ const admit = (recipe, gain, distance4) => {
5323
+ const decision = voices.request({ ...recipe.group ? { group: recipe.group } : {}, priority: recipe.priority ?? busPriority(recipe.bus), gain, distance: distance4, length: 0 });
5324
+ if (!decision.accepted) return null;
5325
+ for (const id of decision.evicted) {
5326
+ active.get(id)?.stop();
5327
+ active.delete(id);
5328
+ }
5329
+ return decision.id;
5330
+ };
5331
+ const playWith = (id, playOptions, random, store) => {
5332
+ const recipe = registry.sound(id);
5333
+ if (!recipe || busSettings.get(recipe.bus)?.muted) return null;
5334
+ const distance4 = playOptions.distance ?? (playOptions.position ? listenerDistance(listener, playOptions.position, options.distance) : 0);
5335
+ const level = falloffGain(recipe.falloff, playOptions.volume ?? 1, distance4);
5336
+ const silent = isSilent(recipe.falloff, level);
5337
+ if (silent && !recipe.keepSilent) return null;
5338
+ const audio = ready();
5339
+ if (!audio) return null;
5340
+ unlockContext(audio);
5341
+ advance();
5342
+ const voiceId = silent ? void 0 : admit(recipe, level, distance4);
5343
+ if (voiceId === null) return null;
5344
+ if (!silent && usesNoise(recipe)) prepareNoise(store, audio, mix.noise.seconds);
5345
+ const plan = planSound(recipe, inputFor(recipe, playOptions, playOptions.seed === void 0 ? random : createRng2(playOptions.seed)));
5346
+ return plan ? start(plan, store, voiceId, playOptions.delay) : null;
5347
+ };
5348
+ const randomFor = (playOptions) => {
5349
+ if (playOptions.channel === void 0) return engineRandom;
5350
+ const existing = named.get(playOptions.channel);
5351
+ if (existing) return existing;
5352
+ const stream = streamFor(seed, playOptions.channel);
5353
+ named.set(playOptions.channel, stream);
5354
+ return stream;
5355
+ };
5356
+ const planWith = (id, playOptions, random) => {
5357
+ const recipe = registry.sound(id);
5358
+ return recipe ? planSound(recipe, inputFor(recipe, playOptions, random)) : null;
5359
+ };
5360
+ const loopHost = {
5361
+ ready,
5362
+ output: outputFor,
5363
+ prepareNoise: () => context && prepareNoise(shared, context, mix.noise.seconds),
5364
+ noise: () => noiseBuffer(shared, context, mix.noise.seconds),
5365
+ register: (loop) => void loops.add(loop),
5366
+ unregister: (loop) => void loops.delete(loop)
5367
+ };
5368
+ return {
5369
+ registry,
5370
+ register: (bank) => registry.register(bank),
5371
+ plan: (id, playOptions = {}) => planWith(id, playOptions, createRng2(playOptions.seed ?? seed)),
5372
+ play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared),
5373
+ playPlan(plan, playOptions = {}) {
5374
+ const audio = ready();
5375
+ if (!audio) return null;
5376
+ advance();
5377
+ const recipe = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...plan.group ? { group: plan.group } : {} };
5378
+ const voiceId = plan.silent ? void 0 : admit(recipe, plan.level, plan.distance);
5379
+ if (voiceId === null) return null;
5380
+ if (planUsesNoise(plan)) prepareNoise(shared, audio, mix.noise.seconds);
5381
+ return start(plan, shared, voiceId, playOptions.delay);
5382
+ },
5383
+ loop: (id, loopOptions = {}) => createLoopHandle(registry.loop(id), loopHost, loopOptions.volume),
5384
+ channel(channelOptions) {
5385
+ const random = channelOptions.random ?? (channelOptions.seed === void 0 ? engineRandom : createRng2(channelOptions.seed));
5386
+ const store = channelOptions.ownNoise ? createNoiseStore(random) : shared;
5387
+ return {
5388
+ random,
5389
+ play: (id, playOptions = {}) => playWith(id, playOptions, random, store),
5390
+ plan: (id, playOptions = {}) => planWith(id, playOptions, createRng2(playOptions.seed ?? seed))
5391
+ };
5392
+ },
5393
+ setListener(next) {
5394
+ listener = next;
5395
+ },
5396
+ setBus(id, setting) {
5397
+ busSettings.set(id, { ...busSettings.get(id), ...setting });
5398
+ if (!context) return;
5399
+ ensureBus(id);
5400
+ busNodes.get(id).gain.value = busGain(busSettings.get(id));
5401
+ },
5402
+ setMix(next) {
5403
+ const previous = mix;
5404
+ mix = resolveMix(next, options.profiles);
5405
+ if (!context || !chain) return;
5406
+ const old = chain;
5407
+ chain = buildChain(context, mix, engineRandom);
5408
+ if (mix.noise.seconds !== previous.noise.seconds) shared.reset();
5409
+ for (const node of busNodes.values()) {
5410
+ node.disconnect();
5411
+ node.connect(chain.master);
5412
+ }
5413
+ for (const loop of loops) loop.rewire();
5414
+ old.disconnect();
5415
+ },
5416
+ setMaster(gain) {
5417
+ mix = { ...mix, master: gain };
5418
+ if (chain) chain.master.gain.value = gain;
5419
+ },
5420
+ unlock() {
5421
+ const audio = ready();
5422
+ if (audio) unlockContext(audio);
5423
+ },
5424
+ level(bus) {
5425
+ watched.add(bus);
5426
+ ensureBus(bus);
5427
+ const meter = meterOf(bus);
5428
+ if (!meter) return 0;
5429
+ meter.analyser.getFloatTimeDomainData(meter.data);
5430
+ let sum2 = 0;
5431
+ for (const sample of meter.data) sum2 += sample * sample;
5432
+ return Math.sqrt(sum2 / meter.data.length);
5433
+ },
5434
+ stats() {
5435
+ advance();
5436
+ const voiceStats = voices.stats();
5437
+ return {
5438
+ context: context ? context.state : "none",
5439
+ voices: voiceStats.active,
5440
+ peak: voiceStats.peak,
5441
+ dropped: voiceStats.dropped,
5442
+ maxVoices: voiceStats.maxVoices,
5443
+ loops: [...loops].filter((loop) => loop.created).length
5444
+ };
5445
+ },
5446
+ dispose() {
5447
+ for (const id of active.keys()) voices.release(id);
5448
+ for (const loop of [...loops]) loop.stop();
5449
+ active.clear();
5450
+ void context?.close();
5451
+ context = null;
5452
+ chain = null;
5453
+ busNodes.clear();
5454
+ meters.clear();
5455
+ shared.reset();
5456
+ }
5457
+ };
5458
+ }
5459
+ function createAudioSink(engine) {
5460
+ const playing = /* @__PURE__ */ new Map();
5461
+ return {
5462
+ engine,
5463
+ play(request) {
5464
+ const handle = engine.play(request.sound, {
5465
+ ...request.position ? { position: request.position } : {},
5466
+ ...request.volume === void 0 ? {} : { volume: request.volume },
5467
+ ...request.params ? { params: request.params } : {},
5468
+ ...request.seed === void 0 ? {} : { seed: request.seed }
5469
+ });
5470
+ if (!handle) return null;
5471
+ playing.set(handle.id, handle);
5472
+ return { id: handle.id };
5473
+ },
5474
+ stop(handle) {
5475
+ playing.get(handle.id)?.stop();
5476
+ playing.delete(handle.id);
5477
+ },
5478
+ loop(sound) {
5479
+ const loop = engine.loop(sound);
5480
+ return { set: (state) => loop.set({ ...state.volume === void 0 ? {} : { volume: state.volume }, ...state.frequency === void 0 ? {} : { frequency: state.frequency } }), stop: () => loop.stop() };
5481
+ },
5482
+ setListener(listener) {
5483
+ engine.setListener(listener);
5484
+ },
5485
+ register(bank) {
5486
+ engine.register(bank);
5487
+ }
5488
+ };
5489
+ }
5490
+ var BENCH_BANK = {
5491
+ id: "audio_bench",
5492
+ sounds: [
5493
+ {
5494
+ id: "bench_ping",
5495
+ bus: "ui",
5496
+ pan: true,
5497
+ duration: 0.25,
5498
+ layers: [
5499
+ {
5500
+ gain: 0.5,
5501
+ wet: 0.3,
5502
+ source: { kind: "osc", wave: "sine", frequency: { from: 880 } },
5503
+ envelope: [
5504
+ [0, 1e-4, "set", "fixed"],
5505
+ [2e-3, 1, "exp"],
5506
+ ["1d", 1e-4, "exp", "fixed"]
5507
+ ]
5508
+ }
5509
+ ],
5510
+ falloff: DISTANCE_CURVES.art_scene,
5511
+ sources: ["sdk: bench probe, a 880 Hz ping through the art scene output stage"]
5512
+ },
5513
+ {
5514
+ id: "bench_hiss",
5515
+ bus: "ui",
5516
+ pan: true,
5517
+ duration: [0.3, 0.9],
5518
+ layers: [
5519
+ {
5520
+ gain: 0.4,
5521
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
5522
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3e3 }, q: 0.7 }] },
5523
+ envelope: [
5524
+ [0, 1e-4, "set", "fixed"],
5525
+ [2e-3, 1, "exp"],
5526
+ ["1d", 1e-4, "exp", "fixed"]
5527
+ ]
5528
+ }
5529
+ ],
5530
+ falloff: DISTANCE_CURVES.art_scene,
5531
+ sources: ["sdk: bench probe, a highpass hiss of random length"]
5532
+ }
5533
+ ]
5534
+ };
5535
+
3770
5536
  // ../game-vfx/dist/index.js
3771
5537
  var __defProp2 = Object.defineProperty;
3772
5538
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -6974,7 +8740,7 @@ var GameTestRoom = (() => {
6974
8740
  mixed ^= mixed >>> 16;
6975
8741
  return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1;
6976
8742
  }
6977
- function streamFor(eventSeed) {
8743
+ function streamFor2(eventSeed) {
6978
8744
  return lehmer(mixEventSeed(eventSeed));
6979
8745
  }
6980
8746
  var TABLE = {
@@ -7070,10 +8836,10 @@ var GameTestRoom = (() => {
7070
8836
  if (!best) return;
7071
8837
  const offsetX = clamp22(x - best.x, -0.2, 0.2);
7072
8838
  const offsetY = clamp22(y - best.y, -0.17, 0.17);
7073
- for (let draw = 0; draw < OBJECT_ID_DRAWS2; draw++) random.next();
8839
+ for (let draw2 = 0; draw2 < OBJECT_ID_DRAWS2; draw2++) random.next();
7074
8840
  const chipTurn = random.between(0, 6);
7075
8841
  const chipScale = random.between(0.8, 1.3) * scale2;
7076
- for (let draw = 0; draw < OBJECT_ID_DRAWS2; draw++) random.next();
8842
+ for (let draw2 = 0; draw2 < OBJECT_ID_DRAWS2; draw2++) random.next();
7077
8843
  best.holes.push({ x: offsetX, y: offsetY, scale: scale2, chipTurn, chipScale });
7078
8844
  impact(host.engine, "concrete", [ORIGIN2[0] + x, y, ORIGIN2[2] + 0.13], [0, 0, 1], 0.8);
7079
8845
  } else if (action === "collapse") {
@@ -7972,7 +9738,7 @@ var GameTestRoom = (() => {
7972
9738
  }
7973
9739
  cue(step) {
7974
9740
  const args = this.value(step.args);
7975
- for (let draw = 0; draw < (step.draws ?? 0); draw++) this.deps.engine.random.next();
9741
+ for (let draw2 = 0; draw2 < (step.draws ?? 0); draw2++) this.deps.engine.random.next();
7976
9742
  this.deps.onCue?.({ cue: step.cue, args, time: this.time });
7977
9743
  }
7978
9744
  scheduleSteps(step) {
@@ -9023,9 +10789,9 @@ var GameTestRoom = (() => {
9023
10789
  this.color[i3] = lerp4(this.rgbStart[i3], this.rgbEnd[i3], t);
9024
10790
  this.color[i3 + 1] = lerp4(this.rgbStart[i3 + 1], this.rgbEnd[i3 + 1], t);
9025
10791
  this.color[i3 + 2] = lerp4(this.rgbStart[i3 + 2], this.rgbEnd[i3 + 2], t);
9026
- let envelope = t < def.fadeIn ? t / def.fadeIn : 1 - (t - def.fadeIn) / (1 - def.fadeIn);
9027
- if (def.flicker) envelope *= 1 - def.flicker * (0.5 + 0.5 * Math.sin(age * 38 + this.seed[i] * 50));
9028
- this.alpha[i] = clamp4(envelope, 0, 1) * def.alpha * this.alphaMul[i];
10792
+ let envelope2 = t < def.fadeIn ? t / def.fadeIn : 1 - (t - def.fadeIn) / (1 - def.fadeIn);
10793
+ if (def.flicker) envelope2 *= 1 - def.flicker * (0.5 + 0.5 * Math.sin(age * 38 + this.seed[i] * 50));
10794
+ this.alpha[i] = clamp4(envelope2, 0, 1) * def.alpha * this.alphaMul[i];
9029
10795
  top = i + 1;
9030
10796
  }
9031
10797
  this.high = top;
@@ -9730,10 +11496,10 @@ var GameTestRoom = (() => {
9730
11496
  this.velocity[offset + 1] = this.direction.y * speed + (options.v ? options.v[1] : 0);
9731
11497
  this.velocity[offset + 2] = this.direction.z * speed + (options.v ? options.v[2] : 0);
9732
11498
  const start = hexToRgb(options.c0 ?? 16777215);
9733
- const end = options.c1 != null ? hexToRgb(options.c1) : start;
11499
+ const end3 = options.c1 != null ? hexToRgb(options.c1) : start;
9734
11500
  for (let channel = 0; channel < FLOATS_PER_VECTOR; channel++) {
9735
11501
  this.colorStart[offset + channel] = start[channel];
9736
- this.colorEnd[offset + channel] = end[channel];
11502
+ this.colorEnd[offset + channel] = end3[channel];
9737
11503
  }
9738
11504
  this.life[index] = this.maxLife[index] = drawRange(random, options.life || 1);
9739
11505
  this.startSize[index] = drawRange(random, options.size || 0.1);
@@ -9868,10 +11634,10 @@ var GameTestRoom = (() => {
9868
11634
  this.velocity[offset + 1] = this.direction.y * speed + (options.v ? options.v[1] : 0);
9869
11635
  this.velocity[offset + 2] = this.direction.z * speed + (options.v ? options.v[2] : 0);
9870
11636
  const start = hexToRgb(options.c0 ?? 16765056);
9871
- const end = options.c1 != null ? hexToRgb(options.c1) : start;
11637
+ const end3 = options.c1 != null ? hexToRgb(options.c1) : start;
9872
11638
  for (let channel = 0; channel < FLOATS_PER_VECTOR2; channel++) {
9873
11639
  this.colorStart[offset + channel] = start[channel];
9874
- this.colorEnd[offset + channel] = end[channel];
11640
+ this.colorEnd[offset + channel] = end3[channel];
9875
11641
  }
9876
11642
  this.life[index] = this.maxLife[index] = drawRange(random, options.life || 0.4);
9877
11643
  this.length[index] = options.len || 0.02;
@@ -10255,7 +12021,7 @@ var GameTestRoom = (() => {
10255
12021
  function streamOf(seed) {
10256
12022
  if (randomMode === "shared") return sharedRandom;
10257
12023
  handles.events++;
10258
- return streamFor(seed ?? Math.imul(handles.events, AUTO_SEED) + (options.seed ?? 0));
12024
+ return streamFor2(seed ?? Math.imul(handles.events, AUTO_SEED) + (options.seed ?? 0));
10259
12025
  }
10260
12026
  function detonate(def, point, call3) {
10261
12027
  composeExplosion(core, def, { x: point[0], y: point[1], z: point[2], direction: call3.direction, random: call3.random, handle: call3.handle });
@@ -10834,12 +12600,12 @@ void main(){
10834
12600
  if (typeof document === "undefined") return blankCanvas();
10835
12601
  return document.createElement("canvas");
10836
12602
  }
10837
- function paint(createCanvas, draw) {
12603
+ function paint(createCanvas, draw2) {
10838
12604
  const canvas = createCanvas();
10839
12605
  canvas.width = TEXTURE_SIZE;
10840
12606
  canvas.height = TEXTURE_SIZE;
10841
12607
  const context = canvas.getContext("2d");
10842
- if (context) draw(context, TEXTURE_SIZE);
12608
+ if (context) draw2(context, TEXTURE_SIZE);
10843
12609
  return canvas;
10844
12610
  }
10845
12611
  function drawScorch(context, size, random) {
@@ -11697,7 +13463,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11697
13463
  var __defProp3 = Object.defineProperty;
11698
13464
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
11699
13465
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
11700
- function createRng2(seed) {
13466
+ function createRng3(seed) {
11701
13467
  let a = seed >>> 0;
11702
13468
  return () => {
11703
13469
  a = a + 1831565813 >>> 0;
@@ -11745,7 +13511,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11745
13511
  __publicField2(this, "m");
11746
13512
  __publicField2(this, "v");
11747
13513
  __publicField2(this, "w");
11748
- __publicField2(this, "rng", createRng2(1374496523));
13514
+ __publicField2(this, "rng", createRng3(1374496523));
11749
13515
  __publicField2(this, "markGeometry", null);
11750
13516
  __publicField2(this, "markMaterial", null);
11751
13517
  this.box = new THREE.BoxGeometry(1, 1, 1);
@@ -11776,7 +13542,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11776
13542
  const idx = seed % 6;
11777
13543
  let g = this.shardGeos[idx];
11778
13544
  if (!g) {
11779
- const r = createRng2(1e3 + idx);
13545
+ const r = createRng3(1e3 + idx);
11780
13546
  const n = 3 + Math.floor(r() * 3);
11781
13547
  const shape = new this.THREE.Shape();
11782
13548
  const pts = [];
@@ -11981,9 +13747,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11981
13747
  };
11982
13748
  var MAX_RADIUS = 1.2;
11983
13749
  var GroundSpots = class {
11984
- constructor(THREE, scene, groundAt, cap) {
13750
+ constructor(THREE, scene2, groundAt, cap) {
11985
13751
  __publicField2(this, "THREE", THREE);
11986
- __publicField2(this, "scene", scene);
13752
+ __publicField2(this, "scene", scene2);
11987
13753
  __publicField2(this, "groundAt", groundAt);
11988
13754
  __publicField2(this, "cap", cap);
11989
13755
  __publicField2(this, "spots", []);
@@ -12381,9 +14147,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12381
14147
  }
12382
14148
  };
12383
14149
  var RebarCages = class {
12384
- constructor(THREE, scene, radius = 4e-3) {
14150
+ constructor(THREE, scene2, radius = 4e-3) {
12385
14151
  __publicField2(this, "THREE", THREE);
12386
- __publicField2(this, "scene", scene);
14152
+ __publicField2(this, "scene", scene2);
12387
14153
  __publicField2(this, "cages", /* @__PURE__ */ new Map());
12388
14154
  __publicField2(this, "geometry");
12389
14155
  __publicField2(this, "material");
@@ -12990,7 +14756,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12990
14756
  /** A star of jagged lines: `rays` rays up to `length` metres, seeded so the same event always draws the same star. */
12991
14757
  addCrack(mv, worldPoint, worldNormal, rays, length3, color, seed) {
12992
14758
  const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 2e-3);
12993
- const rnd2 = createRng2(seed);
14759
+ const rnd2 = createRng3(seed);
12994
14760
  const verts = [];
12995
14761
  const n = clamp6(Math.round(rays), 3, 16);
12996
14762
  const L = Math.max(0.02, length3) / s;
@@ -13191,10 +14957,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13191
14957
  const sy = lerp5(1, 0.25, melt);
13192
14958
  const sxz = 1 + 0.18 * melt;
13193
14959
  for (let i = range2[0] * 3, v = 0; i < range2[1] * 3; i += 3, v++) {
13194
- const noise = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
14960
+ const noise2 = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
13195
14961
  arr[i] = cx + (work[i] - cx) * sxz;
13196
14962
  arr[i + 2] = cz + (work[i + 2] - cz) * sxz;
13197
- arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise * melt * h * 0.04);
14963
+ arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise2 * melt * h * 0.04);
13198
14964
  }
13199
14965
  }
13200
14966
  /** The colour attribute of the host's current geometry (created white when there is none) with vertex colours switched on in its material. */
@@ -13517,10 +15283,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13517
15283
  __publicField2(this, "cages");
13518
15284
  __publicField2(this, "active", /* @__PURE__ */ new Map());
13519
15285
  __publicField2(this, "fires", /* @__PURE__ */ new Map());
15286
+ __publicField2(this, "primedBurning", /* @__PURE__ */ new Set());
13520
15287
  __publicField2(this, "leaks", []);
13521
15288
  __publicField2(this, "homeTags", []);
13522
15289
  __publicField2(this, "throttles", /* @__PURE__ */ new Map());
13523
- __publicField2(this, "rng", createRng2(12648430));
15290
+ __publicField2(this, "rng", createRng3(12648430));
13524
15291
  __publicField2(this, "vfx");
13525
15292
  __publicField2(this, "owner", `destruction-${++rendererCount}`);
13526
15293
  __publicField2(this, "stopLanding");
@@ -13856,8 +15623,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13856
15623
  case "primed":
13857
15624
  if (ev.trigger !== "explosion") {
13858
15625
  this.vfx.fire({ point: ev.point, radius: PRIMED_FIRE_RADIUS, life: ev.delay });
15626
+ this.primedBurning.add(`${ev.object}|${ev.part}`);
13859
15627
  this.activate(ev.object, ev.part);
13860
15628
  this.sound("ignite", ev.point, 0.7);
15629
+ this.sound("burn_loop", ev.point, 0.5);
13861
15630
  }
13862
15631
  break;
13863
15632
  case "spall":
@@ -13875,16 +15644,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13875
15644
  break;
13876
15645
  case "extinguish": {
13877
15646
  const f = this.fires.get(`${ev.object}|${ev.part}`);
15647
+ const primed = this.primedBurning.delete(`${ev.object}|${ev.part}`);
13878
15648
  if (f) {
13879
15649
  this.stopFire(`${ev.object}|${ev.part}`);
13880
15650
  this.vfx.burst("steam", f.point, [0, 1, 0], 14, 1.2);
13881
15651
  }
15652
+ if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5);
13882
15653
  this.activate(ev.object, ev.part);
13883
15654
  this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
13884
15655
  break;
13885
15656
  }
13886
15657
  case "burnout": {
13887
15658
  this.stopFire(`${ev.object}|${ev.part}`);
15659
+ this.primedBurning.delete(`${ev.object}|${ev.part}`);
15660
+ this.sound("burn_end", ev.point, 0.5);
13888
15661
  this.vfx.burst("ash", ev.point, [0, 1, 0], 16, 1);
13889
15662
  this.vfx.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
13890
15663
  const pv = this.partVis(ev.object, ev.part);
@@ -13926,6 +15699,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13926
15699
  break;
13927
15700
  }
13928
15701
  case "explode":
15702
+ this.primedBurning.delete(`${ev.object}|${ev.part}`);
13929
15703
  this.onExplode(ev);
13930
15704
  break;
13931
15705
  case "destroyed":
@@ -14009,7 +15783,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14009
15783
  if (!b || !pv) return;
14010
15784
  const def = this.world.materials.get(pv.material);
14011
15785
  const n = this.worldDir(b, ev.hole.normal);
14012
- const draw = (point, normal) => {
15786
+ const draw2 = (point, normal) => {
14013
15787
  const wp = new this.THREE.Vector3(point[0], point[1], point[2]);
14014
15788
  const mv = this.nearestMesh(pv, wp);
14015
15789
  if (!mv) return;
@@ -14020,8 +15794,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14020
15794
  this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
14021
15795
  }
14022
15796
  };
14023
- draw(ev.worldPoint, n);
14024
- if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
15797
+ draw2(ev.worldPoint, n);
15798
+ if (ev.hole.through && ev.exitPoint) draw2(ev.exitPoint, n.clone().negate());
14025
15799
  if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
14026
15800
  this.sound(impactKey(def), ev.worldPoint, 0.6);
14027
15801
  }
@@ -14219,6 +15993,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14219
15993
  if (options.visual !== false) this.playExplosion(ev, options);
14220
15994
  this.vfx.push(p, ev.radius, 4);
14221
15995
  const c = this.v1;
15996
+ const report = options.report;
15997
+ const confinement = report?.find((r) => r.blast !== void 0)?.blast?.confinement ?? 1;
15998
+ const energy = ev.energy * confinement;
14222
15999
  for (const pc of this.pieceSys.list) {
14223
16000
  this.pieceSys.centerOf(pc, c);
14224
16001
  const dx = c.x - p[0];
@@ -14227,13 +16004,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14227
16004
  const d = Math.hypot(dx, dy, dz);
14228
16005
  if (d > ev.radius) continue;
14229
16006
  const fall = (1 - d / ev.radius) ** 2;
14230
- const speed = Math.min(30, Math.sqrt(2 * ev.energy * 0.02 / pc.mass)) * fall;
16007
+ const reach = report?.find((r) => r.object === pc.objectId)?.blast?.shelter ?? 1;
16008
+ const speed = Math.min(30, Math.sqrt(2 * energy * reach * 0.02 / pc.mass)) * fall;
14231
16009
  const inv = d > 1e-4 ? 1 / d : 0;
14232
16010
  const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
14233
16011
  const spread2 = pc.halfExtents[0];
14234
16012
  this.pieceSys.impulse(pc, [dir[0] * speed * pc.mass, dir[1] * speed * pc.mass, dir[2] * speed * pc.mass], [c.x + (this.rng() - 0.5) * spread2, c.y + spread2 * 0.5, c.z + (this.rng() - 0.5) * spread2]);
14235
16013
  }
14236
- this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(ev.energy) * 0.08));
16014
+ this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(energy) * 0.08));
14237
16015
  this.sound("explode", p, clamp6(0.5 + ev.radius / 10, 0.5, 1));
14238
16016
  }
14239
16017
  playExplosion(ev, options) {
@@ -14416,6 +16194,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14416
16194
  this.fragments.clear();
14417
16195
  this.ground.clear();
14418
16196
  this.cages.clear();
16197
+ this.primedBurning.clear();
14419
16198
  this.vfx.sprites().clear();
14420
16199
  this.ownVfx?.world.clear();
14421
16200
  }
@@ -14444,12 +16223,197 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14444
16223
  this.disposed = true;
14445
16224
  }
14446
16225
  };
16226
+ var ATTACK = 2e-3;
16227
+ var lengthOf = (d, attack) => {
16228
+ if (typeof d === "number") return attack + d;
16229
+ if (Array.isArray(d)) return [attack + d[0], attack + d[1]];
16230
+ return { ...d, add: (d.add ?? 0) + attack };
16231
+ };
16232
+ var envelope = (attack) => [
16233
+ [0, 1e-4, "set", "fixed"],
16234
+ [attack, 1, "exp"],
16235
+ ["1d", 1e-4, "exp", "fixed"]
16236
+ ];
16237
+ function noise(o) {
16238
+ const a = o.a ?? ATTACK;
16239
+ const over = typeof o.d === "number" ? o.d : 0;
16240
+ return {
16241
+ filters: {
16242
+ mode: "serial",
16243
+ bank: [{ type: o.type ?? "lowpass", frequency: o.f2 === void 0 ? { set: o.f } : { set: o.f, to: o.f2, curve: "exp", over }, q: o.q ?? 0.7 }]
16244
+ },
16245
+ duration: lengthOf(o.d, a),
16246
+ gain: o.vol,
16247
+ ...o.delay === void 0 ? {} : { at: o.delay },
16248
+ ...o.wet === void 0 ? {} : { wet: o.wet },
16249
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
16250
+ envelope: envelope(a)
16251
+ };
16252
+ }
16253
+ function tone(o) {
16254
+ return {
16255
+ source: { kind: "osc", wave: o.wave ?? "sine", frequency: { from: o.f, to: o.f2 ?? "from", curve: "exp", over: "duration" } },
16256
+ duration: lengthOf(o.d, ATTACK),
16257
+ gain: o.vol,
16258
+ ...o.delay === void 0 ? {} : { at: o.delay },
16259
+ ...o.wet === void 0 ? {} : { wet: o.wet },
16260
+ envelope: envelope(ATTACK)
16261
+ };
16262
+ }
16263
+ var many = (count, make) => Array.from({ length: count }, make);
16264
+ var scene = (id, layers, line, vars) => ({
16265
+ id,
16266
+ bus: "world",
16267
+ pan: true,
16268
+ layers,
16269
+ falloff: DISTANCE_CURVES.art_scene,
16270
+ ...vars ? { vars } : {},
16271
+ sources: [`cena:${line}`]
16272
+ });
16273
+ var boomLayers = (big) => [
16274
+ noise({ f: 1200, f2: 120, d: 1.8, vol: 2, wet: 1.2 }),
16275
+ tone({ f: 65, f2: 24, d: 1, vol: 1.8 }),
16276
+ noise({ type: "highpass", f: 1500, d: 0.2, vol: 0.8 }),
16277
+ ...big ? [noise({ f: 500, f2: 90, a: 0.2, d: 2.4, vol: 1.2, wet: 1, delay: 0.15 })] : [],
16278
+ ...many(8, () => tone({ f: [2600, 5600], d: 0.05, vol: 0.08, delay: [0.4, 1.6] }))
16279
+ ];
16280
+ var SCENE_SOUNDS = [
16281
+ scene(
16282
+ "glass_shatter",
16283
+ [noise({ type: "highpass", f: 2800, d: 0.45, vol: 0.9, wet: 0.5 }), ...many(12, () => tone({ f: [3e3, 7500], d: [0.06, 0.14], vol: 0.18, delay: [0, 0.55] }))],
16284
+ "L1359 glass"
16285
+ ),
16286
+ scene(
16287
+ "wood_hit",
16288
+ [noise({ type: "bandpass", f: 850, q: 1, d: 0.25, vol: 0.9, wet: 0.3 }), tone({ f: 210, f2: 90, d: 0.1, vol: 0.6 }), noise({ type: "highpass", f: 2e3, d: 0.04, vol: 0.5 })],
16289
+ "L1360 wood"
16290
+ ),
16291
+ scene("thunk", [noise({ type: "bandpass", f: 700, q: 2, d: 0.07, vol: 0.4 })], "L1361 thunk"),
16292
+ scene("ping", [tone({ f: [2600, 3500], f2: 1400, d: 0.14, vol: 0.18, wet: 0.3 })], "L1362 ping"),
16293
+ scene("clang", [tone({ f: [520, 720], f2: 480, d: 0.5, vol: 0.35, wet: 0.4 }), tone({ f: 1370, f2: 1300, d: 0.3, vol: 0.15 }), noise({ type: "highpass", f: 2e3, d: 0.05, vol: 0.5 })], "L1363 clang"),
16294
+ scene("chip", [noise({ type: "bandpass", f: 1600, q: 1.2, d: 0.06, vol: 0.5 })], "L1364 chip"),
16295
+ scene(
16296
+ "crumble",
16297
+ [noise({ f: 900, f2: 200, d: 0.6, vol: 0.9, wet: 0.3 }), ...many(5, () => noise({ type: "bandpass", f: [500, 1400], q: 2, d: 0.05, vol: 0.4, delay: [0.1, 0.5] }))],
16298
+ "L1365 crumble"
16299
+ ),
16300
+ // `thud(x, z, v)`: `v` is the `scale` param (default 1)
16301
+ scene("thud", [tone({ f: 75, f2: 38, d: 0.22, vol: { param: "scale", scale: 0.6 } }), noise({ f: 420, d: 0.16, vol: { param: "scale", scale: 0.45 } })], "L1358 thud", { scale: 1 }),
16302
+ // `hiss(x, z, d)`: `seconds` is the param `d` (default 1.2)
16303
+ scene("hiss", [noise({ type: "highpass", f: 3500, a: 0.05, d: { param: "seconds" }, vol: 0.35 })], "L1366 hiss", { seconds: 1.2 }),
16304
+ scene("boom", boomLayers(false), "L1367 boom"),
16305
+ scene("boom_big", boomLayers(true), "L1367 boom (big)"),
16306
+ scene("burst", [noise({ f: 700, f2: 120, d: 0.7, vol: 1.4, wet: 0.5 }), noise({ type: "highpass", f: 3e3, a: 0.05, d: 1.2, vol: 0.4, delay: 0.1 })], "L1357 burst"),
16307
+ scene("crack", [noise({ type: "highpass", f: 1800, d: 0.05, vol: 0.8 }), tone({ f: 420, f2: 180, d: 0.06, vol: 0.4 })], "L1354 crack")
16308
+ ];
16309
+ var EXPLOSION_BOOM = {
16310
+ id: "explosion_boom",
16311
+ bus: "world",
16312
+ order: "boom",
16313
+ duration: 1.6,
16314
+ tail: 0.1,
16315
+ layers: [
16316
+ {
16317
+ source: { kind: "osc", wave: "sine", frequency: { from: 70, to: 28, curve: "exp", over: 1.2 } },
16318
+ envelope: [
16319
+ [0, 1, "set"],
16320
+ ["1d", 1e-3, "exp", "fixed"]
16321
+ ]
16322
+ }
16323
+ ],
16324
+ falloff: DISTANCE_CURVES.game_boom,
16325
+ sources: ["jogo:L68278-68285 boom"]
16326
+ };
16327
+ var BURN_LOOP = {
16328
+ id: "burn_loop",
16329
+ bus: "world",
16330
+ order: "chain_loop",
16331
+ source: { kind: "noise", loop: true },
16332
+ filters: { mode: "serial", bank: [{ type: "bandpass", frequency: { set: 1800 }, q: 0.6 }] },
16333
+ volume: 0,
16334
+ sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
16335
+ };
16336
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
16337
+ var DESTRUCTION_SOUND_KEYS = {
16338
+ art_scene: {
16339
+ impact_metal: { sound: "clang" },
16340
+ impact_wood: { sound: "wood_hit" },
16341
+ impact_glass: { sound: "ping" },
16342
+ impact_masonry: { sound: "chip" },
16343
+ impact_soft: { sound: "thud" },
16344
+ shatter_glass: { sound: "glass_shatter" },
16345
+ crumble: { sound: "crumble" },
16346
+ splinter: { sound: "crack" },
16347
+ ricochet: { sound: "ping" },
16348
+ unscrew: { sound: "chip" },
16349
+ detach: { sound: "thunk" },
16350
+ ignite: { sound: "hiss", params: { seconds: 0.5 } },
16351
+ extinguish: { sound: "hiss" },
16352
+ melt_sizzle: { sound: "hiss", params: { seconds: 0.6 } },
16353
+ explode: { sound: "boom", above: { volume: 0.9, sound: "boom_big" } }
16354
+ },
16355
+ game: {
16356
+ impact_metal: { sound: "sfx_impact" },
16357
+ impact_wood: { sound: "sfx_wood" },
16358
+ impact_glass: { sound: "sfx_glass" },
16359
+ impact_masonry: { sound: "sfx_impact" },
16360
+ impact_soft: { sound: "sfx_flesh" },
16361
+ shatter_glass: { sound: "sfx_glass" },
16362
+ crumble: { sound: "sfx_impact" },
16363
+ splinter: { sound: "sfx_wood" },
16364
+ explode: { sound: "explosion_boom" }
16365
+ }
16366
+ };
16367
+ var DEFAULT_FIRE = { perFire: 0.05, max: 0.2 };
16368
+ function createDestructionSound(engine, options = {}) {
16369
+ engine.register(DESTRUCTION_SOUND_BANK);
16370
+ engine.register(GAME_SFX_BANK);
16371
+ const fire = { ...DEFAULT_FIRE, ...options.fire };
16372
+ const rules = { ...DESTRUCTION_SOUND_KEYS.art_scene, ...options.style === "game" ? DESTRUCTION_SOUND_KEYS.game : {}, ...options.keys };
16373
+ let loop;
16374
+ let burning = 0;
16375
+ const setFire = () => {
16376
+ if (options.burnLoop === false) return;
16377
+ if (burning <= 0) {
16378
+ loop?.stop();
16379
+ loop = void 0;
16380
+ return;
16381
+ }
16382
+ loop ?? (loop = engine.loop("burn_loop"));
16383
+ loop.set({ volume: Math.min(fire.max, fire.perFire * burning) });
16384
+ };
16385
+ return {
16386
+ onSound(key, point, volume) {
16387
+ if (key === "burn_loop") {
16388
+ burning += 1;
16389
+ setFire();
16390
+ return;
16391
+ }
16392
+ if (key === "burn_end") {
16393
+ if (burning > 0) burning -= 1;
16394
+ setFire();
16395
+ return;
16396
+ }
16397
+ const rule = rules[key];
16398
+ if (!rule) return;
16399
+ const sound = rule.above && volume >= rule.above.volume ? rule.above.sound : rule.sound;
16400
+ engine.play(sound, { position: point, volume: volume * (rule.volumeScale ?? 1), ...rule.params ? { params: rule.params } : {} });
16401
+ },
16402
+ rule: (key) => rules[key] ?? void 0,
16403
+ burning: () => burning,
16404
+ dispose() {
16405
+ loop?.stop();
16406
+ loop = void 0;
16407
+ burning = 0;
16408
+ }
16409
+ };
16410
+ }
14447
16411
 
14448
16412
  // ../game-weapons/dist/index.js
14449
16413
  var __defProp4 = Object.defineProperty;
14450
16414
  var __defNormalProp3 = (obj, key, value) => key in obj ? __defProp4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
14451
16415
  var __publicField3 = (obj, key, value) => __defNormalProp3(obj, typeof key !== "symbol" ? key + "" : key, value);
14452
- var createRng3 = (seed) => {
16416
+ var createRng4 = (seed) => {
14453
16417
  let a = seed >>> 0;
14454
16418
  return () => {
14455
16419
  a = a + 1831565813 >>> 0;
@@ -26598,9 +28562,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
26598
28562
  this.caliber = init.caliber;
26599
28563
  this.action = init.def.action;
26600
28564
  this.rules = init.rules;
26601
- this.rng = createRng3(init.seed);
26602
- this.faultRng = createRng3((init.seed ^ FAULT_RNG_SALT) >>> 0);
26603
- this.fuseRng = createRng3((init.seed ^ FUSE_RNG_SALT) >>> 0);
28565
+ this.rng = createRng4(init.seed);
28566
+ this.faultRng = createRng4((init.seed ^ FAULT_RNG_SALT) >>> 0);
28567
+ this.fuseRng = createRng4((init.seed ^ FUSE_RNG_SALT) >>> 0);
26604
28568
  this.explosive = init.explosive ?? null;
26605
28569
  this._safety = init.rules.startSafe;
26606
28570
  this._condition = findById(init.rules.conditions, init.condition ?? init.rules.defaultCondition, "condition");
@@ -26862,7 +28826,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
26862
28826
  this.isCompatible = init.isCompatible;
26863
28827
  this.makeUnderbarrel = init.makeUnderbarrel;
26864
28828
  this.seed = init.seed;
26865
- this.breakRng = createRng3((init.seed ^ BREAK_RNG_SALT) >>> 0);
28829
+ this.breakRng = createRng4((init.seed ^ BREAK_RNG_SALT) >>> 0);
26866
28830
  this.fitted = (init.snapshot?.attachments ?? init.def.fitted ?? []).map(copyOf);
26867
28831
  if (init.snapshot) this.stockState = init.snapshot.stock;
26868
28832
  this.fitted = this.fitted.map((part) => part.shotsLeft === void 0 ? this.withDurability(part) : part);
@@ -27121,10 +29085,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27121
29085
  __publicField3(this, "lastRoundAt", Number.NEGATIVE_INFINITY);
27122
29086
  }
27123
29087
  /** When the barrels of a rotary gun are at speed: `upSeconds` after the pull began, and at once while they still spin from the last round. */
27124
- spunUpAt(spin, start, end) {
29088
+ spunUpAt(spin, start, end3) {
27125
29089
  if (!spin || start - this.lastRoundAt <= spin.holdSeconds + EPS3) return 0;
27126
29090
  if (this.spinSince === void 0 || start - this.spinFrameEnd > SPIN_PULL_GAP) this.spinSince = start;
27127
- this.spinFrameEnd = end;
29091
+ this.spinFrameEnd = end3;
27128
29092
  return this.spinSince + spin.upSeconds;
27129
29093
  }
27130
29094
  oncePerPull(held) {
@@ -27133,7 +29097,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27133
29097
  this.latched = true;
27134
29098
  return true;
27135
29099
  }
27136
- fireFirearm(a, origin, dir, start, end, held, source) {
29100
+ fireFirearm(a, origin, dir, start, end3, held, source) {
27137
29101
  if (this._reloading) {
27138
29102
  if (this.loadStep !== null && this._ammo > 0) this.stopReload(start);
27139
29103
  else return result("reloading");
@@ -27143,10 +29107,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27143
29107
  this.emit({ type: "empty", at: start });
27144
29108
  return result("empty");
27145
29109
  }
27146
- const t0 = Math.max(start, this.readyAt, this.spunUpAt(a.spin, start, end));
27147
- if (t0 > end + EPS3) return result("cooldown");
29110
+ const t0 = Math.max(start, this.readyAt, this.spunUpAt(a.spin, start, end3));
29111
+ if (t0 > end3 + EPS3) return result("cooldown");
27148
29112
  const res = result("ok");
27149
- const times = planRounds(a, this.mode, this._ammo, t0, end);
29113
+ const times = planRounds(a, this.mode, this._ammo, t0, end3);
27150
29114
  let lastAt = t0;
27151
29115
  for (const t of times) {
27152
29116
  const fault = rollFault({ def: this.def, caliber: this.caliber, condition: this._condition, ammoQuality: this._ammoQuality, heat: this._heat, rules: this.faultRules(), random: this.faultRng });
@@ -27262,7 +29226,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27262
29226
  this.pend = { src: "trigger", amt: d };
27263
29227
  }
27264
29228
  const start = this.clock - overlap;
27265
- const end = start + d;
29229
+ const end3 = start + d;
27266
29230
  this.settle(start);
27267
29231
  const dir = normalize2(direction);
27268
29232
  let res;
@@ -27273,10 +29237,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27273
29237
  else if (launcher) res = launcher.trigger(origin, dir, d, held, source);
27274
29238
  else switch (a.kind) {
27275
29239
  case "firearm":
27276
- res = this.fireFirearm(a, origin, dir, start, end, held, source);
29240
+ res = this.fireFirearm(a, origin, dir, start, end3, held, source);
27277
29241
  break;
27278
29242
  case "melee":
27279
- res = this.fireMelee(a, origin, dir, start, end);
29243
+ res = this.fireMelee(a, origin, dir, start, end3);
27280
29244
  break;
27281
29245
  case "flame":
27282
29246
  case "heat":
@@ -27286,7 +29250,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27286
29250
  res = this.fireExtinguisher(a, origin, dir, d);
27287
29251
  break;
27288
29252
  case "throwable":
27289
- res = this.fireThrowable(a, origin, dir, held, start, end);
29253
+ res = this.fireThrowable(a, origin, dir, held, start, end3);
27290
29254
  break;
27291
29255
  case "launcher":
27292
29256
  res = this.fireLauncher(a, origin, dir, start, held);
@@ -27298,7 +29262,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27298
29262
  res = this.oncePerPull(held) ? result("ok", { uses: [{ kind: "hand", reach: a.reach, origin, direction: dir }] }) : result("cooldown");
27299
29263
  break;
27300
29264
  }
27301
- this.advance(Math.max(0, end - this.clock));
29265
+ this.advance(Math.max(0, end3 - this.clock));
27302
29266
  this.settle(this.clock);
27303
29267
  return res;
27304
29268
  }
@@ -27330,9 +29294,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27330
29294
  this.emit({ type: "shot", shots: 0, rounds: 1, mode: this.mode, at: t });
27331
29295
  return result("ok", { uses: launcherUses(a, this.explosive, origin, dir), recoil: [a.recoil[0], a.recoil[1]], noise: a.noise ?? 0, rounds: 1, roundTimes: [t - start] });
27332
29296
  }
27333
- fireMelee(a, origin, dir, start, end) {
29297
+ fireMelee(a, origin, dir, start, end3) {
27334
29298
  const t0 = Math.max(start, this.readyAt);
27335
- if (t0 > end + EPS3) return result("cooldown");
29299
+ if (t0 > end3 + EPS3) return result("cooldown");
27336
29300
  this.readyAt = t0 + 60 / a.rpm;
27337
29301
  const use = { kind: "melee", damage: a.damage, energy: a.energy, radius: a.radius, reach: a.reach, origin, direction: dir };
27338
29302
  return result("ok", { uses: [use], noise: a.noise });
@@ -27366,8 +29330,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27366
29330
  return result("ok", { uses: [throwableUse({ action: a, explosive: this.explosive, origin, direction: dir, fuse, rules: this.rules.explosives, ...extra })], noise: a.noise });
27367
29331
  }
27368
29332
  /** A timer throwable is cooked from the pull; impact, remote and wire ones (and a long fuse that sets down) act at once. */
27369
- fireThrowable(a, origin, dir, held, start, end) {
27370
- if (this.cook) return this.cookStep(a, origin, dir, end);
29333
+ fireThrowable(a, origin, dir, held, start, end3) {
29334
+ if (this.cook) return this.cookStep(a, origin, dir, end3);
27371
29335
  if (!this.oncePerPull(held)) return result("cooldown");
27372
29336
  if (this._ammo <= 0) return result("empty");
27373
29337
  const fuse = this.sampleThrowFuse(a);
@@ -27377,7 +29341,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27377
29341
  }
27378
29342
  this.cook = { start, fuse, origin, direction: dir };
27379
29343
  this.emit({ type: "cook", phase: "start", fuse, at: start });
27380
- return this.cookStep(a, origin, dir, end);
29344
+ return this.cookStep(a, origin, dir, end3);
27381
29345
  }
27382
29346
  /** The trigger stays down on a cooked throwable: nothing happens until the fuse runs out in the hand. */
27383
29347
  cookStep(a, origin, dir, now) {
@@ -29734,17 +31698,17 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
29734
31698
  }
29735
31699
  }
29736
31700
  const start = time;
29737
- const end = Math.min(def.duration, time + Math.max(0, dt));
31701
+ const end3 = Math.min(def.duration, time + Math.max(0, dt));
29738
31702
  for (let i = 0; i < keys.length && !done; i++) {
29739
31703
  const key = keys[i];
29740
31704
  const spanStart = key.t;
29741
31705
  const spanEnd = i + 1 < keys.length ? keys[i + 1].t : def.duration;
29742
- if ((key.action === "open" || key.action === "close") && !fired.has(i) && end >= spanStart) {
31706
+ if ((key.action === "open" || key.action === "close") && !fired.has(i) && end3 >= spanStart) {
29743
31707
  fired.add(i);
29744
31708
  if (key.action === "open") world.open(containerId);
29745
31709
  else world.close(containerId);
29746
31710
  }
29747
- const overlap = Math.min(end, spanEnd) - Math.max(start, spanStart);
31711
+ const overlap = Math.min(end3, spanEnd) - Math.max(start, spanStart);
29748
31712
  if (overlap <= 0) continue;
29749
31713
  if (key.action === "drink") {
29750
31714
  const ev = world.drink(containerId, (key.rate ?? DEFAULT_DRINK_RATE) * overlap);
@@ -29758,7 +31722,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
29758
31722
  }
29759
31723
  }
29760
31724
  if (!done) {
29761
- time = end;
31725
+ time = end3;
29762
31726
  done = time >= def.duration;
29763
31727
  }
29764
31728
  follow(dt);
@@ -30360,8 +32324,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
30360
32324
  bedHeight = roadY * 1e3 + z * 14 - (x - params.roadSlopeAnchorX) * 5;
30361
32325
  roughness = 0.55;
30362
32326
  }
30363
- const noise = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
30364
- bed[index] = bedHeight + (noise - 0.5) * roughness * 2;
32327
+ const noise2 = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
32328
+ bed[index] = bedHeight + (noise2 - 0.5) * roughness * 2;
30365
32329
  vertical[index] = region === 0 || region === 2 ? 0.95 : 1;
30366
32330
  pin[index] = 0.62 + 0.76 * (valueNoise(column * 0.21 + 40, row * 0.21) * 0.5 + hashCell(column, row) * 0.5) * (region === 3 ? 1.25 : 1);
30367
32331
  }
@@ -30518,8 +32482,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
30518
32482
  for (let column = 0; column < columns; column++) {
30519
32483
  const index = (region.firstRow + row) * columns + column;
30520
32484
  const centre = add2(add2(region.origin, scale(columnAxis, (column + 0.5) * cell)), scale(region.rowAxis, (row + 0.5) * cell));
30521
- const noise = valueNoise(column * 0.35, (region.firstRow + row) * 0.35) * 0.6 + valueNoise(column * 1.3, (region.firstRow + row) * 1.3) * 0.4;
30522
- bed[index] = centre[1] * 1e3 + (noise - 0.5) * strip.roughness * 2;
32485
+ const noise2 = valueNoise(column * 0.35, (region.firstRow + row) * 0.35) * 0.6 + valueNoise(column * 1.3, (region.firstRow + row) * 1.3) * 0.4;
32486
+ bed[index] = centre[1] * 1e3 + (noise2 - 0.5) * strip.roughness * 2;
30523
32487
  vertical[index] = region.isVertical ? 0.95 : 1;
30524
32488
  pin[index] = 0.62 + 0.76 * (valueNoise(column * 0.21 + 40, (region.firstRow + row) * 0.21) * 0.5 + valueNoise(column * 0.9, row * 0.9) * 0.5);
30525
32489
  }
@@ -31432,8 +33396,8 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
31432
33396
  const b2x = ty * b1z - tz * b1y;
31433
33397
  const b2y = tz * b1x - tx * b1z;
31434
33398
  const b2z = tx * b1y - ty * b1x;
31435
- const end = Math.min(1, 0.35 + Math.min(k, 1 - k) * 8);
31436
- const r = this.radiusAt(state, gravity, t, r0, speed0) * end;
33399
+ const end3 = Math.min(1, 0.35 + Math.min(k, 1 - k) * 8);
33400
+ const r = this.radiusAt(state, gravity, t, r0, speed0) * end3;
31437
33401
  for (let j = 0; j < sides; j++) {
31438
33402
  const a = j / sides * Math.PI * 2;
31439
33403
  const c = Math.cos(a);
@@ -31467,15 +33431,15 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
31467
33431
  };
31468
33432
  var rendererCount2 = 0;
31469
33433
  function createFluidRenderer(THREE, options) {
31470
- const { scene, fluids } = options;
33434
+ const { scene: scene2, fluids } = options;
31471
33435
  const quality = options.quality ?? "medium";
31472
33436
  const profile = profileOf(quality);
31473
33437
  const materials = new MaterialCache(THREE, options.envMap ?? null);
31474
33438
  const root = new THREE.Group();
31475
33439
  root.name = "fluid_renderer";
31476
- scene.add(root);
33440
+ scene2.add(root);
31477
33441
  const vfx = options.vfx ?? createVfx({ profile: quality, gravity: fluids.gravity, sprites: { capacity: profile.maxParticles } });
31478
- const ownVfx = options.vfx ? void 0 : createVfxRenderer(THREE, { scene, camera: options.camera ?? new THREE.PerspectiveCamera(), vfx, pointScale: options.pointScale ?? 600 });
33442
+ const ownVfx = options.vfx ? void 0 : createVfxRenderer(THREE, { scene: scene2, camera: options.camera ?? new THREE.PerspectiveCamera(), vfx, pointScale: options.pointScale ?? 600 });
31479
33443
  const particles = new FluidSprites(vfx, `fluids-${++rendererCount2}`);
31480
33444
  const dropGeometry = new THREE.SphereGeometry(1, 8, 6);
31481
33445
  const bodies = /* @__PURE__ */ new Map();
@@ -31612,7 +33576,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
31612
33576
  ownVfx?.dispose();
31613
33577
  dropGeometry.dispose();
31614
33578
  materials.dispose();
31615
- scene.remove(root);
33579
+ scene2.remove(root);
31616
33580
  root.clear();
31617
33581
  },
31618
33582
  stats() {
@@ -32683,6 +34647,16 @@ void main(){
32683
34647
  "ui.slow": "Slow motion 0.25\xD7",
32684
34648
  "ui.fast": "Time \xD710",
32685
34649
  "ui.tap": "Tap",
34650
+ "ui.blast.shelter": "Shelter",
34651
+ "ui.blast.fragmentHits": "Fragments",
34652
+ "ui.blast.confinement": "Confinement",
34653
+ "ui.blast.shelter.hint": "Blasts: walls and other parts between the blast and the target take their share",
34654
+ "ui.blast.fragmentHits.hint": "Blasts: the pieces of what breaks fly on and hit what is behind",
34655
+ "ui.blast.confinement.hint": "Blasts: pressure builds up in an enclosed room",
34656
+ "metric.blast_shelter": "Blast reaching (%)",
34657
+ "metric.blast_confinement": "Blast pressure (x)",
34658
+ "metric.blast_fragments": "Fragment hits",
34659
+ "act.blast_report": "Blast: {shelter}% reached the most sheltered target, pressure x{confinement}, {fragments} fragment hits, {n} targets",
32686
34660
  "ui.quality": "Quality",
32687
34661
  "ui.quality.low": "low",
32688
34662
  "ui.quality.medium": "medium",
@@ -33008,6 +34982,16 @@ void main(){
33008
34982
  "ui.slow": "C\xE2mera lenta 0,25\xD7",
33009
34983
  "ui.fast": "Tempo \xD710",
33010
34984
  "ui.tap": "Torneira",
34985
+ "ui.blast.shelter": "Abrigo",
34986
+ "ui.blast.fragmentHits": "Estilha\xE7os",
34987
+ "ui.blast.confinement": "Confinamento",
34988
+ "ui.blast.shelter.hint": "Explos\xF5es: paredes e outras partes entre a explos\xE3o e o alvo ficam com a sua parte",
34989
+ "ui.blast.fragmentHits.hint": "Explos\xF5es: os peda\xE7os do que quebra voam e acertam o que est\xE1 atr\xE1s",
34990
+ "ui.blast.confinement.hint": "Explos\xF5es: a press\xE3o sobe num ambiente fechado",
34991
+ "metric.blast_shelter": "Explos\xE3o que chegou (%)",
34992
+ "metric.blast_confinement": "Press\xE3o da explos\xE3o (x)",
34993
+ "metric.blast_fragments": "Estilha\xE7os que acertaram",
34994
+ "act.blast_report": "Explos\xE3o: {shelter}% chegou ao alvo mais abrigado, press\xE3o x{confinement}, {fragments} estilha\xE7os acertaram, {n} alvos",
33011
34995
  "ui.quality": "Qualidade",
33012
34996
  "ui.quality.low": "baixa",
33013
34997
  "ui.quality.medium": "m\xE9dia",
@@ -33454,10 +35438,10 @@ void main(){
33454
35438
  }
33455
35439
  return { name: "shelf", parts, joints, tags: ["furniture"] };
33456
35440
  };
33457
- function createArena(THREE, size, scene) {
35441
+ function createArena(THREE, size, scene2) {
33458
35442
  const group = new THREE.Group();
33459
35443
  group.name = "test-room-arena";
33460
- scene.background = new THREE.Color(10466504);
35444
+ scene2.background = new THREE.Color(10466504);
33461
35445
  group.add(new THREE.HemisphereLight(13624319, 7037527, 0.75));
33462
35446
  const sun = new THREE.DirectionalLight(16777215, 2.1);
33463
35447
  sun.position.set(7, 13, 8);
@@ -33730,9 +35714,19 @@ void main(){
33730
35714
 
33731
35715
  // src/explosives.ts
33732
35716
  var CONE_BLAST_RADIUS = 0.5;
35717
+ var summarizeBlast = (results) => {
35718
+ const reports = results.flatMap((r) => r.blast ? [r.blast] : []);
35719
+ if (reports.length === 0) return void 0;
35720
+ return {
35721
+ shelter: Math.min(...reports.map((r) => r.shelter)),
35722
+ confinement: reports[0].confinement,
35723
+ fragmentHits: reports.reduce((n, r) => n + r.fragmentHits, 0),
35724
+ targets: reports.length
35725
+ };
35726
+ };
33733
35727
  var toArray = (vector) => [vector.x, vector.y, vector.z];
33734
35728
  function createExplosives(context) {
33735
- const { THREE, scene, camera: camera2, world, renderer, arsenal, translate, format, action } = context;
35729
+ const { THREE, scene: scene2, camera: camera2, world, renderer, arsenal, translate, format, action } = context;
33736
35730
  const projectiles = [];
33737
35731
  const meshes = [];
33738
35732
  const up = new THREE.Vector3(0, 1, 0);
@@ -33745,7 +35739,7 @@ void main(){
33745
35739
  const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color, roughness: 0.6, metalness }));
33746
35740
  mesh.castShadow = true;
33747
35741
  mesh.position.set(at3[0], at3[1], at3[2]);
33748
- scene.add(mesh);
35742
+ scene2.add(mesh);
33749
35743
  meshes.push(mesh);
33750
35744
  return mesh;
33751
35745
  };
@@ -33800,7 +35794,7 @@ void main(){
33800
35794
  flag.position.y = 0.46;
33801
35795
  marker.add(stake, flag);
33802
35796
  marker.position.set(at3[0] + 0.12, at3[1] - 0.03, at3[2]);
33803
- scene.add(marker);
35797
+ scene2.add(marker);
33804
35798
  meshes.push(marker);
33805
35799
  return marker;
33806
35800
  };
@@ -33808,7 +35802,7 @@ void main(){
33808
35802
  const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(...from), new THREE.Vector3(...to)]);
33809
35803
  const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 15262920 }));
33810
35804
  line.name = "charge_wire";
33811
- scene.add(line);
35805
+ scene2.add(line);
33812
35806
  meshes.push(line);
33813
35807
  return line;
33814
35808
  };
@@ -33876,17 +35870,27 @@ void main(){
33876
35870
  if (box2.isEmpty()) box2.expandByPoint(object.position);
33877
35871
  const at3 = mostExposedPoint(box2, detonation);
33878
35872
  if (!at3) continue;
33879
- reached += world.explode(at3, detonation.energy * blastEffect(detonation, at3).falloff, CONE_BLAST_RADIUS, "player").length;
35873
+ const results = world.explode(at3, detonation.energy * blastEffect(detonation, at3).falloff, CONE_BLAST_RADIUS, "player", context.blast());
35874
+ reached += results.length;
35875
+ told(results);
33880
35876
  }
33881
35877
  return reached;
33882
35878
  };
35879
+ const told = (results) => {
35880
+ const summary = summarizeBlast(results);
35881
+ if (!summary) return;
35882
+ context.blasted(summary);
35883
+ action("blast_report", translate("act.blast_report", { shelter: format(summary.shelter * 100, 0), confinement: format(summary.confinement, 2), fragments: summary.fragmentHits, n: summary.targets }), { data: { ...summary } });
35884
+ };
33883
35885
  const detonate = (projectile, detonation) => {
33884
35886
  const weaponName = context.toolName(projectile.weapon);
33885
35887
  const reach = Math.max(detonation.injuryRadius, detonation.lethalRadius);
33886
- const reached = detonation.energy <= 0 ? 0 : detonation.cone ? coneBlast(detonation) : world.explode(detonation.point, detonation.energy, reach, "player").length;
35888
+ const results = detonation.energy <= 0 || detonation.cone ? [] : world.explode(detonation.point, detonation.energy, reach, "player", context.blast());
35889
+ const reached = detonation.energy <= 0 ? 0 : detonation.cone ? coneBlast(detonation) : results.length;
35890
+ if (!detonation.cone) told(results);
33887
35891
  if (detonation.energy > 0) {
33888
35892
  const isPlayed = context.effects.explosion(detonation.explosive, detonation.point, { direction: detonation.direction }) !== null;
33889
- renderer.explode(detonation.point, detonation.energy, reach, { visual: !isPlayed });
35893
+ renderer.explode(detonation.point, detonation.energy, reach, { visual: !isPlayed, report: results });
33890
35894
  }
33891
35895
  if (detonation.stunSeconds !== void 0) renderer.emit("flash", detonation.point, [0, 1, 0], 1, 1);
33892
35896
  const onPlayer = blastEffect(detonation, toArray(camera2.position));
@@ -34183,7 +36187,7 @@ void main(){
34183
36187
  }
34184
36188
 
34185
36189
  // src/metrics.ts
34186
- var ROOM_METRICS = ["fps", "frame_ms", "draw_calls", "triangles", "objects", "fragments", "particles", "decals"];
36190
+ var ROOM_METRICS = ["fps", "frame_ms", "draw_calls", "triangles", "objects", "fragments", "particles", "decals", "blast_shelter", "blast_confinement", "blast_fragments"];
34187
36191
  var WINDOW_S = 0.5;
34188
36192
  function createRoomMetrics(sources) {
34189
36193
  const wanted = new Set(sources.ids);
@@ -34220,6 +36224,12 @@ void main(){
34220
36224
  const render = ownsRender ? { calls, triangles } : gl.info?.render;
34221
36225
  put("draw_calls", render?.calls);
34222
36226
  put("triangles", render?.triangles);
36227
+ const blast = sources.blast?.();
36228
+ if (blast) {
36229
+ put("blast_shelter", blast.shelter * 100);
36230
+ put("blast_confinement", blast.confinement);
36231
+ put("blast_fragments", blast.fragmentHits);
36232
+ }
34223
36233
  if (!["objects", "fragments", "particles", "decals"].some((id) => wanted.has(id))) return out;
34224
36234
  const destruction = sources.destruction.stats();
34225
36235
  const fluids = sources.fluids.stats();
@@ -34483,13 +36493,13 @@ void main(){
34483
36493
  if (!g || g.done) break;
34484
36494
  g.done = true;
34485
36495
  const [a, b] = ev.parts;
34486
- const many = g.list.length > 1;
36496
+ const many2 = g.list.length > 1;
34487
36497
  c.log.add({
34488
36498
  channel: "event",
34489
36499
  type: "detach",
34490
36500
  objectId: ev.object,
34491
36501
  itemId: c.itemIdOf(ev.object),
34492
- text: many ? t(`ev.detach.many.${ev.by === "tool" ? "tool" : "break"}`, { obj: c.nameOf(ev.object), joints: joints(g.list.length), pieces: pieces(g.groups) }) : t("ev.detach", { obj: c.nameOf(ev.object), a, b, by: t(`by.${ev.by}`), pieces: pieces(g.groups) }),
36502
+ text: many2 ? t(`ev.detach.many.${ev.by === "tool" ? "tool" : "break"}`, { obj: c.nameOf(ev.object), joints: joints(g.list.length), pieces: pieces(g.groups) }) : t("ev.detach", { obj: c.nameOf(ev.object), a, b, by: t(`by.${ev.by}`), pieces: pieces(g.groups) }),
34493
36503
  data: { joint: ev.joint, joints: g.list.map((j) => j.joint), parts: ev.parts, by: ev.by, groups: g.groups }
34494
36504
  });
34495
36505
  break;
@@ -35490,23 +37500,23 @@ void main(){
35490
37500
  }
35491
37501
  if (sheet.timeline) {
35492
37502
  const { segments, ticks, metric } = sheet.timeline;
35493
- const end = Math.max(1e-3, ...segments.map((segment) => segment.end), ...ticks);
37503
+ const end3 = Math.max(1e-3, ...segments.map((segment) => segment.end), ...ticks);
35494
37504
  const strip = mk("div", "tr-timeline");
35495
37505
  for (const segment of segments) {
35496
37506
  const bar2 = mk("div", "tr-segment", segment.name);
35497
- bar2.style.left = `${segment.start / end * 100}%`;
35498
- bar2.style.width = `${(segment.end - segment.start) / end * 100}%`;
37507
+ bar2.style.left = `${segment.start / end3 * 100}%`;
37508
+ bar2.style.width = `${(segment.end - segment.start) / end3 * 100}%`;
35499
37509
  bar2.style.background = segment.color;
35500
37510
  strip.appendChild(bar2);
35501
37511
  }
35502
37512
  for (const tick of ticks) {
35503
37513
  const mark = mk("u", "tr-tick");
35504
- mark.style.left = `${tick / end * 100}%`;
37514
+ mark.style.left = `${tick / end3 * 100}%`;
35505
37515
  strip.appendChild(mark);
35506
37516
  }
35507
37517
  const head = mk("s", "tr-playhead");
35508
37518
  strip.appendChild(head);
35509
- playhead = { node: head, metric, end };
37519
+ playhead = { node: head, metric, end: end3 };
35510
37520
  card.appendChild(strip);
35511
37521
  }
35512
37522
  if (sheet.note) card.appendChild(mk("p", "sub", sheet.note));
@@ -35901,6 +37911,8 @@ void main(){
35901
37911
  o.onFast(fast);
35902
37912
  });
35903
37913
  const tapBtn = ctl(t("ui.tap"), () => o.onTap());
37914
+ const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
37915
+ for (const { key, b } of blastBtns) b.title = t(`ui.blast.${key}.hint`);
35904
37916
  const qSel = mk2("select");
35905
37917
  for (const q of ["low", "medium", "high"]) {
35906
37918
  const op = mk2("option", void 0, t(`ui.quality.${q}`));
@@ -36105,6 +38117,7 @@ void main(){
36105
38117
  }
36106
38118
  slotsEl.forEach((b, i) => b.classList.toggle("on", i === s.slot));
36107
38119
  tapBtn.classList.toggle("on", s.tap);
38120
+ for (const { key: key2, b } of blastBtns) b.classList.toggle("on", s.blast[key2]);
36108
38121
  count.textContent = t("ui.objects", { n: s.objects });
36109
38122
  const ak = JSON.stringify(s.animations);
36110
38123
  if (ak !== lastAnim) {
@@ -36187,9 +38200,9 @@ void main(){
36187
38200
  gl.setSize(width(), height());
36188
38201
  const dom = gl.domElement;
36189
38202
  if (dom && !dom.hasAttribute?.("tabindex")) dom.setAttribute?.("tabindex", "0");
36190
- const scene = options.scene ?? new THREE.Scene();
38203
+ const scene2 = options.scene ?? new THREE.Scene();
36191
38204
  const camera2 = options.camera ?? new THREE.PerspectiveCamera(72, width() / height(), 0.05, 140);
36192
- if (!options.camera) scene.add(camera2);
38205
+ if (!options.camera) scene2.add(camera2);
36193
38206
  const resize = () => {
36194
38207
  const w = width();
36195
38208
  const h = height();
@@ -36200,8 +38213,8 @@ void main(){
36200
38213
  const onResize = () => resize();
36201
38214
  window.addEventListener("resize", onResize);
36202
38215
  const arenaSize = options.arenaSize ?? 14;
36203
- const arena = createArena(THREE, arenaSize, scene);
36204
- scene.add(arena.group);
38216
+ const arena = createArena(THREE, arenaSize, scene2);
38217
+ scene2.add(arena.group);
36205
38218
  const ownWorld = !options.world;
36206
38219
  const world = options.world ?? createDestruction({ seed: 1, ...options.worldOptions, materials: [...DEMO_MATERIALS, ...options.worldOptions?.materials ?? []] });
36207
38220
  const pristineWorld = ownWorld ? world.serialize() : null;
@@ -36229,15 +38242,22 @@ void main(){
36229
38242
  let simTime = 0;
36230
38243
  const log = createLog({ exportCancelled: () => t("log.export_cancelled") });
36231
38244
  log.setClock(() => simTime);
38245
+ const ownAudio = options.audio === void 0;
38246
+ const audio = options.audio === false ? null : options.audio ?? createAudioEngine();
38247
+ const destructionSound = audio ? createDestructionSound(audio) : null;
38248
+ const playSound = (key, point, volume) => {
38249
+ destructionSound?.onSound(key, point, volume);
38250
+ options.onSound?.(key, point, volume);
38251
+ };
36232
38252
  const craterField = options.craters ? createGroundField() : void 0;
36233
38253
  const craterPatch = craterField ? createGroundPatch(THREE, { field: craterField, material: new THREE.MeshStandardMaterial({ color: 9342602, roughness: 0.95 }) }) : void 0;
36234
38254
  if (craterPatch) {
36235
38255
  const floor2 = arena.group.getObjectByName("floor");
36236
38256
  if (floor2) floor2.visible = false;
36237
- scene.add(craterPatch.mesh);
38257
+ scene2.add(craterPatch.mesh);
36238
38258
  }
36239
38259
  const vfx = createVfxRenderer(THREE, {
36240
- scene,
38260
+ scene: scene2,
36241
38261
  camera: camera2,
36242
38262
  overlay: container,
36243
38263
  quality,
@@ -36249,13 +38269,13 @@ void main(){
36249
38269
  })
36250
38270
  });
36251
38271
  const renderer = createDestructionRenderer(THREE, {
36252
- scene,
38272
+ scene: scene2,
36253
38273
  world,
36254
38274
  vfx: vfx.world,
36255
38275
  quality,
36256
38276
  groundAt,
36257
38277
  pointScale,
36258
- onSound: options.onSound,
38278
+ onSound: playSound,
36259
38279
  onPiece: (piece) => {
36260
38280
  rendererPieces.set(piece.objectId, piece);
36261
38281
  loose.push(piece);
@@ -36277,10 +38297,10 @@ void main(){
36277
38297
  if (best) splitOf.set(piece.objectId, best);
36278
38298
  }
36279
38299
  });
36280
- const fluidView = createFluidRenderer(THREE, { scene, fluids, quality, pointScale, vfx: vfx.world });
38300
+ const fluidView = createFluidRenderer(THREE, { scene: scene2, fluids, quality, pointScale, vfx: vfx.world });
36281
38301
  const filmRandom = createFilmLcg(1);
36282
38302
  const film = createFilmManager(THREE, {
36283
- scene,
38303
+ scene: scene2,
36284
38304
  camera: camera2,
36285
38305
  random: filmRandom,
36286
38306
  fluids,
@@ -36477,7 +38497,7 @@ void main(){
36477
38497
  for (const o of arena.objects) {
36478
38498
  const root = buildFromSpec(THREE, o.spec, { colors: o.colors });
36479
38499
  root.position.set(o.position[0], o.position[1], o.position[2]);
36480
- scene.add(root);
38500
+ scene2.add(root);
36481
38501
  root.updateMatrixWorld(true);
36482
38502
  world.add(structuredCloneSpec(o.spec), { id: o.id, position: o.position });
36483
38503
  registerEntry(o.id, o.id, root, o.spec, S.text[`arena.${o.id}`] ?? o.id, void 0, void 0, true, false);
@@ -36504,7 +38524,7 @@ void main(){
36504
38524
  root.position.set(pos[0], pos[1], pos[2]);
36505
38525
  root.rotation.y = rotationY ?? player.yaw;
36506
38526
  root.name = item.id;
36507
- scene.add(root);
38527
+ scene2.add(root);
36508
38528
  root.updateMatrixWorld(true);
36509
38529
  const objectId = `${item.id}_${++counter}`;
36510
38530
  let own;
@@ -36691,7 +38711,7 @@ void main(){
36691
38711
  g.setAttribute("position", new THREE.Float32BufferAttribute(6, 3));
36692
38712
  const line = new THREE.Line(g, tracerMat);
36693
38713
  line.frustumCulled = false;
36694
- scene.add(line);
38714
+ scene2.add(line);
36695
38715
  tr = { line, life: 0 };
36696
38716
  tracers.push(tr);
36697
38717
  }
@@ -36807,15 +38827,15 @@ void main(){
36807
38827
  energy = Math.max(energy, s.energy);
36808
38828
  const results = world.shoot(s.origin, s.direction, s.energy, s.caliber, "player");
36809
38829
  const r0 = results.find((r) => r.part && r.point);
36810
- let end = [s.origin[0] + s.direction[0] * 40, s.origin[1] + s.direction[1] * 40, s.origin[2] + s.direction[2] * 40];
38830
+ let end3 = [s.origin[0] + s.direction[0] * 40, s.origin[1] + s.direction[1] * 40, s.origin[2] + s.direction[2] * 40];
36811
38831
  if (r0?.point && r0.part) {
36812
38832
  hits++;
36813
- end = r0.point;
38833
+ end3 = r0.point;
36814
38834
  if (!first) first = { object: r0.object, part: r0.part, point: r0.point, normal: [0, 1, 0], distance: dist3(s.origin, r0.point) };
36815
38835
  const speed = Math.max(80, s.speed);
36816
38836
  pushPiece(r0.object, s.direction, 2 * s.energy / speed, r0.point);
36817
38837
  }
36818
- if (shots.length <= 3 || rnd2() < 0.34) tracer(m, end);
38838
+ if (shots.length <= 3 || rnd2() < 0.34) tracer(m, end3);
36819
38839
  }
36820
38840
  });
36821
38841
  player.kick(recoil[0], recoil[1]);
@@ -36979,9 +38999,15 @@ void main(){
36979
38999
  bodies.push({ id: "player", box: new THREE.Box3(new THREE.Vector3(x - PLAYER_RADIUS, feet, z - PLAYER_RADIUS), new THREE.Vector3(x + PLAYER_RADIUS, y + 0.1, z + PLAYER_RADIUS)) });
36980
39000
  return bodies;
36981
39001
  }
39002
+ let blastOptions = { ...options.blast };
39003
+ let lastBlast;
39004
+ const blastOn = (key) => Boolean(blastOptions[key]);
39005
+ const toggleBlast = (key) => {
39006
+ blastOptions = { ...blastOptions, [key]: blastOn(key) ? false : true };
39007
+ };
36982
39008
  const explosives = createExplosives({
36983
39009
  THREE,
36984
- scene,
39010
+ scene: scene2,
36985
39011
  camera: camera2,
36986
39012
  world,
36987
39013
  renderer,
@@ -36993,6 +39019,10 @@ void main(){
36993
39019
  toolName,
36994
39020
  withoutHeld,
36995
39021
  groundAt,
39022
+ blast: () => blastOptions,
39023
+ blasted: (summary) => {
39024
+ lastBlast = summary;
39025
+ },
36996
39026
  translate: t,
36997
39027
  format: num,
36998
39028
  action,
@@ -37419,7 +39449,7 @@ void main(){
37419
39449
  let metricsAcc = 0;
37420
39450
  let ownsLoop = false;
37421
39451
  const roomMetricIds = options.metrics === false ? [] : options.metrics ?? ROOM_METRICS;
37422
- const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, effects: vfx.world, ids: roomMetricIds });
39452
+ const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, effects: vfx.world, ids: roomMetricIds, blast: () => lastBlast });
37423
39453
  let qualityName = quality;
37424
39454
  const syncContainers = (sdt) => {
37425
39455
  for (const e of entries.values()) {
@@ -37478,6 +39508,10 @@ void main(){
37478
39508
  s.update(dt);
37479
39509
  flushEvents(id, s);
37480
39510
  }
39511
+ if (audio) {
39512
+ const look = camera2.getWorldDirection(listenerDirection);
39513
+ audio.setListener({ position: [camera2.position.x, camera2.position.y, camera2.position.z], yaw: Math.atan2(-look.x, -look.z) });
39514
+ }
37481
39515
  for (const update2 of frameListeners) update2(dt);
37482
39516
  for (const tr of tracers) {
37483
39517
  if (tr.life > 0) {
@@ -37534,6 +39568,7 @@ void main(){
37534
39568
  timeScale,
37535
39569
  fast: fastForward,
37536
39570
  tap: isTapOn(),
39571
+ blast: { shelter: blastOn("shelter"), fragmentHits: blastOn("fragmentHits"), confinement: blastOn("confinement") },
37537
39572
  quality: qualityName,
37538
39573
  objects: [...entries.values()].filter((e) => !e.arena).length
37539
39574
  };
@@ -37587,7 +39622,7 @@ void main(){
37587
39622
  const ev = evs[pl.fired++];
37588
39623
  action("animation_event", t("act.anim_event", { name: pl.entry.name, clip: pl.clip.id, type: ev.type }), { objectId: pl.entry.so.objectId, itemId: pl.entry.so.itemId, data: { clip: pl.clip.id, type: ev.type, at: ev.t, ...ev.data ?? {} } });
37589
39624
  const key = ev.type === "sound" ? ev.data?.key : void 0;
37590
- if (typeof key === "string") options.onSound?.(key, v3(pl.entry.so.root.getWorldPosition(tmpV2)), 1);
39625
+ if (typeof key === "string") playSound(key, v3(pl.entry.so.root.getWorldPosition(tmpV2)), 1);
37591
39626
  }
37592
39627
  if (pl.t >= pl.clip.duration) {
37593
39628
  playing.splice(i, 1);
@@ -37680,6 +39715,9 @@ void main(){
37680
39715
  };
37681
39716
  const hostListeners = /* @__PURE__ */ new Set();
37682
39717
  const frameListeners = /* @__PURE__ */ new Set();
39718
+ const listenerDirection = new THREE.Vector3();
39719
+ const environment = options.environment ?? createEnvironmentBus();
39720
+ const audioSink = audio ? createAudioSink(audio) : void 0;
37683
39721
  function emitHost(event) {
37684
39722
  for (const listener of hostListeners) listener(event);
37685
39723
  }
@@ -37726,9 +39764,11 @@ void main(){
37726
39764
  }
37727
39765
  const host = {
37728
39766
  THREE,
37729
- scene,
39767
+ scene: scene2,
37730
39768
  camera: camera2,
37731
39769
  locale,
39770
+ ...audioSink ? { audio: audioSink } : {},
39771
+ environment,
37732
39772
  groundAt,
37733
39773
  effects: vfx,
37734
39774
  tool: () => ({
@@ -37977,11 +40017,13 @@ void main(){
37977
40017
  const room = {
37978
40018
  world,
37979
40019
  renderer,
37980
- scene,
40020
+ scene: scene2,
37981
40021
  camera: camera2,
37982
40022
  arsenal,
37983
40023
  fluids,
37984
40024
  fluidRenderer: fluidView,
40025
+ audio,
40026
+ environment,
37985
40027
  log,
37986
40028
  get tool() {
37987
40029
  return tool;
@@ -38042,6 +40084,10 @@ void main(){
38042
40084
  heldTilt = rad;
38043
40085
  },
38044
40086
  toggleTap,
40087
+ blast: () => ({ ...blastOptions }),
40088
+ setBlast: (next) => {
40089
+ blastOptions = { ...blastOptions, ...next };
40090
+ },
38045
40091
  pause() {
38046
40092
  paused = true;
38047
40093
  },
@@ -38075,6 +40121,8 @@ void main(){
38075
40121
  return { room: roomMetrics.sample(), module: moduleMetrics() };
38076
40122
  },
38077
40123
  dispose() {
40124
+ destructionSound?.dispose();
40125
+ if (ownAudio) audio?.dispose();
38078
40126
  for (const detach of detachers.values()) detach();
38079
40127
  detachers.clear();
38080
40128
  for (const store of benches.values()) store.dispose();
@@ -38150,6 +40198,7 @@ void main(){
38150
40198
  setTool(loadout[index]);
38151
40199
  },
38152
40200
  onTap: () => toggleTap(),
40201
+ onBlast: (key) => toggleBlast(key),
38153
40202
  onPlay: (objectId, clipId) => {
38154
40203
  playAnimation(clipId, objectId);
38155
40204
  },
@@ -38179,7 +40228,7 @@ void main(){
38179
40228
  last = now;
38180
40229
  if (!scenarioRunning) update(dt);
38181
40230
  const restoreCamera = vfx.applyCamera();
38182
- gl.render(scene, camera2);
40231
+ gl.render(scene2, camera2);
38183
40232
  restoreCamera();
38184
40233
  roomMetrics.rendered();
38185
40234
  };