@adatechnology/game-destruction 0.8.2 → 0.10.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.
package/dist/index.d.ts CHANGED
@@ -62,6 +62,11 @@ interface MaterialDef {
62
62
  fracture: FractureStyle;
63
63
  /** Fragment size range in metres (default `[0.04, 0.2]`). */
64
64
  fragmentSize?: [number, number];
65
+ /**
66
+ * Thickness of one sheet, in metres, for a material that comes in sheets (paper). A `tear` part much thicker than a sheet is a stack of them (a book,
67
+ * a ream, a magazine) and comes apart as thin loose scraps (`Fragment.inner` for the pages) instead of slabs as thick as the part.
68
+ */
69
+ sheet?: number;
65
70
  /** Damage multipliers per damage type (default 1); `shelter` multiplies the resistance the material adds between a blast and a target (`blast.shelter`). */
66
71
  damageScale?: Partial<Record<DamageType | 'shelter', number>>;
67
72
  /** Debris emitted on a hit that does not break the part. */
@@ -79,6 +84,8 @@ interface MaterialDef {
79
84
  glows?: boolean;
80
85
  /** Transparent (glass): cracks are drawn as a star on the surface. */
81
86
  transparent?: boolean;
87
+ /** Colour of the inside, for the inner fragments and the hit debris (the pages of a book under its cover); default the colour of the part. */
88
+ innerColor?: number;
82
89
  };
83
90
  /** Free tags, e.g. `metal`, `organic`, `conductive`. */
84
91
  tags?: string[];
@@ -329,6 +336,12 @@ interface DestructionOptions {
329
336
  radius?: number;
330
337
  /** Multiplier of the breach area a blast makes over a blow of the same damage: a blast loads the whole crater (default 6). */
331
338
  blastFactor?: number;
339
+ /**
340
+ * Joules per kilogram of a unit (`PartSpec.units`: a brick, a block) that knock it out of its wall: shots and blows wear the unit they land on and
341
+ * a little the ones around it, and a unit that took this much comes out (`chip` event). Default 200 (a 4 kg brick: two 9 mm rounds, one
342
+ * rifle round); 0 turns it off.
343
+ */
344
+ chipEnergy?: number;
332
345
  };
333
346
  /**
334
347
  * Speed of the blast front in m/s. A glass part at distance `d` from an explosion breaks `d / blastFrontSpeed` seconds later, in `update`
@@ -497,8 +510,14 @@ interface PartRuntime {
497
510
  spalls?: Array<[number, number, number, number, number, number]>;
498
511
  /** Through damage so far: `[x, y, z, area]` in object space, newest last. */
499
512
  breach?: Array<[number, number, number, number]>;
513
+ /** Energy each unit of the part took so far (`breach.chipEnergy`), by its index in the unit grid; -1 when the unit came out. */
514
+ chips?: Record<number, number>;
500
515
  /** Mass in kg that left the part in a breach. */
501
516
  removed?: number;
517
+ /** Damage taken by each band across a long wooden part (`splinter`), by band index from its negative end: the band that runs out breaks it there. */
518
+ cuts?: Record<number, number>;
519
+ /** Where along its long axis (part space) the part broke across (`cuts`). */
520
+ cutAt?: number;
502
521
  /** Plastic rotation in radians a reinforced beam or slab took in bending (blasts add it up; `flexure.failRotationDeg` breaks it). */
503
522
  hinge?: number;
504
523
  }
@@ -450,6 +450,62 @@ var GameDestruction = (() => {
450
450
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
451
451
  };
452
452
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
453
+ var splinterBox = (inp, box, limit = inp.maxFragments) => {
454
+ const { rnd } = inp;
455
+ const dims = box.s;
456
+ const [fmin, fmax] = inp.fragmentSize;
457
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
458
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
459
+ const w = Math.max(fmin * 1.5, 0.01);
460
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
461
+ const maxLen = fmax * 4;
462
+ const cells = bsp(box, c, [a, b], rnd);
463
+ const pieces = [];
464
+ for (const cell of cells) {
465
+ const extra = rnd() < 0.4 ? 1 : 0;
466
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
467
+ for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
468
+ }
469
+ return pieces.slice(0, limit).map((p) => ({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] }));
470
+ };
471
+ var severAt = (inp, root, at, gap) => {
472
+ const dims = root.s;
473
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
474
+ const half = dims[L] / 2;
475
+ const lo = clamp(at - gap / 2, -half, half), hi = clamp(at + gap / 2, -half, half);
476
+ const out = [];
477
+ const stub = (from, to) => {
478
+ if (to - from < 0.02) return;
479
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
480
+ c[L] = (from + to) / 2;
481
+ sz[L] = to - from;
482
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
483
+ };
484
+ stub(-half, lo);
485
+ stub(hi, half);
486
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
487
+ band.c[L] = (lo + hi) / 2;
488
+ band.s[L] = Math.max(hi - lo, 0.01);
489
+ for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
490
+ return out;
491
+ };
492
+ var tearStack = (inp, root, t, u, v, n, sheet) => {
493
+ const { dims, rnd, maxFragments } = inp;
494
+ const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
495
+ const per = clamp(n, 3, Math.max(3, Math.floor(maxFragments / layers)));
496
+ const out = [];
497
+ for (let k = 0; k < layers; k++) {
498
+ const off = -dims[t] / 2 + (k + 0.5) * dims[t] / layers;
499
+ for (const b of bsp(root, per, [u, v], rnd)) {
500
+ if (out.length >= maxFragments) return out;
501
+ const c = [b.c[0], b.c[1], b.c[2]];
502
+ c[t] = off;
503
+ out.push({ center: c, size: [b.s[u], b.s[v], sheet], q: frame(u, v, t, (rnd() - 0.5) * 0.8), shape: "scrap", weight: b.s[u] * b.s[v], inner: out.length % 4 !== 0 });
504
+ }
505
+ }
506
+ return out;
507
+ };
508
+ var STACK_LAYER = 0.012;
453
509
  var planFragments = (inp) => {
454
510
  const { style, dims, rnd, maxFragments } = inp;
455
511
  const [fmin, fmax] = inp.fragmentSize;
@@ -471,27 +527,14 @@ var GameDestruction = (() => {
471
527
  return out;
472
528
  }
473
529
  if (style === "splinter") {
474
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
475
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
476
- const w = Math.max(fmin * 1.5, 0.01);
477
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
478
- const maxLen = fmax * 4;
479
- const cells = bsp(root, c, [a, b], rnd);
480
- const pieces = [];
481
- for (const cell of cells) {
482
- let list = [cell];
483
- const extra = rnd() < 0.4 ? 1 : 0;
484
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
485
- list = bsp(cell, k, [g], rnd);
486
- for (const p of list) pieces.push(p);
487
- }
488
- for (const p of pieces.slice(0, maxFragments)) {
489
- out.push({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] });
490
- }
530
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
531
+ for (const p of splinterBox(inp, root)) out.push(p);
491
532
  return out;
492
533
  }
493
534
  if (style === "tear") {
494
535
  const n2 = clamp(Math.round(dims[u] * dims[v] / (mean * mean)), 3, maxFragments);
536
+ const sheet = inp.sheet ?? 0;
537
+ if (sheet > 0 && dims[t] > sheet * 4) return tearStack(inp, root, t, u, v, n2, sheet);
495
538
  const th2 = Math.min(inp.thickness, dims[t]);
496
539
  for (const b of bsp(root, n2, [u, v], rnd)) {
497
540
  out.push({ center: [b.c[0], b.c[1], b.c[2]], size: [b.s[u], b.s[v], th2], q: frame(u, v, t), shape: "scrap", weight: b.s[u] * b.s[v] });
@@ -2137,6 +2180,7 @@ var GameDestruction = (() => {
2137
2180
  0.03,
2138
2181
  0.15
2139
2182
  ],
2183
+ sheet: 1e-3,
2140
2184
  damageScale: {
2141
2185
  slash: 3
2142
2186
  },
@@ -2152,7 +2196,8 @@ var GameDestruction = (() => {
2152
2196
  smoke: "grey"
2153
2197
  },
2154
2198
  visual: {
2155
- charColor: 1709072
2199
+ charColor: 1709072,
2200
+ innerColor: 15524048
2156
2201
  },
2157
2202
  tags: [
2158
2203
  "fabric",
@@ -2697,9 +2742,14 @@ var GameDestruction = (() => {
2697
2742
  var BREACH_AREA = 0.25;
2698
2743
  var BREACH_BLAST_REACH = 0.12;
2699
2744
  var RESIDUAL_CAPACITY = 0.15;
2745
+ var SEVER_BAND = 0.1;
2746
+ var SEVER_ASPECT = 3;
2747
+ var SEVER_BALLISTIC = 2;
2700
2748
  var SUPPORT_TOLERANCE = 0.05;
2701
2749
  var UNIT_FRAGMENT_FACTOR = 4;
2702
2750
  var BREACH_GRID_CAP = 4096;
2751
+ var CHIP_ENERGY = 200;
2752
+ var CHIP_SPLASH = 0.5;
2703
2753
  var CUT_SLICE = 1;
2704
2754
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2705
2755
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2743,6 +2793,7 @@ var GameDestruction = (() => {
2743
2793
  let windCfg = windOpts ? resolveWind(windOpts) : null;
2744
2794
  const ignitionRules = resolveIgnitionRules(options.ignition);
2745
2795
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
2796
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
2746
2797
  const rng = createRng(options.seed ?? 1);
2747
2798
  const rnd = () => rng.next();
2748
2799
  let time = 0;
@@ -3116,41 +3167,68 @@ var GameDestruction = (() => {
3116
3167
  markDamaged(p);
3117
3168
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
3118
3169
  };
3170
+ const freeSpan = (p, supports) => {
3171
+ if (supports.length === 0) return Infinity;
3172
+ const d = p.dims;
3173
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
3174
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
3175
+ let far = 0;
3176
+ for (let i = 0; i <= 16; i++) {
3177
+ const x = (i / 16 - 0.5) * d[L];
3178
+ let near = Infinity;
3179
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
3180
+ far = Math.max(far, near);
3181
+ }
3182
+ return 2 * far;
3183
+ };
3119
3184
  const solveStructure = (o) => {
3120
3185
  o.maxStress = 0;
3121
3186
  if (!o.hasStructure) return;
3122
3187
  const parts = o.parts;
3123
3188
  const n = parts.length;
3124
3189
  const dist = new Array(n).fill(-1);
3125
- const queue = [];
3126
3190
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
3127
3191
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
3128
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
3129
- dist[i] = 0;
3130
- queue.push(i);
3131
- }
3132
- for (let h = 0; h < queue.length; h++) {
3133
- const s = queue[h];
3134
- if (isPartition(s)) continue;
3135
- for (const j of o.byPart.get(parts[s].spec.id)) {
3136
- if (j.st.open || !j.bears) continue;
3137
- const r = j.ia === s ? j.ib : j.ia;
3138
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
3139
- dist[r] = dist[s] + 1;
3140
- queue.push(r);
3192
+ const out = /* @__PURE__ */ new Set();
3193
+ let queue = [];
3194
+ let lowerOf = /* @__PURE__ */ new Map();
3195
+ for (; ; ) {
3196
+ dist.fill(-1);
3197
+ queue = [];
3198
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
3199
+ dist[i] = 0;
3200
+ queue.push(i);
3141
3201
  }
3142
- }
3143
- const incoming = new Array(n).fill(0);
3144
- const lowerOf = /* @__PURE__ */ new Map();
3145
- for (const i of queue) {
3146
- const lower = [];
3147
- for (const j of o.byPart.get(parts[i].spec.id)) {
3148
- if (j.st.open || !j.bears) continue;
3149
- const k = restsOn(o, j, i);
3150
- if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
3202
+ for (let h = 0; h < queue.length; h++) {
3203
+ const s = queue[h];
3204
+ if (isPartition(s)) continue;
3205
+ for (const j of o.byPart.get(parts[s].spec.id)) {
3206
+ if (j.st.open || !j.bears) continue;
3207
+ const r = j.ia === s ? j.ib : j.ia;
3208
+ if (dist[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
3209
+ dist[r] = dist[s] + 1;
3210
+ queue.push(r);
3211
+ }
3212
+ }
3213
+ lowerOf = /* @__PURE__ */ new Map();
3214
+ let dropped = false;
3215
+ for (const i of queue) {
3216
+ const lower = [];
3217
+ for (const j of o.byPart.get(parts[i].spec.id)) {
3218
+ if (j.st.open || !j.bears) continue;
3219
+ const k = restsOn(o, j, i);
3220
+ if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
3221
+ }
3222
+ lowerOf.set(i, lower);
3223
+ const max = parts[i].spec.structure?.maxSpan;
3224
+ if (max !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max) {
3225
+ out.add(i);
3226
+ dropped = true;
3227
+ }
3151
3228
  }
3152
- lowerOf.set(i, lower);
3229
+ if (!dropped) break;
3153
3230
  }
3231
+ const incoming = new Array(n).fill(0);
3154
3232
  const bending = new Array(n).fill(0);
3155
3233
  const through = parts.map(() => []);
3156
3234
  const pass = (k, at, mass) => {
@@ -3662,6 +3740,7 @@ var GameDestruction = (() => {
3662
3740
  const r = rayPart(o, p, origin, d);
3663
3741
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
3664
3742
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
3743
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
3665
3744
  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 };
3666
3745
  }
3667
3746
  }
@@ -3925,6 +4004,62 @@ var GameDestruction = (() => {
3925
4004
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
3926
4005
  openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
3927
4006
  };
4007
+ const unitGrids = /* @__PURE__ */ new WeakMap();
4008
+ const gridOf = (p) => {
4009
+ let grid = unitGrids.get(p);
4010
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP));
4011
+ return grid;
4012
+ };
4013
+ const insideChip = (p, objectPoint) => {
4014
+ const cells = p.st.chipped;
4015
+ if (!cells?.length) return false;
4016
+ const [u, v] = planeAxes(p.thin);
4017
+ const lp = toLocal(p.pos, p.q, objectPoint);
4018
+ return cells.some((c) => Math.abs(lp[u] - c.center[u]) <= c.size[u] / 2 && Math.abs(lp[v] - c.center[v]) <= c.size[v] / 2);
4019
+ };
4020
+ const chipUnits = (o, p, wp, energy, dir, radius, src) => {
4021
+ var _a, _b;
4022
+ if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4023
+ const [u, v] = planeAxes(p.thin);
4024
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4025
+ const grid = gridOf(p);
4026
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
4027
+ const out = [];
4028
+ for (let i = 0; i < grid.length; i++) {
4029
+ const cell = grid[i];
4030
+ if (worn[i] === -1) continue;
4031
+ const gap = Math.hypot(Math.max(0, Math.abs(lp[u] - cell.center[u]) - cell.size[u] / 2), Math.max(0, Math.abs(lp[v] - cell.center[v]) - cell.size[v] / 2));
4032
+ if (gap > radius) continue;
4033
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
4034
+ const total = (worn[i] ?? 0) + energy * share;
4035
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4036
+ if (total < chipEnergy * mass) {
4037
+ worn[i] = total;
4038
+ continue;
4039
+ }
4040
+ worn[i] = -1;
4041
+ out.push(cell);
4042
+ }
4043
+ if (out.length === 0) return;
4044
+ const { c, q } = partXf(o, p);
4045
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4046
+ const holes = localHolesOf(p);
4047
+ for (const cell of out) {
4048
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
4049
+ const center = toWorld(c, q, cell.center);
4050
+ const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
4051
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
4052
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
4053
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
4054
+ p.st.mass = Math.max(0, p.st.mass - mass);
4055
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
4056
+ markDamaged(p);
4057
+ setStage(o, p, 1, {}, src);
4058
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
4059
+ debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4060
+ }
4061
+ for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4062
+ };
3928
4063
  const openBreach = (o, p, center, total, energy, dir, src) => {
3929
4064
  const [u, v] = planeAxes(p.thin);
3930
4065
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -3936,7 +4071,8 @@ var GameDestruction = (() => {
3936
4071
  failPart(o, p, worldCenter, dir, energy, 12, src);
3937
4072
  return;
3938
4073
  }
3939
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP), p.thin, lc, radius);
4074
+ const worn = p.rt.chips;
4075
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
3940
4076
  const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
3941
4077
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
3942
4078
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -4020,6 +4156,8 @@ var GameDestruction = (() => {
4020
4156
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
4021
4157
  const hitLocal = toLocal(c, q, hitW);
4022
4158
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4159
+ const axis = style === "splinter" ? severAxis(p) : -1;
4160
+ const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
4023
4161
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4024
4162
  style,
4025
4163
  dims: p.dims,
@@ -4028,7 +4166,9 @@ var GameDestruction = (() => {
4028
4166
  hit: hitLocal,
4029
4167
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4030
4168
  maxFragments,
4031
- rnd
4169
+ rnd,
4170
+ sheet: p.mat.sheet,
4171
+ ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
4032
4172
  });
4033
4173
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
4034
4174
  const mass = p.st.mass;
@@ -4052,7 +4192,8 @@ var GameDestruction = (() => {
4052
4192
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
4053
4193
  angularVelocity: [(rnd() - 0.5) * 12, (rnd() - 0.5) * 12, (rnd() - 0.5) * 12],
4054
4194
  mass: mass * f.weight / totW,
4055
- ...marks.length > 0 ? { marks } : {}
4195
+ ...marks.length > 0 ? { marks } : {},
4196
+ ...f.inner ? { inner: true } : {}
4056
4197
  };
4057
4198
  });
4058
4199
  const reinforcement = p.spec.reinforcement;
@@ -4086,10 +4227,33 @@ var GameDestruction = (() => {
4086
4227
  emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
4087
4228
  };
4088
4229
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
4230
+ const severAxis = (p) => {
4231
+ if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
4232
+ const d = p.dims;
4233
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
4234
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
4235
+ };
4236
+ const bandOf = (o, p, axis, origin) => {
4237
+ const { c, q } = partXf(o, p);
4238
+ const along = toLocal(c, q, origin)[axis];
4239
+ const L = p.dims[axis];
4240
+ return clamp2(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
4241
+ };
4089
4242
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
4243
+ var _a;
4090
4244
  if (isLost(p) || amount <= 0) return 0;
4091
4245
  const prev = p.st.hp;
4092
4246
  p.st.hp -= amount;
4247
+ const axis = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
4248
+ if (axis >= 0 && p.st.hp > 0) {
4249
+ const band = bandOf(o, p, axis, origin);
4250
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
4251
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
4252
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis]) {
4253
+ p.rt.cutAt = -p.dims[axis] / 2 + (band + 0.5) * SEVER_BAND;
4254
+ p.st.hp = 0;
4255
+ }
4256
+ }
4093
4257
  markDamaged(p);
4094
4258
  const trigger = CHAIN_TRIGGER_OF[type];
4095
4259
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -4128,7 +4292,7 @@ var GameDestruction = (() => {
4128
4292
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
4129
4293
  const mat = p.mat;
4130
4294
  const piercing = round.piercing ?? 1;
4131
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
4295
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry)) || insideChip(p, objLocal(o, entry))) {
4132
4296
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
4133
4297
  }
4134
4298
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -4187,6 +4351,7 @@ var GameDestruction = (() => {
4187
4351
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4188
4352
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
4189
4353
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
4354
+ chipUnits(o, p, entry, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
4190
4355
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4191
4356
  };
4192
4357
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -4214,6 +4379,7 @@ var GameDestruction = (() => {
4214
4379
  const scale = mat.damageScale?.[kind] ?? 1;
4215
4380
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
4216
4381
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
4382
+ if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
4217
4383
  return done;
4218
4384
  };
4219
4385
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -5705,6 +5871,7 @@ var GameDestruction = (() => {
5705
5871
  sway: "sway",
5706
5872
  spall: "plaster came off",
5707
5873
  stage: "damage stage",
5874
+ chip: "brick knocked out",
5708
5875
  expose: "rebar bare",
5709
5876
  body: "body",
5710
5877
  blast: "blast",
@@ -5795,6 +5962,7 @@ var GameDestruction = (() => {
5795
5962
  sway: "balan\xE7o",
5796
5963
  spall: "reboco soltou",
5797
5964
  stage: "est\xE1gio de dano",
5965
+ chip: "tijolo arrancado",
5798
5966
  expose: "vergalh\xE3o \xE0 mostra",
5799
5967
  body: "corpo",
5800
5968
  blast: "explos\xE3o",