@adatechnology/game-test-room 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/README.md +5 -1
- package/dist/index.d.ts +17 -3
- package/dist/index.global.js +555 -203
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +135 -22
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.global.js
CHANGED
|
@@ -126,6 +126,82 @@ var GameTestRoom = (() => {
|
|
|
126
126
|
}
|
|
127
127
|
return { ok: problems.length === 0, problems };
|
|
128
128
|
};
|
|
129
|
+
var STRUCTURE_ROLES = ["foundation", "column", "beam", "slab", "wall", "partition", "roof", "stair", "none"];
|
|
130
|
+
var BEARS = [true, false, "a_on_b", "b_on_a"];
|
|
131
|
+
var finite = (n) => typeof n === "number" && Number.isFinite(n);
|
|
132
|
+
var positive = (n) => finite(n) && n > 0;
|
|
133
|
+
var vec3 = (v) => Array.isArray(v) && v.length === 3 && v.every(finite);
|
|
134
|
+
var nonNegative = (n) => n === void 0 || finite(n) && n >= 0;
|
|
135
|
+
var checkSpec = (spec, options = {}) => {
|
|
136
|
+
const problems = [];
|
|
137
|
+
const warnings = [];
|
|
138
|
+
const materials = options.materials ? new Set(options.materials) : null;
|
|
139
|
+
const parts = Array.isArray(spec?.parts) ? spec.parts : [];
|
|
140
|
+
if (!Array.isArray(spec?.parts) || parts.length === 0) problems.push("the spec needs at least one part");
|
|
141
|
+
const ids = /* @__PURE__ */ new Set();
|
|
142
|
+
for (const p of parts) {
|
|
143
|
+
const id = String(p?.id ?? "");
|
|
144
|
+
const label = `part "${id}"`;
|
|
145
|
+
if (id === "") problems.push("a part has no id");
|
|
146
|
+
else if (ids.has(id)) problems.push(`duplicate part id "${id}"`);
|
|
147
|
+
ids.add(id);
|
|
148
|
+
if (materials && !materials.has(p?.material)) problems.push(`${label} has unknown material "${String(p?.material)}"`);
|
|
149
|
+
else if (typeof p?.material !== "string" || p.material === "") problems.push(`${label} needs a material`);
|
|
150
|
+
const s = p?.shape;
|
|
151
|
+
if (s?.kind === "box") {
|
|
152
|
+
if (!Array.isArray(s.size) || s.size.length !== 3 || !s.size.every(positive)) problems.push(`${label} box size must be three finite positive numbers`);
|
|
153
|
+
} else if (s?.kind === "cylinder") {
|
|
154
|
+
if (!positive(s.radius) || !positive(s.height)) problems.push(`${label} cylinder radius and height must be finite and positive`);
|
|
155
|
+
} else if (s?.kind === "sphere") {
|
|
156
|
+
if (!positive(s.radius)) problems.push(`${label} sphere radius must be finite and positive`);
|
|
157
|
+
} else problems.push(`${label} has an unknown shape`);
|
|
158
|
+
if (!vec3(p?.position)) problems.push(`${label} needs a finite position [x, y, z]`);
|
|
159
|
+
if (p?.rotation !== void 0 && !vec3(p.rotation)) problems.push(`${label} has an invalid rotation`);
|
|
160
|
+
for (const f of ["mass", "hp", "thickness"]) if (!nonNegative(p?.[f])) problems.push(`${label} ${f} must be a finite number >= 0`);
|
|
161
|
+
if (p?.fill !== void 0 && !(finite(p.fill) && p.fill >= 0 && p.fill <= 1)) problems.push(`${label} fill must be between 0 and 1`);
|
|
162
|
+
if (p?.structure) {
|
|
163
|
+
if (!STRUCTURE_ROLES.includes(p.structure.role)) problems.push(`${label} has an invalid structure role "${String(p.structure.role)}"`);
|
|
164
|
+
if (!nonNegative(p.structure.capacity)) problems.push(`${label} structure capacity must be a finite number >= 0`);
|
|
165
|
+
}
|
|
166
|
+
if (p?.contains && !nonNegative(p.contains.liters)) problems.push(`${label} contains.liters must be a finite number >= 0`);
|
|
167
|
+
}
|
|
168
|
+
const jointIds = /* @__PURE__ */ new Set();
|
|
169
|
+
const edges = [];
|
|
170
|
+
for (const j of spec?.joints ?? []) {
|
|
171
|
+
const id = j.id ?? `${j.a}~${j.b}`;
|
|
172
|
+
const label = `joint "${id}"`;
|
|
173
|
+
if (jointIds.has(id)) problems.push(`duplicate joint id "${id}"`);
|
|
174
|
+
jointIds.add(id);
|
|
175
|
+
if (!ids.has(j.a)) problems.push(`${label} references unknown part "${String(j.a)}"`);
|
|
176
|
+
if (!ids.has(j.b)) problems.push(`${label} references unknown part "${String(j.b)}"`);
|
|
177
|
+
if (j.a === j.b) problems.push(`${label} joins part "${String(j.a)}" to itself`);
|
|
178
|
+
if (j.bears !== void 0 && !BEARS.includes(j.bears)) problems.push(`${label} has an invalid bears "${String(j.bears)}"`);
|
|
179
|
+
if (!nonNegative(j.strength)) problems.push(`${label} strength must be a finite number >= 0`);
|
|
180
|
+
if (!nonNegative(j.seconds)) problems.push(`${label} seconds must be a finite number >= 0`);
|
|
181
|
+
if (j.bears !== false && ids.has(j.a) && ids.has(j.b) && j.a !== j.b) edges.push([j.a, j.b]);
|
|
182
|
+
}
|
|
183
|
+
const structural = parts.filter((p) => p?.structure && p.structure.role !== "none");
|
|
184
|
+
if (structural.length && spec.joints) {
|
|
185
|
+
const anchored = parts.filter((p) => p.anchored || p.structure?.role === "foundation").map((p) => p.id);
|
|
186
|
+
const adj = /* @__PURE__ */ new Map();
|
|
187
|
+
for (const [a, b] of edges) {
|
|
188
|
+
adj.set(a, [...adj.get(a) ?? [], b]);
|
|
189
|
+
adj.set(b, [...adj.get(b) ?? [], a]);
|
|
190
|
+
}
|
|
191
|
+
const seen = new Set(anchored);
|
|
192
|
+
const queue = [...anchored];
|
|
193
|
+
while (queue.length) {
|
|
194
|
+
for (const n of adj.get(queue.pop()) ?? []) {
|
|
195
|
+
if (seen.has(n)) continue;
|
|
196
|
+
seen.add(n);
|
|
197
|
+
queue.push(n);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const lost = structural.filter((p) => !seen.has(p.id)).map((p) => p.id);
|
|
201
|
+
if (lost.length) warnings.push(`structural part(s) ${lost.map((i) => `"${i}"`).join(", ")} cannot reach an anchored part, so they will fall`);
|
|
202
|
+
}
|
|
203
|
+
return { ok: problems.length === 0, problems, warnings };
|
|
204
|
+
};
|
|
129
205
|
var finite3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
130
206
|
var checkAnimation = (clip, partIds) => {
|
|
131
207
|
const problems = [];
|
|
@@ -1251,7 +1327,7 @@ var GameTestRoom = (() => {
|
|
|
1251
1327
|
{
|
|
1252
1328
|
id: "green_wood",
|
|
1253
1329
|
density: 650,
|
|
1254
|
-
hpPerKg:
|
|
1330
|
+
hpPerKg: 5,
|
|
1255
1331
|
penetration: 10,
|
|
1256
1332
|
ductility: 0.5,
|
|
1257
1333
|
fracture: "splinter",
|
|
@@ -1260,7 +1336,8 @@ var GameTestRoom = (() => {
|
|
|
1260
1336
|
0.2
|
|
1261
1337
|
],
|
|
1262
1338
|
damageScale: {
|
|
1263
|
-
slash: 1.3
|
|
1339
|
+
slash: 1.3,
|
|
1340
|
+
ballistic: 0.1
|
|
1264
1341
|
},
|
|
1265
1342
|
hitDebris: "splinters",
|
|
1266
1343
|
breakDebris: "leaves",
|
|
@@ -1567,6 +1644,15 @@ var GameTestRoom = (() => {
|
|
|
1567
1644
|
var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
1568
1645
|
var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
|
|
1569
1646
|
var clone = (v) => structuredClone(v);
|
|
1647
|
+
var updateBound = (o) => {
|
|
1648
|
+
if (!o.parts.length) return;
|
|
1649
|
+
const c = [0, 0, 0];
|
|
1650
|
+
for (const p of o.parts) for (let k = 0; k < 3; k++) c[k] += p.pos[k] / o.parts.length;
|
|
1651
|
+
let r = 0;
|
|
1652
|
+
for (const p of o.parts) r = Math.max(r, vec.distance(c, p.pos) + p.bound);
|
|
1653
|
+
o.bc = c;
|
|
1654
|
+
o.br = r;
|
|
1655
|
+
};
|
|
1570
1656
|
var AXIS = { x: 0, y: 1, z: 2 };
|
|
1571
1657
|
var createDestruction = (options = {}) => {
|
|
1572
1658
|
const materials = /* @__PURE__ */ new Map();
|
|
@@ -1702,6 +1788,7 @@ var GameTestRoom = (() => {
|
|
|
1702
1788
|
mat,
|
|
1703
1789
|
st: pst,
|
|
1704
1790
|
q: quatFromEuler(...ps.rotation ?? [0, 0, 0]),
|
|
1791
|
+
pos: [ps.position[0], ps.position[1], ps.position[2]],
|
|
1705
1792
|
mass0,
|
|
1706
1793
|
fuel0,
|
|
1707
1794
|
area: shapeArea(ps.shape),
|
|
@@ -1740,14 +1827,8 @@ var GameTestRoom = (() => {
|
|
|
1740
1827
|
o.byPart.get(r.b).push(j);
|
|
1741
1828
|
o.st.joints.push(jst);
|
|
1742
1829
|
}
|
|
1743
|
-
if (o.
|
|
1744
|
-
|
|
1745
|
-
for (const p of o.parts) for (let k = 0; k < 3; k++) c[k] += p.spec.position[k] / o.parts.length;
|
|
1746
|
-
let r = 0;
|
|
1747
|
-
for (const p of o.parts) r = Math.max(r, vec.distance(c, p.spec.position) + p.bound);
|
|
1748
|
-
o.bc = c;
|
|
1749
|
-
o.br = r;
|
|
1750
|
-
}
|
|
1830
|
+
for (const [jid, c] of carry?.joints ?? []) if (c.st.open && !o.jointMap.has(jid)) o.st.joints.push(c.st);
|
|
1831
|
+
updateBound(o);
|
|
1751
1832
|
o.anyAnchor = o.parts.some((p) => p.anchored);
|
|
1752
1833
|
o.unsup = o.parts.map(() => false);
|
|
1753
1834
|
solveStructure(o);
|
|
@@ -1887,7 +1968,7 @@ var GameTestRoom = (() => {
|
|
|
1887
1968
|
if (!p) throw new Error(`unknown part "${id}" in object "${o.id}"`);
|
|
1888
1969
|
return p;
|
|
1889
1970
|
};
|
|
1890
|
-
const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.
|
|
1971
|
+
const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.pos), q: quat.mul(o.st.rotation, p.q) });
|
|
1891
1972
|
const objLocal = (o, w) => toLocal(o.st.position, o.st.rotation, w);
|
|
1892
1973
|
const objLocalDir = (o, w) => rotateVec(quat.conjugate(o.st.rotation), w);
|
|
1893
1974
|
const jointWorld = (o, j) => toWorld(o.st.position, o.st.rotation, j.pos);
|
|
@@ -1899,14 +1980,14 @@ var GameTestRoom = (() => {
|
|
|
1899
1980
|
const { c, q } = partXf(o, p);
|
|
1900
1981
|
return aabbOf(p.spec.shape, c, q);
|
|
1901
1982
|
};
|
|
1902
|
-
const rayPartRaw = (pos, rot, spec, pq, origin, d) => {
|
|
1903
|
-
const c = toWorld(pos, rot,
|
|
1983
|
+
const rayPartRaw = (pos, rot, spec, pq, origin, d, lp = spec.position) => {
|
|
1984
|
+
const c = toWorld(pos, rot, lp);
|
|
1904
1985
|
const q = quat.mul(rot, pq);
|
|
1905
1986
|
const qi = quat.conjugate(q);
|
|
1906
1987
|
const r = rayShape(spec.shape, rotateVec(qi, vec.sub(origin, c)), rotateVec(qi, d));
|
|
1907
1988
|
return r ? { t0: r.t0, t1: r.t1, n: rotateVec(q, r.n0) } : null;
|
|
1908
1989
|
};
|
|
1909
|
-
const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d);
|
|
1990
|
+
const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d, p.pos);
|
|
1910
1991
|
const raySphereMiss = (origin, d, c, r, maxDist) => {
|
|
1911
1992
|
const oc = vec.sub(origin, c);
|
|
1912
1993
|
const b = vec.dot(oc, d);
|
|
@@ -1931,7 +2012,8 @@ var GameTestRoom = (() => {
|
|
|
1931
2012
|
for (const im of impls.values()) {
|
|
1932
2013
|
for (const ps of im.impl.describe().parts) {
|
|
1933
2014
|
if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
|
|
1934
|
-
const
|
|
2015
|
+
const ov = im.poses.get(ps.id);
|
|
2016
|
+
const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), origin, d, ov?.pos);
|
|
1935
2017
|
if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
|
|
1936
2018
|
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 };
|
|
1937
2019
|
}
|
|
@@ -2066,11 +2148,29 @@ var GameTestRoom = (() => {
|
|
|
2066
2148
|
collapseCheck(o, src);
|
|
2067
2149
|
checkDestroyed(o, src);
|
|
2068
2150
|
};
|
|
2069
|
-
const
|
|
2151
|
+
const jointReach = (o, j, point) => {
|
|
2152
|
+
const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
|
|
2153
|
+
let d2 = 0;
|
|
2154
|
+
for (let k = 0; k < 3; k++) {
|
|
2155
|
+
const lo = Math.min(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
|
|
2156
|
+
const hi = Math.max(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
|
|
2157
|
+
const d = point[k] < lo ? lo - point[k] : point[k] > hi ? point[k] - hi : 0;
|
|
2158
|
+
d2 += d * d;
|
|
2159
|
+
}
|
|
2160
|
+
return Math.sqrt(d2);
|
|
2161
|
+
};
|
|
2162
|
+
const jointReachLimit = (o, j) => {
|
|
2163
|
+
const pa = o.parts[j.ia], pb = o.parts[j.ib];
|
|
2164
|
+
const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
|
|
2165
|
+
return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
|
|
2166
|
+
};
|
|
2167
|
+
const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
|
|
2070
2168
|
if (amount <= 0) return;
|
|
2071
|
-
|
|
2169
|
+
const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
|
|
2170
|
+
if (split && mine.length) amount /= mine.length;
|
|
2171
|
+
for (const j of mine) {
|
|
2072
2172
|
if (j.st.open) continue;
|
|
2073
|
-
if (near &&
|
|
2173
|
+
if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
|
|
2074
2174
|
j.st.hp -= amount;
|
|
2075
2175
|
if (j.st.hp <= 0) {
|
|
2076
2176
|
j.st.hp = 0;
|
|
@@ -2189,7 +2289,7 @@ var GameTestRoom = (() => {
|
|
|
2189
2289
|
if (mat.fracture !== "shatter") addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
|
|
2190
2290
|
} else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
|
|
2191
2291
|
debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
|
|
2192
|
-
wearJoints(o, p, E * 0.
|
|
2292
|
+
wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
|
|
2193
2293
|
const dmg = applyDamage(o, p, E * DMG_PER_J * scale2, "ballistic", entry, d, E * 0.1, 12, src);
|
|
2194
2294
|
return { penetrated, exitEnergy: penetrated ? E - thr : 0, exitPoint, damage: dmg };
|
|
2195
2295
|
};
|
|
@@ -2264,8 +2364,9 @@ var GameTestRoom = (() => {
|
|
|
2264
2364
|
}
|
|
2265
2365
|
for (const im of [...impls.values()]) {
|
|
2266
2366
|
const inRange = im.impl.describe().parts.some((ps) => {
|
|
2267
|
-
const
|
|
2268
|
-
const
|
|
2367
|
+
const ov = im.poses.get(ps.id);
|
|
2368
|
+
const c = toWorld(im.position, im.rotation, ov?.pos ?? ps.position);
|
|
2369
|
+
const q = quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]));
|
|
2269
2370
|
return distanceToShape(ps.shape, toLocal(c, q, point)) < radius;
|
|
2270
2371
|
});
|
|
2271
2372
|
if (inRange) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
|
|
@@ -2517,7 +2618,7 @@ var GameTestRoom = (() => {
|
|
|
2517
2618
|
const spread = (sources, dt) => {
|
|
2518
2619
|
for (const src of sources) {
|
|
2519
2620
|
if (!src.p.st.burning && !src.p.rt.jet) continue;
|
|
2520
|
-
const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.
|
|
2621
|
+
const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.pos);
|
|
2521
2622
|
const A = partWorldAabb(src.o, src.p);
|
|
2522
2623
|
for (const o of objects.values()) {
|
|
2523
2624
|
if (o === src.o) continue;
|
|
@@ -2563,9 +2664,23 @@ var GameTestRoom = (() => {
|
|
|
2563
2664
|
register(impl, opts = {}) {
|
|
2564
2665
|
const id = opts.id ?? impl.id;
|
|
2565
2666
|
if (taken(id)) throw new Error(`object "${id}" already exists`);
|
|
2566
|
-
impls.set(id, { impl, position: opts.position ?? [0, 0, 0], rotation: opts.rotation ?? [0, 0, 0, 1] });
|
|
2667
|
+
impls.set(id, { impl, position: opts.position ?? [0, 0, 0], rotation: opts.rotation ?? [0, 0, 0, 1], poses: /* @__PURE__ */ new Map() });
|
|
2567
2668
|
return id;
|
|
2568
2669
|
},
|
|
2670
|
+
setPartPose(objectId, partId, position, rotation) {
|
|
2671
|
+
const q = quatFromEuler(...rotation ?? [0, 0, 0]);
|
|
2672
|
+
const im = impls.get(objectId);
|
|
2673
|
+
if (im) {
|
|
2674
|
+
im.impl.setPartPose?.(partId, position, rotation);
|
|
2675
|
+
im.poses.set(partId, { pos: [position[0], position[1], position[2]], q });
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
const o = getObj(objectId);
|
|
2679
|
+
const p = getPart(o, partId);
|
|
2680
|
+
p.pos = [position[0], position[1], position[2]];
|
|
2681
|
+
p.q = q;
|
|
2682
|
+
updateBound(o);
|
|
2683
|
+
},
|
|
2569
2684
|
remove(id) {
|
|
2570
2685
|
objects.delete(id);
|
|
2571
2686
|
impls.delete(id);
|
|
@@ -2592,9 +2707,12 @@ var GameTestRoom = (() => {
|
|
|
2592
2707
|
const moved = new Set(names);
|
|
2593
2708
|
const nid = newId ?? nextId();
|
|
2594
2709
|
if (objects.has(nid)) throw new Error(`object "${nid}" already exists`);
|
|
2595
|
-
const carryFor = (keep) => ({
|
|
2710
|
+
const carryFor = (keep, keepOpen = () => false) => ({
|
|
2596
2711
|
parts: new Map(o.parts.filter((p) => keep(p.spec.id)).map((p) => [p.spec.id, { st: p.st, rt: p.rt }])),
|
|
2597
|
-
joints: new Map(
|
|
2712
|
+
joints: new Map([
|
|
2713
|
+
...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 }]),
|
|
2714
|
+
...o.st.joints.filter((st) => st.open && !o.jointMap.has(st.id) && keepOpen(st.a, st.b)).map((st) => [st.id, { st, step: 0 }])
|
|
2715
|
+
]),
|
|
2598
2716
|
collapsed: [...o.collapsed].filter(keep)
|
|
2599
2717
|
});
|
|
2600
2718
|
const sub3 = (keep) => {
|
|
@@ -2606,8 +2724,8 @@ var GameTestRoom = (() => {
|
|
|
2606
2724
|
const inNew = (pid) => moved.has(pid);
|
|
2607
2725
|
const inOld = (pid) => !moved.has(pid);
|
|
2608
2726
|
const pos = o.st.position, rot = o.st.rotation;
|
|
2609
|
-
const fresh = build(sub3(inNew), nid, pos, rot, carryFor(inNew));
|
|
2610
|
-
const rest = build(sub3(inOld), id, pos, rot, carryFor(inOld));
|
|
2727
|
+
const fresh = build(sub3(inNew), nid, pos, rot, carryFor(inNew, (a, b) => inNew(a) && inNew(b)));
|
|
2728
|
+
const rest = build(sub3(inOld), id, pos, rot, carryFor(inOld, (a, b) => !(inNew(a) && inNew(b))));
|
|
2611
2729
|
objects.set(id, rest);
|
|
2612
2730
|
objects.set(nid, fresh);
|
|
2613
2731
|
return nid;
|
|
@@ -2705,34 +2823,37 @@ var GameTestRoom = (() => {
|
|
|
2705
2823
|
}
|
|
2706
2824
|
return getObj(objectId).joints.filter((j) => !j.st.open && j.spec.tools.includes(tool)).map((j) => j.st);
|
|
2707
2825
|
},
|
|
2708
|
-
nearestJoint(objectId, point, maxDistance = 0.3) {
|
|
2826
|
+
nearestJoint(objectId, point, maxDistance = 0.3, tool) {
|
|
2709
2827
|
const im = impls.get(objectId);
|
|
2828
|
+
const cands = [];
|
|
2710
2829
|
if (im) {
|
|
2711
|
-
|
|
2712
|
-
let bd2 = maxDistance;
|
|
2830
|
+
const spec = im.impl.describe();
|
|
2713
2831
|
for (const js of im.impl.state().joints) {
|
|
2714
2832
|
if (js.open) continue;
|
|
2715
2833
|
const pos = implJointPos(im, js.a, js.b);
|
|
2716
|
-
const
|
|
2717
|
-
|
|
2718
|
-
bd2 = d;
|
|
2719
|
-
best2 = js;
|
|
2720
|
-
}
|
|
2834
|
+
const j = (spec.joints ?? []).find((x) => (x.id ?? `${x.a}~${x.b}`) === js.id);
|
|
2835
|
+
cands.push({ st: js, d: pos ? vec.distance(pos, point) : Infinity, ok: !!j && (j.tools ?? JOINT_DEFAULTS[j.kind].tools).includes(tool) });
|
|
2721
2836
|
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
for (const j of o.joints) {
|
|
2728
|
-
if (j.st.open) continue;
|
|
2729
|
-
const d = vec.distance(jointWorld(o, j), point);
|
|
2730
|
-
if (d <= bd) {
|
|
2731
|
-
bd = d;
|
|
2732
|
-
best = j.st;
|
|
2837
|
+
} else {
|
|
2838
|
+
const o = getObj(objectId);
|
|
2839
|
+
for (const j of o.joints) {
|
|
2840
|
+
if (j.st.open) continue;
|
|
2841
|
+
cands.push({ st: j.st, d: vec.distance(jointWorld(o, j), point), ok: tool ? j.spec.tools.includes(tool) : true });
|
|
2733
2842
|
}
|
|
2734
2843
|
}
|
|
2735
|
-
|
|
2844
|
+
const pick = (only) => {
|
|
2845
|
+
let best = null;
|
|
2846
|
+
let bd = maxDistance;
|
|
2847
|
+
for (const c of cands) {
|
|
2848
|
+
if (only && !c.ok) continue;
|
|
2849
|
+
if (c.d <= bd) {
|
|
2850
|
+
bd = c.d;
|
|
2851
|
+
best = c.st;
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
return best;
|
|
2855
|
+
};
|
|
2856
|
+
return (tool ? pick(true) : null) ?? pick(false);
|
|
2736
2857
|
},
|
|
2737
2858
|
extinguish(objectId, partId, coolC = 200) {
|
|
2738
2859
|
const im = impls.get(objectId);
|
|
@@ -2775,6 +2896,10 @@ var GameTestRoom = (() => {
|
|
|
2775
2896
|
}).events;
|
|
2776
2897
|
},
|
|
2777
2898
|
state: (id) => objects.get(id)?.st ?? impls.get(id)?.impl.state() ?? null,
|
|
2899
|
+
snapshot: (id) => {
|
|
2900
|
+
const st = objects.get(id)?.st ?? impls.get(id)?.impl.state();
|
|
2901
|
+
return st ? clone(st) : null;
|
|
2902
|
+
},
|
|
2778
2903
|
part: (id, partId) => objects.get(id)?.byId.get(partId)?.st ?? impls.get(id)?.impl.state().parts.find((p) => p.id === partId) ?? null,
|
|
2779
2904
|
reset(id) {
|
|
2780
2905
|
const im = impls.get(id);
|
|
@@ -3786,6 +3911,8 @@ void main() {
|
|
|
3786
3911
|
var SLEEP_AFTER = 0.5;
|
|
3787
3912
|
var HARD_LANDING = 3;
|
|
3788
3913
|
var SLOP = 2e-3;
|
|
3914
|
+
var NARROW = 0.3;
|
|
3915
|
+
var TOPPLE = 3;
|
|
3789
3916
|
var PieceSystem = class {
|
|
3790
3917
|
constructor(THREE, o) {
|
|
3791
3918
|
__publicField(this, "THREE", THREE);
|
|
@@ -3917,6 +4044,7 @@ void main() {
|
|
|
3917
4044
|
let landPoint = [pos[0], pos[1], pos[2]];
|
|
3918
4045
|
let contactedLast = false;
|
|
3919
4046
|
let touched = false;
|
|
4047
|
+
let unstable = false;
|
|
3920
4048
|
for (let s = 0; s < steps; s++) {
|
|
3921
4049
|
v[1] -= this.o.gravity * h;
|
|
3922
4050
|
pos[0] += v[0] * h;
|
|
@@ -4005,6 +4133,25 @@ void main() {
|
|
|
4005
4133
|
}
|
|
4006
4134
|
}
|
|
4007
4135
|
}
|
|
4136
|
+
let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
|
|
4137
|
+
for (const r of rs) {
|
|
4138
|
+
x0 = Math.min(x0, r[0]);
|
|
4139
|
+
x1 = Math.max(x1, r[0]);
|
|
4140
|
+
z0 = Math.min(z0, r[2]);
|
|
4141
|
+
z1 = Math.max(z1, r[2]);
|
|
4142
|
+
low = Math.min(low, r[1]);
|
|
4143
|
+
}
|
|
4144
|
+
const height = -low;
|
|
4145
|
+
const wx = x1 - x0;
|
|
4146
|
+
const wz = z1 - z0;
|
|
4147
|
+
unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
|
|
4148
|
+
if (unstable) {
|
|
4149
|
+
const dirX = x0 + x1 > 0 ? -1 : 1;
|
|
4150
|
+
const dirZ = z0 + z1 > 0 ? -1 : 1;
|
|
4151
|
+
const k = TOPPLE * Math.min(height, 1) * h;
|
|
4152
|
+
if (wx <= wz) w[2] -= dirX * k;
|
|
4153
|
+
else w[0] += dirZ * k;
|
|
4154
|
+
}
|
|
4008
4155
|
const rd = Math.exp((rs.length >= 3 ? -3 : -1.5) * h);
|
|
4009
4156
|
w[0] *= rd;
|
|
4010
4157
|
w[1] *= rd;
|
|
@@ -4022,7 +4169,7 @@ void main() {
|
|
|
4022
4169
|
b.lastHit = this.time;
|
|
4023
4170
|
this.o.onHardLanding(b, landPoint, landing);
|
|
4024
4171
|
}
|
|
4025
|
-
const still = touched && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
|
|
4172
|
+
const still = touched && !unstable && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
|
|
4026
4173
|
if (still) {
|
|
4027
4174
|
b.still += dt;
|
|
4028
4175
|
if (b.still >= SLEEP_AFTER) {
|
|
@@ -4081,6 +4228,163 @@ void main() {
|
|
|
4081
4228
|
}
|
|
4082
4229
|
}
|
|
4083
4230
|
};
|
|
4231
|
+
function partHintOf(value) {
|
|
4232
|
+
if (typeof value === "string") return value ? { id: value } : null;
|
|
4233
|
+
if (!value || typeof value !== "object") return null;
|
|
4234
|
+
const v = value;
|
|
4235
|
+
if (typeof v.id !== "string" || !v.id) return null;
|
|
4236
|
+
const str = (x) => typeof x === "string" && x ? x : void 0;
|
|
4237
|
+
return { id: v.id, material: str(v.material), parent: str(v.parent), kind: str(v.kind) };
|
|
4238
|
+
}
|
|
4239
|
+
var METAL = /steel|iron|metal|alumin|copper|brass|bronze|tin|titan|chrome|zinc|lead/i;
|
|
4240
|
+
function sanitizeId(name) {
|
|
4241
|
+
let id = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4242
|
+
if (id && /^[0-9]/.test(id)) id = `p_${id}`;
|
|
4243
|
+
return id;
|
|
4244
|
+
}
|
|
4245
|
+
function isMesh(o) {
|
|
4246
|
+
return o.isMesh === true;
|
|
4247
|
+
}
|
|
4248
|
+
function sizeOf(box2) {
|
|
4249
|
+
return [box2.max.x - box2.min.x, box2.max.y - box2.min.y, box2.max.z - box2.min.z];
|
|
4250
|
+
}
|
|
4251
|
+
function specFromObject3D(THREE, root, options = {}) {
|
|
4252
|
+
const fallback = options.fallback ?? "plastic";
|
|
4253
|
+
const minSize = options.minSize ?? 0.015;
|
|
4254
|
+
root.updateMatrixWorld(true);
|
|
4255
|
+
const inv = root.matrixWorld.clone().invert();
|
|
4256
|
+
const entries = [];
|
|
4257
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4258
|
+
const visit = (o) => {
|
|
4259
|
+
if (!o.visible || o.userData?.destructionIgnore) return;
|
|
4260
|
+
if (isMesh(o) && o.geometry) {
|
|
4261
|
+
const geom = o.geometry;
|
|
4262
|
+
if (!geom.boundingBox) geom.computeBoundingBox();
|
|
4263
|
+
if (geom.boundingBox && !geom.boundingBox.isEmpty()) {
|
|
4264
|
+
const box2 = geom.boundingBox.clone().applyMatrix4(new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld));
|
|
4265
|
+
const mats = Array.isArray(o.material) ? o.material : [o.material];
|
|
4266
|
+
const matName = mats[0]?.name ?? "";
|
|
4267
|
+
const ud = o.userData ?? {};
|
|
4268
|
+
const hint = partHintOf(ud.destructionPart);
|
|
4269
|
+
const material = options.materialOf?.(o) ?? (typeof ud.destructionMaterial === "string" ? ud.destructionMaterial : void 0) ?? hint?.material ?? guessMaterial(`${o.name} ${matName} ${typeof ud.material === "string" ? ud.material : ""}`) ?? fallback;
|
|
4270
|
+
const explicit = hint?.id ?? null;
|
|
4271
|
+
const hollow = ud.hollow === true || /hollow/i.test(o.name);
|
|
4272
|
+
const fill = options.fillOf?.(o, material);
|
|
4273
|
+
const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
|
|
4274
|
+
const existing = explicit ? byKey.get(explicit) : void 0;
|
|
4275
|
+
if (existing) {
|
|
4276
|
+
const before = sizeOf(existing.box);
|
|
4277
|
+
const bv = before[0] * before[1] * before[2];
|
|
4278
|
+
const ns = sizeOf(box2);
|
|
4279
|
+
existing.box.union(box2);
|
|
4280
|
+
existing.meshes.push(o);
|
|
4281
|
+
if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
|
|
4282
|
+
existing.hollow = existing.hollow || hollow;
|
|
4283
|
+
if (fill !== void 0) existing.fill = fill;
|
|
4284
|
+
} else {
|
|
4285
|
+
const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box: box2, material, explicitId: explicit, fill, hollow, tags, fixedId: !!explicit, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
|
|
4286
|
+
entries.push(e);
|
|
4287
|
+
if (explicit) byKey.set(explicit, e);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
for (const c of o.children) visit(c);
|
|
4292
|
+
};
|
|
4293
|
+
visit(root);
|
|
4294
|
+
const eps = 5e-3;
|
|
4295
|
+
const overlapVolume = (a, b) => {
|
|
4296
|
+
const x = Math.min(a.max.x, b.max.x) - Math.max(a.min.x, b.min.x) + eps;
|
|
4297
|
+
const y = Math.min(a.max.y, b.max.y) - Math.max(a.min.y, b.min.y) + eps;
|
|
4298
|
+
const z = Math.min(a.max.z, b.max.z) - Math.max(a.min.z, b.min.z) + eps;
|
|
4299
|
+
return x > 0 && y > 0 && z > 0 ? x * y * z : 0;
|
|
4300
|
+
};
|
|
4301
|
+
const largest = (e) => Math.max(...sizeOf(e.box));
|
|
4302
|
+
const kept = [];
|
|
4303
|
+
const tiny = [];
|
|
4304
|
+
for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
|
|
4305
|
+
for (const t of tiny) {
|
|
4306
|
+
let best = null;
|
|
4307
|
+
let bestV = 0;
|
|
4308
|
+
for (const k of kept) {
|
|
4309
|
+
const v = overlapVolume(t.box, k.box);
|
|
4310
|
+
if (v > bestV) {
|
|
4311
|
+
bestV = v;
|
|
4312
|
+
best = k;
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
if (!best) {
|
|
4316
|
+
let bd = 0.05;
|
|
4317
|
+
for (const k of kept) {
|
|
4318
|
+
const d = k.box.distanceToPoint(t.box.getCenter(new THREE.Vector3()));
|
|
4319
|
+
if (d < bd) {
|
|
4320
|
+
bd = d;
|
|
4321
|
+
best = k;
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
if (best) {
|
|
4326
|
+
best.meshes.push(...t.meshes);
|
|
4327
|
+
} else {
|
|
4328
|
+
kept.push(t);
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
const used = /* @__PURE__ */ new Set();
|
|
4332
|
+
const parts = [];
|
|
4333
|
+
const meshesOf = {};
|
|
4334
|
+
let counter = 0;
|
|
4335
|
+
for (const e of kept) {
|
|
4336
|
+
counter++;
|
|
4337
|
+
let base = (e.explicitId ? sanitizeId(e.explicitId) : "") || sanitizeId(e.name) || `part_${counter}`;
|
|
4338
|
+
let id = base;
|
|
4339
|
+
for (let n = 2; used.has(id); n++) id = `${base}_${n}`;
|
|
4340
|
+
used.add(id);
|
|
4341
|
+
base = id;
|
|
4342
|
+
const size = sizeOf(e.box);
|
|
4343
|
+
const center = e.box.getCenter(new THREE.Vector3());
|
|
4344
|
+
const part = {
|
|
4345
|
+
id,
|
|
4346
|
+
material: e.material,
|
|
4347
|
+
shape: { kind: "box", size: [Math.max(size[0], 2e-3), Math.max(size[1], 2e-3), Math.max(size[2], 2e-3)] },
|
|
4348
|
+
position: [center.x, center.y, center.z]
|
|
4349
|
+
};
|
|
4350
|
+
const minSide = Math.min(...size);
|
|
4351
|
+
if (e.fill !== void 0) part.fill = e.fill;
|
|
4352
|
+
else if (e.hollow) part.fill = 0.05;
|
|
4353
|
+
if (minSide < 0.01 && METAL.test(e.material)) {
|
|
4354
|
+
part.fill = e.fill ?? 1;
|
|
4355
|
+
part.thickness = Math.max(minSide, 5e-4);
|
|
4356
|
+
}
|
|
4357
|
+
if (e.tags) part.tags = [...e.tags];
|
|
4358
|
+
const ao = options.anchorBelow ?? -1;
|
|
4359
|
+
if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
|
|
4360
|
+
parts.push(part);
|
|
4361
|
+
meshesOf[id] = e.meshes;
|
|
4362
|
+
}
|
|
4363
|
+
const spec = { parts };
|
|
4364
|
+
if (options.name !== void 0) spec.name = options.name;
|
|
4365
|
+
else if (root.name) spec.name = root.name;
|
|
4366
|
+
if (options.tags) spec.tags = [...options.tags];
|
|
4367
|
+
spec.joints = options.joints === false ? [] : inferJoints(parts);
|
|
4368
|
+
if (options.joints !== false) {
|
|
4369
|
+
const finalId = /* @__PURE__ */ new Map();
|
|
4370
|
+
kept.forEach((e, i) => finalId.set(e, parts[i].id));
|
|
4371
|
+
const idOf = /* @__PURE__ */ new Map();
|
|
4372
|
+
for (const e of kept) if (e.explicitId) idOf.set(e.explicitId, finalId.get(e));
|
|
4373
|
+
const joints = spec.joints;
|
|
4374
|
+
for (const e of kept) {
|
|
4375
|
+
if (!e.parent) continue;
|
|
4376
|
+
const id = finalId.get(e);
|
|
4377
|
+
const parent = idOf.get(e.parent) ?? (used.has(sanitizeId(e.parent)) ? sanitizeId(e.parent) : null);
|
|
4378
|
+
if (!parent || parent === id) continue;
|
|
4379
|
+
const at = joints.findIndex((j) => j.a === parent && j.b === id || j.a === id && j.b === parent);
|
|
4380
|
+
const kind = e.jointKind ?? (at >= 0 ? joints[at].kind : "resting");
|
|
4381
|
+
const joint2 = { id: `${parent}~${id}`, a: parent, b: id, kind };
|
|
4382
|
+
if (at >= 0) joints[at] = joint2;
|
|
4383
|
+
else joints.push(joint2);
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
return { spec, parts: meshesOf };
|
|
4387
|
+
}
|
|
4084
4388
|
var STOPS = [
|
|
4085
4389
|
[450, 0],
|
|
4086
4390
|
[500, 2753536],
|
|
@@ -4166,6 +4470,7 @@ void main() {
|
|
|
4166
4470
|
__publicField(this, "m4");
|
|
4167
4471
|
__publicField(this, "box");
|
|
4168
4472
|
__publicField(this, "decalCount", 0);
|
|
4473
|
+
__publicField(this, "tints", /* @__PURE__ */ new WeakMap());
|
|
4169
4474
|
this.kit = new DecalKit(THREE);
|
|
4170
4475
|
this.a = tmpV(THREE);
|
|
4171
4476
|
this.b = tmpV(THREE);
|
|
@@ -4196,10 +4501,33 @@ void main() {
|
|
|
4196
4501
|
vertexCount(mv) {
|
|
4197
4502
|
return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
|
|
4198
4503
|
}
|
|
4199
|
-
/**
|
|
4504
|
+
/**
|
|
4505
|
+
* Average vertex colour of a mesh whose material uses `vertexColors` (computed once per mesh, from its original geometry), else null. Fragments
|
|
4506
|
+
* and debris of such a mesh multiply it by `material.color`; without it they come out white.
|
|
4507
|
+
*/
|
|
4508
|
+
vertexTint(mv) {
|
|
4509
|
+
if (this.tints.has(mv.mesh)) return this.tints.get(mv.mesh) ?? null;
|
|
4510
|
+
let tint = null;
|
|
4511
|
+
const used = materialsOf(mv.origMaterial).some((m) => m.vertexColors === true);
|
|
4512
|
+
const attr = used ? mv.origGeometry.getAttribute("color") : void 0;
|
|
4513
|
+
if (attr && attr.count > 0 && attr.itemSize >= 3) {
|
|
4514
|
+
let r = 0, g = 0, b = 0;
|
|
4515
|
+
for (let i = 0; i < attr.count; i++) {
|
|
4516
|
+
r += attr.getX(i);
|
|
4517
|
+
g += attr.getY(i);
|
|
4518
|
+
b += attr.getZ(i);
|
|
4519
|
+
}
|
|
4520
|
+
tint = new this.THREE.Color(r / attr.count, g / attr.count, b / attr.count);
|
|
4521
|
+
}
|
|
4522
|
+
this.tints.set(mv.mesh, tint);
|
|
4523
|
+
return tint;
|
|
4524
|
+
}
|
|
4525
|
+
/** Colour of the original material (before any char) times the vertex tint, `0xRRGGBB`, or -1. */
|
|
4200
4526
|
baseColorHex(mv) {
|
|
4201
4527
|
const c = mv.baseColors?.[0] ?? colorOf(materialsOf(mv.origMaterial)[0]);
|
|
4202
|
-
|
|
4528
|
+
if (!c) return -1;
|
|
4529
|
+
const tint = this.vertexTint(mv);
|
|
4530
|
+
return tint ? c.clone().multiply(tint).getHex() : c.getHex();
|
|
4203
4531
|
}
|
|
4204
4532
|
/** Is the original material transparent? */
|
|
4205
4533
|
isTransparent(mv) {
|
|
@@ -4668,8 +4996,8 @@ void main() {
|
|
|
4668
4996
|
} else {
|
|
4669
4997
|
const leftovers = [];
|
|
4670
4998
|
for (const m of meshesIn(root)) {
|
|
4671
|
-
const ud = m.userData?.destructionPart;
|
|
4672
|
-
if (
|
|
4999
|
+
const ud = partHintOf(m.userData?.destructionPart)?.id;
|
|
5000
|
+
if (ud !== void 0 && partIds.has(ud)) add2(ud, m);
|
|
4673
5001
|
else if (partIds.has(m.name)) add2(m.name, m);
|
|
4674
5002
|
else leftovers.push(m);
|
|
4675
5003
|
}
|
|
@@ -4979,7 +5307,10 @@ void main() {
|
|
|
4979
5307
|
if (!mv) return null;
|
|
4980
5308
|
const mat = materialsOf2(mv.mesh.material)[0];
|
|
4981
5309
|
const c = mat?.color;
|
|
4982
|
-
if (c && c.isColor)
|
|
5310
|
+
if (c && c.isColor) {
|
|
5311
|
+
const tint = this.visuals.vertexTint(mv);
|
|
5312
|
+
return tint ? c.clone().multiply(tint).getHex() : c.getHex();
|
|
5313
|
+
}
|
|
4983
5314
|
return def ? fallbackColor(def.id, def) : null;
|
|
4984
5315
|
}
|
|
4985
5316
|
debrisOptions(kind, objectId, partId) {
|
|
@@ -5423,136 +5754,6 @@ void main() {
|
|
|
5423
5754
|
this.disposed = true;
|
|
5424
5755
|
}
|
|
5425
5756
|
};
|
|
5426
|
-
var METAL = /steel|iron|metal|alumin|copper|brass|bronze|tin|titan|chrome|zinc|lead/i;
|
|
5427
|
-
function sanitizeId(name) {
|
|
5428
|
-
let id = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
5429
|
-
if (id && /^[0-9]/.test(id)) id = `p_${id}`;
|
|
5430
|
-
return id;
|
|
5431
|
-
}
|
|
5432
|
-
function isMesh(o) {
|
|
5433
|
-
return o.isMesh === true;
|
|
5434
|
-
}
|
|
5435
|
-
function sizeOf(box2) {
|
|
5436
|
-
return [box2.max.x - box2.min.x, box2.max.y - box2.min.y, box2.max.z - box2.min.z];
|
|
5437
|
-
}
|
|
5438
|
-
function specFromObject3D(THREE, root, options = {}) {
|
|
5439
|
-
const fallback = options.fallback ?? "plastic";
|
|
5440
|
-
const minSize = options.minSize ?? 0.015;
|
|
5441
|
-
root.updateMatrixWorld(true);
|
|
5442
|
-
const inv = root.matrixWorld.clone().invert();
|
|
5443
|
-
const entries = [];
|
|
5444
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
5445
|
-
const visit = (o) => {
|
|
5446
|
-
if (!o.visible || o.userData?.destructionIgnore) return;
|
|
5447
|
-
if (isMesh(o) && o.geometry) {
|
|
5448
|
-
const geom = o.geometry;
|
|
5449
|
-
if (!geom.boundingBox) geom.computeBoundingBox();
|
|
5450
|
-
if (geom.boundingBox && !geom.boundingBox.isEmpty()) {
|
|
5451
|
-
const box2 = geom.boundingBox.clone().applyMatrix4(new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld));
|
|
5452
|
-
const mats = Array.isArray(o.material) ? o.material : [o.material];
|
|
5453
|
-
const matName = mats[0]?.name ?? "";
|
|
5454
|
-
const ud = o.userData ?? {};
|
|
5455
|
-
const material = options.materialOf?.(o) ?? (typeof ud.destructionMaterial === "string" ? ud.destructionMaterial : void 0) ?? guessMaterial(`${o.name} ${matName} ${typeof ud.material === "string" ? ud.material : ""}`) ?? fallback;
|
|
5456
|
-
const explicit = typeof ud.destructionPart === "string" && ud.destructionPart ? ud.destructionPart : null;
|
|
5457
|
-
const hollow = ud.hollow === true || /hollow/i.test(o.name);
|
|
5458
|
-
const fill = options.fillOf?.(o, material);
|
|
5459
|
-
const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
|
|
5460
|
-
const existing = explicit ? byKey.get(explicit) : void 0;
|
|
5461
|
-
if (existing) {
|
|
5462
|
-
const before = sizeOf(existing.box);
|
|
5463
|
-
const bv = before[0] * before[1] * before[2];
|
|
5464
|
-
const ns = sizeOf(box2);
|
|
5465
|
-
existing.box.union(box2);
|
|
5466
|
-
existing.meshes.push(o);
|
|
5467
|
-
if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
|
|
5468
|
-
existing.hollow = existing.hollow || hollow;
|
|
5469
|
-
if (fill !== void 0) existing.fill = fill;
|
|
5470
|
-
} else {
|
|
5471
|
-
const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box: box2, material, explicitId: explicit, fill, hollow, tags, fixedId: !!explicit, name: o.name };
|
|
5472
|
-
entries.push(e);
|
|
5473
|
-
if (explicit) byKey.set(explicit, e);
|
|
5474
|
-
}
|
|
5475
|
-
}
|
|
5476
|
-
}
|
|
5477
|
-
for (const c of o.children) visit(c);
|
|
5478
|
-
};
|
|
5479
|
-
visit(root);
|
|
5480
|
-
const eps = 5e-3;
|
|
5481
|
-
const overlapVolume = (a, b) => {
|
|
5482
|
-
const x = Math.min(a.max.x, b.max.x) - Math.max(a.min.x, b.min.x) + eps;
|
|
5483
|
-
const y = Math.min(a.max.y, b.max.y) - Math.max(a.min.y, b.min.y) + eps;
|
|
5484
|
-
const z = Math.min(a.max.z, b.max.z) - Math.max(a.min.z, b.min.z) + eps;
|
|
5485
|
-
return x > 0 && y > 0 && z > 0 ? x * y * z : 0;
|
|
5486
|
-
};
|
|
5487
|
-
const largest = (e) => Math.max(...sizeOf(e.box));
|
|
5488
|
-
const kept = [];
|
|
5489
|
-
const tiny = [];
|
|
5490
|
-
for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
|
|
5491
|
-
for (const t of tiny) {
|
|
5492
|
-
let best = null;
|
|
5493
|
-
let bestV = 0;
|
|
5494
|
-
for (const k of kept) {
|
|
5495
|
-
const v = overlapVolume(t.box, k.box);
|
|
5496
|
-
if (v > bestV) {
|
|
5497
|
-
bestV = v;
|
|
5498
|
-
best = k;
|
|
5499
|
-
}
|
|
5500
|
-
}
|
|
5501
|
-
if (!best) {
|
|
5502
|
-
let bd = 0.05;
|
|
5503
|
-
for (const k of kept) {
|
|
5504
|
-
const d = k.box.distanceToPoint(t.box.getCenter(new THREE.Vector3()));
|
|
5505
|
-
if (d < bd) {
|
|
5506
|
-
bd = d;
|
|
5507
|
-
best = k;
|
|
5508
|
-
}
|
|
5509
|
-
}
|
|
5510
|
-
}
|
|
5511
|
-
if (best) {
|
|
5512
|
-
best.meshes.push(...t.meshes);
|
|
5513
|
-
} else {
|
|
5514
|
-
kept.push(t);
|
|
5515
|
-
}
|
|
5516
|
-
}
|
|
5517
|
-
const used = /* @__PURE__ */ new Set();
|
|
5518
|
-
const parts = [];
|
|
5519
|
-
const meshesOf = {};
|
|
5520
|
-
let counter = 0;
|
|
5521
|
-
for (const e of kept) {
|
|
5522
|
-
counter++;
|
|
5523
|
-
let base = (e.explicitId ? sanitizeId(e.explicitId) : "") || sanitizeId(e.name) || `part_${counter}`;
|
|
5524
|
-
let id = base;
|
|
5525
|
-
for (let n = 2; used.has(id); n++) id = `${base}_${n}`;
|
|
5526
|
-
used.add(id);
|
|
5527
|
-
base = id;
|
|
5528
|
-
const size = sizeOf(e.box);
|
|
5529
|
-
const center = e.box.getCenter(new THREE.Vector3());
|
|
5530
|
-
const part = {
|
|
5531
|
-
id,
|
|
5532
|
-
material: e.material,
|
|
5533
|
-
shape: { kind: "box", size: [Math.max(size[0], 2e-3), Math.max(size[1], 2e-3), Math.max(size[2], 2e-3)] },
|
|
5534
|
-
position: [center.x, center.y, center.z]
|
|
5535
|
-
};
|
|
5536
|
-
const minSide = Math.min(...size);
|
|
5537
|
-
if (e.fill !== void 0) part.fill = e.fill;
|
|
5538
|
-
else if (e.hollow) part.fill = 0.05;
|
|
5539
|
-
if (minSide < 0.01 && METAL.test(e.material)) {
|
|
5540
|
-
part.fill = e.fill ?? 1;
|
|
5541
|
-
part.thickness = Math.max(minSide, 5e-4);
|
|
5542
|
-
}
|
|
5543
|
-
if (e.tags) part.tags = [...e.tags];
|
|
5544
|
-
const ao = options.anchorBelow ?? -1;
|
|
5545
|
-
if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
|
|
5546
|
-
parts.push(part);
|
|
5547
|
-
meshesOf[id] = e.meshes;
|
|
5548
|
-
}
|
|
5549
|
-
const spec = { parts };
|
|
5550
|
-
if (options.name !== void 0) spec.name = options.name;
|
|
5551
|
-
else if (root.name) spec.name = root.name;
|
|
5552
|
-
if (options.tags) spec.tags = [...options.tags];
|
|
5553
|
-
spec.joints = options.joints === false ? [] : inferJoints(parts);
|
|
5554
|
-
return { spec, parts: meshesOf };
|
|
5555
|
-
}
|
|
5556
5757
|
|
|
5557
5758
|
// ../game-weapons/dist/index.js
|
|
5558
5759
|
var __defProp3 = Object.defineProperty;
|
|
@@ -5739,6 +5940,36 @@ void main() {
|
|
|
5739
5940
|
"grab"
|
|
5740
5941
|
]
|
|
5741
5942
|
},
|
|
5943
|
+
{
|
|
5944
|
+
id: "kick",
|
|
5945
|
+
name: "Kick",
|
|
5946
|
+
category: "melee_blunt",
|
|
5947
|
+
action: {
|
|
5948
|
+
kind: "melee",
|
|
5949
|
+
damage: "blunt",
|
|
5950
|
+
energy: 250,
|
|
5951
|
+
radius: 0.08,
|
|
5952
|
+
reach: 1.1,
|
|
5953
|
+
rpm: 60,
|
|
5954
|
+
noise: 8
|
|
5955
|
+
},
|
|
5956
|
+
mass: 0
|
|
5957
|
+
},
|
|
5958
|
+
{
|
|
5959
|
+
id: "shove",
|
|
5960
|
+
name: "Shove",
|
|
5961
|
+
category: "melee_blunt",
|
|
5962
|
+
action: {
|
|
5963
|
+
kind: "melee",
|
|
5964
|
+
damage: "blunt",
|
|
5965
|
+
energy: 80,
|
|
5966
|
+
radius: 0.1,
|
|
5967
|
+
reach: 1.5,
|
|
5968
|
+
rpm: 90,
|
|
5969
|
+
noise: 5
|
|
5970
|
+
},
|
|
5971
|
+
mass: 0
|
|
5972
|
+
},
|
|
5742
5973
|
{
|
|
5743
5974
|
id: "pistol_9mm",
|
|
5744
5975
|
name: "9 mm service pistol",
|
|
@@ -6616,6 +6847,8 @@ void main() {
|
|
|
6616
6847
|
en: {
|
|
6617
6848
|
weapons: {
|
|
6618
6849
|
hand: "Hand",
|
|
6850
|
+
kick: "Kick",
|
|
6851
|
+
shove: "Shove",
|
|
6619
6852
|
pistol_9mm: "9 mm service pistol",
|
|
6620
6853
|
pistol_45: ".45 ACP pistol",
|
|
6621
6854
|
pistol_22: ".22 LR pistol",
|
|
@@ -6662,6 +6895,8 @@ void main() {
|
|
|
6662
6895
|
"pt-BR": {
|
|
6663
6896
|
weapons: {
|
|
6664
6897
|
hand: "m\xE3o",
|
|
6898
|
+
kick: "chute",
|
|
6899
|
+
shove: "empurr\xE3o",
|
|
6665
6900
|
pistol_9mm: "pistola 9 mm",
|
|
6666
6901
|
pistol_45: "pistola .45",
|
|
6667
6902
|
pistol_22: "pistola .22",
|
|
@@ -7058,6 +7293,8 @@ void main() {
|
|
|
7058
7293
|
var WEAPONS = weapons_default;
|
|
7059
7294
|
var DEFAULT_LOADOUT = [
|
|
7060
7295
|
"hand",
|
|
7296
|
+
"kick",
|
|
7297
|
+
"shove",
|
|
7061
7298
|
"pistol_9mm",
|
|
7062
7299
|
"revolver_357",
|
|
7063
7300
|
"shotgun_pump_12",
|
|
@@ -7065,6 +7302,8 @@ void main() {
|
|
|
7065
7302
|
"rifle_bolt_308",
|
|
7066
7303
|
"hammer",
|
|
7067
7304
|
"axe",
|
|
7305
|
+
"machete",
|
|
7306
|
+
"utility_knife",
|
|
7068
7307
|
"crowbar",
|
|
7069
7308
|
"lighter",
|
|
7070
7309
|
"flamethrower",
|
|
@@ -10529,6 +10768,9 @@ void main() {
|
|
|
10529
10768
|
"mod.destructible_ok": "{name}: destructible contract ok ({parts} parts, hit and state well-formed)",
|
|
10530
10769
|
"mod.destructible_fail": "{name}: destructible contract broken: {problems}",
|
|
10531
10770
|
"mod.animation_ok": '{name}: animation "{clip}" ok ({seconds} s)',
|
|
10771
|
+
"mod.spec_ok": "{name}: spec ok ({parts} parts)",
|
|
10772
|
+
"mod.spec_fail": "{name}: spec broken: {problems}",
|
|
10773
|
+
"mod.spec_warn": "{name}: spec warning: {warning}",
|
|
10532
10774
|
"mod.animation_fail": '{name}: animation "{clip}" broken: {problems}',
|
|
10533
10775
|
"ui.animations": "Animations",
|
|
10534
10776
|
"ui.animations_aim": "Aim at an item or hold it to see its animations.",
|
|
@@ -10589,6 +10831,7 @@ void main() {
|
|
|
10589
10831
|
"ui.filter.event": "events",
|
|
10590
10832
|
"ui.filter.check": "checks",
|
|
10591
10833
|
"ui.autoscroll": "autoscroll",
|
|
10834
|
+
"log.export_cancelled": "export cancelled",
|
|
10592
10835
|
"ui.copy_json": "Copy JSON",
|
|
10593
10836
|
"ui.download_ndjson": "Download NDJSON",
|
|
10594
10837
|
"ui.download_txt": "Download TXT",
|
|
@@ -10645,6 +10888,8 @@ void main() {
|
|
|
10645
10888
|
"sc.shotgun_splinters_crate.check": "12 ga on the crate: {shots} shot(s), {splinters} board(s) splintered, {holes} holes (expected: at least one splintered)",
|
|
10646
10889
|
"sc.kick_chair_leg": "A kick takes a chair leg off",
|
|
10647
10890
|
"sc.kick_chair_leg.check": "Chair: {kicks} kick(s), {groups} group(s), loose pieces {before} \u2192 {after}, {leg} (expected: a loose piece appears)",
|
|
10891
|
+
"sc.kick_detaches_leg_via_tool": "The kick tool (hotbar) takes a chair leg off",
|
|
10892
|
+
"sc.kick_detaches_leg_via_tool.check": "Chair, kick tool: {kicks} kick(s), loose pieces {before} \u2192 {after}, {leg} (expected: a loose piece appears)",
|
|
10648
10893
|
"sc.screwdriver_shelf_board": "Screwdriver takes a screwed shelf board off in about 2.5 s",
|
|
10649
10894
|
"sc.screwdriver_shelf_board.check": "Screwdriver on the shelf board: took {secs} s, expected about {want} s, the shelf is now in {pieces}",
|
|
10650
10895
|
"sc.wrong_tool_refused": "The wrong tool is refused",
|
|
@@ -10812,6 +11057,9 @@ void main() {
|
|
|
10812
11057
|
"mod.destructible_ok": "{name}: contrato de destrui\xE7\xE3o ok ({parts} pe\xE7as, golpe e estado bem formados)",
|
|
10813
11058
|
"mod.destructible_fail": "{name}: contrato de destrui\xE7\xE3o quebrado: {problems}",
|
|
10814
11059
|
"mod.animation_ok": '{name}: anima\xE7\xE3o "{clip}" ok ({seconds} s)',
|
|
11060
|
+
"mod.spec_ok": "{name}: spec ok ({parts} pe\xE7as)",
|
|
11061
|
+
"mod.spec_fail": "{name}: spec quebrada: {problems}",
|
|
11062
|
+
"mod.spec_warn": "{name}: aviso na spec: {warning}",
|
|
10815
11063
|
"mod.animation_fail": '{name}: anima\xE7\xE3o "{clip}" quebrada: {problems}',
|
|
10816
11064
|
"ui.animations": "Anima\xE7\xF5es",
|
|
10817
11065
|
"ui.animations_aim": "Mire num item ou segure-o para ver as anima\xE7\xF5es.",
|
|
@@ -10872,6 +11120,7 @@ void main() {
|
|
|
10872
11120
|
"ui.filter.event": "eventos",
|
|
10873
11121
|
"ui.filter.check": "testes",
|
|
10874
11122
|
"ui.autoscroll": "rolar sozinho",
|
|
11123
|
+
"log.export_cancelled": "exporta\xE7\xE3o cancelada",
|
|
10875
11124
|
"ui.copy_json": "Copiar JSON",
|
|
10876
11125
|
"ui.download_ndjson": "Baixar NDJSON",
|
|
10877
11126
|
"ui.download_txt": "Baixar TXT",
|
|
@@ -10928,6 +11177,8 @@ void main() {
|
|
|
10928
11177
|
"sc.shotgun_splinters_crate.check": "Calibre 12 no caixote: {shots} tiro(s), {splinters} t\xE1bua(s) lascada(s), {holes} furos (esperado: pelo menos uma lascada)",
|
|
10929
11178
|
"sc.kick_chair_leg": "Um chute solta a perna da cadeira",
|
|
10930
11179
|
"sc.kick_chair_leg.check": "Cadeira: {kicks} chute(s), {groups} grupo(s), pe\xE7as soltas {before} \u2192 {after}, {leg} (esperado: aparece uma pe\xE7a solta)",
|
|
11180
|
+
"sc.kick_detaches_leg_via_tool": "A ferramenta chute (barra de atalhos) solta a perna da cadeira",
|
|
11181
|
+
"sc.kick_detaches_leg_via_tool.check": "Cadeira, ferramenta chute: {kicks} chute(s), pe\xE7as soltas {before} \u2192 {after}, {leg} (esperado: aparece uma pe\xE7a solta)",
|
|
10931
11182
|
"sc.screwdriver_shelf_board": "Chave de fenda solta a prateleira parafusada em cerca de 2,5 s",
|
|
10932
11183
|
"sc.screwdriver_shelf_board.check": "Chave de fenda na prateleira: levou {secs} s, esperado cerca de {want} s, a estante agora est\xE1 em {pieces}",
|
|
10933
11184
|
"sc.wrong_tool_refused": "A ferramenta errada \xE9 recusada",
|
|
@@ -11429,7 +11680,16 @@ void main() {
|
|
|
11429
11680
|
const s = t - m * 60;
|
|
11430
11681
|
return `${pad(m)}:${s.toFixed(2).padStart(5, "0")}`;
|
|
11431
11682
|
};
|
|
11432
|
-
function createLog() {
|
|
11683
|
+
function createLog(options = {}) {
|
|
11684
|
+
const host = globalThis.claude;
|
|
11685
|
+
let downloadsP = null;
|
|
11686
|
+
if (host && typeof host.use === "function") {
|
|
11687
|
+
try {
|
|
11688
|
+
downloadsP = Promise.resolve(host.use("downloads")).then((d) => d ?? null, () => null);
|
|
11689
|
+
} catch {
|
|
11690
|
+
downloadsP = null;
|
|
11691
|
+
}
|
|
11692
|
+
}
|
|
11433
11693
|
let list = [];
|
|
11434
11694
|
let seq = 0;
|
|
11435
11695
|
let clock = () => 0;
|
|
@@ -11495,9 +11755,20 @@ void main() {
|
|
|
11495
11755
|
toJSON: () => JSON.stringify(read(), null, 2),
|
|
11496
11756
|
toNDJSON: () => read().map((e) => JSON.stringify(e)).join("\n") + (list.length ? "\n" : ""),
|
|
11497
11757
|
toText: () => read().map((e) => `[${formatTime(e.t)}] ${e.channel.padEnd(6)} ${e.text}`).join("\n"),
|
|
11498
|
-
download(format = "ndjson") {
|
|
11499
|
-
if (typeof document === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined" || !URL.createObjectURL) return;
|
|
11758
|
+
async download(format = "ndjson") {
|
|
11500
11759
|
const body = format === "json" ? log.toJSON() : format === "txt" ? log.toText() : log.toNDJSON();
|
|
11760
|
+
const downloads = downloadsP ? await downloadsP : null;
|
|
11761
|
+
if (downloads) {
|
|
11762
|
+
const filename = format === "ndjson" ? "test-room-log.ndjson.txt" : `test-room-log.${format}`;
|
|
11763
|
+
try {
|
|
11764
|
+
await downloads.save({ filename, data: body });
|
|
11765
|
+
} catch (err) {
|
|
11766
|
+
const code = err?.code;
|
|
11767
|
+
log.add({ channel: "action", type: "export_cancelled", text: options.exportCancelled?.() ?? "export cancelled", data: { format, ...typeof code === "string" ? { code } : {} } });
|
|
11768
|
+
}
|
|
11769
|
+
return;
|
|
11770
|
+
}
|
|
11771
|
+
if (typeof document === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined" || !URL.createObjectURL) return;
|
|
11501
11772
|
const mime = format === "json" ? "application/json" : "text/plain";
|
|
11502
11773
|
const url = URL.createObjectURL(new Blob([body], { type: mime }));
|
|
11503
11774
|
const a = document.createElement("a");
|
|
@@ -12242,6 +12513,10 @@ void main() {
|
|
|
12242
12513
|
yaw += yawDeg * Math.PI / 180;
|
|
12243
12514
|
apply();
|
|
12244
12515
|
},
|
|
12516
|
+
moveBy(dx, dz) {
|
|
12517
|
+
move(dx, dz);
|
|
12518
|
+
apply();
|
|
12519
|
+
},
|
|
12245
12520
|
walk(forward, right) {
|
|
12246
12521
|
move(-Math.sin(yaw) * forward + Math.cos(yaw) * right, -Math.cos(yaw) * forward - Math.sin(yaw) * right);
|
|
12247
12522
|
apply();
|
|
@@ -12373,6 +12648,23 @@ void main() {
|
|
|
12373
12648
|
{ kicks, before, after }
|
|
12374
12649
|
);
|
|
12375
12650
|
}),
|
|
12651
|
+
sc("kick_detaches_leg_via_tool", ({ room, step, expect }) => {
|
|
12652
|
+
const id = at(room, "chair", 0, -4);
|
|
12653
|
+
const before = room.renderer.pieces().length;
|
|
12654
|
+
room.setTool("kick");
|
|
12655
|
+
let kicks = 0;
|
|
12656
|
+
for (; kicks < 8 && room.renderer.pieces().length <= before && part(room, id, "leg_1"); kicks++) {
|
|
12657
|
+
const st = room.world.state(id);
|
|
12658
|
+
const leg = toWorld(st.position, st.rotation, [-0.18, 0.2, -0.18]);
|
|
12659
|
+
const side = rotateVec(st.rotation, [-0.8, 0, 0]);
|
|
12660
|
+
look(room, [leg[0] + side[0], leg[1] + side[1], leg[2] + side[2]], leg);
|
|
12661
|
+
room.use();
|
|
12662
|
+
step(1.05);
|
|
12663
|
+
}
|
|
12664
|
+
step(0.5);
|
|
12665
|
+
const after = room.renderer.pieces().length;
|
|
12666
|
+
expect(after > before, t("sc.kick_detaches_leg_via_tool.check", { kicks, before, after, leg: part(room, id, "leg_1") ? t("leg.still") : t("leg.gone") }), { kicks, before, after });
|
|
12667
|
+
}),
|
|
12376
12668
|
sc("screwdriver_shelf_board", ({ room, step, expect }) => {
|
|
12377
12669
|
const id = at(room, "shelf", 0, -4);
|
|
12378
12670
|
room.setTool("screwdriver");
|
|
@@ -12703,16 +12995,25 @@ void main() {
|
|
|
12703
12995
|
};
|
|
12704
12996
|
logBtn(t("ui.copy_json"), () => {
|
|
12705
12997
|
const text = o.log.toJSON();
|
|
12706
|
-
|
|
12707
|
-
void navigator.clipboard.writeText(text);
|
|
12708
|
-
} catch {
|
|
12998
|
+
const select = () => {
|
|
12709
12999
|
const ta = mk("textarea");
|
|
12710
13000
|
ta.value = text;
|
|
13001
|
+
ta.setAttribute("readonly", "");
|
|
13002
|
+
ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
|
|
12711
13003
|
document.body.appendChild(ta);
|
|
13004
|
+
ta.focus();
|
|
12712
13005
|
ta.select();
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
13006
|
+
let copied = false;
|
|
13007
|
+
try {
|
|
13008
|
+
copied = !!document.execCommand?.("copy");
|
|
13009
|
+
} catch {
|
|
13010
|
+
copied = false;
|
|
13011
|
+
}
|
|
13012
|
+
if (copied) ta.remove();
|
|
13013
|
+
else ta.onblur = () => ta.remove();
|
|
13014
|
+
};
|
|
13015
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) navigator.clipboard.writeText(text).catch(select);
|
|
13016
|
+
else select();
|
|
12716
13017
|
});
|
|
12717
13018
|
logBtn(t("ui.download_ndjson"), () => o.log.download("ndjson"));
|
|
12718
13019
|
logBtn(t("ui.download_txt"), () => o.log.download("txt"));
|
|
@@ -13124,7 +13425,7 @@ void main() {
|
|
|
13124
13425
|
const entries = /* @__PURE__ */ new Map();
|
|
13125
13426
|
const splitOf = /* @__PURE__ */ new Map();
|
|
13126
13427
|
let simTime = 0;
|
|
13127
|
-
const log = createLog();
|
|
13428
|
+
const log = createLog({ exportCancelled: () => t("log.export_cancelled") });
|
|
13128
13429
|
log.setClock(() => simTime);
|
|
13129
13430
|
const renderer = createDestructionRenderer(THREE, {
|
|
13130
13431
|
scene,
|
|
@@ -13498,6 +13799,7 @@ void main() {
|
|
|
13498
13799
|
addArena();
|
|
13499
13800
|
timeScale = 1;
|
|
13500
13801
|
fastForward = false;
|
|
13802
|
+
walkPlan = null;
|
|
13501
13803
|
};
|
|
13502
13804
|
const aimO = [0, 0, 0];
|
|
13503
13805
|
const aimD = [0, 0, -1];
|
|
@@ -13749,11 +14051,11 @@ void main() {
|
|
|
13749
14051
|
1
|
|
13750
14052
|
);
|
|
13751
14053
|
};
|
|
13752
|
-
const meleeUse = (u) => {
|
|
13753
|
-
const w = toolName();
|
|
14054
|
+
const meleeUse = (u, id = toolId) => {
|
|
14055
|
+
const w = toolName(id);
|
|
13754
14056
|
const r = rayWorld(u.origin, u.direction, u.reach);
|
|
13755
14057
|
if (!r) {
|
|
13756
|
-
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon:
|
|
14058
|
+
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon: id } });
|
|
13757
14059
|
return;
|
|
13758
14060
|
}
|
|
13759
14061
|
world.hitPart(r.object, r.part, { type: u.damage, point: r.point, direction: u.direction, energy: u.energy, radius: u.radius, source: "player" });
|
|
@@ -13762,7 +14064,7 @@ void main() {
|
|
|
13762
14064
|
objectId: r.object,
|
|
13763
14065
|
itemId: entryOf(r.object)?.so.itemId,
|
|
13764
14066
|
part: r.part,
|
|
13765
|
-
data: { weapon:
|
|
14067
|
+
data: { weapon: id, damage: u.damage, energyJ: u.energy, distanceM: Math.round(r.distance * 100) / 100 }
|
|
13766
14068
|
});
|
|
13767
14069
|
};
|
|
13768
14070
|
const toolWork = (u, dt) => {
|
|
@@ -13773,7 +14075,7 @@ void main() {
|
|
|
13773
14075
|
log.coalesce(`use:${toolId}`, { channel: "action", type: "tool", text: t("act.tool.none", { weapon: w }), data: { weapon: toolId } }, 1);
|
|
13774
14076
|
return;
|
|
13775
14077
|
}
|
|
13776
|
-
const j = world.nearestJoint(r.object, r.point, 0.45);
|
|
14078
|
+
const j = world.nearestJoint(r.object, r.point, 0.45, u.tool);
|
|
13777
14079
|
if (!j) {
|
|
13778
14080
|
progress = null;
|
|
13779
14081
|
log.coalesce(`use:${toolId}`, { channel: "action", type: "tool", text: t("act.tool.nojoint", { weapon: w, where: hitWhere(r) }), objectId: r.object, part: r.part }, 1);
|
|
@@ -13952,6 +14254,13 @@ void main() {
|
|
|
13952
14254
|
}
|
|
13953
14255
|
}
|
|
13954
14256
|
};
|
|
14257
|
+
const kick = () => {
|
|
14258
|
+
if (!arsenal.weapons.has("kick")) return;
|
|
14259
|
+
aim();
|
|
14260
|
+
const res = toolState("kick").trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 0, false, "player");
|
|
14261
|
+
if (res.status !== "ok") return;
|
|
14262
|
+
for (const u of res.uses) if (u.kind === "melee") meleeUse(u, "kick");
|
|
14263
|
+
};
|
|
13955
14264
|
const setTool = (id) => {
|
|
13956
14265
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
13957
14266
|
tool.release();
|
|
@@ -14207,7 +14516,7 @@ void main() {
|
|
|
14207
14516
|
break;
|
|
14208
14517
|
case "KeyV":
|
|
14209
14518
|
if (heldEntry?.uses[2]) useItem(heldEntry.uses[2]);
|
|
14210
|
-
else
|
|
14519
|
+
else kick();
|
|
14211
14520
|
break;
|
|
14212
14521
|
case "KeyT":
|
|
14213
14522
|
toggleTap();
|
|
@@ -14306,8 +14615,19 @@ void main() {
|
|
|
14306
14615
|
if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(host.objectId, e.fluidPart, c.liters);
|
|
14307
14616
|
}
|
|
14308
14617
|
};
|
|
14618
|
+
let paused = false;
|
|
14619
|
+
let walkPlan = null;
|
|
14620
|
+
const stepWalk = (dt) => {
|
|
14621
|
+
if (!walkPlan) return;
|
|
14622
|
+
const f = walkPlan.remaining <= dt ? 1 : dt / walkPlan.remaining;
|
|
14623
|
+
player.moveBy((walkPlan.x - camera.position.x) * f, (walkPlan.z - camera.position.z) * f);
|
|
14624
|
+
walkPlan.remaining -= dt;
|
|
14625
|
+
if (walkPlan.remaining <= 1e-9) walkPlan = null;
|
|
14626
|
+
};
|
|
14309
14627
|
const stepSim = (dt) => {
|
|
14628
|
+
if (paused) return;
|
|
14310
14629
|
simTime += dt;
|
|
14630
|
+
stepWalk(dt);
|
|
14311
14631
|
const reps = fastForward ? 10 : 1;
|
|
14312
14632
|
updateAnimations(dt);
|
|
14313
14633
|
updateItemUse(dt);
|
|
@@ -14336,13 +14656,13 @@ void main() {
|
|
|
14336
14656
|
if (tiltKey || heldTilt > 0) {
|
|
14337
14657
|
heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
|
|
14338
14658
|
}
|
|
14339
|
-
if (useHeld) {
|
|
14659
|
+
if (useHeld && !paused) {
|
|
14340
14660
|
if (tool.def.category === "hand") {
|
|
14341
14661
|
if (useFirst) grab();
|
|
14342
14662
|
} else useTool(dt, !useFirst);
|
|
14343
14663
|
useFirst = false;
|
|
14344
14664
|
}
|
|
14345
|
-
followHeld(dt);
|
|
14665
|
+
if (!paused) followHeld(dt);
|
|
14346
14666
|
stepSim(dt * timeScale);
|
|
14347
14667
|
if (progress && simTime - progress.at > 0.25) progress = null;
|
|
14348
14668
|
hudAcc += dt;
|
|
@@ -14403,6 +14723,16 @@ void main() {
|
|
|
14403
14723
|
const pose = poseAt(pl.clip, id === "" ? null : id, pl.t);
|
|
14404
14724
|
obj.position.set(rest.p[0] + pose.position[0], rest.p[1] + pose.position[1], rest.p[2] + pose.position[2]);
|
|
14405
14725
|
obj.rotation.set(rest.r[0] + pose.rotation[0], rest.r[1] + pose.rotation[1], rest.r[2] + pose.rotation[2]);
|
|
14726
|
+
const ps = id === "" ? void 0 : specs.get(pl.entry.so.objectId)?.parts.find((p) => p.id === id);
|
|
14727
|
+
if (ps) {
|
|
14728
|
+
const r0 = ps.rotation ?? ZERO;
|
|
14729
|
+
world.setPartPose(
|
|
14730
|
+
pl.entry.so.objectId,
|
|
14731
|
+
id,
|
|
14732
|
+
[ps.position[0] + pose.position[0], ps.position[1] + pose.position[1], ps.position[2] + pose.position[2]],
|
|
14733
|
+
[r0[0] + pose.rotation[0], r0[1] + pose.rotation[1], r0[2] + pose.rotation[2]]
|
|
14734
|
+
);
|
|
14735
|
+
}
|
|
14406
14736
|
}
|
|
14407
14737
|
};
|
|
14408
14738
|
const updateAnimations = (dt) => {
|
|
@@ -14490,8 +14820,14 @@ void main() {
|
|
|
14490
14820
|
partIds = probe.describe().parts.map((p) => p.id);
|
|
14491
14821
|
moduleCheck(r.ok, r.ok ? t("mod.destructible_ok", { name: item.name, parts: partIds.length }) : t("mod.destructible_fail", { name: item.name, problems: r.problems.join("; ") }), { ...base, check: "destructible", problems: r.problems }, item.id);
|
|
14492
14822
|
for (const p of r.problems) fail(`${item.id}: ${p}`);
|
|
14493
|
-
}
|
|
14494
|
-
|
|
14823
|
+
}
|
|
14824
|
+
if (!item.impl || item.spec) {
|
|
14825
|
+
const spec = resolveSpec(item, root).spec;
|
|
14826
|
+
if (!item.impl) partIds = spec.parts.map((p) => p.id);
|
|
14827
|
+
const r = checkSpec(spec, { materials: world.materials.keys() });
|
|
14828
|
+
moduleCheck(r.ok, r.ok ? t("mod.spec_ok", { name: item.name, parts: spec.parts.length }) : t("mod.spec_fail", { name: item.name, problems: r.problems.join("; ") }), { ...base, check: "spec", problems: r.problems, warnings: r.warnings }, item.id);
|
|
14829
|
+
for (const p of r.problems) fail(`${item.id}: ${p}`);
|
|
14830
|
+
for (const w of r.warnings) log.add({ channel: "check", type: "warn", text: `\u26A0 ${t("mod.spec_warn", { name: item.name, warning: w })}`, itemId: item.id, data: { ok: true, ...base, check: "spec", warning: w } });
|
|
14495
14831
|
}
|
|
14496
14832
|
} catch (err) {
|
|
14497
14833
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -14530,6 +14866,8 @@ void main() {
|
|
|
14530
14866
|
const all = list ?? allScenarios();
|
|
14531
14867
|
const report = { passed: 0, failed: 0, results: [] };
|
|
14532
14868
|
scenarioRunning = true;
|
|
14869
|
+
const wasPaused = paused;
|
|
14870
|
+
paused = false;
|
|
14533
14871
|
try {
|
|
14534
14872
|
for (const sc of all) {
|
|
14535
14873
|
hardReset();
|
|
@@ -14561,6 +14899,7 @@ void main() {
|
|
|
14561
14899
|
}
|
|
14562
14900
|
} finally {
|
|
14563
14901
|
scenarioRunning = false;
|
|
14902
|
+
paused = wasPaused;
|
|
14564
14903
|
}
|
|
14565
14904
|
return report;
|
|
14566
14905
|
};
|
|
@@ -14623,6 +14962,19 @@ void main() {
|
|
|
14623
14962
|
heldTilt = rad;
|
|
14624
14963
|
},
|
|
14625
14964
|
toggleTap,
|
|
14965
|
+
pause() {
|
|
14966
|
+
paused = true;
|
|
14967
|
+
},
|
|
14968
|
+
resume() {
|
|
14969
|
+
paused = false;
|
|
14970
|
+
},
|
|
14971
|
+
get paused() {
|
|
14972
|
+
return paused;
|
|
14973
|
+
},
|
|
14974
|
+
walk(to, seconds) {
|
|
14975
|
+
walkPlan = seconds > 0 ? { x: to[0], z: to[2], remaining: seconds } : null;
|
|
14976
|
+
if (!walkPlan) player.moveBy(to[0] - camera.position.x, to[2] - camera.position.z);
|
|
14977
|
+
},
|
|
14626
14978
|
setSlowMotion(on) {
|
|
14627
14979
|
timeScale = on ? 0.25 : 1;
|
|
14628
14980
|
},
|