@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 +19 -2
- package/dist/index.global.js +100 -48
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +100 -48
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
/**
|
|
143
|
-
|
|
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;
|
package/dist/index.global.js
CHANGED
|
@@ -1136,7 +1136,7 @@ var GameDestruction = (() => {
|
|
|
1136
1136
|
{
|
|
1137
1137
|
id: "green_wood",
|
|
1138
1138
|
density: 650,
|
|
1139
|
-
hpPerKg:
|
|
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.
|
|
1635
|
-
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
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.
|
|
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
|
|
2159
|
-
const
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
2608
|
-
|
|
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
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
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
|
-
|
|
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);
|