@adatechnology/game-test-room 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -134,6 +134,31 @@ var GameTestRoom = (() => {
134
134
  var positive = (n) => finite(n) && n > 0;
135
135
  var vec3 = (v) => Array.isArray(v) && v.length === 3 && v.every(finite);
136
136
  var nonNegative = (n) => n === void 0 || finite(n) && n >= 0;
137
+ var FACES = ["both", "+", "-"];
138
+ var BONDS = ["running", "stack"];
139
+ var checkStagedFields = (p, label, materials, problems) => {
140
+ const known = (m, what) => {
141
+ if (materials && !materials.has(m)) problems.push(`${label} ${what} has unknown material "${String(m)}"`);
142
+ };
143
+ (p?.layers ?? []).forEach((l, i) => {
144
+ known(l?.material, `layer ${i}`);
145
+ if (!positive(l?.thickness)) problems.push(`${label} layer ${i} thickness must be finite and positive`);
146
+ if (!FACES.includes(l?.faces)) problems.push(`${label} layer ${i} has an invalid faces "${String(l?.faces)}"`);
147
+ });
148
+ const r = p?.reinforcement;
149
+ if (r) {
150
+ known(r.material, "reinforcement");
151
+ if (!positive(r.spacing) || !positive(r.diameter)) problems.push(`${label} reinforcement spacing and diameter must be finite and positive`);
152
+ if (r.residualCapacity !== void 0 && !(finite(r.residualCapacity) && r.residualCapacity >= 0 && r.residualCapacity <= 1)) problems.push(`${label} reinforcement residualCapacity must be between 0 and 1`);
153
+ }
154
+ const u = p?.units;
155
+ if (u) {
156
+ known(u.mortar, "units mortar");
157
+ if (!vec3(u.size) || !u.size.every(positive)) problems.push(`${label} units size must be three finite positive numbers`);
158
+ if (!BONDS.includes(u.bond)) problems.push(`${label} units has an invalid bond "${String(u.bond)}"`);
159
+ }
160
+ if (p?.breach && p.breach.area !== void 0 && !positive(p.breach.area)) problems.push(`${label} breach area must be finite and positive`);
161
+ };
137
162
  var checkSpec = (spec, options = {}) => {
138
163
  const problems = [];
139
164
  const warnings = [];
@@ -166,6 +191,7 @@ var GameTestRoom = (() => {
166
191
  if (!nonNegative(p.structure.capacity)) problems.push(`${label} structure capacity must be a finite number >= 0`);
167
192
  }
168
193
  if (p?.contains && !nonNegative(p.contains.liters)) problems.push(`${label} contains.liters must be a finite number >= 0`);
194
+ checkStagedFields(p, label, materials, problems);
169
195
  }
170
196
  const jointIds = /* @__PURE__ */ new Set();
171
197
  const edges = [];
@@ -242,6 +268,32 @@ var GameTestRoom = (() => {
242
268
  });
243
269
  return { ok: problems.length === 0, problems };
244
270
  };
271
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
272
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
273
+ var deepMerge = (base, patch) => {
274
+ if (!patch) return base;
275
+ const result2 = { ...base };
276
+ for (const [key, value] of Object.entries(patch)) {
277
+ if (value === void 0 || UNSAFE_KEYS.has(key)) continue;
278
+ const current = result2[key];
279
+ result2[key] = isPlainObject(current) && isPlainObject(value) ? deepMerge(current, value) : value;
280
+ }
281
+ return result2;
282
+ };
283
+ var mergeById = (shipped, extra) => {
284
+ const entries = /* @__PURE__ */ new Map();
285
+ for (const entry of [...shipped, ...extra ?? []]) entries.set(entry.id, entry);
286
+ return entries;
287
+ };
288
+ var applyPatches = (entries, patches) => {
289
+ for (const [id, patch] of Object.entries(patches ?? {})) {
290
+ const entry = entries.get(id);
291
+ if (entry) entries.set(id, deepMerge(entry, patch));
292
+ }
293
+ };
294
+ var removeIds = (entries, ids) => {
295
+ for (const id of ids ?? []) entries.delete(id);
296
+ };
245
297
  var SNAKE_CASE = /^[a-z][a-z0-9_]*$/;
246
298
  var finite2 = (n) => typeof n === "number" && Number.isFinite(n);
247
299
  function checkControl(control, problems) {
@@ -691,6 +743,147 @@ var GameTestRoom = (() => {
691
743
  if (out.length === 0) out.push({ center: [0, 0, 0], size: [dims[u], dims[v], th], q: frame(u, v, t), shape: "shard", weight: 1 });
692
744
  return out;
693
745
  };
746
+ var planeAxes = (thin) => thin === 1 ? [0, 2] : [thin === 0 ? 2 : 0, 1];
747
+ var unitGrid = (dims, thin, units, cap) => {
748
+ const [u, v] = planeAxes(thin);
749
+ let unitU = Math.min(units.size[0], dims[u]), unitV = Math.min(units.size[1], dims[v]);
750
+ const rowsFor = (h) => Math.ceil(dims[v] / h - 1e-9);
751
+ const colsFor = (w) => Math.ceil(dims[u] / w - 1e-9) + (units.bond === "running" ? 1 : 0);
752
+ const merge = Math.ceil(Math.sqrt(rowsFor(unitV) * colsFor(unitU) / cap));
753
+ if (merge > 1) {
754
+ unitU = Math.min(unitU * merge, dims[u]);
755
+ unitV = Math.min(unitV * merge, dims[v]);
756
+ }
757
+ const cells = [];
758
+ for (let row = 0; row < rowsFor(unitV); row++) {
759
+ const v0 = -dims[v] / 2 + row * unitV, v1 = Math.min(v0 + unitV, dims[v] / 2);
760
+ const shift = units.bond === "running" && row % 2 === 1 ? unitU / 2 : 0;
761
+ for (let col = 0; col < colsFor(unitU); col++) {
762
+ const u0 = Math.max(-dims[u] / 2 + col * unitU - shift, -dims[u] / 2), u1 = Math.min(-dims[u] / 2 + (col + 1) * unitU - shift, dims[u] / 2);
763
+ if (u1 - u0 < 0.01) continue;
764
+ const center = [0, 0, 0], size = [0, 0, 0];
765
+ center[u] = (u0 + u1) / 2;
766
+ center[v] = (v0 + v1) / 2;
767
+ size[u] = u1 - u0;
768
+ size[v] = v1 - v0;
769
+ size[thin] = dims[thin];
770
+ cells.push({ center, size });
771
+ }
772
+ }
773
+ return cells;
774
+ };
775
+ var planeDistance = (cell, thin, point) => {
776
+ const [u, v] = planeAxes(thin);
777
+ const du = cell.center[u] - point[u], dv = cell.center[v] - point[v];
778
+ return Math.sqrt(du * du + dv * dv);
779
+ };
780
+ var cellsWithin = (cells, thin, point, radius) => {
781
+ const inside = cells.filter((c) => planeDistance(c, thin, point) <= radius);
782
+ if (inside.length > 0 || cells.length === 0) return inside;
783
+ return [cells.reduce((best, c) => planeDistance(c, thin, point) < planeDistance(best, thin, point) ? c : best)];
784
+ };
785
+ var cellsOutside = (cells, thin, point, radius) => cells.filter((c) => planeDistance(c, thin, point) > radius);
786
+ var MAX_MARKS_PER_FRAGMENT = 12;
787
+ var marksInBox = (center, size, rotation, holes) => {
788
+ const inverse = rotation ? quat.conjugate(rotation) : void 0;
789
+ const marks = [];
790
+ for (const hole of holes) {
791
+ const offset = [hole.point[0] - center[0], hole.point[1] - center[1], hole.point[2] - center[2]];
792
+ const point = inverse ? rotateVec(inverse, offset) : offset;
793
+ const inside = point.every((value, k) => Math.abs(value) <= size[k] / 2 + hole.diameter / 2);
794
+ if (!inside) continue;
795
+ marks.push({ point, normal: inverse ? rotateVec(inverse, hole.normal) : hole.normal, diameter: hole.diameter });
796
+ if (marks.length >= MAX_MARKS_PER_FRAGMENT) break;
797
+ }
798
+ return marks;
799
+ };
800
+ var MAX_SPALLS = 64;
801
+ var thinAxisOf = (dims) => dims[0] <= dims[1] && dims[0] <= dims[2] ? 0 : dims[1] <= dims[2] ? 1 : 2;
802
+ var resolveLayer = (def, index, mat) => ({ index, def, mat, energy: mat.penetration * def.thickness * 1e3 });
803
+ var onFace = (layer, face) => layer.def.faces === "both" || layer.def.faces === (face > 0 ? "+" : "-");
804
+ var isSpalled = (spalls, layer, face, point) => {
805
+ for (const s of spalls ?? []) {
806
+ if (s[0] !== layer || s[1] !== face) continue;
807
+ const dx = s[2] - point[0], dy = s[3] - point[1], dz = s[4] - point[2];
808
+ if (dx * dx + dy * dy + dz * dz <= s[5] * s[5]) return true;
809
+ }
810
+ return false;
811
+ };
812
+ var outerLayer = (layers, spalls, face, point) => layers.find((l) => onFace(l, face) && !isSpalled(spalls, l.index, face, point));
813
+ var layersEnergy = (layers, spalls, face, point) => {
814
+ let sum2 = 0;
815
+ for (const l of layers) if (onFace(l, face) && !isSpalled(spalls, l.index, face, point)) sum2 += l.energy;
816
+ return sum2;
817
+ };
818
+ var layersThickness = (layers, face) => {
819
+ let sum2 = 0;
820
+ for (const l of layers) if (onFace(l, face)) sum2 += l.def.thickness;
821
+ return sum2;
822
+ };
823
+ var COVER = 0.03;
824
+ var MAX_BARS_PER_DIRECTION = 24;
825
+ var MAX_MEMBER_BARS_PER_SIDE = 6;
826
+ var STEEL_DENSITY = 7850;
827
+ var spread = (length3, cover, spacing, cap) => {
828
+ const room = Math.max(0, length3 - 2 * cover);
829
+ const count = Math.min(cap, Math.max(2, Math.floor(room / spacing) + 1));
830
+ const step = count > 1 && room / (count - 1) < spacing ? room / (count - 1) : spacing;
831
+ return Array.from({ length: count }, (_, i) => (i - (count - 1) / 2) * step);
832
+ };
833
+ var bar = (axis, along, fixed) => {
834
+ const a = [0, 0, 0], b = [0, 0, 0];
835
+ for (const [index, value] of fixed) {
836
+ a[index] = value;
837
+ b[index] = value;
838
+ }
839
+ a[axis] = -along;
840
+ b[axis] = along;
841
+ return [a, b];
842
+ };
843
+ var isMember = (dims) => {
844
+ const [a = 0, b = 0, c = 0] = [...dims].sort((x, y) => x - y);
845
+ return b / a < 3 && c / b >= 3;
846
+ };
847
+ var rebarBars = (dims, thin, spacing) => {
848
+ const out = [];
849
+ if (isMember(dims)) {
850
+ const long = dims.indexOf(Math.max(...dims));
851
+ const [u2 = 0, v2 = 0] = [0, 1, 2].filter((i) => i !== long);
852
+ const us = spread(dims[u2], COVER, spacing, MAX_MEMBER_BARS_PER_SIDE);
853
+ const vs = spread(dims[v2], COVER, spacing, MAX_MEMBER_BARS_PER_SIDE);
854
+ us.forEach((x, i) => vs.forEach((y, j) => {
855
+ if (i === 0 || j === 0 || i === us.length - 1 || j === vs.length - 1) out.push(bar(long, dims[long] / 2 - 0.02, [[u2, x], [v2, y]]));
856
+ }));
857
+ return out;
858
+ }
859
+ const [u = 0, v = 0] = [0, 1, 2].filter((i) => i !== thin);
860
+ for (const y of spread(dims[v], COVER, spacing, MAX_BARS_PER_DIRECTION)) out.push(bar(u, dims[u] / 2 - 0.02, [[v, y], [thin, 0]]));
861
+ for (const x of spread(dims[u], COVER, spacing, MAX_BARS_PER_DIRECTION)) out.push(bar(v, dims[v] / 2 - 0.02, [[u, x], [thin, 0]]));
862
+ return out;
863
+ };
864
+ var clipBarsToDisc = (bars, u, v, center, radius) => {
865
+ const out = [];
866
+ for (const [a, b] of bars) {
867
+ const axis = a[u] !== b[u] ? u : v;
868
+ const other = axis === u ? v : u;
869
+ const off = a[other] - center[other === u ? 0 : 1];
870
+ if (Math.abs(off) >= radius) continue;
871
+ const half = Math.sqrt(radius * radius - off * off);
872
+ const mid = center[axis === u ? 0 : 1];
873
+ const lo = Math.max(Math.min(a[axis], b[axis]), mid - half), hi = Math.min(Math.max(a[axis], b[axis]), mid + half);
874
+ if (hi - lo < 1e-6) continue;
875
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
876
+ p[axis] = lo;
877
+ q[axis] = hi;
878
+ out.push([p, q]);
879
+ }
880
+ return out;
881
+ };
882
+ var rebarMass = (bars, diameter, density = STEEL_DENSITY) => {
883
+ let length3 = 0;
884
+ for (const [a, b] of bars) length3 += Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]);
885
+ return Math.PI * (diameter / 2) * (diameter / 2) * length3 * density;
886
+ };
694
887
  var materials_default = [
695
888
  {
696
889
  id: "steel",
@@ -1692,9 +1885,19 @@ var GameTestRoom = (() => {
1692
1885
  var DMG_PER_J = 0.02;
1693
1886
  var BLAST_COUPLING = 0.1;
1694
1887
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
1888
+ var BREACH_RADIUS = 0.55;
1889
+ var BREACH_BLAST_FACTOR = 6;
1890
+ var BREACH_AREA = 0.25;
1891
+ var RESIDUAL_CAPACITY = 0.15;
1892
+ var UNIT_FRAGMENT_FACTOR = 4;
1893
+ var BREACH_GRID_CAP = 4096;
1695
1894
  var CUT_SLICE = 1;
1696
1895
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
1697
1896
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
1897
+ var capacityScale = (p) => {
1898
+ if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
1899
+ return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
1900
+ };
1698
1901
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
1699
1902
  var clone = (v) => structuredClone(v);
1700
1903
  var overloadRate = (stress) => Math.min(0.6, 0.12 + 0.8 * (stress - 1));
@@ -1717,6 +1920,8 @@ var GameTestRoom = (() => {
1717
1920
  const maxDents = options.maxDents ?? 24;
1718
1921
  const maxHoles = options.maxHoles ?? 32;
1719
1922
  const fireSpread = options.fireSpread ?? 0.6;
1923
+ const breachRadius = options.breach?.radius ?? BREACH_RADIUS;
1924
+ const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
1720
1925
  const rng = createRng(options.seed ?? 1);
1721
1926
  const rnd = () => rng.next();
1722
1927
  let time = 0;
@@ -1821,6 +2026,13 @@ var GameTestRoom = (() => {
1821
2026
  const maxHp = Math.max(1, ps.hp ?? mass0 * mat.hpPerKg);
1822
2027
  const fuel0 = mat.thermal.ignitionC !== void 0 ? mass0 * (mat.thermal.fuelFraction ?? 0) : 0;
1823
2028
  const c = carry?.parts.get(ps.id);
2029
+ const staged = !!(ps.layers?.length || ps.reinforcement || ps.units || ps.breach);
2030
+ const layers = (ps.layers ?? []).map((l, i) => {
2031
+ const lm = materials.get(l.material);
2032
+ if (!lm) throw new Error(`unknown material "${l.material}" on layer ${i} of part "${ps.id}"`);
2033
+ return resolveLayer(l, i, lm);
2034
+ });
2035
+ if (ps.reinforcement && !materials.has(ps.reinforcement.material)) throw new Error(`unknown material "${ps.reinforcement.material}" on the reinforcement of part "${ps.id}"`);
1824
2036
  const pst = c?.st ?? {
1825
2037
  id: ps.id,
1826
2038
  material: ps.material,
@@ -1836,7 +2048,8 @@ var GameTestRoom = (() => {
1836
2048
  dents: [],
1837
2049
  holes: [],
1838
2050
  cracks: [],
1839
- group: 0
2051
+ group: 0,
2052
+ ...staged ? { stage: 0 } : {}
1840
2053
  };
1841
2054
  if (!c && ps.contains) pst.liters = ps.contains.liters;
1842
2055
  const p = {
@@ -1855,7 +2068,10 @@ var GameTestRoom = (() => {
1855
2068
  rt: c?.rt ?? { ...freshRuntime(), prevTemp: pst.temperature },
1856
2069
  structural: !!ps.structure && ps.structure.role !== "none",
1857
2070
  anchored: !!ps.anchored || ps.structure?.role === "foundation",
1858
- cap: ps.structure ? partCapacity(ps, mat) : 0
2071
+ cap: ps.structure ? partCapacity(ps, mat) : 0,
2072
+ staged,
2073
+ thin: thinAxisOf(shapeDims(ps.shape)),
2074
+ layers
1859
2075
  };
1860
2076
  if (p.structural) o.hasStructure = true;
1861
2077
  if (p.rt.chainDue !== void 0 && !isLost(p)) primed.set(p, o);
@@ -1995,7 +2211,7 @@ var GameTestRoom = (() => {
1995
2211
  o.unsup[i] = o.anyAnchor && !isLost(p);
1996
2212
  continue;
1997
2213
  }
1998
- p.st.stress = p.st.load / p.cap;
2214
+ p.st.stress = p.st.load / (p.cap * capacityScale(p));
1999
2215
  if (p.st.stress > o.maxStress) o.maxStress = p.st.stress;
2000
2216
  }
2001
2217
  if (o.maxStress > 0.8) o.active = true;
@@ -2014,6 +2230,7 @@ var GameTestRoom = (() => {
2014
2230
  for (const p of ps) {
2015
2231
  o.collapsed.add(p.spec.id);
2016
2232
  p.st.status = "collapsed";
2233
+ setStage(o, p, 3, {}, src);
2017
2234
  }
2018
2235
  emit({ ...base(o, src), type: "collapse", parts: ps.map((p) => p.spec.id), mass: ps.reduce((sum2, p) => sum2 + p.st.mass, 0) });
2019
2236
  }
@@ -2067,6 +2284,7 @@ var GameTestRoom = (() => {
2067
2284
  if (isLost(p) || ignore?.has(`${o.id}\0${p.spec.id}`)) continue;
2068
2285
  const r = rayPart(o, p, origin, d);
2069
2286
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
2287
+ if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
2070
2288
  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 };
2071
2289
  }
2072
2290
  }
@@ -2175,6 +2393,7 @@ var GameTestRoom = (() => {
2175
2393
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
2176
2394
  markDamaged(p);
2177
2395
  emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
2396
+ setStage(o, p, 1, {}, src);
2178
2397
  return hole;
2179
2398
  };
2180
2399
  const addCrack = (o, p, wp, wn, length3, src) => {
@@ -2183,6 +2402,7 @@ var GameTestRoom = (() => {
2183
2402
  if (p.st.cracks.length > 16) p.st.cracks.shift();
2184
2403
  markDamaged(p);
2185
2404
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
2405
+ setStage(o, p, 1, {}, src);
2186
2406
  };
2187
2407
  const debris = (o, p, kind, point, normal, count, speed, src) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed });
2188
2408
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
@@ -2231,6 +2451,7 @@ var GameTestRoom = (() => {
2231
2451
  if (split && mine.length) amount /= mine.length;
2232
2452
  for (const j of mine) {
2233
2453
  if (j.st.open) continue;
2454
+ if (j.spec.kind === "rebar" && (o.parts[j.ia].spec.reinforcement || o.parts[j.ib].spec.reinforcement)) continue;
2234
2455
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
2235
2456
  j.st.hp -= amount;
2236
2457
  if (j.st.hp <= 0) {
@@ -2244,23 +2465,172 @@ var GameTestRoom = (() => {
2244
2465
  }
2245
2466
  }
2246
2467
  };
2468
+ const setStage = (o, p, stage, extra = {}, src) => {
2469
+ if (!p.staged || (p.st.stage ?? 0) >= stage) return;
2470
+ p.st.stage = stage;
2471
+ emit({ ...base(o, src), type: "stage", part: p.spec.id, stage, ...extra });
2472
+ };
2473
+ const faceOf = (o, p, worldNormal) => rotateVec(quat.conjugate(partXf(o, p).q), worldNormal)[p.thin] >= 0 ? 1 : -1;
2474
+ const spallOuter = (o, p, wp, face, normal, radius, src) => {
2475
+ var _a;
2476
+ if (p.layers.length === 0) return;
2477
+ const lp = objLocal(o, wp);
2478
+ const layer = outerLayer(p.layers, p.rt.spalls, face, lp);
2479
+ if (!layer) return;
2480
+ const spalls = (_a = p.rt).spalls ?? (_a.spalls = []);
2481
+ spalls.push([layer.index, face, lp[0], lp[1], lp[2], radius]);
2482
+ if (spalls.length > MAX_SPALLS) spalls.shift();
2483
+ markDamaged(p);
2484
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
2485
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
2486
+ setStage(o, p, 1, {}, src);
2487
+ };
2488
+ const insideOpening = (o, p, objectPoint) => {
2489
+ const op = p.st.opening;
2490
+ if (!op) return false;
2491
+ const [u, v] = planeAxes(p.thin);
2492
+ const lp = toLocal(p.pos, p.q, objectPoint);
2493
+ const lc = toLocal(p.pos, p.q, op.center);
2494
+ return Math.hypot(lp[u] - lc[u], lp[v] - lc[v]) <= op.radius;
2495
+ };
2496
+ const breachAreaOf = (p, damage, blast) => damage / (p.mat.hpPerKg * p.mat.density) / Math.max(p.thickness, 5e-3) * (blast ? breachBlastFactor : 1);
2497
+ const localHolesOf = (p) => p.st.holes.map((h) => ({ point: toLocal(p.pos, p.q, h.point), normal: rotateVec(quat.conjugate(p.q), h.normal), diameter: h.diameter }));
2498
+ const unitsOf = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
2499
+ const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
2500
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
2501
+ const { c, q } = partXf(o, p);
2502
+ const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
2503
+ const [u, v] = planeAxes(p.thin);
2504
+ const faceArea = (cell) => cell.size[u] * cell.size[v];
2505
+ const total = cells.reduce((sum2, cell) => sum2 + faceArea(cell), 0) || 1;
2506
+ const shape = p.spec.units ? "chunk" : FRAGMENT_SHAPE[p.mat.fracture] ?? "chunk";
2507
+ return cells.map((cell, i) => {
2508
+ const cw = toWorld(c, q, cell.center);
2509
+ const out = vec.normalize(vec.sub(cw, hitW), [0, 1, 0]);
2510
+ const dv = dir ? vec.normalize(vec.madd(out, dir, 0.6)) : out;
2511
+ const k = speed * (0.6 + 0.8 * rnd());
2512
+ const marks = marksInBox(cell.center, cell.size, void 0, holes);
2513
+ return {
2514
+ id: `${p.spec.id}#${tag}${i}`,
2515
+ part: p.spec.id,
2516
+ material: p.spec.material,
2517
+ shape,
2518
+ center: cw,
2519
+ size: [cell.size[0], cell.size[1], cell.size[2]],
2520
+ rotation: q,
2521
+ velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
2522
+ angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
2523
+ mass: mass * faceArea(cell) / total,
2524
+ ...marks.length > 0 ? { marks } : {}
2525
+ };
2526
+ });
2527
+ };
2528
+ const worldBars = (o, p, bars) => {
2529
+ const { c, q } = partXf(o, p);
2530
+ return bars.map(([a, b]) => [toWorld(c, q, a), toWorld(c, q, b)]);
2531
+ };
2532
+ const addBreach = (o, p, wp, area, energy, dir, src) => {
2533
+ var _a;
2534
+ if (!p.staged || area <= 0 || (p.st.stage ?? 0) >= 2 || p.st.stripped || isLost(p)) return;
2535
+ const lp = objLocal(o, wp);
2536
+ const list = (_a = p.rt).breach ?? (_a.breach = []);
2537
+ list.push([lp[0], lp[1], lp[2], area]);
2538
+ if (list.length > MAX_SPALLS) list.shift();
2539
+ let total = 0;
2540
+ const sum2 = [0, 0, 0];
2541
+ for (const [x, y, z, a] of list) {
2542
+ if (vec.distance([x, y, z], lp) > breachRadius) continue;
2543
+ total += a;
2544
+ sum2[0] += x * a;
2545
+ sum2[1] += y * a;
2546
+ sum2[2] += z * a;
2547
+ }
2548
+ if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
2549
+ openBreach(o, p, [sum2[0] / total, sum2[1] / total, sum2[2] / total], total, energy, dir, src);
2550
+ };
2551
+ const openBreach = (o, p, center, total, energy, dir, src) => {
2552
+ const [u, v] = planeAxes(p.thin);
2553
+ const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
2554
+ const lc = toLocal(p.pos, p.q, center);
2555
+ lc[p.thin] = 0;
2556
+ const { c, q } = partXf(o, p);
2557
+ const worldCenter = toWorld(c, q, lc);
2558
+ if (2 * radius >= Math.min(p.dims[u], p.dims[v])) {
2559
+ failPart(o, p, worldCenter, dir, energy, 12, src);
2560
+ return;
2561
+ }
2562
+ const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP), p.thin, lc, radius);
2563
+ const removed = p.st.mass * cells.reduce((sum2, cell) => sum2 + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
2564
+ const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
2565
+ const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
2566
+ const holes = localHolesOf(p);
2567
+ p.st.holes = p.st.holes.filter((_, i) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[i]]).length > 0));
2568
+ p.st.mass = Math.max(0, p.st.mass - removed);
2569
+ p.rt.removed = (p.rt.removed ?? 0) + removed;
2570
+ p.st.opening = { center: objLocal(o, worldCenter), radius };
2571
+ markDamaged(p);
2572
+ setStage(o, p, 1, {}, src);
2573
+ setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
2574
+ debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
2575
+ const reinforcement = p.spec.reinforcement;
2576
+ if (reinforcement) {
2577
+ const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
2578
+ if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
2579
+ }
2580
+ if (p.structural) solveStructure(o);
2581
+ };
2582
+ const stripPart = (o, p, reinforcement, src) => {
2583
+ const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
2584
+ p.st.stripped = true;
2585
+ p.st.hp = p.st.maxHp;
2586
+ p.st.mass = rebarMass(bars, reinforcement.diameter, materials.get(reinforcement.material)?.density);
2587
+ p.st.holes = [];
2588
+ p.st.burning = false;
2589
+ p.rt.jet = false;
2590
+ markDamaged(p);
2591
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
2592
+ solveStructure(o);
2593
+ collapseCheck(o, src);
2594
+ };
2595
+ const unitFragmentPlan = (p, cap) => {
2596
+ const grid = unitGrid(p.dims, p.thin, unitsOf(p), cap);
2597
+ const op = p.st.opening;
2598
+ const cells = op ? cellsOutside(grid, p.thin, toLocal(p.pos, p.q, op.center), op.radius) : grid;
2599
+ const [u, v] = planeAxes(p.thin);
2600
+ return cells.map((cell) => ({ center: cell.center, size: cell.size, q: quat.identity(), shape: "chunk", weight: cell.size[u] * cell.size[v] }));
2601
+ };
2602
+ const loseCage = (o, p, src) => {
2603
+ p.st.status = "broken";
2604
+ p.st.hp = 0;
2605
+ p.st.mass = 0;
2606
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: [] });
2607
+ debris(o, p, "sparks", partXf(o, p).c, [0, 1, 0], 6, 3, src);
2608
+ partLost(o, p, src);
2609
+ };
2247
2610
  const failPart = (o, p, origin, dir, fragEnergy, cap, src) => {
2248
2611
  if (isLost(p)) return;
2612
+ if (p.st.stripped) {
2613
+ loseCage(o, p, src);
2614
+ return;
2615
+ }
2249
2616
  const ex = p.spec.explodes;
2250
2617
  const style = p.mat.fracture === "none" ? "snap" : p.mat.fracture;
2251
2618
  const { c, q } = partXf(o, p);
2252
2619
  const hitW = origin ?? c;
2253
2620
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
2254
- const plan = planFragments({
2621
+ const hitLocal = toLocal(c, q, hitW);
2622
+ const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
2623
+ const plan = unitPlan.length > 0 ? unitPlan : planFragments({
2255
2624
  style,
2256
2625
  dims: p.dims,
2257
2626
  thickness: p.thickness,
2258
2627
  grain,
2259
- hit: toLocal(c, q, hitW),
2628
+ hit: hitLocal,
2260
2629
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
2261
2630
  maxFragments,
2262
2631
  rnd
2263
2632
  });
2633
+ const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
2264
2634
  const mass = p.st.mass;
2265
2635
  const totW = plan.reduce((s, f) => s + f.weight, 0) || 1;
2266
2636
  const energy = ex ? ex.energy * 0.2 : fragEnergy;
@@ -2270,6 +2640,7 @@ var GameTestRoom = (() => {
2270
2640
  const out = vec.normalize(vec.sub(cw, hitW), [0, 1, 0]);
2271
2641
  const dv = dir ? vec.normalize(vec.madd(out, dir, 0.6)) : out;
2272
2642
  const k = speed * (0.6 + 0.8 * rnd());
2643
+ const marks = marksInBox(f.center, f.size, f.q, holes);
2273
2644
  return {
2274
2645
  id: `${p.spec.id}#${i}`,
2275
2646
  part: p.spec.id,
@@ -2280,16 +2651,25 @@ var GameTestRoom = (() => {
2280
2651
  rotation: quat.mul(q, f.q),
2281
2652
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
2282
2653
  angularVelocity: [(rnd() - 0.5) * 12, (rnd() - 0.5) * 12, (rnd() - 0.5) * 12],
2283
- mass: mass * f.weight / totW
2654
+ mass: mass * f.weight / totW,
2655
+ ...marks.length > 0 ? { marks } : {}
2284
2656
  };
2285
2657
  });
2286
- p.st.status = "broken";
2287
- p.st.hp = 0;
2288
- p.st.mass = 0;
2289
- p.st.burning = false;
2290
- p.rt.jet = false;
2658
+ const reinforcement = p.spec.reinforcement;
2659
+ if (!reinforcement) {
2660
+ p.st.status = "broken";
2661
+ p.st.hp = 0;
2662
+ p.st.mass = 0;
2663
+ p.st.burning = false;
2664
+ p.rt.jet = false;
2665
+ }
2291
2666
  emit({ ...base(o, src), type: "fracture", part: p.spec.id, style, fragments, point: hitW });
2292
2667
  debris(o, p, p.mat.breakDebris, c, [0, 1, 0], clamp2(Math.round(Math.cbrt(partVolume(p.spec.shape)) * 30), 3, 20), 2, src);
2668
+ setStage(o, p, 3, {}, src);
2669
+ if (reinforcement) {
2670
+ stripPart(o, p, reinforcement, src);
2671
+ return;
2672
+ }
2293
2673
  if (ex) {
2294
2674
  emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius });
2295
2675
  blasts.push({ point: c, energy: ex.energy, radius: ex.radius, source: src });
@@ -2323,10 +2703,17 @@ var GameTestRoom = (() => {
2323
2703
  };
2324
2704
  const ballistic2 = (o, p, entry, n, d, E, caliber, pathLen, src) => {
2325
2705
  const mat = p.mat;
2706
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
2707
+ return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
2708
+ }
2326
2709
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
2327
2710
  const angle = Math.asin(sin) * 180 / Math.PI;
2328
- const effM = p.thickness / Math.max(sin, 0.3);
2329
- const thr = mat.penetration * effM * 1e3;
2711
+ const slant = Math.max(sin, 0.3);
2712
+ const effM = p.thickness / slant;
2713
+ const face = p.layers.length > 0 ? faceOf(o, p, n) : 1;
2714
+ const exitLocal = objLocal(o, vec.madd(entry, d, Math.min(pathLen, effM)));
2715
+ 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;
2716
+ const thr = coats > 0 ? (mat.penetration * p.thickness * 1e3 + coats) / slant : mat.penetration * effM * 1e3;
2330
2717
  const penetrated = E > thr;
2331
2718
  const scale2 = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2332
2719
  const id = p.spec.id;
@@ -2337,13 +2724,16 @@ var GameTestRoom = (() => {
2337
2724
  const dmg2 = applyDamage(o, p, E * DMG_PER_J * scale2 * 0.2, "ballistic", entry, d, E * 0.1, 12, src);
2338
2725
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
2339
2726
  }
2727
+ if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
2340
2728
  const brittle = mat.ductility < 0.3;
2341
2729
  const ratio = E / Math.max(thr, 1e-6);
2342
2730
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
2343
2731
  let exitPoint;
2344
2732
  if (penetrated) {
2345
- exitPoint = vec.madd(entry, d, Math.min(pathLen, effM));
2733
+ exitPoint = vec.madd(entry, d, Math.min(pathLen, effM + layersThickness(p.layers, face) / slant + layersThickness(p.layers, -face) / slant));
2346
2734
  const hole = addHole(o, p, entry, n, caliber / 1e3 * (mat.ductility >= 0.3 ? 1 : 1.5), true, exitPoint, src);
2735
+ if (p.layers.length > 0) spallOuter(o, p, exitPoint, -face, vec.scale(n, -1), Math.max(0.03, caliber / 1e3 * 4), src);
2736
+ addBreach(o, p, entry, Math.PI * (hole.diameter / 2) * (hole.diameter / 2), E, d, src);
2347
2737
  if (brittle && mat.fracture === "shatter") addCrack(o, p, entry, n, crackLen, src);
2348
2738
  if (p.spec.contains && (p.st.liters ?? 0) > 0) {
2349
2739
  emit({
@@ -2388,8 +2778,11 @@ var GameTestRoom = (() => {
2388
2778
  }
2389
2779
  if (kind === "blunt") wearJoints(o, p, E * 0.3, void 0, 0, src);
2390
2780
  else wearJoints(o, p, E * 0.3, point, 0.06, src);
2781
+ if (kind === "blunt" && E > 0 && p.layers.length > 0) spallOuter(o, p, point, faceOf(o, p, n), n, Math.max(contactR, 0.05), src);
2391
2782
  const scale2 = mat.damageScale?.[kind] ?? 1;
2392
- return applyDamage(o, p, Ec * DMG_PER_J * scale2 * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
2783
+ const done = applyDamage(o, p, Ec * DMG_PER_J * scale2 * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
2784
+ addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
2785
+ return done;
2393
2786
  };
2394
2787
  const blastObject = (o, point, energy, radius, src) => {
2395
2788
  const start = cur.length;
@@ -2425,7 +2818,10 @@ var GameTestRoom = (() => {
2425
2818
  }
2426
2819
  }
2427
2820
  wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2428
- res.damage += applyDamage(o, p, Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1), "explosive", point, null, Ein * 0.3, 40, src);
2821
+ 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);
2822
+ const dealt = applyDamage(o, p, Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1), "explosive", point, null, Ein * 0.3, 40, src);
2823
+ res.damage += dealt;
2824
+ addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2429
2825
  }
2430
2826
  res.events = cur.slice(start);
2431
2827
  return res;
@@ -2696,7 +3092,7 @@ var GameTestRoom = (() => {
2696
3092
  }
2697
3093
  o.active = hot;
2698
3094
  };
2699
- const spread = (sources, dt) => {
3095
+ const spread2 = (sources, dt) => {
2700
3096
  for (const src of sources) {
2701
3097
  if (!src.p.st.burning && !src.p.rt.jet) continue;
2702
3098
  const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.pos);
@@ -2997,7 +3393,7 @@ var GameTestRoom = (() => {
2997
3393
  time += dt;
2998
3394
  const sources = [];
2999
3395
  for (const o of [...objects.values()]) if (o.active) stepObject(o, dt, sources);
3000
- if (sources.length) spread(sources, dt);
3396
+ if (sources.length) spread2(sources, dt);
3001
3397
  detonatePrimed(time);
3002
3398
  for (const im of [...impls.values()]) {
3003
3399
  if (!im.impl.update) continue;
@@ -3036,7 +3432,7 @@ var GameTestRoom = (() => {
3036
3432
  counter,
3037
3433
  objects: [...objects.values()].map((o) => {
3038
3434
  const runtime = { parts: {}, joints: {}, collapsed: [...o.collapsed] };
3039
- for (const p of o.parts) runtime.parts[p.spec.id] = { ...p.rt };
3435
+ for (const p of o.parts) runtime.parts[p.spec.id] = clone(p.rt);
3040
3436
  for (const j of o.joints) runtime.joints[j.spec.id] = j.step;
3041
3437
  return { id: o.id, spec: clone(o.spec), state: clone(o.st), runtime };
3042
3438
  }),
@@ -3053,7 +3449,7 @@ var GameTestRoom = (() => {
3053
3449
  const state = clone(e.state);
3054
3450
  const carry = {
3055
3451
  parts: new Map(
3056
- state.parts.map((st) => [st.id, { st, rt: e.runtime?.parts[st.id] ? { ...e.runtime.parts[st.id] } : { ...freshRuntime(), prevTemp: st.temperature } }])
3452
+ state.parts.map((st) => [st.id, { st, rt: e.runtime?.parts[st.id] ? clone(e.runtime.parts[st.id]) : { ...freshRuntime(), prevTemp: st.temperature } }])
3057
3453
  ),
3058
3454
  joints: new Map(state.joints.map((st) => [st.id, { st, step: e.runtime?.joints[st.id] ?? 0 }])),
3059
3455
  collapsed: e.runtime?.collapsed
@@ -3250,6 +3646,10 @@ var GameTestRoom = (() => {
3250
3646
  primed: "about to explode",
3251
3647
  stress: "overload",
3252
3648
  collapse: "collapsed",
3649
+ spall: "plaster came off",
3650
+ stage: "damage stage",
3651
+ expose: "rebar bare",
3652
+ body: "body",
3253
3653
  destroyed: "destroyed"
3254
3654
  },
3255
3655
  fractureStyles: { shatter: "shatter", crumble: "crumble", splinter: "splinter", tear: "tear", snap: "snap", none: "none" },
@@ -3332,6 +3732,10 @@ var GameTestRoom = (() => {
3332
3732
  primed: "prestes a explodir",
3333
3733
  stress: "sobrecarga",
3334
3734
  collapse: "desabou",
3735
+ spall: "reboco soltou",
3736
+ stage: "est\xE1gio de dano",
3737
+ expose: "vergalh\xE3o \xE0 mostra",
3738
+ body: "corpo",
3335
3739
  destroyed: "destru\xEDdo"
3336
3740
  },
3337
3741
  fractureStyles: { shatter: "estilha\xE7ar", crumble: "esfarelar", splinter: "lascar", tear: "rasgar", snap: "partir", none: "nenhuma" },
@@ -3396,6 +3800,8 @@ var GameTestRoom = (() => {
3396
3800
  __publicField(this, "v");
3397
3801
  __publicField(this, "w");
3398
3802
  __publicField(this, "rng", createRng2(1374496523));
3803
+ __publicField(this, "markGeometry", null);
3804
+ __publicField(this, "markMaterial", null);
3399
3805
  this.box = new THREE.BoxGeometry(1, 1, 1);
3400
3806
  this.q = new THREE.Quaternion();
3401
3807
  this.dq = new THREE.Quaternion();
@@ -3442,6 +3848,25 @@ var GameTestRoom = (() => {
3442
3848
  }
3443
3849
  return g;
3444
3850
  }
3851
+ /**
3852
+ * Bullet holes that stay on a fragment (`Fragment.marks`): dark discs that are children of its mesh, so they fall, spin and settle with it. The mesh is
3853
+ * a unit box scaled to the fragment, so each disc is scaled back; exact for a mark on a face, which is where a bullet hole is.
3854
+ */
3855
+ addMarks(mesh, f) {
3856
+ if (!f.marks?.length) return;
3857
+ this.markGeometry ?? (this.markGeometry = new this.THREE.CircleGeometry(1, 10));
3858
+ this.markMaterial ?? (this.markMaterial = new this.THREE.MeshBasicMaterial({ color: 723465, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
3859
+ const sc = mesh.scale;
3860
+ for (const mark of f.marks) {
3861
+ const disc = new this.THREE.Mesh(this.markGeometry, this.markMaterial);
3862
+ const radius = Math.max(4e-3, mark.diameter) * 0.6;
3863
+ const n = this.v.set(mark.normal[0], mark.normal[1], mark.normal[2]).normalize();
3864
+ disc.position.set(mark.point[0] / sc.x + n.x * 2e-3 / sc.x, mark.point[1] / sc.y + n.y * 2e-3 / sc.y, mark.point[2] / sc.z + n.z * 2e-3 / sc.z);
3865
+ disc.quaternion.setFromUnitVectors(this.w.set(0, 0, 1), n);
3866
+ disc.scale.set(radius / sc.x, radius / sc.y, radius / sc.z);
3867
+ mesh.add(disc);
3868
+ }
3869
+ }
3445
3870
  /** Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget. */
3446
3871
  spawn(f, material, home, maxAllowed) {
3447
3872
  const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
@@ -3475,6 +3900,8 @@ var GameTestRoom = (() => {
3475
3900
  alive--;
3476
3901
  }
3477
3902
  }
3903
+ this.addMarks(mesh, f);
3904
+ return mesh;
3478
3905
  }
3479
3906
  /** Add velocity to every fragment within `radius` of a point (blasts). */
3480
3907
  blast(point, radius, speed) {
@@ -3598,6 +4025,8 @@ var GameTestRoom = (() => {
3598
4025
  dispose() {
3599
4026
  this.clear();
3600
4027
  this.box.dispose();
4028
+ this.markGeometry?.dispose();
4029
+ this.markMaterial?.dispose();
3601
4030
  for (const g of this.shardGeos) g?.dispose();
3602
4031
  this.shardGeos = [];
3603
4032
  for (const m of this.materials.values()) m.dispose();
@@ -4342,6 +4771,76 @@ void main() {
4342
4771
  }
4343
4772
  }
4344
4773
  };
4774
+ var RebarCages = class {
4775
+ constructor(THREE, scene, radius = 4e-3) {
4776
+ __publicField(this, "THREE", THREE);
4777
+ __publicField(this, "scene", scene);
4778
+ __publicField(this, "cages", /* @__PURE__ */ new Map());
4779
+ __publicField(this, "geometry");
4780
+ __publicField(this, "material");
4781
+ __publicField(this, "up");
4782
+ __publicField(this, "dir");
4783
+ __publicField(this, "mid");
4784
+ __publicField(this, "q");
4785
+ __publicField(this, "scale");
4786
+ __publicField(this, "m");
4787
+ this.geometry = new THREE.CylinderGeometry(radius, radius, 1, 6);
4788
+ this.material = new THREE.MeshStandardMaterial({ color: 7628380, roughness: 0.55, metalness: 0.6 });
4789
+ this.up = new THREE.Vector3(0, 1, 0);
4790
+ this.dir = new THREE.Vector3();
4791
+ this.mid = new THREE.Vector3();
4792
+ this.q = new THREE.Quaternion();
4793
+ this.scale = new THREE.Vector3();
4794
+ this.m = new THREE.Matrix4();
4795
+ }
4796
+ /** Bars drawn now, over every part. */
4797
+ get count() {
4798
+ let n = 0;
4799
+ for (const mesh of this.cages.values()) n += mesh.count;
4800
+ return n;
4801
+ }
4802
+ /** Meshes of the live cages (tests and picking). */
4803
+ meshes() {
4804
+ return [...this.cages.values()];
4805
+ }
4806
+ /** Draw `bars` (world space) as the cage of `key`; an empty list removes it. */
4807
+ set(key, bars) {
4808
+ this.remove(key);
4809
+ if (bars.length === 0) return;
4810
+ const mesh = new this.THREE.InstancedMesh(this.geometry, this.material, bars.length);
4811
+ bars.forEach(([a, b], i) => {
4812
+ this.dir.set(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
4813
+ const length3 = this.dir.length();
4814
+ this.mid.set((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2);
4815
+ this.q.setFromUnitVectors(this.up, length3 > 1e-9 ? this.dir.divideScalar(length3) : this.up);
4816
+ this.m.compose(this.mid, this.q, this.scale.set(1, Math.max(length3, 1e-4), 1));
4817
+ mesh.setMatrixAt(i, this.m);
4818
+ });
4819
+ mesh.instanceMatrix.needsUpdate = true;
4820
+ mesh.castShadow = true;
4821
+ this.scene.add(mesh);
4822
+ this.cages.set(key, mesh);
4823
+ }
4824
+ remove(key) {
4825
+ const mesh = this.cages.get(key);
4826
+ if (!mesh) return;
4827
+ this.scene.remove(mesh);
4828
+ mesh.dispose();
4829
+ this.cages.delete(key);
4830
+ }
4831
+ /** Remove the cages of every part of `object` (keys are `object|part`). */
4832
+ removeObject(object) {
4833
+ for (const key of [...this.cages.keys()]) if (key.startsWith(`${object}|`)) this.remove(key);
4834
+ }
4835
+ clear() {
4836
+ for (const key of [...this.cages.keys()]) this.remove(key);
4837
+ }
4838
+ dispose() {
4839
+ this.clear();
4840
+ this.geometry.dispose();
4841
+ this.material.dispose();
4842
+ }
4843
+ };
4345
4844
  function partHintOf(value) {
4346
4845
  if (typeof value === "string") return value ? { id: value } : null;
4347
4846
  if (!value || typeof value !== "object") return null;
@@ -4540,6 +5039,7 @@ void main() {
4540
5039
  __publicField(this, "disc", null);
4541
5040
  __publicField(this, "discMat", null);
4542
5041
  __publicField(this, "lineMats", /* @__PURE__ */ new Map());
5042
+ __publicField(this, "patchMats", /* @__PURE__ */ new Map());
4543
5043
  }
4544
5044
  discGeometry() {
4545
5045
  return this.disc ?? (this.disc = new this.THREE.CircleGeometry(1, 14));
@@ -4555,6 +5055,15 @@ void main() {
4555
5055
  polygonOffsetUnits: -4
4556
5056
  }));
4557
5057
  }
5058
+ /** Opaque patch in the colour of what shows under a spalled coat. */
5059
+ patchMaterial(color) {
5060
+ let m = this.patchMats.get(color);
5061
+ if (!m) {
5062
+ m = new this.THREE.MeshBasicMaterial({ color, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
5063
+ this.patchMats.set(color, m);
5064
+ }
5065
+ return m;
5066
+ }
4558
5067
  lineMaterial(color) {
4559
5068
  let m = this.lineMats.get(color);
4560
5069
  if (!m) {
@@ -4567,6 +5076,8 @@ void main() {
4567
5076
  this.disc?.dispose();
4568
5077
  this.discMat?.dispose();
4569
5078
  for (const m of this.lineMats.values()) m.dispose();
5079
+ for (const m of this.patchMats.values()) m.dispose();
5080
+ this.patchMats.clear();
4570
5081
  this.disc = null;
4571
5082
  this.discMat = null;
4572
5083
  this.lineMats.clear();
@@ -4809,9 +5320,9 @@ void main() {
4809
5320
  const n = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
4810
5321
  const n2 = (Math.imul(v + 7, 40503) >>> 0) % 1e3 / 1e3;
4811
5322
  const lowness = 1 - (work[i + 1] - minY) / h;
4812
- const spread = 1 + 0.12 * melt * (0.4 + n2) * (0.3 + lowness);
4813
- arr[i] = work[i] * spread;
4814
- arr[i + 2] = work[i + 2] * spread;
5323
+ const spread2 = 1 + 0.12 * melt * (0.4 + n2) * (0.3 + lowness);
5324
+ arr[i] = work[i] * spread2;
5325
+ arr[i + 2] = work[i + 2] * spread2;
4815
5326
  arr[i + 1] = Math.max(minY, work[i + 1] - n * melt * h * 0.08);
4816
5327
  }
4817
5328
  }
@@ -4857,6 +5368,16 @@ void main() {
4857
5368
  disc.renderOrder = 2;
4858
5369
  this.addDecal(mv, { obj: disc });
4859
5370
  }
5371
+ /** A patch where a surface coat came off, in the colour of the core under it; drawn under the bullet holes. */
5372
+ addSpall(mv, worldPoint, worldNormal, radius, color) {
5373
+ const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 15e-4);
5374
+ const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.patchMaterial(color));
5375
+ disc.position.copy(pos);
5376
+ disc.quaternion.copy(quat2);
5377
+ disc.scale.setScalar(Math.max(0.01, radius) / s);
5378
+ disc.renderOrder = 1;
5379
+ this.addDecal(mv, { obj: disc });
5380
+ }
4860
5381
  /** A star of jagged lines: `rays` rays up to `length` metres, seeded so the same event always draws the same star. */
4861
5382
  addCrack(mv, worldPoint, worldNormal, rays, length3, color, seed) {
4862
5383
  const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 2e-3);
@@ -5380,6 +5901,7 @@ void main() {
5380
5901
  __publicField(this, "fragments");
5381
5902
  __publicField(this, "ground");
5382
5903
  __publicField(this, "pieceSys");
5904
+ __publicField(this, "cages");
5383
5905
  __publicField(this, "active", /* @__PURE__ */ new Map());
5384
5906
  __publicField(this, "fires", /* @__PURE__ */ new Map());
5385
5907
  __publicField(this, "leaks", []);
@@ -5431,6 +5953,7 @@ void main() {
5431
5953
  max: this.maxFragments,
5432
5954
  life: o.fragmentLife ?? 12
5433
5955
  });
5956
+ this.cages = new RebarCages(THREE, o.scene);
5434
5957
  this.pieceSys = new PieceSystem(THREE, {
5435
5958
  groundAt: this.groundAt,
5436
5959
  gravity: this.gravity,
@@ -5732,6 +6255,15 @@ void main() {
5732
6255
  this.sound("ignite", ev.point, 0.7);
5733
6256
  }
5734
6257
  break;
6258
+ case "spall":
6259
+ this.onSpall(ev);
6260
+ break;
6261
+ case "stage":
6262
+ this.onStage(ev);
6263
+ break;
6264
+ case "expose":
6265
+ this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
6266
+ break;
5735
6267
  case "extinguish": {
5736
6268
  const f = this.fires.get(`${ev.object}|${ev.part}`);
5737
6269
  if (f) {
@@ -5897,6 +6429,43 @@ void main() {
5897
6429
  }
5898
6430
  this.sound(impactKey(def), ev.worldPoint, 0.5);
5899
6431
  }
6432
+ /** Outward normal of the face of `mv` nearest to a world point: the thin axis of its world box (right for a wall; a sloped face is left to the vertex-range binding). */
6433
+ faceNormal(mv, point) {
6434
+ const box2 = this.visuals.worldBox(mv, this.box);
6435
+ const size = box2.getSize(this.v1);
6436
+ const axis = size.x <= size.y && size.x <= size.z ? "x" : size.y <= size.z ? "y" : "z";
6437
+ const side = point[axis] >= (box2.min[axis] + box2.max[axis]) / 2 ? 1 : -1;
6438
+ return new this.THREE.Vector3(axis === "x" ? side : 0, axis === "y" ? side : 0, axis === "z" ? side : 0);
6439
+ }
6440
+ /** A coat came off: a patch in the colour of the core under it, below the bullet holes and cracks. */
6441
+ onSpall(ev) {
6442
+ const pv = this.partVis(ev.object, ev.part);
6443
+ if (!pv) return;
6444
+ const wp = new this.THREE.Vector3(ev.point[0], ev.point[1], ev.point[2]);
6445
+ const mv = this.nearestMesh(pv, wp);
6446
+ if (!mv) return;
6447
+ const def = this.world.materials.get(pv.material);
6448
+ this.visuals.addSpall(mv, wp, this.faceNormal(mv, wp), Math.sqrt(ev.area / Math.PI), fallbackColor(pv.material, def));
6449
+ }
6450
+ /**
6451
+ * Stage 2 of a staged part: the unit-sized fragments come out and the part stays drawn (hiding it is what `fracture` does). The opening itself
6452
+ * (cutting the wall's faces) is not drawn yet: it needs the per-vertex binding of the house renderer.
6453
+ */
6454
+ onStage(ev) {
6455
+ if (!ev.fragments || !ev.opening) return;
6456
+ const pv = this.partVis(ev.object, ev.part);
6457
+ const def = pv ? this.world.materials.get(pv.material) : void 0;
6458
+ const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
6459
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
6460
+ const step = this.low ? 2 : 1;
6461
+ for (let i = 0; i < ev.fragments.length; i += step) {
6462
+ const fr = ev.fragments[i];
6463
+ const fdef = this.world.materials.get(fr.material);
6464
+ this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), false), ev.object, maxAlive);
6465
+ }
6466
+ this.particles.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
6467
+ this.sound(impactKey(def), ev.opening.center, 0.8);
6468
+ }
5900
6469
  onFracture(ev) {
5901
6470
  const b = this.bounds.get(ev.object);
5902
6471
  const pv = b?.parts.get(ev.part);
@@ -6053,8 +6622,8 @@ void main() {
6053
6622
  const speed = Math.min(30, Math.sqrt(2 * ev.energy * 0.02 / pc.mass)) * fall;
6054
6623
  const inv = d > 1e-4 ? 1 / d : 0;
6055
6624
  const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
6056
- const spread = pc.halfExtents[0];
6057
- 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) * spread, c.y + spread * 0.5, c.z + (this.rng() - 0.5) * spread]);
6625
+ const spread2 = pc.halfExtents[0];
6626
+ 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]);
6058
6627
  }
6059
6628
  this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(ev.energy) * 0.08));
6060
6629
  this.sound("explode", p, clamp3(0.5 + ev.radius / 10, 0.5, 1));
@@ -6207,6 +6776,7 @@ void main() {
6207
6776
  for (const home of homes) {
6208
6777
  this.fragments.removeHome(home);
6209
6778
  this.ground.removeHome(home);
6779
+ this.cages.removeObject(home);
6210
6780
  }
6211
6781
  }
6212
6782
  restoreHome(home) {
@@ -6243,6 +6813,7 @@ void main() {
6243
6813
  }
6244
6814
  this.fragments.clear();
6245
6815
  this.ground.clear();
6816
+ this.cages.clear();
6246
6817
  this.particles.clear();
6247
6818
  if (this.light) {
6248
6819
  this.light.intensity = 0;
@@ -6253,6 +6824,9 @@ void main() {
6253
6824
  stats() {
6254
6825
  return { fragments: this.fragments.count, particles: this.particles.live, pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
6255
6826
  }
6827
+ rebarCount() {
6828
+ return this.cages.count;
6829
+ }
6256
6830
  dispose() {
6257
6831
  if (this.disposed) return;
6258
6832
  this.unsubscribe();
@@ -6260,6 +6834,7 @@ void main() {
6260
6834
  for (const id of [...this.bounds.keys()]) this.unbind(id);
6261
6835
  this.fragments.dispose();
6262
6836
  this.ground.dispose();
6837
+ this.cages.dispose();
6263
6838
  this.particles.dispose();
6264
6839
  this.visuals.dispose();
6265
6840
  this.light?.dispose?.();
@@ -17340,6 +17915,207 @@ void main() {
17340
17915
  if (context.isCompatible && !context.isCompatible(def, attachment, fitted)) return { ok: false, reason: "rejected" };
17341
17916
  return { ok: true, mount: place.kind };
17342
17917
  };
17918
+ var melee_weapons_default = [
17919
+ {
17920
+ id: "tactical_knife",
17921
+ name: "Tactical knife",
17922
+ category: "melee_blade",
17923
+ action: {
17924
+ kind: "melee",
17925
+ damage: "slash",
17926
+ energy: 45,
17927
+ radius: 0.03,
17928
+ reach: 0.6,
17929
+ rpm: 100,
17930
+ noise: 1
17931
+ },
17932
+ mass: 0.3
17933
+ },
17934
+ {
17935
+ id: "meat_cleaver",
17936
+ name: "Meat cleaver",
17937
+ category: "melee_blade",
17938
+ action: {
17939
+ kind: "melee",
17940
+ damage: "slash",
17941
+ energy: 120,
17942
+ radius: 0.05,
17943
+ reach: 0.6,
17944
+ rpm: 70,
17945
+ noise: 2
17946
+ },
17947
+ mass: 0.6
17948
+ },
17949
+ {
17950
+ id: "hatchet",
17951
+ name: "Hatchet",
17952
+ category: "melee_blade",
17953
+ action: {
17954
+ kind: "melee",
17955
+ damage: "slash",
17956
+ energy: 180,
17957
+ radius: 0.06,
17958
+ reach: 0.7,
17959
+ rpm: 55,
17960
+ noise: 2
17961
+ },
17962
+ mass: 0.8
17963
+ },
17964
+ {
17965
+ id: "nail_bat",
17966
+ name: "Nail bat",
17967
+ category: "melee_blunt",
17968
+ action: {
17969
+ kind: "melee",
17970
+ damage: "blunt",
17971
+ energy: 170,
17972
+ radius: 0.1,
17973
+ reach: 1,
17974
+ rpm: 60,
17975
+ noise: 3
17976
+ },
17977
+ mass: 1.2
17978
+ },
17979
+ {
17980
+ id: "iron_pipe",
17981
+ name: "Iron pipe",
17982
+ category: "melee_blunt",
17983
+ action: {
17984
+ kind: "melee",
17985
+ damage: "blunt",
17986
+ energy: 140,
17987
+ radius: 0.06,
17988
+ reach: 1,
17989
+ rpm: 60,
17990
+ noise: 4
17991
+ },
17992
+ mass: 1.2
17993
+ },
17994
+ {
17995
+ id: "pickaxe",
17996
+ name: "Pickaxe",
17997
+ category: "melee_blade",
17998
+ action: {
17999
+ kind: "melee",
18000
+ damage: "slash",
18001
+ energy: 260,
18002
+ radius: 0.05,
18003
+ reach: 1,
18004
+ rpm: 40,
18005
+ noise: 4
18006
+ },
18007
+ mass: 2.8,
18008
+ tags: [
18009
+ "heavy"
18010
+ ]
18011
+ },
18012
+ {
18013
+ id: "sickle",
18014
+ name: "Sickle",
18015
+ category: "melee_blade",
18016
+ action: {
18017
+ kind: "melee",
18018
+ damage: "slash",
18019
+ energy: 70,
18020
+ radius: 0.05,
18021
+ reach: 1.1,
18022
+ rpm: 70,
18023
+ noise: 2
18024
+ },
18025
+ mass: 1.2
18026
+ },
18027
+ {
18028
+ id: "police_baton",
18029
+ name: "Police baton",
18030
+ category: "melee_blunt",
18031
+ action: {
18032
+ kind: "melee",
18033
+ damage: "blunt",
18034
+ energy: 70,
18035
+ radius: 0.04,
18036
+ reach: 0.7,
18037
+ rpm: 90,
18038
+ noise: 3
18039
+ },
18040
+ mass: 0.6
18041
+ },
18042
+ {
18043
+ id: "cast_iron_skillet",
18044
+ name: "Cast iron skillet",
18045
+ category: "melee_blunt",
18046
+ action: {
18047
+ kind: "melee",
18048
+ damage: "blunt",
18049
+ energy: 110,
18050
+ radius: 0.12,
18051
+ reach: 0.6,
18052
+ rpm: 60,
18053
+ noise: 8
18054
+ },
18055
+ mass: 1.2
18056
+ },
18057
+ {
18058
+ id: "improvised_spear",
18059
+ name: "Improvised spear",
18060
+ category: "melee_blade",
18061
+ action: {
18062
+ kind: "melee",
18063
+ damage: "slash",
18064
+ energy: 90,
18065
+ radius: 0.02,
18066
+ reach: 1.6,
18067
+ rpm: 50,
18068
+ noise: 1
18069
+ },
18070
+ mass: 1.1
18071
+ },
18072
+ {
18073
+ id: "chainsaw",
18074
+ name: "Chainsaw",
18075
+ category: "melee_blade",
18076
+ action: {
18077
+ kind: "melee",
18078
+ damage: "slash",
18079
+ energy: 400,
18080
+ radius: 0.1,
18081
+ reach: 0.9,
18082
+ rpm: 300,
18083
+ noise: 120
18084
+ },
18085
+ mass: 6,
18086
+ tags: [
18087
+ "heavy"
18088
+ ]
18089
+ },
18090
+ {
18091
+ id: "recurve_bow",
18092
+ name: "Recurve bow",
18093
+ category: "bow",
18094
+ action: {
18095
+ kind: "firearm",
18096
+ caliber: "arrow",
18097
+ rpm: 20,
18098
+ modes: [
18099
+ "single"
18100
+ ],
18101
+ magazine: 1,
18102
+ reload: 1.5,
18103
+ barrel: 1,
18104
+ spreadDeg: 0.4,
18105
+ bloomDeg: 0.4,
18106
+ recoveryDegPerS: 2,
18107
+ recoil: [
18108
+ 0.3,
18109
+ 0.1
18110
+ ],
18111
+ range: 40
18112
+ },
18113
+ mass: 1.2,
18114
+ tags: [
18115
+ "silent"
18116
+ ]
18117
+ }
18118
+ ];
17343
18119
  var findDuplicateIds = (kind, entries) => {
17344
18120
  const seen = /* @__PURE__ */ new Set();
17345
18121
  const problems = [];
@@ -17424,18 +18200,6 @@ void main() {
17424
18200
  ...[...explosives.values()].flatMap(checkExplosiveNumbers),
17425
18201
  ...checkRuleDefaults(rules)
17426
18202
  ];
17427
- var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
17428
- var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
17429
- var deepMerge = (base, patch) => {
17430
- if (!patch) return base;
17431
- const result2 = { ...base };
17432
- for (const [key, value] of Object.entries(patch)) {
17433
- if (value === void 0 || UNSAFE_KEYS.has(key)) continue;
17434
- const current = result2[key];
17435
- result2[key] = isPlainObject(current) && isPlainObject(value) ? deepMerge(current, value) : value;
17436
- }
17437
- return result2;
17438
- };
17439
18203
  var modesEn = { single: "single action", semi: "semi-automatic", burst: "burst", auto: "automatic", pump: "pump action", bolt: "bolt action", lever: "lever action", break: "break action", volley: "volley" };
17440
18204
  var categoriesEn = {
17441
18205
  pistol: "pistol",
@@ -17627,20 +18391,6 @@ void main() {
17627
18391
  if (explosive.fuseRange) return explosive.fuseRange[0] + (explosive.fuseRange[1] - explosive.fuseRange[0]) * random();
17628
18392
  return explosive.fuse;
17629
18393
  };
17630
- var mergeById = (shipped, extra) => {
17631
- const entries = /* @__PURE__ */ new Map();
17632
- for (const entry of [...shipped, ...extra ?? []]) entries.set(entry.id, entry);
17633
- return entries;
17634
- };
17635
- var applyPatches = (entries, patches) => {
17636
- for (const [id, patch] of Object.entries(patches ?? {})) {
17637
- const entry = entries.get(id);
17638
- if (entry) entries.set(id, deepMerge(entry, patch));
17639
- }
17640
- };
17641
- var removeIds = (entries, ids) => {
17642
- for (const id of ids ?? []) entries.delete(id);
17643
- };
17644
18394
  var resolveThrowables = (weapons, explosives) => {
17645
18395
  for (const [id, def] of weapons) {
17646
18396
  if (def.action.kind !== "throwable" || def.action.explosive === void 0) continue;
@@ -18864,8 +19614,8 @@ void main() {
18864
19614
  return energy * f * f;
18865
19615
  };
18866
19616
  const bloomNow = this.extraAt(t);
18867
- const spread = a.spreadDeg + bloomNow;
18868
- const aim = sampleCone(dir, spread, this.rng);
19617
+ const spread2 = a.spreadDeg + bloomNow;
19618
+ const aim = sampleCone(dir, spread2, this.rng);
18869
19619
  const pellets = cal.pellets ?? 1;
18870
19620
  const pelletSpread = pellets > 1 ? a.pelletSpreadDeg ?? 3 : 0;
18871
19621
  for (let p = 0; p < pellets; p++) {
@@ -18876,6 +19626,7 @@ void main() {
18876
19626
  caliber: cal.diameter,
18877
19627
  caliberId: cal.id,
18878
19628
  speed,
19629
+ mass: cal.bulletGrams,
18879
19630
  energyAt,
18880
19631
  pellet: p,
18881
19632
  weapon: this.def.id
@@ -19061,7 +19812,7 @@ void main() {
19061
19812
  var EXPLOSIVES = explosives_default;
19062
19813
  var ARCHETYPES = archetypes_default;
19063
19814
  var ARCHETYPE_IDS = ARCHETYPES.map((def) => def.id);
19064
- var WEAPONS = [...ARCHETYPES, ...firearms_default, ...launchers_default, ...explosive_weapons_default];
19815
+ var WEAPONS = [...ARCHETYPES, ...firearms_default, ...launchers_default, ...explosive_weapons_default, ...melee_weapons_default];
19065
19816
  var DEFAULT_LOADOUT = [
19066
19817
  "hand",
19067
19818
  "kick",
@@ -25509,19 +26260,19 @@ void main() {
25509
26260
  const dd = mk("dd");
25510
26261
  const text = mk("span", void 0, row.text);
25511
26262
  dd.appendChild(text);
25512
- let bar = null;
26263
+ let bar2 = null;
25513
26264
  if (row.ratio !== void 0) {
25514
- bar = mk("div", "tr-bar");
25515
- bar.appendChild(mk("i"));
25516
- dd.appendChild(bar);
26265
+ bar2 = mk("div", "tr-bar");
26266
+ bar2.appendChild(mk("i"));
26267
+ dd.appendChild(bar2);
25517
26268
  }
25518
26269
  metricList.append(mk("dt", void 0, row.name), dd);
25519
- metricNodes.set(row.id, { text, bar });
26270
+ metricNodes.set(row.id, { text, bar: bar2 });
25520
26271
  }
25521
26272
  };
25522
- const fillBar = (bar, ratio) => {
25523
- bar.className = `tr-bar${ratio > 1 ? " bad" : ratio >= 0.8 ? " warn" : ""}`;
25524
- const fill = bar.firstElementChild;
26273
+ const fillBar = (bar2, ratio) => {
26274
+ bar2.className = `tr-bar${ratio > 1 ? " bad" : ratio >= 0.8 ? " warn" : ""}`;
26275
+ const fill = bar2.firstElementChild;
25525
26276
  fill.style.width = `${Math.round(Math.max(0, Math.min(1, ratio)) * 100)}%`;
25526
26277
  };
25527
26278
  return {
@@ -25576,6 +26327,10 @@ void main() {
25576
26327
  .tr-search{width:100%;margin:0 0 6px;padding:5px 8px;border-radius:5px;border:1px solid #3a404c;background:#0f1116;color:#e8eaee;font:inherit}
25577
26328
  .tr-search::placeholder{color:#7d8594}
25578
26329
  .tr-item small{color:#7d8594;margin-left:4px}
26330
+ .tr-thumb{display:block;width:100%;height:26px;background:currentColor;-webkit-mask:var(--tr-thumb) center/contain no-repeat;mask:var(--tr-thumb) center/contain no-repeat}
26331
+ .tr-item{min-width:0}
26332
+ .tr-item .tr-btn.has-thumb{display:flex;flex-direction:column;gap:3px;min-width:0}.tr-item .tr-btn.has-thumb .tr-name{overflow:hidden;text-overflow:ellipsis}
26333
+ .tr-insp .tr-thumb{width:140px;height:28px;margin-bottom:3px}
25579
26334
  .tr-empty{color:#9aa3b2;padding:4px 2px;grid-column:1/-1}
25580
26335
  .tr-cross{position:absolute;left:50%;top:50%;width:18px;height:18px;margin:-9px 0 0 -9px}
25581
26336
  .tr-cross:before,.tr-cross:after{content:"";position:absolute;background:#fff;opacity:.85;box-shadow:0 0 2px #000}
@@ -25641,6 +26396,17 @@ void main() {
25641
26396
  if (text !== void 0) e.textContent = text;
25642
26397
  return e;
25643
26398
  };
26399
+ var thumbSource = (thumbnail) => {
26400
+ const source = thumbnail.trim().startsWith("<") ? `data:image/svg+xml,${encodeURIComponent(thumbnail.trim())}` : thumbnail.trim();
26401
+ return source.replace(/["\\\n\r]/g, (char) => encodeURIComponent(char));
26402
+ };
26403
+ var thumb = (thumbnail, credit) => {
26404
+ const e = mk2("span", "tr-thumb");
26405
+ if (credit) e.title = credit;
26406
+ else e.setAttribute("aria-hidden", "true");
26407
+ e.style.setProperty("--tr-thumb", `url("${thumbSource(thumbnail)}")`);
26408
+ return e;
26409
+ };
25644
26410
  function createUi(o) {
25645
26411
  const { t } = o;
25646
26412
  const style = mk2("style");
@@ -25794,7 +26560,8 @@ void main() {
25794
26560
  if (!found.length) items.appendChild(mk2("div", "tr-empty", t("ui.no_results")));
25795
26561
  for (const { item: it, group } of found) {
25796
26562
  const wrap = mk2("div", "tr-item");
25797
- const b = mk2("button", "tr-btn", it.name);
26563
+ const b = mk2("button", it.thumbnail ? "tr-btn has-thumb" : "tr-btn", it.thumbnail ? void 0 : it.name);
26564
+ if (it.thumbnail) b.append(thumb(it.thumbnail), mk2("span", "tr-name", it.name));
25798
26565
  if (searching) b.appendChild(mk2("small", void 0, group));
25799
26566
  b.type = "button";
25800
26567
  b.title = it.note ?? it.name;
@@ -25930,7 +26697,7 @@ void main() {
25930
26697
  };
25931
26698
  toggle.onclick = togglePanels;
25932
26699
  root.append(cross4, top, bottom, help, left, right, toggle);
25933
- const bar = (value, max, cls) => {
26700
+ const bar2 = (value, max, cls) => {
25934
26701
  const b = mk2("div", `tr-bar ${cls}`);
25935
26702
  const i = mk2("i");
25936
26703
  i.style.width = `${Math.max(0, Math.min(100, value / Math.max(1e-9, max) * 100))}%`;
@@ -25953,12 +26720,13 @@ void main() {
25953
26720
  insp.appendChild(mk2("div", void 0, t("ui.aim_at_something")));
25954
26721
  return;
25955
26722
  }
26723
+ if (i.thumbnail) insp.appendChild(thumb(i.thumbnail, i.thumbnailCredit));
25956
26724
  insp.appendChild(mk2("div", "t", `${i.object} \u203A ${i.part}`));
25957
26725
  insp.appendChild(row(t("ins.material"), i.material));
25958
26726
  insp.appendChild(row(t("ins.status"), i.status + (i.burning ? ` \xB7 ${t("ins.burning")}` : "")));
25959
26727
  insp.appendChild(row(t("ins.hp"), `${num(i.hp, 0)} / ${num(i.maxHp, 0)}`));
25960
26728
  const frac = i.hp / Math.max(1e-9, i.maxHp);
25961
- insp.appendChild(bar(i.hp, i.maxHp, frac < 0.25 ? "bad" : frac < 0.6 ? "warn" : "hp"));
26729
+ insp.appendChild(bar2(i.hp, i.maxHp, frac < 0.25 ? "bad" : frac < 0.6 ? "warn" : "hp"));
25962
26730
  insp.appendChild(row(t("ins.temperature"), `${num(i.temperature, 0)} \xB0C`));
25963
26731
  if (i.char > 0.01) insp.appendChild(row(t("ins.char"), `${num(i.char * 100, 0)} %`));
25964
26732
  if (i.melt > 0.01) insp.appendChild(row(t("ins.melt"), `${num(i.melt * 100, 0)} %`));
@@ -25969,7 +26737,7 @@ void main() {
25969
26737
  if (i.load !== void 0) insp.appendChild(row(t("ins.load"), `${num(i.load, 0)} kg`));
25970
26738
  if (i.stress !== void 0) {
25971
26739
  insp.appendChild(row(t("ins.stress"), `${num(i.stress * 100, 0)} %`));
25972
- insp.appendChild(bar(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
26740
+ insp.appendChild(bar2(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
25973
26741
  }
25974
26742
  if (i.joints.length) {
25975
26743
  insp.appendChild(mk2("div", void 0, t("ins.joints") + ":"));
@@ -27324,6 +28092,8 @@ void main() {
27324
28092
  stress: ext.stress,
27325
28093
  joints,
27326
28094
  note: e?.item?.note,
28095
+ thumbnail: e?.item?.thumbnail,
28096
+ thumbnailCredit: e?.item?.thumbnailCredit,
27327
28097
  distance: r.distance
27328
28098
  };
27329
28099
  };
@@ -27723,7 +28493,9 @@ void main() {
27723
28493
  fluid: ti.fluid,
27724
28494
  weapon: ti.weapon,
27725
28495
  impl: ti.impl,
27726
- animations: ti.animations
28496
+ animations: ti.animations,
28497
+ thumbnail: ti.thumbnail,
28498
+ thumbnailCredit: ti.thumbnailCredit
27727
28499
  };
27728
28500
  catalogMap.set(item.id, item);
27729
28501
  const at = catalog.findIndex((c) => c.id === item.id);