@adatechnology/game-destruction 0.12.0 → 0.13.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.js CHANGED
@@ -2706,6 +2706,7 @@ var CONFINEMENT_DEFAULTS = { rays: 26, reach: 1.5, gain: 1.5, power: 2, max: 3 }
2706
2706
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
2707
2707
  var VENT_DIAMETER = 8e-3;
2708
2708
  var FLAME_LIGHTS_LEAK_C = 470;
2709
+ var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
2709
2710
  var BREACH_RADIUS = 0.55;
2710
2711
  var BREACH_BLAST_FACTOR = 6;
2711
2712
  var BREACH_AREA = 0.25;
@@ -2721,6 +2722,7 @@ var UNIT_FRAGMENT_FACTOR = 4;
2721
2722
  var BREACH_GRID_CAP = 4096;
2722
2723
  var CHIP_ENERGY = 200;
2723
2724
  var CHIP_SPLASH = 0.5;
2725
+ var CRATER_SPENT = 0.6;
2724
2726
  var BAR_BOW = 0.08;
2725
2727
  var BULLET_JOINT_SPAN = 0.3;
2726
2728
  var CUT_SLICE = 1;
@@ -2741,12 +2743,22 @@ var sectionLoss = (p) => {
2741
2743
  const cells = p.st.chipped;
2742
2744
  if (!cells?.length) return 0;
2743
2745
  const [u, v] = planeAxes(p.thin);
2744
- const along = p.dims[u] >= p.dims[v] ? u : v;
2746
+ const role = p.spec.structure?.role;
2747
+ const up = rotateVec(p.q, [0, 0, 0].map((_, k) => k === v ? 1 : 0));
2748
+ const upright = Math.abs(up[1]) > 0.7 ? v : Math.abs(rotateVec(p.q, [0, 0, 0].map((_, k) => k === u ? 1 : 0))[1]) > 0.7 ? u : -1;
2749
+ const along = (role === "wall" || role === "partition" || role === "column") && upright >= 0 ? upright : p.dims[u] >= p.dims[v] ? u : v;
2745
2750
  const across = along === u ? v : u;
2746
2751
  let worst = 0;
2747
2752
  for (const c of cells) {
2748
- let gone = 0;
2749
- for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
2753
+ const spans = [];
2754
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) spans.push([d.center[across] - d.size[across] / 2, d.center[across] + d.size[across] / 2]);
2755
+ spans.sort((a, b) => a[0] - b[0]);
2756
+ let gone = 0, end = -Infinity;
2757
+ for (const [a, b] of spans) {
2758
+ if (b <= end) continue;
2759
+ gone += b - Math.max(a, end);
2760
+ end = b;
2761
+ }
2750
2762
  worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
2751
2763
  }
2752
2764
  return Math.min(1, worst);
@@ -3042,7 +3054,18 @@ var createDestruction = (options = {}) => {
3042
3054
  if (k < 0 || j.spec.kind !== "tie") return k;
3043
3055
  return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
3044
3056
  };
3057
+ let poseVersion = 0;
3058
+ const contactMemo = /* @__PURE__ */ new WeakMap();
3045
3059
  const contactBox = (p, support) => {
3060
+ let byTouching = contactMemo.get(p);
3061
+ if (!byTouching) contactMemo.set(p, byTouching = /* @__PURE__ */ new Map());
3062
+ const known = byTouching.get(support);
3063
+ if (known && known.version === poseVersion) return known.box;
3064
+ const box = measureContact(p, support);
3065
+ byTouching.set(support, { version: poseVersion, box });
3066
+ return box;
3067
+ };
3068
+ const measureContact = (p, support) => {
3046
3069
  const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
3047
3070
  const lo = [0, 0, 0], hi = [0, 0, 0];
3048
3071
  for (let k = 0; k < 3; k++) {
@@ -3837,7 +3860,7 @@ var createDestruction = (options = {}) => {
3837
3860
  p.st.holes.push(hole);
3838
3861
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
3839
3862
  markDamaged(p);
3840
- emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
3863
+ emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {}, ...p.spec.powder ? { powder: { ...p.spec.powder } } : {} });
3841
3864
  setStage(o, p, 1, {}, src);
3842
3865
  return hole;
3843
3866
  };
@@ -3854,6 +3877,22 @@ var createDestruction = (options = {}) => {
3854
3877
  const detachEvents = (o, opened, by, src) => {
3855
3878
  for (const j of opened) emit({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
3856
3879
  };
3880
+ const slideOffResting = (o, rotation) => {
3881
+ const upY = 1 - 2 * (rotation[0] * rotation[0] + rotation[2] * rotation[2]);
3882
+ if (upY >= RESTING_HOLD_COS) return;
3883
+ const opened = o.joints.filter((j) => j.spec.kind === "resting" && !j.st.open);
3884
+ if (opened.length === 0) return;
3885
+ run(() => {
3886
+ for (const j of opened) {
3887
+ j.st.open = true;
3888
+ j.st.openedBy = "break";
3889
+ }
3890
+ solveStructure(o);
3891
+ recomputeGroups(o);
3892
+ detachEvents(o, opened, "break");
3893
+ collapseCheck(o);
3894
+ });
3895
+ };
3857
3896
  const checkDestroyed = (o, src) => {
3858
3897
  if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
3859
3898
  o.st.destroyed = true;
@@ -3954,7 +3993,7 @@ var createDestruction = (options = {}) => {
3954
3993
  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 }));
3955
3994
  const unitsOf = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
3956
3995
  const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
3957
- const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
3996
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag, inner = false) => {
3958
3997
  const { c, q } = partXf(o, p);
3959
3998
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
3960
3999
  const [u, v] = planeAxes(p.thin);
@@ -3978,7 +4017,8 @@ var createDestruction = (options = {}) => {
3978
4017
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
3979
4018
  angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
3980
4019
  mass: mass * faceArea(cell) / total,
3981
- ...marks.length > 0 ? { marks } : {}
4020
+ ...marks.length > 0 ? { marks } : {},
4021
+ ...inner ? { inner } : {}
3982
4022
  };
3983
4023
  });
3984
4024
  };
@@ -4019,7 +4059,7 @@ var createDestruction = (options = {}) => {
4019
4059
  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);
4020
4060
  };
4021
4061
  const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
4022
- var _a, _b;
4062
+ var _a, _b, _c;
4023
4063
  if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4024
4064
  const rule = p.mat.hits?.[kind];
4025
4065
  const perKg = rule?.unitJoules ?? chipEnergy;
@@ -4029,7 +4069,9 @@ var createDestruction = (options = {}) => {
4029
4069
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4030
4070
  const grid = gridOf(p);
4031
4071
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
4072
+ const cut = (_b = p.rt).craters ?? (_b.craters = {});
4032
4073
  const out = [];
4074
+ const shot = kind === "ballistic";
4033
4075
  for (let i = 0; i < grid.length; i++) {
4034
4076
  const cell = grid[i];
4035
4077
  if (worn[i] === -1) continue;
@@ -4042,18 +4084,39 @@ var createDestruction = (options = {}) => {
4042
4084
  worn[i] = total;
4043
4085
  continue;
4044
4086
  }
4045
- worn[i] = -1;
4046
- out.push(cell);
4087
+ if (!shot) {
4088
+ worn[i] = -1;
4089
+ out.push(cell);
4090
+ continue;
4091
+ }
4092
+ const size = [cell.size[0], cell.size[1], cell.size[2]];
4093
+ const center = [cell.center[0], cell.center[1], cell.center[2]];
4094
+ for (const a of [u, v]) {
4095
+ const w = Math.min(cell.size[a], craterAcross(hitEnergy) * (0.85 + 0.3 * rnd()));
4096
+ const lo = cell.center[a] - cell.size[a] / 2 + w / 2, hi = cell.center[a] + cell.size[a] / 2 - w / 2;
4097
+ size[a] = w;
4098
+ center[a] = clamp2(lp[a], lo, hi);
4099
+ }
4100
+ const taken2 = (cut[i] ?? 0) + size[u] * size[v];
4101
+ if (taken2 >= cell.size[u] * cell.size[v] * CRATER_SPENT) {
4102
+ worn[i] = -1;
4103
+ out.push({ ...cell, crater: true });
4104
+ continue;
4105
+ }
4106
+ worn[i] = 0;
4107
+ cut[i] = taken2;
4108
+ out.push({ center, size, crater: true });
4047
4109
  }
4048
4110
  if (out.length === 0) return;
4049
4111
  const { c, q } = partXf(o, p);
4050
- const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4112
+ const chipped = (_c = p.st).chipped ?? (_c.chipped = []);
4051
4113
  const holes = localHolesOf(p);
4052
4114
  for (const cell of out) {
4053
4115
  const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
4054
4116
  const center = toWorld(c, q, cell.center);
4055
4117
  const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
4056
- const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
4118
+ const tag = `c${chipped.length}_`;
4119
+ const fragments = cell.crater ? craterFragments(o, p, cell, dir, speed, mass, tag) : cellFragments(o, p, [cell], wp, dir, speed, mass, tag, true);
4057
4120
  p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
4058
4121
  chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
4059
4122
  p.st.mass = Math.max(0, p.st.mass - mass);
@@ -4070,6 +4133,44 @@ var createDestruction = (options = {}) => {
4070
4133
  collapseCheck(o, src);
4071
4134
  }
4072
4135
  };
4136
+ const craterAcross = (energy) => clamp2(0.03 + 12e-4 * Math.sqrt(Math.max(0, energy)), 0.03, 0.14);
4137
+ const craterFragments = (o, p, cell, dir, speed, mass, tag) => {
4138
+ const { c, q } = partXf(o, p);
4139
+ const [u, v] = planeAxes(p.thin);
4140
+ const d = dir ?? rotateVec(q, [p.thin === 0 ? 1 : 0, p.thin === 1 ? 1 : 0, p.thin === 2 ? 1 : 0]);
4141
+ const exitSign = Math.sign(rotateVec(quat.conjugate(q), d)[p.thin]) || 1;
4142
+ const bit = Math.max(0.01, Math.min(cell.size[u], cell.size[v]) / 3);
4143
+ const n = clamp2(Math.round(cell.size[u] * cell.size[v] / (bit * bit * 3)), 4, 10) + Math.floor(rnd() * 3);
4144
+ const out = [];
4145
+ for (let i = 0; i < n; i++) {
4146
+ const back = i % 3 === 2;
4147
+ const lc = [cell.center[0], cell.center[1], cell.center[2]];
4148
+ lc[u] += (rnd() - 0.5) * cell.size[u] * 0.7;
4149
+ lc[v] += (rnd() - 0.5) * cell.size[v] * 0.7;
4150
+ lc[p.thin] += (back ? -exitSign : exitSign) * (p.thickness / 2 + 0.01);
4151
+ const size = [0, 0, 0];
4152
+ size[u] = bit * (0.6 + 0.8 * rnd());
4153
+ size[v] = bit * (0.5 + 0.7 * rnd());
4154
+ size[p.thin] = bit * (0.4 + 0.6 * rnd());
4155
+ const spread3 = [(rnd() - 0.5) * 0.9, (rnd() - 0.3) * 0.9, (rnd() - 0.5) * 0.9];
4156
+ const k = speed * (back ? 0.4 + 0.4 * rnd() : 0.7 + 0.8 * rnd());
4157
+ const vdir = vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3));
4158
+ out.push({
4159
+ id: `${p.spec.id}#${tag}${i}`,
4160
+ part: p.spec.id,
4161
+ material: p.spec.material,
4162
+ shape: "chunk",
4163
+ center: toWorld(c, q, lc),
4164
+ size,
4165
+ rotation: q,
4166
+ velocity: [vdir[0] * k, vdir[1] * k + 0.5 * rnd(), vdir[2] * k],
4167
+ angularVelocity: [(rnd() - 0.5) * 20, (rnd() - 0.5) * 20, (rnd() - 0.5) * 20],
4168
+ mass: mass / n,
4169
+ inner: true
4170
+ });
4171
+ }
4172
+ return out;
4173
+ };
4073
4174
  const exposeRebar = (o, p, src) => {
4074
4175
  const r = p.spec.reinforcement;
4075
4176
  if (!r) return;
@@ -4357,7 +4458,7 @@ var createDestruction = (options = {}) => {
4357
4458
  });
4358
4459
  }
4359
4460
  const patch = [scale * 4, scale * 4, scale * 4];
4360
- emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry, size: patch, rotation: q, fragments });
4461
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry, size: patch, rotation: q, fragments, torn: true });
4361
4462
  };
4362
4463
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
4363
4464
  const mat = p.mat;
@@ -4416,12 +4517,13 @@ var createDestruction = (options = {}) => {
4416
4517
  } else if (mat.ductility >= 0.5) {
4417
4518
  addDent(o, p, entry, n, Math.max(caliber * 3 / 1e3, 0.012), clamp2(Math.min(Math.max(p.thickness * 3, 3e-3), ratio * 0.01), 1e-3, 0.05), src);
4418
4519
  } else if (brittle) {
4419
- addCrack(o, p, entry, n, crackLen, src);
4520
+ if (!p.spec.units) addCrack(o, p, entry, n, crackLen, src);
4420
4521
  if (mat.fracture !== "shatter") addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4421
4522
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4422
4523
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4423
4524
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
4424
- const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
4525
+ const hollowTear = mat.fracture === "tear" && p.spec.fill !== void 0 && p.spec.fill < 0.5;
4526
+ const absorbed = penetrated && (mat.ductility >= 0.5 && mat.fracture !== "tear" || hollowTear) ? Math.min(E, thr) : E;
4425
4527
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry, d, absorbed * 0.1, 12, src);
4426
4528
  if (!isLost(p)) tearAt(o, p, "ballistic", entry, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
4427
4529
  chipUnits(o, p, "ballistic", entry, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
@@ -4506,10 +4608,25 @@ var createDestruction = (options = {}) => {
4506
4608
  if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
4507
4609
  return reach;
4508
4610
  };
4611
+ const blastProbe = { partsTested: 0, partsNear: 0 };
4612
+ const blastCandidates = (o, point, radius) => {
4613
+ const local = toLocal(o.st.position, o.st.rotation, point);
4614
+ const near = [];
4615
+ for (const p of o.parts) {
4616
+ const reach = radius + p.bound;
4617
+ const dx = local[0] - p.pos[0];
4618
+ const dy = local[1] - p.pos[1];
4619
+ const dz = local[2] - p.pos[2];
4620
+ if (dx * dx + dy * dy + dz * dz < reach * reach) near.push(p);
4621
+ }
4622
+ blastProbe.partsTested += o.parts.length;
4623
+ blastProbe.partsNear += near.length;
4624
+ return near;
4625
+ };
4509
4626
  const blastObject = (o, point, energy, radius, src) => {
4510
4627
  const start = cur.length;
4511
4628
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
4512
- for (const p of o.parts) blastPart(o, p, point, energy, radius, src, res);
4629
+ for (const p of blastCandidates(o, point, radius)) blastPart(o, p, point, energy, radius, src, res);
4513
4630
  res.events = cur.slice(start);
4514
4631
  return res;
4515
4632
  };
@@ -4633,7 +4750,7 @@ var createDestruction = (options = {}) => {
4633
4750
  let order = 0;
4634
4751
  for (const o of inRange) {
4635
4752
  if (only && only !== o) continue;
4636
- for (const p of o.parts) {
4753
+ for (const p of blastCandidates(o, point, radius)) {
4637
4754
  if (isLost(p)) continue;
4638
4755
  const dist = distToPart(o, p, point);
4639
4756
  if (dist < radius) items.push({ dist, order: order++, o, p });
@@ -5180,6 +5297,7 @@ var createDestruction = (options = {}) => {
5180
5297
  const p = getPart(o, partId);
5181
5298
  p.pos = [position[0], position[1], position[2]];
5182
5299
  p.q = q;
5300
+ poseVersion++;
5183
5301
  updateBound(o);
5184
5302
  },
5185
5303
  remove(id) {
@@ -5198,6 +5316,7 @@ var createDestruction = (options = {}) => {
5198
5316
  const o = getObj(id);
5199
5317
  o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
5200
5318
  applySoilPose(o);
5319
+ slideOffResting(o, rotation);
5201
5320
  },
5202
5321
  splitGroup(id, group, newId) {
5203
5322
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -5221,7 +5340,7 @@ var createDestruction = (options = {}) => {
5221
5340
  if (o.spec.name !== void 0) s.name = o.spec.name;
5222
5341
  if (o.spec.tags) s.tags = [...o.spec.tags];
5223
5342
  if (o.spec.soil !== void 0) s.soil = o.spec.soil;
5224
- return clone(s);
5343
+ return s;
5225
5344
  };
5226
5345
  const inNew = (pid) => moved.has(pid);
5227
5346
  const inOld = (pid) => !moved.has(pid);
@@ -5248,7 +5367,12 @@ var createDestruction = (options = {}) => {
5248
5367
  const p = getPart(o, partId);
5249
5368
  return whole(() => doHit(o, h, p));
5250
5369
  },
5370
+ blastProbe() {
5371
+ return { ...blastProbe };
5372
+ },
5251
5373
  explode(point, energy, radius, source, blast) {
5374
+ blastProbe.partsTested = 0;
5375
+ blastProbe.partsNear = 0;
5252
5376
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
5253
5377
  },
5254
5378
  raycast(origin, direction, maxDistance = Infinity) {
@@ -5258,7 +5382,8 @@ var createDestruction = (options = {}) => {
5258
5382
  sweepBox(box, direction, distance, options2 = {}) {
5259
5383
  const d = vec.normalize(direction);
5260
5384
  const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
5261
- const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
5385
+ const hosted = options2.ignore === void 0 || ignored ? void 0 : impls.get(options2.ignore);
5386
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : hosted ? new Set(hosted.impl.describe().parts.map((p) => `${hosted.impl.id}\0${p.id}`)) : void 0;
5262
5387
  let best = null;
5263
5388
  for (const origin of sweepOrigins(box, d, options2.spacing ?? 0.2)) {
5264
5389
  const r = raycastInternal(origin, d, distance + SWEEP_BACK, ignore);