@adatechnology/game-destruction 0.1.0 → 0.2.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
@@ -117,6 +117,13 @@ interface DestructionWorld {
117
117
  ids(): string[];
118
118
  /** Move an object (the renderer or physics owns transforms; keep the world in sync so world-space hits land). */
119
119
  setTransform(id: string, position: Vec3, rotation: Quat): void;
120
+ /**
121
+ * Move a part's collision shape (raycast, hits, blast and joint distance) to an object-local pose; the part's spec is unchanged. Animations are
122
+ * visual, so a host that plays a clip calls this every frame with `rest + clip offset`. `rotation` is Euler XYZ (radians), default none. For a
123
+ * registered impl it forwards to the optional `DestructibleImpl.setPartPose` and otherwise only moves the world's raycast shape. Dents and holes
124
+ * stay in object space.
125
+ */
126
+ setPartPose(objectId: string, partId: string, position: Vec3, rotation?: Vec3): void;
120
127
  /**
121
128
  * Split a loose group into its own object (after a `detach`, when the renderer makes the piece a separate body). The parts move with their
122
129
  * state; the new object starts at the old object's transform. Returns the new id.
@@ -139,14 +146,24 @@ interface DestructionWorld {
139
146
  disassemble(objectId: string, jointId: string, tool: ToolKind, dt: number): number;
140
147
  /** Joints of an object that `tool` can undo now (open joints excluded). */
141
148
  workableJoints(objectId: string, tool: ToolKind): JointState[];
142
- /** Joint nearest to a world point (for "aim at the screw"). */
143
- nearestJoint(objectId: string, point: Vec3, maxDistance?: number): JointState | null;
149
+ /**
150
+ * Joint nearest to a world point (for "aim at the screw"). With `tool`, the nearest open-able joint that tool can undo within `maxDistance`
151
+ * wins; only when none qualifies does it fall back to the nearest joint of any kind.
152
+ */
153
+ nearestJoint(objectId: string, point: Vec3, maxDistance?: number, tool?: ToolKind): JointState | null;
144
154
  /** Put out fire on a part or the whole object, and cool it by `coolC` (default 200). */
145
155
  extinguish(objectId: string, partId?: string, coolC?: number): void;
146
156
  /** Advance fire, heat transfer, cooling, melting, leaks and delayed explosions. */
147
157
  update(dt: number): DestructionEvent[];
158
+ /**
159
+ * The object's LIVE state: the same object the world mutates every update (parts, joints, groups). Treat it as read-only and change the world only
160
+ * through the API (`hit`, `disassemble`, `setLiters`...). Do not keep it across frames to compare "before" and "after": use `snapshot`.
161
+ */
148
162
  state(id: string): ObjectState | null;
163
+ /** The part's LIVE state (see `state`): read-only, mutated by the world. */
149
164
  part(id: string, partId: string): PartState | null;
165
+ /** A deep copy of the object's state at this moment: safe to keep, compare and mutate. Null for an unknown id. */
166
+ snapshot(id: string): ObjectState | null;
150
167
  /** Repair an object to its spec (test benches use it for "reset"). */
151
168
  reset(id: string): void;
152
169
  on(type: DestructionEventType | '*', fn: (event: DestructionEvent) => void): () => void;
@@ -1136,7 +1136,7 @@ var GameDestruction = (() => {
1136
1136
  {
1137
1137
  id: "green_wood",
1138
1138
  density: 650,
1139
- hpPerKg: 120,
1139
+ hpPerKg: 5,
1140
1140
  penetration: 10,
1141
1141
  ductility: 0.5,
1142
1142
  fracture: "splinter",
@@ -1145,7 +1145,8 @@ var GameDestruction = (() => {
1145
1145
  0.2
1146
1146
  ],
1147
1147
  damageScale: {
1148
- slash: 1.3
1148
+ slash: 1.3,
1149
+ ballistic: 0.1
1149
1150
  },
1150
1151
  hitDebris: "splinters",
1151
1152
  breakDebris: "leaves",
@@ -1455,9 +1456,20 @@ var GameDestruction = (() => {
1455
1456
  var DMG_PER_J = 0.02;
1456
1457
  var BLAST_COUPLING = 0.1;
1457
1458
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
1459
+ var CUT_SLICE = 1;
1460
+ var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
1458
1461
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
1459
1462
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
1460
1463
  var clone = (v) => structuredClone(v);
1464
+ var updateBound = (o) => {
1465
+ if (!o.parts.length) return;
1466
+ const c = [0, 0, 0];
1467
+ for (const p of o.parts) for (let k = 0; k < 3; k++) c[k] += p.pos[k] / o.parts.length;
1468
+ let r = 0;
1469
+ for (const p of o.parts) r = Math.max(r, vec.distance(c, p.pos) + p.bound);
1470
+ o.bc = c;
1471
+ o.br = r;
1472
+ };
1461
1473
  var AXIS = { x: 0, y: 1, z: 2 };
1462
1474
  var createDestruction = (options = {}) => {
1463
1475
  const materials = /* @__PURE__ */ new Map();
@@ -1593,10 +1605,12 @@ var GameDestruction = (() => {
1593
1605
  mat,
1594
1606
  st: pst,
1595
1607
  q: quatFromEuler(...ps.rotation ?? [0, 0, 0]),
1608
+ pos: [ps.position[0], ps.position[1], ps.position[2]],
1596
1609
  mass0,
1597
1610
  fuel0,
1598
1611
  area: shapeArea(ps.shape),
1599
1612
  thickness: partThickness(ps),
1613
+ cutFactor: cutFactorOf(ps.shape),
1600
1614
  dims: shapeDims(ps.shape),
1601
1615
  bound: shapeBound(ps.shape),
1602
1616
  rt: c?.rt ?? { ...freshRuntime(), prevTemp: pst.temperature },
@@ -1631,14 +1645,8 @@ var GameDestruction = (() => {
1631
1645
  o.byPart.get(r.b).push(j);
1632
1646
  o.st.joints.push(jst);
1633
1647
  }
1634
- if (o.parts.length) {
1635
- const c = [0, 0, 0];
1636
- for (const p of o.parts) for (let k = 0; k < 3; k++) c[k] += p.spec.position[k] / o.parts.length;
1637
- let r = 0;
1638
- for (const p of o.parts) r = Math.max(r, vec.distance(c, p.spec.position) + p.bound);
1639
- o.bc = c;
1640
- o.br = r;
1641
- }
1648
+ for (const [jid, c] of carry?.joints ?? []) if (c.st.open && !o.jointMap.has(jid)) o.st.joints.push(c.st);
1649
+ updateBound(o);
1642
1650
  o.anyAnchor = o.parts.some((p) => p.anchored);
1643
1651
  o.unsup = o.parts.map(() => false);
1644
1652
  solveStructure(o);
@@ -1778,7 +1786,7 @@ var GameDestruction = (() => {
1778
1786
  if (!p) throw new Error(`unknown part "${id}" in object "${o.id}"`);
1779
1787
  return p;
1780
1788
  };
1781
- const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.spec.position), q: quat.mul(o.st.rotation, p.q) });
1789
+ const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.pos), q: quat.mul(o.st.rotation, p.q) });
1782
1790
  const objLocal = (o, w) => toLocal(o.st.position, o.st.rotation, w);
1783
1791
  const objLocalDir = (o, w) => rotateVec(quat.conjugate(o.st.rotation), w);
1784
1792
  const jointWorld = (o, j) => toWorld(o.st.position, o.st.rotation, j.pos);
@@ -1790,14 +1798,14 @@ var GameDestruction = (() => {
1790
1798
  const { c, q } = partXf(o, p);
1791
1799
  return aabbOf(p.spec.shape, c, q);
1792
1800
  };
1793
- const rayPartRaw = (pos, rot, spec, pq, origin, d) => {
1794
- const c = toWorld(pos, rot, spec.position);
1801
+ const rayPartRaw = (pos, rot, spec, pq, origin, d, lp = spec.position) => {
1802
+ const c = toWorld(pos, rot, lp);
1795
1803
  const q = quat.mul(rot, pq);
1796
1804
  const qi = quat.conjugate(q);
1797
1805
  const r = rayShape(spec.shape, rotateVec(qi, vec.sub(origin, c)), rotateVec(qi, d));
1798
1806
  return r ? { t0: r.t0, t1: r.t1, n: rotateVec(q, r.n0) } : null;
1799
1807
  };
1800
- const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d);
1808
+ const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d, p.pos);
1801
1809
  const raySphereMiss = (origin, d, c, r, maxDist) => {
1802
1810
  const oc = vec.sub(origin, c);
1803
1811
  const b = vec.dot(oc, d);
@@ -1822,7 +1830,8 @@ var GameDestruction = (() => {
1822
1830
  for (const im of impls.values()) {
1823
1831
  for (const ps of im.impl.describe().parts) {
1824
1832
  if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
1825
- const r = rayPartRaw(im.position, im.rotation, ps, quatFromEuler(...ps.rotation ?? [0, 0, 0]), origin, d);
1833
+ const ov = im.poses.get(ps.id);
1834
+ const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), origin, d, ov?.pos);
1826
1835
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
1827
1836
  if (!best || r.t0 < best.distance) best = { object: im.impl.id, part: ps.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
1828
1837
  }
@@ -1957,11 +1966,29 @@ var GameDestruction = (() => {
1957
1966
  collapseCheck(o, src);
1958
1967
  checkDestroyed(o, src);
1959
1968
  };
1960
- const wearJoints = (o, p, amount, near, radius = 0.06, src) => {
1969
+ const jointReach = (o, j, point) => {
1970
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
1971
+ let d2 = 0;
1972
+ for (let k = 0; k < 3; k++) {
1973
+ const lo = Math.min(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
1974
+ const hi = Math.max(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
1975
+ const d = point[k] < lo ? lo - point[k] : point[k] > hi ? point[k] - hi : 0;
1976
+ d2 += d * d;
1977
+ }
1978
+ return Math.sqrt(d2);
1979
+ };
1980
+ const jointReachLimit = (o, j) => {
1981
+ const pa = o.parts[j.ia], pb = o.parts[j.ib];
1982
+ const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
1983
+ return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
1984
+ };
1985
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
1961
1986
  if (amount <= 0) return;
1962
- for (const j of o.byPart.get(p.spec.id) ?? []) {
1987
+ const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
1988
+ if (split && mine.length) amount /= mine.length;
1989
+ for (const j of mine) {
1963
1990
  if (j.st.open) continue;
1964
- if (near && vec.distance(jointWorld(o, j), near) > radius) continue;
1991
+ if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
1965
1992
  j.st.hp -= amount;
1966
1993
  if (j.st.hp <= 0) {
1967
1994
  j.st.hp = 0;
@@ -2080,7 +2107,7 @@ var GameDestruction = (() => {
2080
2107
  if (mat.fracture !== "shatter") addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
2081
2108
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
2082
2109
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
2083
- wearJoints(o, p, E * 0.2, entry, 0.06, src);
2110
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
2084
2111
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
2085
2112
  return { penetrated, exitEnergy: penetrated ? E - thr : 0, exitPoint, damage: dmg };
2086
2113
  };
@@ -2105,7 +2132,7 @@ var GameDestruction = (() => {
2105
2132
  if (kind === "blunt") wearJoints(o, p, E * 0.3, void 0, 0, src);
2106
2133
  else wearJoints(o, p, E * 0.3, point, 0.06, src);
2107
2134
  const scale = mat.damageScale?.[kind] ?? 1;
2108
- return applyDamage(o, p, Ec * DMG_PER_J * scale, kind, point, d, E * 0.1, 12, src);
2135
+ return applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
2109
2136
  };
2110
2137
  const blastObject = (o, point, energy, radius, src) => {
2111
2138
  const start = cur.length;
@@ -2155,8 +2182,9 @@ var GameDestruction = (() => {
2155
2182
  }
2156
2183
  for (const im of [...impls.values()]) {
2157
2184
  const inRange = im.impl.describe().parts.some((ps) => {
2158
- const c = toWorld(im.position, im.rotation, ps.position);
2159
- const q = quat.mul(im.rotation, quatFromEuler(...ps.rotation ?? [0, 0, 0]));
2185
+ const ov = im.poses.get(ps.id);
2186
+ const c = toWorld(im.position, im.rotation, ov?.pos ?? ps.position);
2187
+ const q = quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]));
2160
2188
  return distanceToShape(ps.shape, toLocal(c, q, point)) < radius;
2161
2189
  });
2162
2190
  if (inRange) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
@@ -2408,7 +2436,7 @@ var GameDestruction = (() => {
2408
2436
  const spread = (sources, dt) => {
2409
2437
  for (const src of sources) {
2410
2438
  if (!src.p.st.burning && !src.p.rt.jet) continue;
2411
- const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.spec.position);
2439
+ const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.pos);
2412
2440
  const A = partWorldAabb(src.o, src.p);
2413
2441
  for (const o of objects.values()) {
2414
2442
  if (o === src.o) continue;
@@ -2454,9 +2482,23 @@ var GameDestruction = (() => {
2454
2482
  register(impl, opts = {}) {
2455
2483
  const id = opts.id ?? impl.id;
2456
2484
  if (taken(id)) throw new Error(`object "${id}" already exists`);
2457
- impls.set(id, { impl, position: opts.position ?? [0, 0, 0], rotation: opts.rotation ?? [0, 0, 0, 1] });
2485
+ impls.set(id, { impl, position: opts.position ?? [0, 0, 0], rotation: opts.rotation ?? [0, 0, 0, 1], poses: /* @__PURE__ */ new Map() });
2458
2486
  return id;
2459
2487
  },
2488
+ setPartPose(objectId, partId, position, rotation) {
2489
+ const q = quatFromEuler(...rotation ?? [0, 0, 0]);
2490
+ const im = impls.get(objectId);
2491
+ if (im) {
2492
+ im.impl.setPartPose?.(partId, position, rotation);
2493
+ im.poses.set(partId, { pos: [position[0], position[1], position[2]], q });
2494
+ return;
2495
+ }
2496
+ const o = getObj(objectId);
2497
+ const p = getPart(o, partId);
2498
+ p.pos = [position[0], position[1], position[2]];
2499
+ p.q = q;
2500
+ updateBound(o);
2501
+ },
2460
2502
  remove(id) {
2461
2503
  objects.delete(id);
2462
2504
  impls.delete(id);
@@ -2483,9 +2525,12 @@ var GameDestruction = (() => {
2483
2525
  const moved = new Set(names);
2484
2526
  const nid = newId ?? nextId();
2485
2527
  if (objects.has(nid)) throw new Error(`object "${nid}" already exists`);
2486
- const carryFor = (keep) => ({
2528
+ const carryFor = (keep, keepOpen = () => false) => ({
2487
2529
  parts: new Map(o.parts.filter((p) => keep(p.spec.id)).map((p) => [p.spec.id, { st: p.st, rt: p.rt }])),
2488
- joints: new Map(o.joints.filter((j) => keep(j.spec.a) && keep(j.spec.b)).map((j) => [j.spec.id, { st: j.st, step: j.step }])),
2530
+ joints: new Map([
2531
+ ...o.joints.filter((j) => keep(j.spec.a) && keep(j.spec.b) || j.st.open && keepOpen(j.spec.a, j.spec.b)).map((j) => [j.spec.id, { st: j.st, step: j.step }]),
2532
+ ...o.st.joints.filter((st) => st.open && !o.jointMap.has(st.id) && keepOpen(st.a, st.b)).map((st) => [st.id, { st, step: 0 }])
2533
+ ]),
2489
2534
  collapsed: [...o.collapsed].filter(keep)
2490
2535
  });
2491
2536
  const sub = (keep) => {
@@ -2497,8 +2542,8 @@ var GameDestruction = (() => {
2497
2542
  const inNew = (pid) => moved.has(pid);
2498
2543
  const inOld = (pid) => !moved.has(pid);
2499
2544
  const pos = o.st.position, rot = o.st.rotation;
2500
- const fresh = build(sub(inNew), nid, pos, rot, carryFor(inNew));
2501
- const rest = build(sub(inOld), id, pos, rot, carryFor(inOld));
2545
+ const fresh = build(sub(inNew), nid, pos, rot, carryFor(inNew, (a, b) => inNew(a) && inNew(b)));
2546
+ const rest = build(sub(inOld), id, pos, rot, carryFor(inOld, (a, b) => !(inNew(a) && inNew(b))));
2502
2547
  objects.set(id, rest);
2503
2548
  objects.set(nid, fresh);
2504
2549
  return nid;
@@ -2596,34 +2641,37 @@ var GameDestruction = (() => {
2596
2641
  }
2597
2642
  return getObj(objectId).joints.filter((j) => !j.st.open && j.spec.tools.includes(tool)).map((j) => j.st);
2598
2643
  },
2599
- nearestJoint(objectId, point, maxDistance = 0.3) {
2644
+ nearestJoint(objectId, point, maxDistance = 0.3, tool) {
2600
2645
  const im = impls.get(objectId);
2646
+ const cands = [];
2601
2647
  if (im) {
2602
- let best2 = null;
2603
- let bd2 = maxDistance;
2648
+ const spec = im.impl.describe();
2604
2649
  for (const js of im.impl.state().joints) {
2605
2650
  if (js.open) continue;
2606
2651
  const pos = implJointPos(im, js.a, js.b);
2607
- const d = pos ? vec.distance(pos, point) : Infinity;
2608
- if (d <= bd2) {
2609
- bd2 = d;
2610
- best2 = js;
2611
- }
2652
+ const j = (spec.joints ?? []).find((x) => (x.id ?? `${x.a}~${x.b}`) === js.id);
2653
+ cands.push({ st: js, d: pos ? vec.distance(pos, point) : Infinity, ok: !!j && (j.tools ?? JOINT_DEFAULTS[j.kind].tools).includes(tool) });
2612
2654
  }
2613
- return best2;
2614
- }
2615
- const o = getObj(objectId);
2616
- let best = null;
2617
- let bd = maxDistance;
2618
- for (const j of o.joints) {
2619
- if (j.st.open) continue;
2620
- const d = vec.distance(jointWorld(o, j), point);
2621
- if (d <= bd) {
2622
- bd = d;
2623
- best = j.st;
2655
+ } else {
2656
+ const o = getObj(objectId);
2657
+ for (const j of o.joints) {
2658
+ if (j.st.open) continue;
2659
+ cands.push({ st: j.st, d: vec.distance(jointWorld(o, j), point), ok: tool ? j.spec.tools.includes(tool) : true });
2624
2660
  }
2625
2661
  }
2626
- return best;
2662
+ const pick = (only) => {
2663
+ let best = null;
2664
+ let bd = maxDistance;
2665
+ for (const c of cands) {
2666
+ if (only && !c.ok) continue;
2667
+ if (c.d <= bd) {
2668
+ bd = c.d;
2669
+ best = c.st;
2670
+ }
2671
+ }
2672
+ return best;
2673
+ };
2674
+ return (tool ? pick(true) : null) ?? pick(false);
2627
2675
  },
2628
2676
  extinguish(objectId, partId, coolC = 200) {
2629
2677
  const im = impls.get(objectId);
@@ -2666,6 +2714,10 @@ var GameDestruction = (() => {
2666
2714
  }).events;
2667
2715
  },
2668
2716
  state: (id) => objects.get(id)?.st ?? impls.get(id)?.impl.state() ?? null,
2717
+ snapshot: (id) => {
2718
+ const st = objects.get(id)?.st ?? impls.get(id)?.impl.state();
2719
+ return st ? clone(st) : null;
2720
+ },
2669
2721
  part: (id, partId) => objects.get(id)?.byId.get(partId)?.st ?? impls.get(id)?.impl.state().parts.find((p) => p.id === partId) ?? null,
2670
2722
  reset(id) {
2671
2723
  const im = impls.get(id);