@adatechnology/game-test-room 0.1.0 → 0.1.2
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 +559 -204
- 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",
|
|
@@ -1564,9 +1641,20 @@ var GameTestRoom = (() => {
|
|
|
1564
1641
|
var DMG_PER_J = 0.02;
|
|
1565
1642
|
var BLAST_COUPLING = 0.1;
|
|
1566
1643
|
var FLAMMABLE_FLUIDS = ["gas", "fuel"];
|
|
1644
|
+
var CUT_SLICE = 1;
|
|
1645
|
+
var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
|
|
1567
1646
|
var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
1568
1647
|
var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
|
|
1569
1648
|
var clone = (v) => structuredClone(v);
|
|
1649
|
+
var updateBound = (o) => {
|
|
1650
|
+
if (!o.parts.length) return;
|
|
1651
|
+
const c = [0, 0, 0];
|
|
1652
|
+
for (const p of o.parts) for (let k = 0; k < 3; k++) c[k] += p.pos[k] / o.parts.length;
|
|
1653
|
+
let r = 0;
|
|
1654
|
+
for (const p of o.parts) r = Math.max(r, vec.distance(c, p.pos) + p.bound);
|
|
1655
|
+
o.bc = c;
|
|
1656
|
+
o.br = r;
|
|
1657
|
+
};
|
|
1570
1658
|
var AXIS = { x: 0, y: 1, z: 2 };
|
|
1571
1659
|
var createDestruction = (options = {}) => {
|
|
1572
1660
|
const materials = /* @__PURE__ */ new Map();
|
|
@@ -1702,10 +1790,12 @@ var GameTestRoom = (() => {
|
|
|
1702
1790
|
mat,
|
|
1703
1791
|
st: pst,
|
|
1704
1792
|
q: quatFromEuler(...ps.rotation ?? [0, 0, 0]),
|
|
1793
|
+
pos: [ps.position[0], ps.position[1], ps.position[2]],
|
|
1705
1794
|
mass0,
|
|
1706
1795
|
fuel0,
|
|
1707
1796
|
area: shapeArea(ps.shape),
|
|
1708
1797
|
thickness: partThickness(ps),
|
|
1798
|
+
cutFactor: cutFactorOf(ps.shape),
|
|
1709
1799
|
dims: shapeDims(ps.shape),
|
|
1710
1800
|
bound: shapeBound(ps.shape),
|
|
1711
1801
|
rt: c?.rt ?? { ...freshRuntime(), prevTemp: pst.temperature },
|
|
@@ -1740,14 +1830,8 @@ var GameTestRoom = (() => {
|
|
|
1740
1830
|
o.byPart.get(r.b).push(j);
|
|
1741
1831
|
o.st.joints.push(jst);
|
|
1742
1832
|
}
|
|
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
|
-
}
|
|
1833
|
+
for (const [jid, c] of carry?.joints ?? []) if (c.st.open && !o.jointMap.has(jid)) o.st.joints.push(c.st);
|
|
1834
|
+
updateBound(o);
|
|
1751
1835
|
o.anyAnchor = o.parts.some((p) => p.anchored);
|
|
1752
1836
|
o.unsup = o.parts.map(() => false);
|
|
1753
1837
|
solveStructure(o);
|
|
@@ -1887,7 +1971,7 @@ var GameTestRoom = (() => {
|
|
|
1887
1971
|
if (!p) throw new Error(`unknown part "${id}" in object "${o.id}"`);
|
|
1888
1972
|
return p;
|
|
1889
1973
|
};
|
|
1890
|
-
const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.
|
|
1974
|
+
const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.pos), q: quat.mul(o.st.rotation, p.q) });
|
|
1891
1975
|
const objLocal = (o, w) => toLocal(o.st.position, o.st.rotation, w);
|
|
1892
1976
|
const objLocalDir = (o, w) => rotateVec(quat.conjugate(o.st.rotation), w);
|
|
1893
1977
|
const jointWorld = (o, j) => toWorld(o.st.position, o.st.rotation, j.pos);
|
|
@@ -1899,14 +1983,14 @@ var GameTestRoom = (() => {
|
|
|
1899
1983
|
const { c, q } = partXf(o, p);
|
|
1900
1984
|
return aabbOf(p.spec.shape, c, q);
|
|
1901
1985
|
};
|
|
1902
|
-
const rayPartRaw = (pos, rot, spec, pq, origin, d) => {
|
|
1903
|
-
const c = toWorld(pos, rot,
|
|
1986
|
+
const rayPartRaw = (pos, rot, spec, pq, origin, d, lp = spec.position) => {
|
|
1987
|
+
const c = toWorld(pos, rot, lp);
|
|
1904
1988
|
const q = quat.mul(rot, pq);
|
|
1905
1989
|
const qi = quat.conjugate(q);
|
|
1906
1990
|
const r = rayShape(spec.shape, rotateVec(qi, vec.sub(origin, c)), rotateVec(qi, d));
|
|
1907
1991
|
return r ? { t0: r.t0, t1: r.t1, n: rotateVec(q, r.n0) } : null;
|
|
1908
1992
|
};
|
|
1909
|
-
const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d);
|
|
1993
|
+
const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d, p.pos);
|
|
1910
1994
|
const raySphereMiss = (origin, d, c, r, maxDist) => {
|
|
1911
1995
|
const oc = vec.sub(origin, c);
|
|
1912
1996
|
const b = vec.dot(oc, d);
|
|
@@ -1931,7 +2015,8 @@ var GameTestRoom = (() => {
|
|
|
1931
2015
|
for (const im of impls.values()) {
|
|
1932
2016
|
for (const ps of im.impl.describe().parts) {
|
|
1933
2017
|
if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
|
|
1934
|
-
const
|
|
2018
|
+
const ov = im.poses.get(ps.id);
|
|
2019
|
+
const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), origin, d, ov?.pos);
|
|
1935
2020
|
if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
|
|
1936
2021
|
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
2022
|
}
|
|
@@ -2066,11 +2151,29 @@ var GameTestRoom = (() => {
|
|
|
2066
2151
|
collapseCheck(o, src);
|
|
2067
2152
|
checkDestroyed(o, src);
|
|
2068
2153
|
};
|
|
2069
|
-
const
|
|
2154
|
+
const jointReach = (o, j, point) => {
|
|
2155
|
+
const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
|
|
2156
|
+
let d2 = 0;
|
|
2157
|
+
for (let k = 0; k < 3; k++) {
|
|
2158
|
+
const lo = Math.min(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
|
|
2159
|
+
const hi = Math.max(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
|
|
2160
|
+
const d = point[k] < lo ? lo - point[k] : point[k] > hi ? point[k] - hi : 0;
|
|
2161
|
+
d2 += d * d;
|
|
2162
|
+
}
|
|
2163
|
+
return Math.sqrt(d2);
|
|
2164
|
+
};
|
|
2165
|
+
const jointReachLimit = (o, j) => {
|
|
2166
|
+
const pa = o.parts[j.ia], pb = o.parts[j.ib];
|
|
2167
|
+
const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
|
|
2168
|
+
return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
|
|
2169
|
+
};
|
|
2170
|
+
const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
|
|
2070
2171
|
if (amount <= 0) return;
|
|
2071
|
-
|
|
2172
|
+
const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
|
|
2173
|
+
if (split && mine.length) amount /= mine.length;
|
|
2174
|
+
for (const j of mine) {
|
|
2072
2175
|
if (j.st.open) continue;
|
|
2073
|
-
if (near &&
|
|
2176
|
+
if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
|
|
2074
2177
|
j.st.hp -= amount;
|
|
2075
2178
|
if (j.st.hp <= 0) {
|
|
2076
2179
|
j.st.hp = 0;
|
|
@@ -2189,7 +2292,7 @@ var GameTestRoom = (() => {
|
|
|
2189
2292
|
if (mat.fracture !== "shatter") addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
|
|
2190
2293
|
} else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
|
|
2191
2294
|
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.
|
|
2295
|
+
wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
|
|
2193
2296
|
const dmg = applyDamage(o, p, E * DMG_PER_J * scale2, "ballistic", entry, d, E * 0.1, 12, src);
|
|
2194
2297
|
return { penetrated, exitEnergy: penetrated ? E - thr : 0, exitPoint, damage: dmg };
|
|
2195
2298
|
};
|
|
@@ -2214,7 +2317,7 @@ var GameTestRoom = (() => {
|
|
|
2214
2317
|
if (kind === "blunt") wearJoints(o, p, E * 0.3, void 0, 0, src);
|
|
2215
2318
|
else wearJoints(o, p, E * 0.3, point, 0.06, src);
|
|
2216
2319
|
const scale2 = mat.damageScale?.[kind] ?? 1;
|
|
2217
|
-
return applyDamage(o, p, Ec * DMG_PER_J * scale2, kind, point, d, E * 0.1, 12, src);
|
|
2320
|
+
return applyDamage(o, p, Ec * DMG_PER_J * scale2 * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
|
|
2218
2321
|
};
|
|
2219
2322
|
const blastObject = (o, point, energy, radius, src) => {
|
|
2220
2323
|
const start = cur.length;
|
|
@@ -2264,8 +2367,9 @@ var GameTestRoom = (() => {
|
|
|
2264
2367
|
}
|
|
2265
2368
|
for (const im of [...impls.values()]) {
|
|
2266
2369
|
const inRange = im.impl.describe().parts.some((ps) => {
|
|
2267
|
-
const
|
|
2268
|
-
const
|
|
2370
|
+
const ov = im.poses.get(ps.id);
|
|
2371
|
+
const c = toWorld(im.position, im.rotation, ov?.pos ?? ps.position);
|
|
2372
|
+
const q = quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]));
|
|
2269
2373
|
return distanceToShape(ps.shape, toLocal(c, q, point)) < radius;
|
|
2270
2374
|
});
|
|
2271
2375
|
if (inRange) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
|
|
@@ -2517,7 +2621,7 @@ var GameTestRoom = (() => {
|
|
|
2517
2621
|
const spread = (sources, dt) => {
|
|
2518
2622
|
for (const src of sources) {
|
|
2519
2623
|
if (!src.p.st.burning && !src.p.rt.jet) continue;
|
|
2520
|
-
const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.
|
|
2624
|
+
const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.pos);
|
|
2521
2625
|
const A = partWorldAabb(src.o, src.p);
|
|
2522
2626
|
for (const o of objects.values()) {
|
|
2523
2627
|
if (o === src.o) continue;
|
|
@@ -2563,9 +2667,23 @@ var GameTestRoom = (() => {
|
|
|
2563
2667
|
register(impl, opts = {}) {
|
|
2564
2668
|
const id = opts.id ?? impl.id;
|
|
2565
2669
|
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] });
|
|
2670
|
+
impls.set(id, { impl, position: opts.position ?? [0, 0, 0], rotation: opts.rotation ?? [0, 0, 0, 1], poses: /* @__PURE__ */ new Map() });
|
|
2567
2671
|
return id;
|
|
2568
2672
|
},
|
|
2673
|
+
setPartPose(objectId, partId, position, rotation) {
|
|
2674
|
+
const q = quatFromEuler(...rotation ?? [0, 0, 0]);
|
|
2675
|
+
const im = impls.get(objectId);
|
|
2676
|
+
if (im) {
|
|
2677
|
+
im.impl.setPartPose?.(partId, position, rotation);
|
|
2678
|
+
im.poses.set(partId, { pos: [position[0], position[1], position[2]], q });
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
const o = getObj(objectId);
|
|
2682
|
+
const p = getPart(o, partId);
|
|
2683
|
+
p.pos = [position[0], position[1], position[2]];
|
|
2684
|
+
p.q = q;
|
|
2685
|
+
updateBound(o);
|
|
2686
|
+
},
|
|
2569
2687
|
remove(id) {
|
|
2570
2688
|
objects.delete(id);
|
|
2571
2689
|
impls.delete(id);
|
|
@@ -2592,9 +2710,12 @@ var GameTestRoom = (() => {
|
|
|
2592
2710
|
const moved = new Set(names);
|
|
2593
2711
|
const nid = newId ?? nextId();
|
|
2594
2712
|
if (objects.has(nid)) throw new Error(`object "${nid}" already exists`);
|
|
2595
|
-
const carryFor = (keep) => ({
|
|
2713
|
+
const carryFor = (keep, keepOpen = () => false) => ({
|
|
2596
2714
|
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(
|
|
2715
|
+
joints: new Map([
|
|
2716
|
+
...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 }]),
|
|
2717
|
+
...o.st.joints.filter((st) => st.open && !o.jointMap.has(st.id) && keepOpen(st.a, st.b)).map((st) => [st.id, { st, step: 0 }])
|
|
2718
|
+
]),
|
|
2598
2719
|
collapsed: [...o.collapsed].filter(keep)
|
|
2599
2720
|
});
|
|
2600
2721
|
const sub3 = (keep) => {
|
|
@@ -2606,8 +2727,8 @@ var GameTestRoom = (() => {
|
|
|
2606
2727
|
const inNew = (pid) => moved.has(pid);
|
|
2607
2728
|
const inOld = (pid) => !moved.has(pid);
|
|
2608
2729
|
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));
|
|
2730
|
+
const fresh = build(sub3(inNew), nid, pos, rot, carryFor(inNew, (a, b) => inNew(a) && inNew(b)));
|
|
2731
|
+
const rest = build(sub3(inOld), id, pos, rot, carryFor(inOld, (a, b) => !(inNew(a) && inNew(b))));
|
|
2611
2732
|
objects.set(id, rest);
|
|
2612
2733
|
objects.set(nid, fresh);
|
|
2613
2734
|
return nid;
|
|
@@ -2705,34 +2826,37 @@ var GameTestRoom = (() => {
|
|
|
2705
2826
|
}
|
|
2706
2827
|
return getObj(objectId).joints.filter((j) => !j.st.open && j.spec.tools.includes(tool)).map((j) => j.st);
|
|
2707
2828
|
},
|
|
2708
|
-
nearestJoint(objectId, point, maxDistance = 0.3) {
|
|
2829
|
+
nearestJoint(objectId, point, maxDistance = 0.3, tool) {
|
|
2709
2830
|
const im = impls.get(objectId);
|
|
2831
|
+
const cands = [];
|
|
2710
2832
|
if (im) {
|
|
2711
|
-
|
|
2712
|
-
let bd2 = maxDistance;
|
|
2833
|
+
const spec = im.impl.describe();
|
|
2713
2834
|
for (const js of im.impl.state().joints) {
|
|
2714
2835
|
if (js.open) continue;
|
|
2715
2836
|
const pos = implJointPos(im, js.a, js.b);
|
|
2716
|
-
const
|
|
2717
|
-
|
|
2718
|
-
bd2 = d;
|
|
2719
|
-
best2 = js;
|
|
2720
|
-
}
|
|
2837
|
+
const j = (spec.joints ?? []).find((x) => (x.id ?? `${x.a}~${x.b}`) === js.id);
|
|
2838
|
+
cands.push({ st: js, d: pos ? vec.distance(pos, point) : Infinity, ok: !!j && (j.tools ?? JOINT_DEFAULTS[j.kind].tools).includes(tool) });
|
|
2721
2839
|
}
|
|
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;
|
|
2840
|
+
} else {
|
|
2841
|
+
const o = getObj(objectId);
|
|
2842
|
+
for (const j of o.joints) {
|
|
2843
|
+
if (j.st.open) continue;
|
|
2844
|
+
cands.push({ st: j.st, d: vec.distance(jointWorld(o, j), point), ok: tool ? j.spec.tools.includes(tool) : true });
|
|
2733
2845
|
}
|
|
2734
2846
|
}
|
|
2735
|
-
|
|
2847
|
+
const pick = (only) => {
|
|
2848
|
+
let best = null;
|
|
2849
|
+
let bd = maxDistance;
|
|
2850
|
+
for (const c of cands) {
|
|
2851
|
+
if (only && !c.ok) continue;
|
|
2852
|
+
if (c.d <= bd) {
|
|
2853
|
+
bd = c.d;
|
|
2854
|
+
best = c.st;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return best;
|
|
2858
|
+
};
|
|
2859
|
+
return (tool ? pick(true) : null) ?? pick(false);
|
|
2736
2860
|
},
|
|
2737
2861
|
extinguish(objectId, partId, coolC = 200) {
|
|
2738
2862
|
const im = impls.get(objectId);
|
|
@@ -2775,6 +2899,10 @@ var GameTestRoom = (() => {
|
|
|
2775
2899
|
}).events;
|
|
2776
2900
|
},
|
|
2777
2901
|
state: (id) => objects.get(id)?.st ?? impls.get(id)?.impl.state() ?? null,
|
|
2902
|
+
snapshot: (id) => {
|
|
2903
|
+
const st = objects.get(id)?.st ?? impls.get(id)?.impl.state();
|
|
2904
|
+
return st ? clone(st) : null;
|
|
2905
|
+
},
|
|
2778
2906
|
part: (id, partId) => objects.get(id)?.byId.get(partId)?.st ?? impls.get(id)?.impl.state().parts.find((p) => p.id === partId) ?? null,
|
|
2779
2907
|
reset(id) {
|
|
2780
2908
|
const im = impls.get(id);
|
|
@@ -3786,6 +3914,8 @@ void main() {
|
|
|
3786
3914
|
var SLEEP_AFTER = 0.5;
|
|
3787
3915
|
var HARD_LANDING = 3;
|
|
3788
3916
|
var SLOP = 2e-3;
|
|
3917
|
+
var NARROW = 0.3;
|
|
3918
|
+
var TOPPLE = 3;
|
|
3789
3919
|
var PieceSystem = class {
|
|
3790
3920
|
constructor(THREE, o) {
|
|
3791
3921
|
__publicField(this, "THREE", THREE);
|
|
@@ -3917,6 +4047,7 @@ void main() {
|
|
|
3917
4047
|
let landPoint = [pos[0], pos[1], pos[2]];
|
|
3918
4048
|
let contactedLast = false;
|
|
3919
4049
|
let touched = false;
|
|
4050
|
+
let unstable = false;
|
|
3920
4051
|
for (let s = 0; s < steps; s++) {
|
|
3921
4052
|
v[1] -= this.o.gravity * h;
|
|
3922
4053
|
pos[0] += v[0] * h;
|
|
@@ -4005,6 +4136,25 @@ void main() {
|
|
|
4005
4136
|
}
|
|
4006
4137
|
}
|
|
4007
4138
|
}
|
|
4139
|
+
let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
|
|
4140
|
+
for (const r of rs) {
|
|
4141
|
+
x0 = Math.min(x0, r[0]);
|
|
4142
|
+
x1 = Math.max(x1, r[0]);
|
|
4143
|
+
z0 = Math.min(z0, r[2]);
|
|
4144
|
+
z1 = Math.max(z1, r[2]);
|
|
4145
|
+
low = Math.min(low, r[1]);
|
|
4146
|
+
}
|
|
4147
|
+
const height = -low;
|
|
4148
|
+
const wx = x1 - x0;
|
|
4149
|
+
const wz = z1 - z0;
|
|
4150
|
+
unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
|
|
4151
|
+
if (unstable) {
|
|
4152
|
+
const dirX = x0 + x1 > 0 ? -1 : 1;
|
|
4153
|
+
const dirZ = z0 + z1 > 0 ? -1 : 1;
|
|
4154
|
+
const k = TOPPLE * Math.min(height, 1) * h;
|
|
4155
|
+
if (wx <= wz) w[2] -= dirX * k;
|
|
4156
|
+
else w[0] += dirZ * k;
|
|
4157
|
+
}
|
|
4008
4158
|
const rd = Math.exp((rs.length >= 3 ? -3 : -1.5) * h);
|
|
4009
4159
|
w[0] *= rd;
|
|
4010
4160
|
w[1] *= rd;
|
|
@@ -4022,7 +4172,7 @@ void main() {
|
|
|
4022
4172
|
b.lastHit = this.time;
|
|
4023
4173
|
this.o.onHardLanding(b, landPoint, landing);
|
|
4024
4174
|
}
|
|
4025
|
-
const still = touched && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
|
|
4175
|
+
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
4176
|
if (still) {
|
|
4027
4177
|
b.still += dt;
|
|
4028
4178
|
if (b.still >= SLEEP_AFTER) {
|
|
@@ -4081,6 +4231,163 @@ void main() {
|
|
|
4081
4231
|
}
|
|
4082
4232
|
}
|
|
4083
4233
|
};
|
|
4234
|
+
function partHintOf(value) {
|
|
4235
|
+
if (typeof value === "string") return value ? { id: value } : null;
|
|
4236
|
+
if (!value || typeof value !== "object") return null;
|
|
4237
|
+
const v = value;
|
|
4238
|
+
if (typeof v.id !== "string" || !v.id) return null;
|
|
4239
|
+
const str = (x) => typeof x === "string" && x ? x : void 0;
|
|
4240
|
+
return { id: v.id, material: str(v.material), parent: str(v.parent), kind: str(v.kind) };
|
|
4241
|
+
}
|
|
4242
|
+
var METAL = /steel|iron|metal|alumin|copper|brass|bronze|tin|titan|chrome|zinc|lead/i;
|
|
4243
|
+
function sanitizeId(name) {
|
|
4244
|
+
let id = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4245
|
+
if (id && /^[0-9]/.test(id)) id = `p_${id}`;
|
|
4246
|
+
return id;
|
|
4247
|
+
}
|
|
4248
|
+
function isMesh(o) {
|
|
4249
|
+
return o.isMesh === true;
|
|
4250
|
+
}
|
|
4251
|
+
function sizeOf(box2) {
|
|
4252
|
+
return [box2.max.x - box2.min.x, box2.max.y - box2.min.y, box2.max.z - box2.min.z];
|
|
4253
|
+
}
|
|
4254
|
+
function specFromObject3D(THREE, root, options = {}) {
|
|
4255
|
+
const fallback = options.fallback ?? "plastic";
|
|
4256
|
+
const minSize = options.minSize ?? 0.015;
|
|
4257
|
+
root.updateMatrixWorld(true);
|
|
4258
|
+
const inv = root.matrixWorld.clone().invert();
|
|
4259
|
+
const entries = [];
|
|
4260
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4261
|
+
const visit = (o) => {
|
|
4262
|
+
if (!o.visible || o.userData?.destructionIgnore) return;
|
|
4263
|
+
if (isMesh(o) && o.geometry) {
|
|
4264
|
+
const geom = o.geometry;
|
|
4265
|
+
if (!geom.boundingBox) geom.computeBoundingBox();
|
|
4266
|
+
if (geom.boundingBox && !geom.boundingBox.isEmpty()) {
|
|
4267
|
+
const box2 = geom.boundingBox.clone().applyMatrix4(new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld));
|
|
4268
|
+
const mats = Array.isArray(o.material) ? o.material : [o.material];
|
|
4269
|
+
const matName = mats[0]?.name ?? "";
|
|
4270
|
+
const ud = o.userData ?? {};
|
|
4271
|
+
const hint = partHintOf(ud.destructionPart);
|
|
4272
|
+
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;
|
|
4273
|
+
const explicit = hint?.id ?? null;
|
|
4274
|
+
const hollow = ud.hollow === true || /hollow/i.test(o.name);
|
|
4275
|
+
const fill = options.fillOf?.(o, material);
|
|
4276
|
+
const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
|
|
4277
|
+
const existing = explicit ? byKey.get(explicit) : void 0;
|
|
4278
|
+
if (existing) {
|
|
4279
|
+
const before = sizeOf(existing.box);
|
|
4280
|
+
const bv = before[0] * before[1] * before[2];
|
|
4281
|
+
const ns = sizeOf(box2);
|
|
4282
|
+
existing.box.union(box2);
|
|
4283
|
+
existing.meshes.push(o);
|
|
4284
|
+
if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
|
|
4285
|
+
existing.hollow = existing.hollow || hollow;
|
|
4286
|
+
if (fill !== void 0) existing.fill = fill;
|
|
4287
|
+
} else {
|
|
4288
|
+
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 };
|
|
4289
|
+
entries.push(e);
|
|
4290
|
+
if (explicit) byKey.set(explicit, e);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
for (const c of o.children) visit(c);
|
|
4295
|
+
};
|
|
4296
|
+
visit(root);
|
|
4297
|
+
const eps = 5e-3;
|
|
4298
|
+
const overlapVolume = (a, b) => {
|
|
4299
|
+
const x = Math.min(a.max.x, b.max.x) - Math.max(a.min.x, b.min.x) + eps;
|
|
4300
|
+
const y = Math.min(a.max.y, b.max.y) - Math.max(a.min.y, b.min.y) + eps;
|
|
4301
|
+
const z = Math.min(a.max.z, b.max.z) - Math.max(a.min.z, b.min.z) + eps;
|
|
4302
|
+
return x > 0 && y > 0 && z > 0 ? x * y * z : 0;
|
|
4303
|
+
};
|
|
4304
|
+
const largest = (e) => Math.max(...sizeOf(e.box));
|
|
4305
|
+
const kept = [];
|
|
4306
|
+
const tiny = [];
|
|
4307
|
+
for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
|
|
4308
|
+
for (const t of tiny) {
|
|
4309
|
+
let best = null;
|
|
4310
|
+
let bestV = 0;
|
|
4311
|
+
for (const k of kept) {
|
|
4312
|
+
const v = overlapVolume(t.box, k.box);
|
|
4313
|
+
if (v > bestV) {
|
|
4314
|
+
bestV = v;
|
|
4315
|
+
best = k;
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4318
|
+
if (!best) {
|
|
4319
|
+
let bd = 0.05;
|
|
4320
|
+
for (const k of kept) {
|
|
4321
|
+
const d = k.box.distanceToPoint(t.box.getCenter(new THREE.Vector3()));
|
|
4322
|
+
if (d < bd) {
|
|
4323
|
+
bd = d;
|
|
4324
|
+
best = k;
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
if (best) {
|
|
4329
|
+
best.meshes.push(...t.meshes);
|
|
4330
|
+
} else {
|
|
4331
|
+
kept.push(t);
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
const used = /* @__PURE__ */ new Set();
|
|
4335
|
+
const parts = [];
|
|
4336
|
+
const meshesOf = {};
|
|
4337
|
+
let counter = 0;
|
|
4338
|
+
for (const e of kept) {
|
|
4339
|
+
counter++;
|
|
4340
|
+
let base = (e.explicitId ? sanitizeId(e.explicitId) : "") || sanitizeId(e.name) || `part_${counter}`;
|
|
4341
|
+
let id = base;
|
|
4342
|
+
for (let n = 2; used.has(id); n++) id = `${base}_${n}`;
|
|
4343
|
+
used.add(id);
|
|
4344
|
+
base = id;
|
|
4345
|
+
const size = sizeOf(e.box);
|
|
4346
|
+
const center = e.box.getCenter(new THREE.Vector3());
|
|
4347
|
+
const part = {
|
|
4348
|
+
id,
|
|
4349
|
+
material: e.material,
|
|
4350
|
+
shape: { kind: "box", size: [Math.max(size[0], 2e-3), Math.max(size[1], 2e-3), Math.max(size[2], 2e-3)] },
|
|
4351
|
+
position: [center.x, center.y, center.z]
|
|
4352
|
+
};
|
|
4353
|
+
const minSide = Math.min(...size);
|
|
4354
|
+
if (e.fill !== void 0) part.fill = e.fill;
|
|
4355
|
+
else if (e.hollow) part.fill = 0.05;
|
|
4356
|
+
if (minSide < 0.01 && METAL.test(e.material)) {
|
|
4357
|
+
part.fill = e.fill ?? 1;
|
|
4358
|
+
part.thickness = Math.max(minSide, 5e-4);
|
|
4359
|
+
}
|
|
4360
|
+
if (e.tags) part.tags = [...e.tags];
|
|
4361
|
+
const ao = options.anchorBelow ?? -1;
|
|
4362
|
+
if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
|
|
4363
|
+
parts.push(part);
|
|
4364
|
+
meshesOf[id] = e.meshes;
|
|
4365
|
+
}
|
|
4366
|
+
const spec = { parts };
|
|
4367
|
+
if (options.name !== void 0) spec.name = options.name;
|
|
4368
|
+
else if (root.name) spec.name = root.name;
|
|
4369
|
+
if (options.tags) spec.tags = [...options.tags];
|
|
4370
|
+
spec.joints = options.joints === false ? [] : inferJoints(parts);
|
|
4371
|
+
if (options.joints !== false) {
|
|
4372
|
+
const finalId = /* @__PURE__ */ new Map();
|
|
4373
|
+
kept.forEach((e, i) => finalId.set(e, parts[i].id));
|
|
4374
|
+
const idOf = /* @__PURE__ */ new Map();
|
|
4375
|
+
for (const e of kept) if (e.explicitId) idOf.set(e.explicitId, finalId.get(e));
|
|
4376
|
+
const joints = spec.joints;
|
|
4377
|
+
for (const e of kept) {
|
|
4378
|
+
if (!e.parent) continue;
|
|
4379
|
+
const id = finalId.get(e);
|
|
4380
|
+
const parent = idOf.get(e.parent) ?? (used.has(sanitizeId(e.parent)) ? sanitizeId(e.parent) : null);
|
|
4381
|
+
if (!parent || parent === id) continue;
|
|
4382
|
+
const at = joints.findIndex((j) => j.a === parent && j.b === id || j.a === id && j.b === parent);
|
|
4383
|
+
const kind = e.jointKind ?? (at >= 0 ? joints[at].kind : "resting");
|
|
4384
|
+
const joint2 = { id: `${parent}~${id}`, a: parent, b: id, kind };
|
|
4385
|
+
if (at >= 0) joints[at] = joint2;
|
|
4386
|
+
else joints.push(joint2);
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
return { spec, parts: meshesOf };
|
|
4390
|
+
}
|
|
4084
4391
|
var STOPS = [
|
|
4085
4392
|
[450, 0],
|
|
4086
4393
|
[500, 2753536],
|
|
@@ -4166,6 +4473,7 @@ void main() {
|
|
|
4166
4473
|
__publicField(this, "m4");
|
|
4167
4474
|
__publicField(this, "box");
|
|
4168
4475
|
__publicField(this, "decalCount", 0);
|
|
4476
|
+
__publicField(this, "tints", /* @__PURE__ */ new WeakMap());
|
|
4169
4477
|
this.kit = new DecalKit(THREE);
|
|
4170
4478
|
this.a = tmpV(THREE);
|
|
4171
4479
|
this.b = tmpV(THREE);
|
|
@@ -4196,10 +4504,33 @@ void main() {
|
|
|
4196
4504
|
vertexCount(mv) {
|
|
4197
4505
|
return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
|
|
4198
4506
|
}
|
|
4199
|
-
/**
|
|
4507
|
+
/**
|
|
4508
|
+
* Average vertex colour of a mesh whose material uses `vertexColors` (computed once per mesh, from its original geometry), else null. Fragments
|
|
4509
|
+
* and debris of such a mesh multiply it by `material.color`; without it they come out white.
|
|
4510
|
+
*/
|
|
4511
|
+
vertexTint(mv) {
|
|
4512
|
+
if (this.tints.has(mv.mesh)) return this.tints.get(mv.mesh) ?? null;
|
|
4513
|
+
let tint = null;
|
|
4514
|
+
const used = materialsOf(mv.origMaterial).some((m) => m.vertexColors === true);
|
|
4515
|
+
const attr = used ? mv.origGeometry.getAttribute("color") : void 0;
|
|
4516
|
+
if (attr && attr.count > 0 && attr.itemSize >= 3) {
|
|
4517
|
+
let r = 0, g = 0, b = 0;
|
|
4518
|
+
for (let i = 0; i < attr.count; i++) {
|
|
4519
|
+
r += attr.getX(i);
|
|
4520
|
+
g += attr.getY(i);
|
|
4521
|
+
b += attr.getZ(i);
|
|
4522
|
+
}
|
|
4523
|
+
tint = new this.THREE.Color(r / attr.count, g / attr.count, b / attr.count);
|
|
4524
|
+
}
|
|
4525
|
+
this.tints.set(mv.mesh, tint);
|
|
4526
|
+
return tint;
|
|
4527
|
+
}
|
|
4528
|
+
/** Colour of the original material (before any char) times the vertex tint, `0xRRGGBB`, or -1. */
|
|
4200
4529
|
baseColorHex(mv) {
|
|
4201
4530
|
const c = mv.baseColors?.[0] ?? colorOf(materialsOf(mv.origMaterial)[0]);
|
|
4202
|
-
|
|
4531
|
+
if (!c) return -1;
|
|
4532
|
+
const tint = this.vertexTint(mv);
|
|
4533
|
+
return tint ? c.clone().multiply(tint).getHex() : c.getHex();
|
|
4203
4534
|
}
|
|
4204
4535
|
/** Is the original material transparent? */
|
|
4205
4536
|
isTransparent(mv) {
|
|
@@ -4668,8 +4999,8 @@ void main() {
|
|
|
4668
4999
|
} else {
|
|
4669
5000
|
const leftovers = [];
|
|
4670
5001
|
for (const m of meshesIn(root)) {
|
|
4671
|
-
const ud = m.userData?.destructionPart;
|
|
4672
|
-
if (
|
|
5002
|
+
const ud = partHintOf(m.userData?.destructionPart)?.id;
|
|
5003
|
+
if (ud !== void 0 && partIds.has(ud)) add2(ud, m);
|
|
4673
5004
|
else if (partIds.has(m.name)) add2(m.name, m);
|
|
4674
5005
|
else leftovers.push(m);
|
|
4675
5006
|
}
|
|
@@ -4979,7 +5310,10 @@ void main() {
|
|
|
4979
5310
|
if (!mv) return null;
|
|
4980
5311
|
const mat = materialsOf2(mv.mesh.material)[0];
|
|
4981
5312
|
const c = mat?.color;
|
|
4982
|
-
if (c && c.isColor)
|
|
5313
|
+
if (c && c.isColor) {
|
|
5314
|
+
const tint = this.visuals.vertexTint(mv);
|
|
5315
|
+
return tint ? c.clone().multiply(tint).getHex() : c.getHex();
|
|
5316
|
+
}
|
|
4983
5317
|
return def ? fallbackColor(def.id, def) : null;
|
|
4984
5318
|
}
|
|
4985
5319
|
debrisOptions(kind, objectId, partId) {
|
|
@@ -5423,136 +5757,6 @@ void main() {
|
|
|
5423
5757
|
this.disposed = true;
|
|
5424
5758
|
}
|
|
5425
5759
|
};
|
|
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
5760
|
|
|
5557
5761
|
// ../game-weapons/dist/index.js
|
|
5558
5762
|
var __defProp3 = Object.defineProperty;
|
|
@@ -5739,6 +5943,36 @@ void main() {
|
|
|
5739
5943
|
"grab"
|
|
5740
5944
|
]
|
|
5741
5945
|
},
|
|
5946
|
+
{
|
|
5947
|
+
id: "kick",
|
|
5948
|
+
name: "Kick",
|
|
5949
|
+
category: "melee_blunt",
|
|
5950
|
+
action: {
|
|
5951
|
+
kind: "melee",
|
|
5952
|
+
damage: "blunt",
|
|
5953
|
+
energy: 250,
|
|
5954
|
+
radius: 0.08,
|
|
5955
|
+
reach: 1.1,
|
|
5956
|
+
rpm: 60,
|
|
5957
|
+
noise: 8
|
|
5958
|
+
},
|
|
5959
|
+
mass: 0
|
|
5960
|
+
},
|
|
5961
|
+
{
|
|
5962
|
+
id: "shove",
|
|
5963
|
+
name: "Shove",
|
|
5964
|
+
category: "melee_blunt",
|
|
5965
|
+
action: {
|
|
5966
|
+
kind: "melee",
|
|
5967
|
+
damage: "blunt",
|
|
5968
|
+
energy: 80,
|
|
5969
|
+
radius: 0.1,
|
|
5970
|
+
reach: 1.5,
|
|
5971
|
+
rpm: 90,
|
|
5972
|
+
noise: 5
|
|
5973
|
+
},
|
|
5974
|
+
mass: 0
|
|
5975
|
+
},
|
|
5742
5976
|
{
|
|
5743
5977
|
id: "pistol_9mm",
|
|
5744
5978
|
name: "9 mm service pistol",
|
|
@@ -6616,6 +6850,8 @@ void main() {
|
|
|
6616
6850
|
en: {
|
|
6617
6851
|
weapons: {
|
|
6618
6852
|
hand: "Hand",
|
|
6853
|
+
kick: "Kick",
|
|
6854
|
+
shove: "Shove",
|
|
6619
6855
|
pistol_9mm: "9 mm service pistol",
|
|
6620
6856
|
pistol_45: ".45 ACP pistol",
|
|
6621
6857
|
pistol_22: ".22 LR pistol",
|
|
@@ -6662,6 +6898,8 @@ void main() {
|
|
|
6662
6898
|
"pt-BR": {
|
|
6663
6899
|
weapons: {
|
|
6664
6900
|
hand: "m\xE3o",
|
|
6901
|
+
kick: "chute",
|
|
6902
|
+
shove: "empurr\xE3o",
|
|
6665
6903
|
pistol_9mm: "pistola 9 mm",
|
|
6666
6904
|
pistol_45: "pistola .45",
|
|
6667
6905
|
pistol_22: "pistola .22",
|
|
@@ -7058,6 +7296,8 @@ void main() {
|
|
|
7058
7296
|
var WEAPONS = weapons_default;
|
|
7059
7297
|
var DEFAULT_LOADOUT = [
|
|
7060
7298
|
"hand",
|
|
7299
|
+
"kick",
|
|
7300
|
+
"shove",
|
|
7061
7301
|
"pistol_9mm",
|
|
7062
7302
|
"revolver_357",
|
|
7063
7303
|
"shotgun_pump_12",
|
|
@@ -7065,6 +7305,8 @@ void main() {
|
|
|
7065
7305
|
"rifle_bolt_308",
|
|
7066
7306
|
"hammer",
|
|
7067
7307
|
"axe",
|
|
7308
|
+
"machete",
|
|
7309
|
+
"utility_knife",
|
|
7068
7310
|
"crowbar",
|
|
7069
7311
|
"lighter",
|
|
7070
7312
|
"flamethrower",
|
|
@@ -10529,6 +10771,9 @@ void main() {
|
|
|
10529
10771
|
"mod.destructible_ok": "{name}: destructible contract ok ({parts} parts, hit and state well-formed)",
|
|
10530
10772
|
"mod.destructible_fail": "{name}: destructible contract broken: {problems}",
|
|
10531
10773
|
"mod.animation_ok": '{name}: animation "{clip}" ok ({seconds} s)',
|
|
10774
|
+
"mod.spec_ok": "{name}: spec ok ({parts} parts)",
|
|
10775
|
+
"mod.spec_fail": "{name}: spec broken: {problems}",
|
|
10776
|
+
"mod.spec_warn": "{name}: spec warning: {warning}",
|
|
10532
10777
|
"mod.animation_fail": '{name}: animation "{clip}" broken: {problems}',
|
|
10533
10778
|
"ui.animations": "Animations",
|
|
10534
10779
|
"ui.animations_aim": "Aim at an item or hold it to see its animations.",
|
|
@@ -10589,6 +10834,7 @@ void main() {
|
|
|
10589
10834
|
"ui.filter.event": "events",
|
|
10590
10835
|
"ui.filter.check": "checks",
|
|
10591
10836
|
"ui.autoscroll": "autoscroll",
|
|
10837
|
+
"log.export_cancelled": "export cancelled",
|
|
10592
10838
|
"ui.copy_json": "Copy JSON",
|
|
10593
10839
|
"ui.download_ndjson": "Download NDJSON",
|
|
10594
10840
|
"ui.download_txt": "Download TXT",
|
|
@@ -10645,6 +10891,8 @@ void main() {
|
|
|
10645
10891
|
"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
10892
|
"sc.kick_chair_leg": "A kick takes a chair leg off",
|
|
10647
10893
|
"sc.kick_chair_leg.check": "Chair: {kicks} kick(s), {groups} group(s), loose pieces {before} \u2192 {after}, {leg} (expected: a loose piece appears)",
|
|
10894
|
+
"sc.kick_detaches_leg_via_tool": "The kick tool (hotbar) takes a chair leg off",
|
|
10895
|
+
"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
10896
|
"sc.screwdriver_shelf_board": "Screwdriver takes a screwed shelf board off in about 2.5 s",
|
|
10649
10897
|
"sc.screwdriver_shelf_board.check": "Screwdriver on the shelf board: took {secs} s, expected about {want} s, the shelf is now in {pieces}",
|
|
10650
10898
|
"sc.wrong_tool_refused": "The wrong tool is refused",
|
|
@@ -10812,6 +11060,9 @@ void main() {
|
|
|
10812
11060
|
"mod.destructible_ok": "{name}: contrato de destrui\xE7\xE3o ok ({parts} pe\xE7as, golpe e estado bem formados)",
|
|
10813
11061
|
"mod.destructible_fail": "{name}: contrato de destrui\xE7\xE3o quebrado: {problems}",
|
|
10814
11062
|
"mod.animation_ok": '{name}: anima\xE7\xE3o "{clip}" ok ({seconds} s)',
|
|
11063
|
+
"mod.spec_ok": "{name}: spec ok ({parts} pe\xE7as)",
|
|
11064
|
+
"mod.spec_fail": "{name}: spec quebrada: {problems}",
|
|
11065
|
+
"mod.spec_warn": "{name}: aviso na spec: {warning}",
|
|
10815
11066
|
"mod.animation_fail": '{name}: anima\xE7\xE3o "{clip}" quebrada: {problems}',
|
|
10816
11067
|
"ui.animations": "Anima\xE7\xF5es",
|
|
10817
11068
|
"ui.animations_aim": "Mire num item ou segure-o para ver as anima\xE7\xF5es.",
|
|
@@ -10872,6 +11123,7 @@ void main() {
|
|
|
10872
11123
|
"ui.filter.event": "eventos",
|
|
10873
11124
|
"ui.filter.check": "testes",
|
|
10874
11125
|
"ui.autoscroll": "rolar sozinho",
|
|
11126
|
+
"log.export_cancelled": "exporta\xE7\xE3o cancelada",
|
|
10875
11127
|
"ui.copy_json": "Copiar JSON",
|
|
10876
11128
|
"ui.download_ndjson": "Baixar NDJSON",
|
|
10877
11129
|
"ui.download_txt": "Baixar TXT",
|
|
@@ -10928,6 +11180,8 @@ void main() {
|
|
|
10928
11180
|
"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
11181
|
"sc.kick_chair_leg": "Um chute solta a perna da cadeira",
|
|
10930
11182
|
"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)",
|
|
11183
|
+
"sc.kick_detaches_leg_via_tool": "A ferramenta chute (barra de atalhos) solta a perna da cadeira",
|
|
11184
|
+
"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
11185
|
"sc.screwdriver_shelf_board": "Chave de fenda solta a prateleira parafusada em cerca de 2,5 s",
|
|
10932
11186
|
"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
11187
|
"sc.wrong_tool_refused": "A ferramenta errada \xE9 recusada",
|
|
@@ -11429,7 +11683,16 @@ void main() {
|
|
|
11429
11683
|
const s = t - m * 60;
|
|
11430
11684
|
return `${pad(m)}:${s.toFixed(2).padStart(5, "0")}`;
|
|
11431
11685
|
};
|
|
11432
|
-
function createLog() {
|
|
11686
|
+
function createLog(options = {}) {
|
|
11687
|
+
const host = globalThis.claude;
|
|
11688
|
+
let downloadsP = null;
|
|
11689
|
+
if (host && typeof host.use === "function") {
|
|
11690
|
+
try {
|
|
11691
|
+
downloadsP = Promise.resolve(host.use("downloads")).then((d) => d ?? null, () => null);
|
|
11692
|
+
} catch {
|
|
11693
|
+
downloadsP = null;
|
|
11694
|
+
}
|
|
11695
|
+
}
|
|
11433
11696
|
let list = [];
|
|
11434
11697
|
let seq = 0;
|
|
11435
11698
|
let clock = () => 0;
|
|
@@ -11495,9 +11758,20 @@ void main() {
|
|
|
11495
11758
|
toJSON: () => JSON.stringify(read(), null, 2),
|
|
11496
11759
|
toNDJSON: () => read().map((e) => JSON.stringify(e)).join("\n") + (list.length ? "\n" : ""),
|
|
11497
11760
|
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;
|
|
11761
|
+
async download(format = "ndjson") {
|
|
11500
11762
|
const body = format === "json" ? log.toJSON() : format === "txt" ? log.toText() : log.toNDJSON();
|
|
11763
|
+
const downloads = downloadsP ? await downloadsP : null;
|
|
11764
|
+
if (downloads) {
|
|
11765
|
+
const filename = format === "ndjson" ? "test-room-log.ndjson.txt" : `test-room-log.${format}`;
|
|
11766
|
+
try {
|
|
11767
|
+
await downloads.save({ filename, data: body });
|
|
11768
|
+
} catch (err) {
|
|
11769
|
+
const code = err?.code;
|
|
11770
|
+
log.add({ channel: "action", type: "export_cancelled", text: options.exportCancelled?.() ?? "export cancelled", data: { format, ...typeof code === "string" ? { code } : {} } });
|
|
11771
|
+
}
|
|
11772
|
+
return;
|
|
11773
|
+
}
|
|
11774
|
+
if (typeof document === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined" || !URL.createObjectURL) return;
|
|
11501
11775
|
const mime = format === "json" ? "application/json" : "text/plain";
|
|
11502
11776
|
const url = URL.createObjectURL(new Blob([body], { type: mime }));
|
|
11503
11777
|
const a = document.createElement("a");
|
|
@@ -12242,6 +12516,10 @@ void main() {
|
|
|
12242
12516
|
yaw += yawDeg * Math.PI / 180;
|
|
12243
12517
|
apply();
|
|
12244
12518
|
},
|
|
12519
|
+
moveBy(dx, dz) {
|
|
12520
|
+
move(dx, dz);
|
|
12521
|
+
apply();
|
|
12522
|
+
},
|
|
12245
12523
|
walk(forward, right) {
|
|
12246
12524
|
move(-Math.sin(yaw) * forward + Math.cos(yaw) * right, -Math.cos(yaw) * forward - Math.sin(yaw) * right);
|
|
12247
12525
|
apply();
|
|
@@ -12373,6 +12651,23 @@ void main() {
|
|
|
12373
12651
|
{ kicks, before, after }
|
|
12374
12652
|
);
|
|
12375
12653
|
}),
|
|
12654
|
+
sc("kick_detaches_leg_via_tool", ({ room, step, expect }) => {
|
|
12655
|
+
const id = at(room, "chair", 0, -4);
|
|
12656
|
+
const before = room.renderer.pieces().length;
|
|
12657
|
+
room.setTool("kick");
|
|
12658
|
+
let kicks = 0;
|
|
12659
|
+
for (; kicks < 8 && room.renderer.pieces().length <= before && part(room, id, "leg_1"); kicks++) {
|
|
12660
|
+
const st = room.world.state(id);
|
|
12661
|
+
const leg = toWorld(st.position, st.rotation, [-0.18, 0.2, -0.18]);
|
|
12662
|
+
const side = rotateVec(st.rotation, [-0.8, 0, 0]);
|
|
12663
|
+
look(room, [leg[0] + side[0], leg[1] + side[1], leg[2] + side[2]], leg);
|
|
12664
|
+
room.use();
|
|
12665
|
+
step(1.05);
|
|
12666
|
+
}
|
|
12667
|
+
step(0.5);
|
|
12668
|
+
const after = room.renderer.pieces().length;
|
|
12669
|
+
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 });
|
|
12670
|
+
}),
|
|
12376
12671
|
sc("screwdriver_shelf_board", ({ room, step, expect }) => {
|
|
12377
12672
|
const id = at(room, "shelf", 0, -4);
|
|
12378
12673
|
room.setTool("screwdriver");
|
|
@@ -12703,16 +12998,25 @@ void main() {
|
|
|
12703
12998
|
};
|
|
12704
12999
|
logBtn(t("ui.copy_json"), () => {
|
|
12705
13000
|
const text = o.log.toJSON();
|
|
12706
|
-
|
|
12707
|
-
void navigator.clipboard.writeText(text);
|
|
12708
|
-
} catch {
|
|
13001
|
+
const select = () => {
|
|
12709
13002
|
const ta = mk("textarea");
|
|
12710
13003
|
ta.value = text;
|
|
13004
|
+
ta.setAttribute("readonly", "");
|
|
13005
|
+
ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
|
|
12711
13006
|
document.body.appendChild(ta);
|
|
13007
|
+
ta.focus();
|
|
12712
13008
|
ta.select();
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
13009
|
+
let copied = false;
|
|
13010
|
+
try {
|
|
13011
|
+
copied = !!document.execCommand?.("copy");
|
|
13012
|
+
} catch {
|
|
13013
|
+
copied = false;
|
|
13014
|
+
}
|
|
13015
|
+
if (copied) ta.remove();
|
|
13016
|
+
else ta.onblur = () => ta.remove();
|
|
13017
|
+
};
|
|
13018
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) navigator.clipboard.writeText(text).catch(select);
|
|
13019
|
+
else select();
|
|
12716
13020
|
});
|
|
12717
13021
|
logBtn(t("ui.download_ndjson"), () => o.log.download("ndjson"));
|
|
12718
13022
|
logBtn(t("ui.download_txt"), () => o.log.download("txt"));
|
|
@@ -13124,7 +13428,7 @@ void main() {
|
|
|
13124
13428
|
const entries = /* @__PURE__ */ new Map();
|
|
13125
13429
|
const splitOf = /* @__PURE__ */ new Map();
|
|
13126
13430
|
let simTime = 0;
|
|
13127
|
-
const log = createLog();
|
|
13431
|
+
const log = createLog({ exportCancelled: () => t("log.export_cancelled") });
|
|
13128
13432
|
log.setClock(() => simTime);
|
|
13129
13433
|
const renderer = createDestructionRenderer(THREE, {
|
|
13130
13434
|
scene,
|
|
@@ -13498,6 +13802,7 @@ void main() {
|
|
|
13498
13802
|
addArena();
|
|
13499
13803
|
timeScale = 1;
|
|
13500
13804
|
fastForward = false;
|
|
13805
|
+
walkPlan = null;
|
|
13501
13806
|
};
|
|
13502
13807
|
const aimO = [0, 0, 0];
|
|
13503
13808
|
const aimD = [0, 0, -1];
|
|
@@ -13749,11 +14054,11 @@ void main() {
|
|
|
13749
14054
|
1
|
|
13750
14055
|
);
|
|
13751
14056
|
};
|
|
13752
|
-
const meleeUse = (u) => {
|
|
13753
|
-
const w = toolName();
|
|
14057
|
+
const meleeUse = (u, id = toolId) => {
|
|
14058
|
+
const w = toolName(id);
|
|
13754
14059
|
const r = rayWorld(u.origin, u.direction, u.reach);
|
|
13755
14060
|
if (!r) {
|
|
13756
|
-
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon:
|
|
14061
|
+
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon: id } });
|
|
13757
14062
|
return;
|
|
13758
14063
|
}
|
|
13759
14064
|
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 +14067,7 @@ void main() {
|
|
|
13762
14067
|
objectId: r.object,
|
|
13763
14068
|
itemId: entryOf(r.object)?.so.itemId,
|
|
13764
14069
|
part: r.part,
|
|
13765
|
-
data: { weapon:
|
|
14070
|
+
data: { weapon: id, damage: u.damage, energyJ: u.energy, distanceM: Math.round(r.distance * 100) / 100 }
|
|
13766
14071
|
});
|
|
13767
14072
|
};
|
|
13768
14073
|
const toolWork = (u, dt) => {
|
|
@@ -13773,7 +14078,7 @@ void main() {
|
|
|
13773
14078
|
log.coalesce(`use:${toolId}`, { channel: "action", type: "tool", text: t("act.tool.none", { weapon: w }), data: { weapon: toolId } }, 1);
|
|
13774
14079
|
return;
|
|
13775
14080
|
}
|
|
13776
|
-
const j = world.nearestJoint(r.object, r.point, 0.45);
|
|
14081
|
+
const j = world.nearestJoint(r.object, r.point, 0.45, u.tool);
|
|
13777
14082
|
if (!j) {
|
|
13778
14083
|
progress = null;
|
|
13779
14084
|
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 +14257,13 @@ void main() {
|
|
|
13952
14257
|
}
|
|
13953
14258
|
}
|
|
13954
14259
|
};
|
|
14260
|
+
const kick = () => {
|
|
14261
|
+
if (!arsenal.weapons.has("kick")) return;
|
|
14262
|
+
aim();
|
|
14263
|
+
const res = toolState("kick").trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 0, false, "player");
|
|
14264
|
+
if (res.status !== "ok") return;
|
|
14265
|
+
for (const u of res.uses) if (u.kind === "melee") meleeUse(u, "kick");
|
|
14266
|
+
};
|
|
13955
14267
|
const setTool = (id) => {
|
|
13956
14268
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
13957
14269
|
tool.release();
|
|
@@ -14207,7 +14519,7 @@ void main() {
|
|
|
14207
14519
|
break;
|
|
14208
14520
|
case "KeyV":
|
|
14209
14521
|
if (heldEntry?.uses[2]) useItem(heldEntry.uses[2]);
|
|
14210
|
-
else
|
|
14522
|
+
else kick();
|
|
14211
14523
|
break;
|
|
14212
14524
|
case "KeyT":
|
|
14213
14525
|
toggleTap();
|
|
@@ -14306,8 +14618,19 @@ void main() {
|
|
|
14306
14618
|
if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(host.objectId, e.fluidPart, c.liters);
|
|
14307
14619
|
}
|
|
14308
14620
|
};
|
|
14621
|
+
let paused = false;
|
|
14622
|
+
let walkPlan = null;
|
|
14623
|
+
const stepWalk = (dt) => {
|
|
14624
|
+
if (!walkPlan) return;
|
|
14625
|
+
const f = walkPlan.remaining <= dt ? 1 : dt / walkPlan.remaining;
|
|
14626
|
+
player.moveBy((walkPlan.x - camera.position.x) * f, (walkPlan.z - camera.position.z) * f);
|
|
14627
|
+
walkPlan.remaining -= dt;
|
|
14628
|
+
if (walkPlan.remaining <= 1e-9) walkPlan = null;
|
|
14629
|
+
};
|
|
14309
14630
|
const stepSim = (dt) => {
|
|
14631
|
+
if (paused) return;
|
|
14310
14632
|
simTime += dt;
|
|
14633
|
+
stepWalk(dt);
|
|
14311
14634
|
const reps = fastForward ? 10 : 1;
|
|
14312
14635
|
updateAnimations(dt);
|
|
14313
14636
|
updateItemUse(dt);
|
|
@@ -14336,13 +14659,13 @@ void main() {
|
|
|
14336
14659
|
if (tiltKey || heldTilt > 0) {
|
|
14337
14660
|
heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
|
|
14338
14661
|
}
|
|
14339
|
-
if (useHeld) {
|
|
14662
|
+
if (useHeld && !paused) {
|
|
14340
14663
|
if (tool.def.category === "hand") {
|
|
14341
14664
|
if (useFirst) grab();
|
|
14342
14665
|
} else useTool(dt, !useFirst);
|
|
14343
14666
|
useFirst = false;
|
|
14344
14667
|
}
|
|
14345
|
-
followHeld(dt);
|
|
14668
|
+
if (!paused) followHeld(dt);
|
|
14346
14669
|
stepSim(dt * timeScale);
|
|
14347
14670
|
if (progress && simTime - progress.at > 0.25) progress = null;
|
|
14348
14671
|
hudAcc += dt;
|
|
@@ -14403,6 +14726,16 @@ void main() {
|
|
|
14403
14726
|
const pose = poseAt(pl.clip, id === "" ? null : id, pl.t);
|
|
14404
14727
|
obj.position.set(rest.p[0] + pose.position[0], rest.p[1] + pose.position[1], rest.p[2] + pose.position[2]);
|
|
14405
14728
|
obj.rotation.set(rest.r[0] + pose.rotation[0], rest.r[1] + pose.rotation[1], rest.r[2] + pose.rotation[2]);
|
|
14729
|
+
const ps = id === "" ? void 0 : specs.get(pl.entry.so.objectId)?.parts.find((p) => p.id === id);
|
|
14730
|
+
if (ps) {
|
|
14731
|
+
const r0 = ps.rotation ?? ZERO;
|
|
14732
|
+
world.setPartPose(
|
|
14733
|
+
pl.entry.so.objectId,
|
|
14734
|
+
id,
|
|
14735
|
+
[ps.position[0] + pose.position[0], ps.position[1] + pose.position[1], ps.position[2] + pose.position[2]],
|
|
14736
|
+
[r0[0] + pose.rotation[0], r0[1] + pose.rotation[1], r0[2] + pose.rotation[2]]
|
|
14737
|
+
);
|
|
14738
|
+
}
|
|
14406
14739
|
}
|
|
14407
14740
|
};
|
|
14408
14741
|
const updateAnimations = (dt) => {
|
|
@@ -14490,8 +14823,14 @@ void main() {
|
|
|
14490
14823
|
partIds = probe.describe().parts.map((p) => p.id);
|
|
14491
14824
|
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
14825
|
for (const p of r.problems) fail(`${item.id}: ${p}`);
|
|
14493
|
-
}
|
|
14494
|
-
|
|
14826
|
+
}
|
|
14827
|
+
if (!item.impl || item.spec) {
|
|
14828
|
+
const spec = resolveSpec(item, root).spec;
|
|
14829
|
+
if (!item.impl) partIds = spec.parts.map((p) => p.id);
|
|
14830
|
+
const r = checkSpec(spec, { materials: world.materials.keys() });
|
|
14831
|
+
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);
|
|
14832
|
+
for (const p of r.problems) fail(`${item.id}: ${p}`);
|
|
14833
|
+
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
14834
|
}
|
|
14496
14835
|
} catch (err) {
|
|
14497
14836
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -14530,6 +14869,8 @@ void main() {
|
|
|
14530
14869
|
const all = list ?? allScenarios();
|
|
14531
14870
|
const report = { passed: 0, failed: 0, results: [] };
|
|
14532
14871
|
scenarioRunning = true;
|
|
14872
|
+
const wasPaused = paused;
|
|
14873
|
+
paused = false;
|
|
14533
14874
|
try {
|
|
14534
14875
|
for (const sc of all) {
|
|
14535
14876
|
hardReset();
|
|
@@ -14561,6 +14902,7 @@ void main() {
|
|
|
14561
14902
|
}
|
|
14562
14903
|
} finally {
|
|
14563
14904
|
scenarioRunning = false;
|
|
14905
|
+
paused = wasPaused;
|
|
14564
14906
|
}
|
|
14565
14907
|
return report;
|
|
14566
14908
|
};
|
|
@@ -14623,6 +14965,19 @@ void main() {
|
|
|
14623
14965
|
heldTilt = rad;
|
|
14624
14966
|
},
|
|
14625
14967
|
toggleTap,
|
|
14968
|
+
pause() {
|
|
14969
|
+
paused = true;
|
|
14970
|
+
},
|
|
14971
|
+
resume() {
|
|
14972
|
+
paused = false;
|
|
14973
|
+
},
|
|
14974
|
+
get paused() {
|
|
14975
|
+
return paused;
|
|
14976
|
+
},
|
|
14977
|
+
walk(to, seconds) {
|
|
14978
|
+
walkPlan = seconds > 0 ? { x: to[0], z: to[2], remaining: seconds } : null;
|
|
14979
|
+
if (!walkPlan) player.moveBy(to[0] - camera.position.x, to[2] - camera.position.z);
|
|
14980
|
+
},
|
|
14626
14981
|
setSlowMotion(on) {
|
|
14627
14982
|
timeScale = on ? 0.25 : 1;
|
|
14628
14983
|
},
|