@adatechnology/game-destruction 0.1.0 → 0.1.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.
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",
@@ -1458,6 +1459,15 @@ var GameDestruction = (() => {
1458
1459
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
1459
1460
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
1460
1461
  var clone = (v) => structuredClone(v);
1462
+ var updateBound = (o) => {
1463
+ if (!o.parts.length) return;
1464
+ const c = [0, 0, 0];
1465
+ for (const p of o.parts) for (let k = 0; k < 3; k++) c[k] += p.pos[k] / o.parts.length;
1466
+ let r = 0;
1467
+ for (const p of o.parts) r = Math.max(r, vec.distance(c, p.pos) + p.bound);
1468
+ o.bc = c;
1469
+ o.br = r;
1470
+ };
1461
1471
  var AXIS = { x: 0, y: 1, z: 2 };
1462
1472
  var createDestruction = (options = {}) => {
1463
1473
  const materials = /* @__PURE__ */ new Map();
@@ -1593,6 +1603,7 @@ var GameDestruction = (() => {
1593
1603
  mat,
1594
1604
  st: pst,
1595
1605
  q: quatFromEuler(...ps.rotation ?? [0, 0, 0]),
1606
+ pos: [ps.position[0], ps.position[1], ps.position[2]],
1596
1607
  mass0,
1597
1608
  fuel0,
1598
1609
  area: shapeArea(ps.shape),
@@ -1631,14 +1642,8 @@ var GameDestruction = (() => {
1631
1642
  o.byPart.get(r.b).push(j);
1632
1643
  o.st.joints.push(jst);
1633
1644
  }
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
- }
1645
+ for (const [jid, c] of carry?.joints ?? []) if (c.st.open && !o.jointMap.has(jid)) o.st.joints.push(c.st);
1646
+ updateBound(o);
1642
1647
  o.anyAnchor = o.parts.some((p) => p.anchored);
1643
1648
  o.unsup = o.parts.map(() => false);
1644
1649
  solveStructure(o);
@@ -1778,7 +1783,7 @@ var GameDestruction = (() => {
1778
1783
  if (!p) throw new Error(`unknown part "${id}" in object "${o.id}"`);
1779
1784
  return p;
1780
1785
  };
1781
- const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.spec.position), q: quat.mul(o.st.rotation, p.q) });
1786
+ const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.pos), q: quat.mul(o.st.rotation, p.q) });
1782
1787
  const objLocal = (o, w) => toLocal(o.st.position, o.st.rotation, w);
1783
1788
  const objLocalDir = (o, w) => rotateVec(quat.conjugate(o.st.rotation), w);
1784
1789
  const jointWorld = (o, j) => toWorld(o.st.position, o.st.rotation, j.pos);
@@ -1790,14 +1795,14 @@ var GameDestruction = (() => {
1790
1795
  const { c, q } = partXf(o, p);
1791
1796
  return aabbOf(p.spec.shape, c, q);
1792
1797
  };
1793
- const rayPartRaw = (pos, rot, spec, pq, origin, d) => {
1794
- const c = toWorld(pos, rot, spec.position);
1798
+ const rayPartRaw = (pos, rot, spec, pq, origin, d, lp = spec.position) => {
1799
+ const c = toWorld(pos, rot, lp);
1795
1800
  const q = quat.mul(rot, pq);
1796
1801
  const qi = quat.conjugate(q);
1797
1802
  const r = rayShape(spec.shape, rotateVec(qi, vec.sub(origin, c)), rotateVec(qi, d));
1798
1803
  return r ? { t0: r.t0, t1: r.t1, n: rotateVec(q, r.n0) } : null;
1799
1804
  };
1800
- const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d);
1805
+ const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d, p.pos);
1801
1806
  const raySphereMiss = (origin, d, c, r, maxDist) => {
1802
1807
  const oc = vec.sub(origin, c);
1803
1808
  const b = vec.dot(oc, d);
@@ -1822,7 +1827,8 @@ var GameDestruction = (() => {
1822
1827
  for (const im of impls.values()) {
1823
1828
  for (const ps of im.impl.describe().parts) {
1824
1829
  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);
1830
+ const ov = im.poses.get(ps.id);
1831
+ const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), origin, d, ov?.pos);
1826
1832
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
1827
1833
  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
1834
  }
@@ -1957,11 +1963,29 @@ var GameDestruction = (() => {
1957
1963
  collapseCheck(o, src);
1958
1964
  checkDestroyed(o, src);
1959
1965
  };
1960
- const wearJoints = (o, p, amount, near, radius = 0.06, src) => {
1966
+ const jointReach = (o, j, point) => {
1967
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
1968
+ let d2 = 0;
1969
+ for (let k = 0; k < 3; k++) {
1970
+ const lo = Math.min(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
1971
+ const hi = Math.max(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
1972
+ const d = point[k] < lo ? lo - point[k] : point[k] > hi ? point[k] - hi : 0;
1973
+ d2 += d * d;
1974
+ }
1975
+ return Math.sqrt(d2);
1976
+ };
1977
+ const jointReachLimit = (o, j) => {
1978
+ const pa = o.parts[j.ia], pb = o.parts[j.ib];
1979
+ const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
1980
+ return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
1981
+ };
1982
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
1961
1983
  if (amount <= 0) return;
1962
- for (const j of o.byPart.get(p.spec.id) ?? []) {
1984
+ const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
1985
+ if (split && mine.length) amount /= mine.length;
1986
+ for (const j of mine) {
1963
1987
  if (j.st.open) continue;
1964
- if (near && vec.distance(jointWorld(o, j), near) > radius) continue;
1988
+ if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
1965
1989
  j.st.hp -= amount;
1966
1990
  if (j.st.hp <= 0) {
1967
1991
  j.st.hp = 0;
@@ -2080,7 +2104,7 @@ var GameDestruction = (() => {
2080
2104
  if (mat.fracture !== "shatter") addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
2081
2105
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
2082
2106
  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);
2107
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
2084
2108
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
2085
2109
  return { penetrated, exitEnergy: penetrated ? E - thr : 0, exitPoint, damage: dmg };
2086
2110
  };
@@ -2155,8 +2179,9 @@ var GameDestruction = (() => {
2155
2179
  }
2156
2180
  for (const im of [...impls.values()]) {
2157
2181
  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]));
2182
+ const ov = im.poses.get(ps.id);
2183
+ const c = toWorld(im.position, im.rotation, ov?.pos ?? ps.position);
2184
+ const q = quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]));
2160
2185
  return distanceToShape(ps.shape, toLocal(c, q, point)) < radius;
2161
2186
  });
2162
2187
  if (inRange) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
@@ -2408,7 +2433,7 @@ var GameDestruction = (() => {
2408
2433
  const spread = (sources, dt) => {
2409
2434
  for (const src of sources) {
2410
2435
  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);
2436
+ const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.pos);
2412
2437
  const A = partWorldAabb(src.o, src.p);
2413
2438
  for (const o of objects.values()) {
2414
2439
  if (o === src.o) continue;
@@ -2454,9 +2479,23 @@ var GameDestruction = (() => {
2454
2479
  register(impl, opts = {}) {
2455
2480
  const id = opts.id ?? impl.id;
2456
2481
  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] });
2482
+ impls.set(id, { impl, position: opts.position ?? [0, 0, 0], rotation: opts.rotation ?? [0, 0, 0, 1], poses: /* @__PURE__ */ new Map() });
2458
2483
  return id;
2459
2484
  },
2485
+ setPartPose(objectId, partId, position, rotation) {
2486
+ const q = quatFromEuler(...rotation ?? [0, 0, 0]);
2487
+ const im = impls.get(objectId);
2488
+ if (im) {
2489
+ im.impl.setPartPose?.(partId, position, rotation);
2490
+ im.poses.set(partId, { pos: [position[0], position[1], position[2]], q });
2491
+ return;
2492
+ }
2493
+ const o = getObj(objectId);
2494
+ const p = getPart(o, partId);
2495
+ p.pos = [position[0], position[1], position[2]];
2496
+ p.q = q;
2497
+ updateBound(o);
2498
+ },
2460
2499
  remove(id) {
2461
2500
  objects.delete(id);
2462
2501
  impls.delete(id);
@@ -2483,9 +2522,12 @@ var GameDestruction = (() => {
2483
2522
  const moved = new Set(names);
2484
2523
  const nid = newId ?? nextId();
2485
2524
  if (objects.has(nid)) throw new Error(`object "${nid}" already exists`);
2486
- const carryFor = (keep) => ({
2525
+ const carryFor = (keep, keepOpen = () => false) => ({
2487
2526
  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 }])),
2527
+ joints: new Map([
2528
+ ...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 }]),
2529
+ ...o.st.joints.filter((st) => st.open && !o.jointMap.has(st.id) && keepOpen(st.a, st.b)).map((st) => [st.id, { st, step: 0 }])
2530
+ ]),
2489
2531
  collapsed: [...o.collapsed].filter(keep)
2490
2532
  });
2491
2533
  const sub = (keep) => {
@@ -2497,8 +2539,8 @@ var GameDestruction = (() => {
2497
2539
  const inNew = (pid) => moved.has(pid);
2498
2540
  const inOld = (pid) => !moved.has(pid);
2499
2541
  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));
2542
+ const fresh = build(sub(inNew), nid, pos, rot, carryFor(inNew, (a, b) => inNew(a) && inNew(b)));
2543
+ const rest = build(sub(inOld), id, pos, rot, carryFor(inOld, (a, b) => !(inNew(a) && inNew(b))));
2502
2544
  objects.set(id, rest);
2503
2545
  objects.set(nid, fresh);
2504
2546
  return nid;
@@ -2596,34 +2638,37 @@ var GameDestruction = (() => {
2596
2638
  }
2597
2639
  return getObj(objectId).joints.filter((j) => !j.st.open && j.spec.tools.includes(tool)).map((j) => j.st);
2598
2640
  },
2599
- nearestJoint(objectId, point, maxDistance = 0.3) {
2641
+ nearestJoint(objectId, point, maxDistance = 0.3, tool) {
2600
2642
  const im = impls.get(objectId);
2643
+ const cands = [];
2601
2644
  if (im) {
2602
- let best2 = null;
2603
- let bd2 = maxDistance;
2645
+ const spec = im.impl.describe();
2604
2646
  for (const js of im.impl.state().joints) {
2605
2647
  if (js.open) continue;
2606
2648
  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
- }
2649
+ const j = (spec.joints ?? []).find((x) => (x.id ?? `${x.a}~${x.b}`) === js.id);
2650
+ cands.push({ st: js, d: pos ? vec.distance(pos, point) : Infinity, ok: !!j && (j.tools ?? JOINT_DEFAULTS[j.kind].tools).includes(tool) });
2612
2651
  }
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;
2652
+ } else {
2653
+ const o = getObj(objectId);
2654
+ for (const j of o.joints) {
2655
+ if (j.st.open) continue;
2656
+ cands.push({ st: j.st, d: vec.distance(jointWorld(o, j), point), ok: tool ? j.spec.tools.includes(tool) : true });
2624
2657
  }
2625
2658
  }
2626
- return best;
2659
+ const pick = (only) => {
2660
+ let best = null;
2661
+ let bd = maxDistance;
2662
+ for (const c of cands) {
2663
+ if (only && !c.ok) continue;
2664
+ if (c.d <= bd) {
2665
+ bd = c.d;
2666
+ best = c.st;
2667
+ }
2668
+ }
2669
+ return best;
2670
+ };
2671
+ return (tool ? pick(true) : null) ?? pick(false);
2627
2672
  },
2628
2673
  extinguish(objectId, partId, coolC = 200) {
2629
2674
  const im = impls.get(objectId);
@@ -2666,6 +2711,10 @@ var GameDestruction = (() => {
2666
2711
  }).events;
2667
2712
  },
2668
2713
  state: (id) => objects.get(id)?.st ?? impls.get(id)?.impl.state() ?? null,
2714
+ snapshot: (id) => {
2715
+ const st = objects.get(id)?.st ?? impls.get(id)?.impl.state();
2716
+ return st ? clone(st) : null;
2717
+ },
2669
2718
  part: (id, partId) => objects.get(id)?.byId.get(partId)?.st ?? impls.get(id)?.impl.state().parts.find((p) => p.id === partId) ?? null,
2670
2719
  reset(id) {
2671
2720
  const im = impls.get(id);