@adatechnology/game-destruction 0.9.0 → 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
@@ -336,6 +336,12 @@ interface DestructionOptions {
336
336
  radius?: number;
337
337
  /** Multiplier of the breach area a blast makes over a blow of the same damage: a blast loads the whole crater (default 6). */
338
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;
339
345
  };
340
346
  /**
341
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`
@@ -504,8 +510,14 @@ interface PartRuntime {
504
510
  spalls?: Array<[number, number, number, number, number, number]>;
505
511
  /** Through damage so far: `[x, y, z, area]` in object space, newest last. */
506
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>;
507
515
  /** Mass in kg that left the part in a breach. */
508
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;
509
521
  /** Plastic rotation in radians a reinforced beam or slab took in bending (blasts add it up; `flexure.failRotationDeg` breaks it). */
510
522
  hinge?: number;
511
523
  }
@@ -450,6 +450,45 @@ 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
+ };
453
492
  var tearStack = (inp, root, t, u, v, n, sheet) => {
454
493
  const { dims, rnd, maxFragments } = inp;
455
494
  const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
@@ -488,23 +527,8 @@ var GameDestruction = (() => {
488
527
  return out;
489
528
  }
490
529
  if (style === "splinter") {
491
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
492
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
493
- const w = Math.max(fmin * 1.5, 0.01);
494
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
495
- const maxLen = fmax * 4;
496
- const cells = bsp(root, c, [a, b], rnd);
497
- const pieces = [];
498
- for (const cell of cells) {
499
- let list = [cell];
500
- const extra = rnd() < 0.4 ? 1 : 0;
501
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
502
- list = bsp(cell, k, [g], rnd);
503
- for (const p of list) pieces.push(p);
504
- }
505
- for (const p of pieces.slice(0, maxFragments)) {
506
- 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] });
507
- }
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);
508
532
  return out;
509
533
  }
510
534
  if (style === "tear") {
@@ -2718,9 +2742,14 @@ var GameDestruction = (() => {
2718
2742
  var BREACH_AREA = 0.25;
2719
2743
  var BREACH_BLAST_REACH = 0.12;
2720
2744
  var RESIDUAL_CAPACITY = 0.15;
2745
+ var SEVER_BAND = 0.1;
2746
+ var SEVER_ASPECT = 3;
2747
+ var SEVER_BALLISTIC = 2;
2721
2748
  var SUPPORT_TOLERANCE = 0.05;
2722
2749
  var UNIT_FRAGMENT_FACTOR = 4;
2723
2750
  var BREACH_GRID_CAP = 4096;
2751
+ var CHIP_ENERGY = 200;
2752
+ var CHIP_SPLASH = 0.5;
2724
2753
  var CUT_SLICE = 1;
2725
2754
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2726
2755
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2764,6 +2793,7 @@ var GameDestruction = (() => {
2764
2793
  let windCfg = windOpts ? resolveWind(windOpts) : null;
2765
2794
  const ignitionRules = resolveIgnitionRules(options.ignition);
2766
2795
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
2796
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
2767
2797
  const rng = createRng(options.seed ?? 1);
2768
2798
  const rnd = () => rng.next();
2769
2799
  let time = 0;
@@ -3137,41 +3167,68 @@ var GameDestruction = (() => {
3137
3167
  markDamaged(p);
3138
3168
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
3139
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
+ };
3140
3184
  const solveStructure = (o) => {
3141
3185
  o.maxStress = 0;
3142
3186
  if (!o.hasStructure) return;
3143
3187
  const parts = o.parts;
3144
3188
  const n = parts.length;
3145
3189
  const dist = new Array(n).fill(-1);
3146
- const queue = [];
3147
3190
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
3148
3191
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
3149
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
3150
- dist[i] = 0;
3151
- queue.push(i);
3152
- }
3153
- for (let h = 0; h < queue.length; h++) {
3154
- const s = queue[h];
3155
- if (isPartition(s)) continue;
3156
- for (const j of o.byPart.get(parts[s].spec.id)) {
3157
- if (j.st.open || !j.bears) continue;
3158
- const r = j.ia === s ? j.ib : j.ia;
3159
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
3160
- dist[r] = dist[s] + 1;
3161
- 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);
3162
3201
  }
3163
- }
3164
- const incoming = new Array(n).fill(0);
3165
- const lowerOf = /* @__PURE__ */ new Map();
3166
- for (const i of queue) {
3167
- const lower = [];
3168
- for (const j of o.byPart.get(parts[i].spec.id)) {
3169
- if (j.st.open || !j.bears) continue;
3170
- const k = restsOn(o, j, i);
3171
- 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
+ }
3172
3212
  }
3173
- lowerOf.set(i, lower);
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
+ }
3228
+ }
3229
+ if (!dropped) break;
3174
3230
  }
3231
+ const incoming = new Array(n).fill(0);
3175
3232
  const bending = new Array(n).fill(0);
3176
3233
  const through = parts.map(() => []);
3177
3234
  const pass = (k, at, mass) => {
@@ -3683,6 +3740,7 @@ var GameDestruction = (() => {
3683
3740
  const r = rayPart(o, p, origin, d);
3684
3741
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
3685
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;
3686
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 };
3687
3745
  }
3688
3746
  }
@@ -3946,6 +4004,62 @@ var GameDestruction = (() => {
3946
4004
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
3947
4005
  openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
3948
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
+ };
3949
4063
  const openBreach = (o, p, center, total, energy, dir, src) => {
3950
4064
  const [u, v] = planeAxes(p.thin);
3951
4065
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -3957,7 +4071,8 @@ var GameDestruction = (() => {
3957
4071
  failPart(o, p, worldCenter, dir, energy, 12, src);
3958
4072
  return;
3959
4073
  }
3960
- 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);
3961
4076
  const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
3962
4077
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
3963
4078
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -4041,6 +4156,8 @@ var GameDestruction = (() => {
4041
4156
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
4042
4157
  const hitLocal = toLocal(c, q, hitW);
4043
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);
4044
4161
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4045
4162
  style,
4046
4163
  dims: p.dims,
@@ -4050,7 +4167,8 @@ var GameDestruction = (() => {
4050
4167
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4051
4168
  maxFragments,
4052
4169
  rnd,
4053
- sheet: p.mat.sheet
4170
+ sheet: p.mat.sheet,
4171
+ ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
4054
4172
  });
4055
4173
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
4056
4174
  const mass = p.st.mass;
@@ -4109,10 +4227,33 @@ var GameDestruction = (() => {
4109
4227
  emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
4110
4228
  };
4111
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
+ };
4112
4242
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
4243
+ var _a;
4113
4244
  if (isLost(p) || amount <= 0) return 0;
4114
4245
  const prev = p.st.hp;
4115
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
+ }
4116
4257
  markDamaged(p);
4117
4258
  const trigger = CHAIN_TRIGGER_OF[type];
4118
4259
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -4151,7 +4292,7 @@ var GameDestruction = (() => {
4151
4292
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
4152
4293
  const mat = p.mat;
4153
4294
  const piercing = round.piercing ?? 1;
4154
- 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))) {
4155
4296
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
4156
4297
  }
4157
4298
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -4210,6 +4351,7 @@ var GameDestruction = (() => {
4210
4351
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4211
4352
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
4212
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);
4213
4355
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4214
4356
  };
4215
4357
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -4237,6 +4379,7 @@ var GameDestruction = (() => {
4237
4379
  const scale = mat.damageScale?.[kind] ?? 1;
4238
4380
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
4239
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);
4240
4383
  return done;
4241
4384
  };
4242
4385
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -5728,6 +5871,7 @@ var GameDestruction = (() => {
5728
5871
  sway: "sway",
5729
5872
  spall: "plaster came off",
5730
5873
  stage: "damage stage",
5874
+ chip: "brick knocked out",
5731
5875
  expose: "rebar bare",
5732
5876
  body: "body",
5733
5877
  blast: "blast",
@@ -5818,6 +5962,7 @@ var GameDestruction = (() => {
5818
5962
  sway: "balan\xE7o",
5819
5963
  spall: "reboco soltou",
5820
5964
  stage: "est\xE1gio de dano",
5965
+ chip: "tijolo arrancado",
5821
5966
  expose: "vergalh\xE3o \xE0 mostra",
5822
5967
  body: "corpo",
5823
5968
  blast: "explos\xE3o",