@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/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
- try {
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
- document.execCommand?.("copy");
2398
- ta.remove();
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: toolId } });
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: toolId, damage: u.damage, energyJ: u.energy, distanceM: Math.round(r.distance * 100) / 100 }
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 toggleTap();
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
- } else {
4178
- partIds = resolveSpec(item, root).spec.parts.map((p) => p.id);
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
  },