@adatechnology/game-test-room 0.8.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.
@@ -1190,7 +1190,8 @@ var GameTestRoom = (() => {
1190
1190
  0.25
1191
1191
  ],
1192
1192
  damageScale: {
1193
- blunt: 1.5
1193
+ blunt: 1.5,
1194
+ shelter: 3
1194
1195
  },
1195
1196
  hitDebris: "shards",
1196
1197
  breakDebris: "shards",
@@ -1241,7 +1242,10 @@ var GameTestRoom = (() => {
1241
1242
  "glass",
1242
1243
  "brittle"
1243
1244
  ],
1244
- compressive: 2e5
1245
+ compressive: 2e5,
1246
+ damageScale: {
1247
+ shelter: 3
1248
+ }
1245
1249
  },
1246
1250
  {
1247
1251
  id: "ceramic",
@@ -1921,10 +1925,14 @@ var GameTestRoom = (() => {
1921
1925
  };
1922
1926
  var DMG_PER_J = 0.02;
1923
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 };
1924
1931
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
1925
1932
  var BREACH_RADIUS = 0.55;
1926
1933
  var BREACH_BLAST_FACTOR = 6;
1927
1934
  var BREACH_AREA = 0.25;
1935
+ var BREACH_BLAST_REACH = Infinity;
1928
1936
  var RESIDUAL_CAPACITY = 0.15;
1929
1937
  var UNIT_FRAGMENT_FACTOR = 4;
1930
1938
  var BREACH_GRID_CAP = 4096;
@@ -1959,6 +1967,7 @@ var GameTestRoom = (() => {
1959
1967
  const fireSpread = options.fireSpread ?? 0.6;
1960
1968
  const breachRadius = options.breach?.radius ?? BREACH_RADIUS;
1961
1969
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
1970
+ const breachBlastReach = options.breach?.blastReach ?? BREACH_BLAST_REACH;
1962
1971
  const rng = createRng(options.seed ?? 1);
1963
1972
  const rnd2 = () => rng.next();
1964
1973
  let time = 0;
@@ -2314,7 +2323,7 @@ var GameTestRoom = (() => {
2314
2323
  if (disc < 0) return true;
2315
2324
  return -b - Math.sqrt(disc) > maxDist;
2316
2325
  };
2317
- const raycastInternal = (origin, dir, maxDist, ignore) => {
2326
+ const raycastInternal = (origin, dir, maxDist, ignore, worldOnly = false) => {
2318
2327
  const d = vec.normalize(dir);
2319
2328
  let best = null;
2320
2329
  for (const o of objects.values()) {
@@ -2327,7 +2336,7 @@ var GameTestRoom = (() => {
2327
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 };
2328
2337
  }
2329
2338
  }
2330
- for (const im of impls.values()) {
2339
+ for (const im of worldOnly ? [] : impls.values()) {
2331
2340
  for (const ps of im.impl.describe().parts) {
2332
2341
  if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
2333
2342
  const ov = im.poses.get(ps.id);
@@ -2740,7 +2749,7 @@ var GameTestRoom = (() => {
2740
2749
  else if (chained && (trigger === "explosion" || 1 - p.st.hp / p.st.maxHp >= shotDamageFractionOf(p.spec))) prime(o, p, trigger, src);
2741
2750
  return done;
2742
2751
  };
2743
- 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) => {
2744
2753
  const mat = p.mat;
2745
2754
  if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
2746
2755
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
@@ -2753,7 +2762,9 @@ var GameTestRoom = (() => {
2753
2762
  const exitLocal = objLocal(o, vec.madd(entry, d, Math.min(pathLen, effM)));
2754
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;
2755
2764
  const thr = coats > 0 ? (mat.penetration * p.thickness * 1e3 + coats) / slant : mat.penetration * effM * 1e3;
2756
- 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;
2757
2768
  const scale2 = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2758
2769
  const id = p.spec.id;
2759
2770
  if (!penetrated && mat.ricochetDeg && angle < mat.ricochetDeg) {
@@ -2765,7 +2776,7 @@ var GameTestRoom = (() => {
2765
2776
  }
2766
2777
  if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
2767
2778
  const brittle = mat.ductility < 0.3;
2768
- const ratio = E / Math.max(thr, 1e-6);
2779
+ const ratio = Ep / Math.max(thr, 1e-6);
2769
2780
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
2770
2781
  let exitPoint;
2771
2782
  if (penetrated) {
@@ -2795,7 +2806,7 @@ var GameTestRoom = (() => {
2795
2806
  debris2(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
2796
2807
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
2797
2808
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale2, "ballistic", entry, d, E * 0.1, 12, src);
2798
- return { penetrated, exitEnergy: penetrated ? E - thr : 0, exitPoint, damage: dmg };
2809
+ return { penetrated, exitEnergy: penetrated ? E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
2799
2810
  };
2800
2811
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
2801
2812
  const mat = p.mat;
@@ -2823,54 +2834,110 @@ var GameTestRoom = (() => {
2823
2834
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
2824
2835
  return done;
2825
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
+ };
2826
2880
  const blastObject = (o, point, energy, radius, src) => {
2827
2881
  const start = cur.length;
2828
2882
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
2829
- for (const p of o.parts) {
2830
- if (isLost(p)) continue;
2831
- const { c, q } = partXf(o, p);
2832
- const lp = toLocal(c, q, point);
2833
- const dist = distanceToShape(p.spec.shape, lp);
2834
- if (dist >= radius) continue;
2835
- const f = (1 - dist / radius) ** 2;
2836
- const Ein = energy * f * BLAST_COUPLING;
2837
- const sp = surfacePoint(p.spec.shape, lp);
2838
- const wp = toWorld(c, q, sp.point);
2839
- const wn = rotateVec(q, sp.normal);
2840
- if (!res.part) {
2841
- res.part = p.spec.id;
2842
- res.point = wp;
2843
- }
2844
- const mat = p.mat;
2845
- if (Ein > 20) {
2846
- 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);
2847
- else if (mat.ductility < 0.3 && Ein > 60) addCrack(o, p, wp, wn, clamp2(0.05 + Ein * 5e-4, 0.05, 1), src);
2848
- debris2(o, p, mat.hitDebris, wp, wn, clamp2(Math.round(Ein / 80) + 1, 1, 12), clamp2(Math.sqrt(Ein) / 4, 2, 15), src);
2849
- }
2850
- const th = mat.thermal;
2851
- if (th.ignitionC !== void 0 && p.st.fuel > 0) {
2852
- p.st.temperature += 300 * f;
2853
- o.active = true;
2854
- if (f >= 0.25 && !p.st.burning) {
2855
- p.st.temperature = Math.max(p.st.temperature, th.ignitionC + 1);
2856
- checkIgnite(o, p, src);
2857
- }
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;
2858
2894
  }
2859
- wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2860
- const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
2861
- if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
2862
- frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
2863
- 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;
2864
2931
  }
2865
- 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);
2866
- const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
2867
- res.damage += dealt;
2868
- addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2869
2932
  }
2870
- res.events = cur.slice(start);
2871
- 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);
2872
2937
  };
2873
- const blastWorld = (point, energy, radius, src) => {
2938
+ const FLAG_LOST = 1;
2939
+ const FLAG_OPEN = 2;
2940
+ const blastPlain = (point, energy, radius, src) => {
2874
2941
  const out = [];
2875
2942
  for (const o of [...objects.values()]) {
2876
2943
  if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), point) - o.br >= radius) continue;
@@ -2878,16 +2945,225 @@ var GameTestRoom = (() => {
2878
2945
  if (r.part) out.push(r);
2879
2946
  }
2880
2947
  for (const im of [...impls.values()]) {
2881
- const inRange = im.impl.describe().parts.some((ps) => {
2882
- const ov = im.poses.get(ps.id);
2883
- const c = toWorld(im.position, im.rotation, ov?.pos ?? ps.position);
2884
- const q = quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]));
2885
- return distanceToShape(ps.shape, toLocal(c, q, point)) < radius;
2886
- });
2887
- 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 } : {} }));
2888
2949
  }
2889
2950
  return out;
2890
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));
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 });
3098
+ return out;
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
+ };
2891
3167
  const heatObject = (o, point, T, radius, duration, src) => {
2892
3168
  const start = cur.length;
2893
3169
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
@@ -2924,7 +3200,7 @@ var GameTestRoom = (() => {
2924
3200
  if (hit.type === "ballistic") {
2925
3201
  const loc = locate(o, point, dir, 0.05, only);
2926
3202
  if (loc) {
2927
- 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);
2928
3204
  res = { ...res, part: loc.p.spec.id, point: loc.point, damage: out.damage, penetrated: out.penetrated, exitEnergy: out.exitEnergy };
2929
3205
  }
2930
3206
  } else if (hit.type === "blunt" || hit.type === "slash") {
@@ -2946,7 +3222,13 @@ var GameTestRoom = (() => {
2946
3222
  }
2947
3223
  }
2948
3224
  } else if (hit.type === "explosive") {
2949
- 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
+ }
2950
3232
  } else {
2951
3233
  res = heatObject(o, point, hit.temperature ?? 800, hit.radius ?? 0.25, hit.duration ?? 1, src);
2952
3234
  }
@@ -3203,6 +3485,14 @@ var GameTestRoom = (() => {
3203
3485
  for (const e of r.events) if (e.type === "collapse" && !r.value.some((h) => h.events.includes(e))) last.events.push(e);
3204
3486
  return r.value;
3205
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
+ };
3206
3496
  const whole = (fn) => {
3207
3497
  const r = run(fn);
3208
3498
  r.value.events = r.events;
@@ -3290,7 +3580,7 @@ var GameTestRoom = (() => {
3290
3580
  },
3291
3581
  hit(objectId, h) {
3292
3582
  const im = impls.get(objectId);
3293
- if (im) return whole(() => implHit(im, h));
3583
+ if (im) return whole(() => h.type === "explosive" ? blastImpl(im, h) : implHit(im, h));
3294
3584
  const o = getObj(objectId);
3295
3585
  return whole(() => doHit(o, h));
3296
3586
  },
@@ -3301,14 +3591,14 @@ var GameTestRoom = (() => {
3301
3591
  const p = getPart(o, partId);
3302
3592
  return whole(() => doHit(o, h, p));
3303
3593
  },
3304
- explode(point, energy, radius, source) {
3305
- 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)));
3306
3596
  },
3307
3597
  raycast(origin, direction, maxDistance = Infinity) {
3308
3598
  const r = raycastInternal(origin, direction, maxDistance);
3309
3599
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
3310
3600
  },
3311
- shoot(origin, direction, energy, caliber, source) {
3601
+ shoot(origin, direction, energy, caliber, source, piercing) {
3312
3602
  return withCollapses(run(() => {
3313
3603
  const results = [];
3314
3604
  const d = vec.normalize(direction);
@@ -3320,7 +3610,7 @@ var GameTestRoom = (() => {
3320
3610
  if (!rh) break;
3321
3611
  const im = impls.get(rh.object);
3322
3612
  if (im) {
3323
- 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 } : {} });
3324
3614
  r.part = rh.part;
3325
3615
  r.point = rh.point;
3326
3616
  r.exitEnergy = r.penetrated ? E * 0.5 : 0;
@@ -3334,7 +3624,7 @@ var GameTestRoom = (() => {
3334
3624
  const o = getObj(rh.object);
3335
3625
  const p = getPart(o, rh.part);
3336
3626
  const start = cur.length;
3337
- 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);
3338
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) });
3339
3629
  ignore.add(`${o.id}\0${p.spec.id}`);
3340
3630
  if (!out.penetrated || !out.exitPoint) break;
@@ -3704,6 +3994,8 @@ var GameTestRoom = (() => {
3704
3994
  stage: "damage stage",
3705
3995
  expose: "rebar bare",
3706
3996
  body: "body",
3997
+ blast: "blast",
3998
+ fragment_hit: "fragment hit",
3707
3999
  destroyed: "destroyed"
3708
4000
  },
3709
4001
  fractureStyles: { shatter: "shatter", crumble: "crumble", splinter: "splinter", tear: "tear", snap: "snap", none: "none" },
@@ -3790,6 +4082,8 @@ var GameTestRoom = (() => {
3790
4082
  stage: "est\xE1gio de dano",
3791
4083
  expose: "vergalh\xE3o \xE0 mostra",
3792
4084
  body: "corpo",
4085
+ blast: "explos\xE3o",
4086
+ fragment_hit: "estilha\xE7o atingiu",
3793
4087
  destroyed: "destru\xEDdo"
3794
4088
  },
3795
4089
  fractureStyles: { shatter: "estilha\xE7ar", crumble: "esfarelar", splinter: "lascar", tear: "rasgar", snap: "partir", none: "nenhuma" },
@@ -15699,6 +15993,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15699
15993
  if (options.visual !== false) this.playExplosion(ev, options);
15700
15994
  this.vfx.push(p, ev.radius, 4);
15701
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;
15702
15999
  for (const pc of this.pieceSys.list) {
15703
16000
  this.pieceSys.centerOf(pc, c);
15704
16001
  const dx = c.x - p[0];
@@ -15707,13 +16004,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15707
16004
  const d = Math.hypot(dx, dy, dz);
15708
16005
  if (d > ev.radius) continue;
15709
16006
  const fall = (1 - d / ev.radius) ** 2;
15710
- 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;
15711
16009
  const inv = d > 1e-4 ? 1 / d : 0;
15712
16010
  const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
15713
16011
  const spread2 = pc.halfExtents[0];
15714
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]);
15715
16013
  }
15716
- 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));
15717
16015
  this.sound("explode", p, clamp6(0.5 + ev.radius / 10, 0.5, 1));
15718
16016
  }
15719
16017
  playExplosion(ev, options) {
@@ -34349,6 +34647,16 @@ void main(){
34349
34647
  "ui.slow": "Slow motion 0.25\xD7",
34350
34648
  "ui.fast": "Time \xD710",
34351
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",
34352
34660
  "ui.quality": "Quality",
34353
34661
  "ui.quality.low": "low",
34354
34662
  "ui.quality.medium": "medium",
@@ -34674,6 +34982,16 @@ void main(){
34674
34982
  "ui.slow": "C\xE2mera lenta 0,25\xD7",
34675
34983
  "ui.fast": "Tempo \xD710",
34676
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",
34677
34995
  "ui.quality": "Qualidade",
34678
34996
  "ui.quality.low": "baixa",
34679
34997
  "ui.quality.medium": "m\xE9dia",
@@ -35396,6 +35714,16 @@ void main(){
35396
35714
 
35397
35715
  // src/explosives.ts
35398
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
+ };
35399
35727
  var toArray = (vector) => [vector.x, vector.y, vector.z];
35400
35728
  function createExplosives(context) {
35401
35729
  const { THREE, scene: scene2, camera: camera2, world, renderer, arsenal, translate, format, action } = context;
@@ -35542,17 +35870,27 @@ void main(){
35542
35870
  if (box2.isEmpty()) box2.expandByPoint(object.position);
35543
35871
  const at3 = mostExposedPoint(box2, detonation);
35544
35872
  if (!at3) continue;
35545
- 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);
35546
35876
  }
35547
35877
  return reached;
35548
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
+ };
35549
35885
  const detonate = (projectile, detonation) => {
35550
35886
  const weaponName = context.toolName(projectile.weapon);
35551
35887
  const reach = Math.max(detonation.injuryRadius, detonation.lethalRadius);
35552
- 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);
35553
35891
  if (detonation.energy > 0) {
35554
35892
  const isPlayed = context.effects.explosion(detonation.explosive, detonation.point, { direction: detonation.direction }) !== null;
35555
- renderer.explode(detonation.point, detonation.energy, reach, { visual: !isPlayed });
35893
+ renderer.explode(detonation.point, detonation.energy, reach, { visual: !isPlayed, report: results });
35556
35894
  }
35557
35895
  if (detonation.stunSeconds !== void 0) renderer.emit("flash", detonation.point, [0, 1, 0], 1, 1);
35558
35896
  const onPlayer = blastEffect(detonation, toArray(camera2.position));
@@ -35849,7 +36187,7 @@ void main(){
35849
36187
  }
35850
36188
 
35851
36189
  // src/metrics.ts
35852
- 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"];
35853
36191
  var WINDOW_S = 0.5;
35854
36192
  function createRoomMetrics(sources) {
35855
36193
  const wanted = new Set(sources.ids);
@@ -35886,6 +36224,12 @@ void main(){
35886
36224
  const render = ownsRender ? { calls, triangles } : gl.info?.render;
35887
36225
  put("draw_calls", render?.calls);
35888
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
+ }
35889
36233
  if (!["objects", "fragments", "particles", "decals"].some((id) => wanted.has(id))) return out;
35890
36234
  const destruction = sources.destruction.stats();
35891
36235
  const fluids = sources.fluids.stats();
@@ -37567,6 +37911,8 @@ void main(){
37567
37911
  o.onFast(fast);
37568
37912
  });
37569
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`);
37570
37916
  const qSel = mk2("select");
37571
37917
  for (const q of ["low", "medium", "high"]) {
37572
37918
  const op = mk2("option", void 0, t(`ui.quality.${q}`));
@@ -37771,6 +38117,7 @@ void main(){
37771
38117
  }
37772
38118
  slotsEl.forEach((b, i) => b.classList.toggle("on", i === s.slot));
37773
38119
  tapBtn.classList.toggle("on", s.tap);
38120
+ for (const { key: key2, b } of blastBtns) b.classList.toggle("on", s.blast[key2]);
37774
38121
  count.textContent = t("ui.objects", { n: s.objects });
37775
38122
  const ak = JSON.stringify(s.animations);
37776
38123
  if (ak !== lastAnim) {
@@ -38652,6 +38999,12 @@ void main(){
38652
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)) });
38653
39000
  return bodies;
38654
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
+ };
38655
39008
  const explosives = createExplosives({
38656
39009
  THREE,
38657
39010
  scene: scene2,
@@ -38666,6 +39019,10 @@ void main(){
38666
39019
  toolName,
38667
39020
  withoutHeld,
38668
39021
  groundAt,
39022
+ blast: () => blastOptions,
39023
+ blasted: (summary) => {
39024
+ lastBlast = summary;
39025
+ },
38669
39026
  translate: t,
38670
39027
  format: num,
38671
39028
  action,
@@ -39092,7 +39449,7 @@ void main(){
39092
39449
  let metricsAcc = 0;
39093
39450
  let ownsLoop = false;
39094
39451
  const roomMetricIds = options.metrics === false ? [] : options.metrics ?? ROOM_METRICS;
39095
- 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 });
39096
39453
  let qualityName = quality;
39097
39454
  const syncContainers = (sdt) => {
39098
39455
  for (const e of entries.values()) {
@@ -39211,6 +39568,7 @@ void main(){
39211
39568
  timeScale,
39212
39569
  fast: fastForward,
39213
39570
  tap: isTapOn(),
39571
+ blast: { shelter: blastOn("shelter"), fragmentHits: blastOn("fragmentHits"), confinement: blastOn("confinement") },
39214
39572
  quality: qualityName,
39215
39573
  objects: [...entries.values()].filter((e) => !e.arena).length
39216
39574
  };
@@ -39726,6 +40084,10 @@ void main(){
39726
40084
  heldTilt = rad;
39727
40085
  },
39728
40086
  toggleTap,
40087
+ blast: () => ({ ...blastOptions }),
40088
+ setBlast: (next) => {
40089
+ blastOptions = { ...blastOptions, ...next };
40090
+ },
39729
40091
  pause() {
39730
40092
  paused = true;
39731
40093
  },
@@ -39836,6 +40198,7 @@ void main(){
39836
40198
  setTool(loadout[index]);
39837
40199
  },
39838
40200
  onTap: () => toggleTap(),
40201
+ onBlast: (key) => toggleBlast(key),
39839
40202
  onPlay: (objectId, clipId) => {
39840
40203
  playAnimation(clipId, objectId);
39841
40204
  },