@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.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createDestructionRenderer, specFromObject3D } from "@adatechnology/game
|
|
|
4
4
|
import { createArsenal, DEFAULT_LOADOUT } from "@adatechnology/game-weapons";
|
|
5
5
|
import { createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
|
|
6
6
|
import { createFluidRenderer } from "@adatechnology/game-fluids-three";
|
|
7
|
-
import { checkAnimation, checkDestructible } from "@adatechnology/game-contracts";
|
|
7
|
+
import { checkAnimation, checkDestructible, checkSpec } from "@adatechnology/game-contracts";
|
|
8
8
|
|
|
9
9
|
// src/items.ts
|
|
10
10
|
import { buildFrame } from "@adatechnology/game-destruction";
|
|
@@ -212,6 +212,9 @@ var en = {
|
|
|
212
212
|
"mod.destructible_ok": "{name}: destructible contract ok ({parts} parts, hit and state well-formed)",
|
|
213
213
|
"mod.destructible_fail": "{name}: destructible contract broken: {problems}",
|
|
214
214
|
"mod.animation_ok": '{name}: animation "{clip}" ok ({seconds} s)',
|
|
215
|
+
"mod.spec_ok": "{name}: spec ok ({parts} parts)",
|
|
216
|
+
"mod.spec_fail": "{name}: spec broken: {problems}",
|
|
217
|
+
"mod.spec_warn": "{name}: spec warning: {warning}",
|
|
215
218
|
"mod.animation_fail": '{name}: animation "{clip}" broken: {problems}',
|
|
216
219
|
"ui.animations": "Animations",
|
|
217
220
|
"ui.animations_aim": "Aim at an item or hold it to see its animations.",
|
|
@@ -272,6 +275,7 @@ var en = {
|
|
|
272
275
|
"ui.filter.event": "events",
|
|
273
276
|
"ui.filter.check": "checks",
|
|
274
277
|
"ui.autoscroll": "autoscroll",
|
|
278
|
+
"log.export_cancelled": "export cancelled",
|
|
275
279
|
"ui.copy_json": "Copy JSON",
|
|
276
280
|
"ui.download_ndjson": "Download NDJSON",
|
|
277
281
|
"ui.download_txt": "Download TXT",
|
|
@@ -328,6 +332,8 @@ var en = {
|
|
|
328
332
|
"sc.shotgun_splinters_crate.check": "12 ga on the crate: {shots} shot(s), {splinters} board(s) splintered, {holes} holes (expected: at least one splintered)",
|
|
329
333
|
"sc.kick_chair_leg": "A kick takes a chair leg off",
|
|
330
334
|
"sc.kick_chair_leg.check": "Chair: {kicks} kick(s), {groups} group(s), loose pieces {before} \u2192 {after}, {leg} (expected: a loose piece appears)",
|
|
335
|
+
"sc.kick_detaches_leg_via_tool": "The kick tool (hotbar) takes a chair leg off",
|
|
336
|
+
"sc.kick_detaches_leg_via_tool.check": "Chair, kick tool: {kicks} kick(s), loose pieces {before} \u2192 {after}, {leg} (expected: a loose piece appears)",
|
|
331
337
|
"sc.screwdriver_shelf_board": "Screwdriver takes a screwed shelf board off in about 2.5 s",
|
|
332
338
|
"sc.screwdriver_shelf_board.check": "Screwdriver on the shelf board: took {secs} s, expected about {want} s, the shelf is now in {pieces}",
|
|
333
339
|
"sc.wrong_tool_refused": "The wrong tool is refused",
|
|
@@ -495,6 +501,9 @@ var ptBR = {
|
|
|
495
501
|
"mod.destructible_ok": "{name}: contrato de destrui\xE7\xE3o ok ({parts} pe\xE7as, golpe e estado bem formados)",
|
|
496
502
|
"mod.destructible_fail": "{name}: contrato de destrui\xE7\xE3o quebrado: {problems}",
|
|
497
503
|
"mod.animation_ok": '{name}: anima\xE7\xE3o "{clip}" ok ({seconds} s)',
|
|
504
|
+
"mod.spec_ok": "{name}: spec ok ({parts} pe\xE7as)",
|
|
505
|
+
"mod.spec_fail": "{name}: spec quebrada: {problems}",
|
|
506
|
+
"mod.spec_warn": "{name}: aviso na spec: {warning}",
|
|
498
507
|
"mod.animation_fail": '{name}: anima\xE7\xE3o "{clip}" quebrada: {problems}',
|
|
499
508
|
"ui.animations": "Anima\xE7\xF5es",
|
|
500
509
|
"ui.animations_aim": "Mire num item ou segure-o para ver as anima\xE7\xF5es.",
|
|
@@ -555,6 +564,7 @@ var ptBR = {
|
|
|
555
564
|
"ui.filter.event": "eventos",
|
|
556
565
|
"ui.filter.check": "testes",
|
|
557
566
|
"ui.autoscroll": "rolar sozinho",
|
|
567
|
+
"log.export_cancelled": "exporta\xE7\xE3o cancelada",
|
|
558
568
|
"ui.copy_json": "Copiar JSON",
|
|
559
569
|
"ui.download_ndjson": "Baixar NDJSON",
|
|
560
570
|
"ui.download_txt": "Baixar TXT",
|
|
@@ -611,6 +621,8 @@ var ptBR = {
|
|
|
611
621
|
"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)",
|
|
612
622
|
"sc.kick_chair_leg": "Um chute solta a perna da cadeira",
|
|
613
623
|
"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)",
|
|
624
|
+
"sc.kick_detaches_leg_via_tool": "A ferramenta chute (barra de atalhos) solta a perna da cadeira",
|
|
625
|
+
"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)",
|
|
614
626
|
"sc.screwdriver_shelf_board": "Chave de fenda solta a prateleira parafusada em cerca de 2,5 s",
|
|
615
627
|
"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}",
|
|
616
628
|
"sc.wrong_tool_refused": "A ferramenta errada \xE9 recusada",
|
|
@@ -1112,7 +1124,16 @@ var formatTime = (t) => {
|
|
|
1112
1124
|
const s = t - m * 60;
|
|
1113
1125
|
return `${pad(m)}:${s.toFixed(2).padStart(5, "0")}`;
|
|
1114
1126
|
};
|
|
1115
|
-
function createLog() {
|
|
1127
|
+
function createLog(options = {}) {
|
|
1128
|
+
const host = globalThis.claude;
|
|
1129
|
+
let downloadsP = null;
|
|
1130
|
+
if (host && typeof host.use === "function") {
|
|
1131
|
+
try {
|
|
1132
|
+
downloadsP = Promise.resolve(host.use("downloads")).then((d) => d ?? null, () => null);
|
|
1133
|
+
} catch {
|
|
1134
|
+
downloadsP = null;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1116
1137
|
let list = [];
|
|
1117
1138
|
let seq = 0;
|
|
1118
1139
|
let clock = () => 0;
|
|
@@ -1178,9 +1199,20 @@ function createLog() {
|
|
|
1178
1199
|
toJSON: () => JSON.stringify(read(), null, 2),
|
|
1179
1200
|
toNDJSON: () => read().map((e) => JSON.stringify(e)).join("\n") + (list.length ? "\n" : ""),
|
|
1180
1201
|
toText: () => read().map((e) => `[${formatTime(e.t)}] ${e.channel.padEnd(6)} ${e.text}`).join("\n"),
|
|
1181
|
-
download(format = "ndjson") {
|
|
1182
|
-
if (typeof document === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined" || !URL.createObjectURL) return;
|
|
1202
|
+
async download(format = "ndjson") {
|
|
1183
1203
|
const body = format === "json" ? log.toJSON() : format === "txt" ? log.toText() : log.toNDJSON();
|
|
1204
|
+
const downloads = downloadsP ? await downloadsP : null;
|
|
1205
|
+
if (downloads) {
|
|
1206
|
+
const filename = format === "ndjson" ? "test-room-log.ndjson.txt" : `test-room-log.${format}`;
|
|
1207
|
+
try {
|
|
1208
|
+
await downloads.save({ filename, data: body });
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
const code = err?.code;
|
|
1211
|
+
log.add({ channel: "action", type: "export_cancelled", text: options.exportCancelled?.() ?? "export cancelled", data: { format, ...typeof code === "string" ? { code } : {} } });
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (typeof document === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined" || !URL.createObjectURL) return;
|
|
1184
1216
|
const mime = format === "json" ? "application/json" : "text/plain";
|
|
1185
1217
|
const url = URL.createObjectURL(new Blob([body], { type: mime }));
|
|
1186
1218
|
const a = document.createElement("a");
|
|
@@ -1925,6 +1957,10 @@ function createPlayer(THREE, o) {
|
|
|
1925
1957
|
yaw += yawDeg * Math.PI / 180;
|
|
1926
1958
|
apply();
|
|
1927
1959
|
},
|
|
1960
|
+
moveBy(dx, dz) {
|
|
1961
|
+
move(dx, dz);
|
|
1962
|
+
apply();
|
|
1963
|
+
},
|
|
1928
1964
|
walk(forward, right) {
|
|
1929
1965
|
move(-Math.sin(yaw) * forward + Math.cos(yaw) * right, -Math.cos(yaw) * forward - Math.sin(yaw) * right);
|
|
1930
1966
|
apply();
|
|
@@ -1946,7 +1982,7 @@ function createPlayer(THREE, o) {
|
|
|
1946
1982
|
}
|
|
1947
1983
|
|
|
1948
1984
|
// src/scenarios.ts
|
|
1949
|
-
import { JOINT_DEFAULTS } from "@adatechnology/game-destruction";
|
|
1985
|
+
import { JOINT_DEFAULTS, rotateVec, toWorld } from "@adatechnology/game-destruction";
|
|
1950
1986
|
function makeScenarios(t, num, _locale = "en") {
|
|
1951
1987
|
const yn = (b) => t(b ? "yes" : "no");
|
|
1952
1988
|
const cn = (what, n) => t(`count.${what}.${n === 1 ? "one" : "other"}`, { n });
|
|
@@ -2057,6 +2093,23 @@ function makeScenarios(t, num, _locale = "en") {
|
|
|
2057
2093
|
{ kicks, before, after }
|
|
2058
2094
|
);
|
|
2059
2095
|
}),
|
|
2096
|
+
sc("kick_detaches_leg_via_tool", ({ room, step, expect }) => {
|
|
2097
|
+
const id = at(room, "chair", 0, -4);
|
|
2098
|
+
const before = room.renderer.pieces().length;
|
|
2099
|
+
room.setTool("kick");
|
|
2100
|
+
let kicks = 0;
|
|
2101
|
+
for (; kicks < 8 && room.renderer.pieces().length <= before && part(room, id, "leg_1"); kicks++) {
|
|
2102
|
+
const st = room.world.state(id);
|
|
2103
|
+
const leg = toWorld(st.position, st.rotation, [-0.18, 0.2, -0.18]);
|
|
2104
|
+
const side = rotateVec(st.rotation, [-0.8, 0, 0]);
|
|
2105
|
+
look(room, [leg[0] + side[0], leg[1] + side[1], leg[2] + side[2]], leg);
|
|
2106
|
+
room.use();
|
|
2107
|
+
step(1.05);
|
|
2108
|
+
}
|
|
2109
|
+
step(0.5);
|
|
2110
|
+
const after = room.renderer.pieces().length;
|
|
2111
|
+
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 });
|
|
2112
|
+
}),
|
|
2060
2113
|
sc("screwdriver_shelf_board", ({ room, step, expect }) => {
|
|
2061
2114
|
const id = at(room, "shelf", 0, -4);
|
|
2062
2115
|
room.setTool("screwdriver");
|
|
@@ -2387,16 +2440,25 @@ function createUi(o) {
|
|
|
2387
2440
|
};
|
|
2388
2441
|
logBtn(t("ui.copy_json"), () => {
|
|
2389
2442
|
const text = o.log.toJSON();
|
|
2390
|
-
|
|
2391
|
-
void navigator.clipboard.writeText(text);
|
|
2392
|
-
} catch {
|
|
2443
|
+
const select = () => {
|
|
2393
2444
|
const ta = mk("textarea");
|
|
2394
2445
|
ta.value = text;
|
|
2446
|
+
ta.setAttribute("readonly", "");
|
|
2447
|
+
ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
|
|
2395
2448
|
document.body.appendChild(ta);
|
|
2449
|
+
ta.focus();
|
|
2396
2450
|
ta.select();
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2451
|
+
let copied = false;
|
|
2452
|
+
try {
|
|
2453
|
+
copied = !!document.execCommand?.("copy");
|
|
2454
|
+
} catch {
|
|
2455
|
+
copied = false;
|
|
2456
|
+
}
|
|
2457
|
+
if (copied) ta.remove();
|
|
2458
|
+
else ta.onblur = () => ta.remove();
|
|
2459
|
+
};
|
|
2460
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) navigator.clipboard.writeText(text).catch(select);
|
|
2461
|
+
else select();
|
|
2400
2462
|
});
|
|
2401
2463
|
logBtn(t("ui.download_ndjson"), () => o.log.download("ndjson"));
|
|
2402
2464
|
logBtn(t("ui.download_txt"), () => o.log.download("txt"));
|
|
@@ -2808,7 +2870,7 @@ function createTestRoom(options) {
|
|
|
2808
2870
|
const entries = /* @__PURE__ */ new Map();
|
|
2809
2871
|
const splitOf = /* @__PURE__ */ new Map();
|
|
2810
2872
|
let simTime = 0;
|
|
2811
|
-
const log = createLog();
|
|
2873
|
+
const log = createLog({ exportCancelled: () => t("log.export_cancelled") });
|
|
2812
2874
|
log.setClock(() => simTime);
|
|
2813
2875
|
const renderer = createDestructionRenderer(THREE, {
|
|
2814
2876
|
scene,
|
|
@@ -3182,6 +3244,7 @@ function createTestRoom(options) {
|
|
|
3182
3244
|
addArena();
|
|
3183
3245
|
timeScale = 1;
|
|
3184
3246
|
fastForward = false;
|
|
3247
|
+
walkPlan = null;
|
|
3185
3248
|
};
|
|
3186
3249
|
const aimO = [0, 0, 0];
|
|
3187
3250
|
const aimD = [0, 0, -1];
|
|
@@ -3433,11 +3496,11 @@ function createTestRoom(options) {
|
|
|
3433
3496
|
1
|
|
3434
3497
|
);
|
|
3435
3498
|
};
|
|
3436
|
-
const meleeUse = (u) => {
|
|
3437
|
-
const w = toolName();
|
|
3499
|
+
const meleeUse = (u, id = toolId) => {
|
|
3500
|
+
const w = toolName(id);
|
|
3438
3501
|
const r = rayWorld(u.origin, u.direction, u.reach);
|
|
3439
3502
|
if (!r) {
|
|
3440
|
-
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon:
|
|
3503
|
+
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon: id } });
|
|
3441
3504
|
return;
|
|
3442
3505
|
}
|
|
3443
3506
|
world.hitPart(r.object, r.part, { type: u.damage, point: r.point, direction: u.direction, energy: u.energy, radius: u.radius, source: "player" });
|
|
@@ -3446,7 +3509,7 @@ function createTestRoom(options) {
|
|
|
3446
3509
|
objectId: r.object,
|
|
3447
3510
|
itemId: entryOf(r.object)?.so.itemId,
|
|
3448
3511
|
part: r.part,
|
|
3449
|
-
data: { weapon:
|
|
3512
|
+
data: { weapon: id, damage: u.damage, energyJ: u.energy, distanceM: Math.round(r.distance * 100) / 100 }
|
|
3450
3513
|
});
|
|
3451
3514
|
};
|
|
3452
3515
|
const toolWork = (u, dt) => {
|
|
@@ -3457,7 +3520,7 @@ function createTestRoom(options) {
|
|
|
3457
3520
|
log.coalesce(`use:${toolId}`, { channel: "action", type: "tool", text: t("act.tool.none", { weapon: w }), data: { weapon: toolId } }, 1);
|
|
3458
3521
|
return;
|
|
3459
3522
|
}
|
|
3460
|
-
const j = world.nearestJoint(r.object, r.point, 0.45);
|
|
3523
|
+
const j = world.nearestJoint(r.object, r.point, 0.45, u.tool);
|
|
3461
3524
|
if (!j) {
|
|
3462
3525
|
progress = null;
|
|
3463
3526
|
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);
|
|
@@ -3636,6 +3699,13 @@ function createTestRoom(options) {
|
|
|
3636
3699
|
}
|
|
3637
3700
|
}
|
|
3638
3701
|
};
|
|
3702
|
+
const kick = () => {
|
|
3703
|
+
if (!arsenal.weapons.has("kick")) return;
|
|
3704
|
+
aim();
|
|
3705
|
+
const res = toolState("kick").trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 0, false, "player");
|
|
3706
|
+
if (res.status !== "ok") return;
|
|
3707
|
+
for (const u of res.uses) if (u.kind === "melee") meleeUse(u, "kick");
|
|
3708
|
+
};
|
|
3639
3709
|
const setTool = (id) => {
|
|
3640
3710
|
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
3641
3711
|
tool.release();
|
|
@@ -3891,7 +3961,7 @@ function createTestRoom(options) {
|
|
|
3891
3961
|
break;
|
|
3892
3962
|
case "KeyV":
|
|
3893
3963
|
if (heldEntry?.uses[2]) useItem(heldEntry.uses[2]);
|
|
3894
|
-
else
|
|
3964
|
+
else kick();
|
|
3895
3965
|
break;
|
|
3896
3966
|
case "KeyT":
|
|
3897
3967
|
toggleTap();
|
|
@@ -3990,8 +4060,19 @@ function createTestRoom(options) {
|
|
|
3990
4060
|
if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(host.objectId, e.fluidPart, c.liters);
|
|
3991
4061
|
}
|
|
3992
4062
|
};
|
|
4063
|
+
let paused = false;
|
|
4064
|
+
let walkPlan = null;
|
|
4065
|
+
const stepWalk = (dt) => {
|
|
4066
|
+
if (!walkPlan) return;
|
|
4067
|
+
const f = walkPlan.remaining <= dt ? 1 : dt / walkPlan.remaining;
|
|
4068
|
+
player.moveBy((walkPlan.x - camera.position.x) * f, (walkPlan.z - camera.position.z) * f);
|
|
4069
|
+
walkPlan.remaining -= dt;
|
|
4070
|
+
if (walkPlan.remaining <= 1e-9) walkPlan = null;
|
|
4071
|
+
};
|
|
3993
4072
|
const stepSim = (dt) => {
|
|
4073
|
+
if (paused) return;
|
|
3994
4074
|
simTime += dt;
|
|
4075
|
+
stepWalk(dt);
|
|
3995
4076
|
const reps = fastForward ? 10 : 1;
|
|
3996
4077
|
updateAnimations(dt);
|
|
3997
4078
|
updateItemUse(dt);
|
|
@@ -4020,13 +4101,13 @@ function createTestRoom(options) {
|
|
|
4020
4101
|
if (tiltKey || heldTilt > 0) {
|
|
4021
4102
|
heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
|
|
4022
4103
|
}
|
|
4023
|
-
if (useHeld) {
|
|
4104
|
+
if (useHeld && !paused) {
|
|
4024
4105
|
if (tool.def.category === "hand") {
|
|
4025
4106
|
if (useFirst) grab();
|
|
4026
4107
|
} else useTool(dt, !useFirst);
|
|
4027
4108
|
useFirst = false;
|
|
4028
4109
|
}
|
|
4029
|
-
followHeld(dt);
|
|
4110
|
+
if (!paused) followHeld(dt);
|
|
4030
4111
|
stepSim(dt * timeScale);
|
|
4031
4112
|
if (progress && simTime - progress.at > 0.25) progress = null;
|
|
4032
4113
|
hudAcc += dt;
|
|
@@ -4087,6 +4168,16 @@ function createTestRoom(options) {
|
|
|
4087
4168
|
const pose = poseAt(pl.clip, id === "" ? null : id, pl.t);
|
|
4088
4169
|
obj.position.set(rest.p[0] + pose.position[0], rest.p[1] + pose.position[1], rest.p[2] + pose.position[2]);
|
|
4089
4170
|
obj.rotation.set(rest.r[0] + pose.rotation[0], rest.r[1] + pose.rotation[1], rest.r[2] + pose.rotation[2]);
|
|
4171
|
+
const ps = id === "" ? void 0 : specs.get(pl.entry.so.objectId)?.parts.find((p) => p.id === id);
|
|
4172
|
+
if (ps) {
|
|
4173
|
+
const r0 = ps.rotation ?? ZERO;
|
|
4174
|
+
world.setPartPose(
|
|
4175
|
+
pl.entry.so.objectId,
|
|
4176
|
+
id,
|
|
4177
|
+
[ps.position[0] + pose.position[0], ps.position[1] + pose.position[1], ps.position[2] + pose.position[2]],
|
|
4178
|
+
[r0[0] + pose.rotation[0], r0[1] + pose.rotation[1], r0[2] + pose.rotation[2]]
|
|
4179
|
+
);
|
|
4180
|
+
}
|
|
4090
4181
|
}
|
|
4091
4182
|
};
|
|
4092
4183
|
const updateAnimations = (dt) => {
|
|
@@ -4174,8 +4265,14 @@ function createTestRoom(options) {
|
|
|
4174
4265
|
partIds = probe.describe().parts.map((p) => p.id);
|
|
4175
4266
|
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);
|
|
4176
4267
|
for (const p of r.problems) fail(`${item.id}: ${p}`);
|
|
4177
|
-
}
|
|
4178
|
-
|
|
4268
|
+
}
|
|
4269
|
+
if (!item.impl || item.spec) {
|
|
4270
|
+
const spec = resolveSpec(item, root).spec;
|
|
4271
|
+
if (!item.impl) partIds = spec.parts.map((p) => p.id);
|
|
4272
|
+
const r = checkSpec(spec, { materials: world.materials.keys() });
|
|
4273
|
+
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);
|
|
4274
|
+
for (const p of r.problems) fail(`${item.id}: ${p}`);
|
|
4275
|
+
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 } });
|
|
4179
4276
|
}
|
|
4180
4277
|
} catch (err) {
|
|
4181
4278
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -4214,6 +4311,8 @@ function createTestRoom(options) {
|
|
|
4214
4311
|
const all = list ?? allScenarios();
|
|
4215
4312
|
const report = { passed: 0, failed: 0, results: [] };
|
|
4216
4313
|
scenarioRunning = true;
|
|
4314
|
+
const wasPaused = paused;
|
|
4315
|
+
paused = false;
|
|
4217
4316
|
try {
|
|
4218
4317
|
for (const sc of all) {
|
|
4219
4318
|
hardReset();
|
|
@@ -4245,6 +4344,7 @@ function createTestRoom(options) {
|
|
|
4245
4344
|
}
|
|
4246
4345
|
} finally {
|
|
4247
4346
|
scenarioRunning = false;
|
|
4347
|
+
paused = wasPaused;
|
|
4248
4348
|
}
|
|
4249
4349
|
return report;
|
|
4250
4350
|
};
|
|
@@ -4307,6 +4407,19 @@ function createTestRoom(options) {
|
|
|
4307
4407
|
heldTilt = rad;
|
|
4308
4408
|
},
|
|
4309
4409
|
toggleTap,
|
|
4410
|
+
pause() {
|
|
4411
|
+
paused = true;
|
|
4412
|
+
},
|
|
4413
|
+
resume() {
|
|
4414
|
+
paused = false;
|
|
4415
|
+
},
|
|
4416
|
+
get paused() {
|
|
4417
|
+
return paused;
|
|
4418
|
+
},
|
|
4419
|
+
walk(to, seconds) {
|
|
4420
|
+
walkPlan = seconds > 0 ? { x: to[0], z: to[2], remaining: seconds } : null;
|
|
4421
|
+
if (!walkPlan) player.moveBy(to[0] - camera.position.x, to[2] - camera.position.z);
|
|
4422
|
+
},
|
|
4310
4423
|
setSlowMotion(on) {
|
|
4311
4424
|
timeScale = on ? 0.25 : 1;
|
|
4312
4425
|
},
|