@adatechnology/game-test-room 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -179,6 +179,8 @@ interface TestRoomOptions {
179
179
  arenaSize?: number;
180
180
  /** Spawn these catalogue ids on start, in a row in front of the player. */
181
181
  initial?: string[];
182
+ /** Open on one catalogue group (`furniture`, `structure`, `weapons`…): see `TestRoom.openArea`. */
183
+ area?: string;
182
184
  /** Called for every destruction event (to log, play sounds, sync over LAN). Fluid events reach the log too. */
183
185
  onEvent?: (event: DestructionEvent) => void;
184
186
  onSound?: (key: string, point: Vec3, volume: number) => void;
@@ -245,6 +247,11 @@ interface TestRoom {
245
247
  setFastForward(on: boolean): void;
246
248
  /** Equip a weapon by id, adding it to the hotbar when it is not there. Same as selecting a catalogue item with `TestRoomItem.weapon`. */
247
249
  equip(weaponId: string): void;
250
+ /**
251
+ * Set the room up to test one catalogue group or loaded module (by its group or id): clear it and open the catalogue on that tab. A module
252
+ * that brings an `area` is laid out by it; any other group is laid in rows (small items in front, fixed big ones behind). Unknown: nothing.
253
+ */
254
+ openArea(group: string): SpawnedObject[];
248
255
  /** The scenarios the panel offers: the built-in `SCENARIOS` in the room locale plus `TestRoomOptions.scenarios`. */
249
256
  readonly scenarios: TestRoomScenario[];
250
257
  /** The liquid renderer of `@adatechnology/game-fluids-three` (containers, streams, stains). */
@@ -11242,6 +11242,9 @@ void main() {
11242
11242
  "ui.quality.medium": "medium",
11243
11243
  "ui.quality.high": "high",
11244
11244
  "ui.equip": "Equip as a tool",
11245
+ "ui.search_items": "Search items (all groups)",
11246
+ "ui.search_tests": "Search tests",
11247
+ "ui.no_results": "Nothing found",
11245
11248
  "ui.objects": "{n} objects",
11246
11249
  "ui.tests": "Tests",
11247
11250
  "ui.run_all": "Run all",
@@ -11531,6 +11534,9 @@ void main() {
11531
11534
  "ui.quality.medium": "m\xE9dia",
11532
11535
  "ui.quality.high": "alta",
11533
11536
  "ui.equip": "Equipar como ferramenta",
11537
+ "ui.search_items": "Buscar item (todas as abas)",
11538
+ "ui.search_tests": "Buscar teste",
11539
+ "ui.no_results": "Nada encontrado",
11534
11540
  "ui.objects": "{n} objetos",
11535
11541
  "ui.tests": "Testes",
11536
11542
  "ui.run_all": "Rodar todos",
@@ -13268,7 +13274,11 @@ void main() {
13268
13274
  var CSS = `
13269
13275
  .tr-root{position:absolute;inset:0;pointer-events:none;font:12px/1.35 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:#e8eaee;overflow:hidden}
13270
13276
  .tr-root *{box-sizing:border-box}
13271
- .tr-root button,.tr-root select,.tr-root label{pointer-events:auto}
13277
+ .tr-root button,.tr-root select,.tr-root label,.tr-root input{pointer-events:auto}
13278
+ .tr-search{width:100%;margin:0 0 6px;padding:5px 8px;border-radius:5px;border:1px solid #3a404c;background:#0f1116;color:#e8eaee;font:inherit}
13279
+ .tr-search::placeholder{color:#7d8594}
13280
+ .tr-item small{color:#7d8594;margin-left:4px}
13281
+ .tr-empty{color:#9aa3b2;padding:4px 2px;grid-column:1/-1}
13272
13282
  .tr-cross{position:absolute;left:50%;top:50%;width:18px;height:18px;margin:-9px 0 0 -9px}
13273
13283
  .tr-cross:before,.tr-cross:after{content:"";position:absolute;background:#fff;opacity:.85;box-shadow:0 0 2px #000}
13274
13284
  .tr-cross:before{left:8px;top:0;width:2px;height:18px}.tr-cross:after{top:8px;left:0;height:2px;width:18px}
@@ -13319,6 +13329,7 @@ void main() {
13319
13329
  .tr-summary{margin-top:6px;color:#9aa3b2}
13320
13330
  @media (max-width:980px){.tr-left,.tr-right{display:none}.tr-root.open .tr-left,.tr-root.open .tr-right{display:flex}.tr-toggle{display:block;pointer-events:auto}.tr-root.open .tr-toggle{right:min(310px,34vw)}.tr-left{width:min(400px,46vw)}.tr-help{display:none}}
13321
13331
  `;
13332
+ var fold = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
13322
13333
  var mk = (tag, cls, text) => {
13323
13334
  const e = document.createElement(tag);
13324
13335
  if (cls) e.className = cls;
@@ -13458,15 +13469,27 @@ void main() {
13458
13469
  const tests = mk("div", "tr-panel tr-tests");
13459
13470
  right.append(cat, anim, tests);
13460
13471
  cat.appendChild(mk("h3", void 0, t("ui.catalog")));
13472
+ const itemSearch = mk("input", "tr-search");
13473
+ itemSearch.type = "search";
13474
+ itemSearch.placeholder = t("ui.search_items");
13461
13475
  const tabs = mk("div", "tr-tabs");
13462
13476
  const items = mk("div", "tr-items");
13463
- cat.append(tabs, items);
13477
+ cat.append(itemSearch, tabs, items);
13464
13478
  let activeGroup = "";
13479
+ const visibleItems = () => {
13480
+ const term = fold(itemSearch.value.trim());
13481
+ if (!term) return (o.groups().get(activeGroup) ?? []).map((item) => ({ item, group: activeGroup }));
13482
+ return [...o.groups()].flatMap(([group, list2]) => list2.filter((item) => fold(`${item.name} ${item.id} ${group}`).includes(term)).map((item) => ({ item, group })));
13483
+ };
13465
13484
  const renderItems = () => {
13466
13485
  items.textContent = "";
13467
- for (const it of o.groups().get(activeGroup) ?? []) {
13486
+ const searching = itemSearch.value.trim() !== "";
13487
+ const found = visibleItems();
13488
+ if (!found.length) items.appendChild(mk("div", "tr-empty", t("ui.no_results")));
13489
+ for (const { item: it, group } of found) {
13468
13490
  const wrap = mk("div", "tr-item");
13469
13491
  const b = mk("button", "tr-btn", it.name);
13492
+ if (searching) b.appendChild(mk("small", void 0, group));
13470
13493
  b.type = "button";
13471
13494
  b.title = it.note ?? it.name;
13472
13495
  b.onclick = () => o.onSpawn(it.id);
@@ -13497,6 +13520,8 @@ void main() {
13497
13520
  renderItems();
13498
13521
  };
13499
13522
  renderTabs();
13523
+ itemSearch.oninput = renderItems;
13524
+ for (const box2 of [itemSearch]) box2.addEventListener("keydown", (e) => e.stopPropagation());
13500
13525
  const controls = mk("div", "tr-row");
13501
13526
  const ctl = (label, fn) => {
13502
13527
  const b = mk("button", "tr-btn", label);
@@ -13538,8 +13563,16 @@ void main() {
13538
13563
  const runAll = mk("button", "tr-btn", t("ui.run_all"));
13539
13564
  runAll.type = "button";
13540
13565
  const summary = mk("div", "tr-summary");
13566
+ const testSearch = mk("input", "tr-search");
13567
+ testSearch.type = "search";
13568
+ testSearch.placeholder = t("ui.search_tests");
13569
+ testSearch.addEventListener("keydown", (e) => e.stopPropagation());
13541
13570
  const list = mk("div", "list");
13542
- tests.append(runAll, summary, list);
13571
+ tests.append(runAll, summary, testSearch, list);
13572
+ testSearch.oninput = () => {
13573
+ const term = fold(testSearch.value.trim());
13574
+ for (const b of scBtns.values()) b.style.display = !term || fold(b.textContent ?? "").includes(term) ? "" : "none";
13575
+ };
13543
13576
  const scBtns = /* @__PURE__ */ new Map();
13544
13577
  let busy = false;
13545
13578
  const mark = (rep) => {
@@ -13738,6 +13771,11 @@ void main() {
13738
13771
  renderTabs();
13739
13772
  renderScenarios();
13740
13773
  },
13774
+ showGroup(group) {
13775
+ activeGroup = group;
13776
+ itemSearch.value = "";
13777
+ renderTabs();
13778
+ },
13741
13779
  togglePanels,
13742
13780
  dispose() {
13743
13781
  offLog();
@@ -13808,7 +13846,13 @@ void main() {
13808
13846
  const p = world.part(arena.table.id, arena.table.topPart);
13809
13847
  return !!p && p.status !== "broken" && p.status !== "melted" && p.status !== "ashes";
13810
13848
  };
13811
- const groundAt = (x, z) => x > arena.table.minX && x < arena.table.maxX && z > arena.table.minZ && z < arena.table.maxZ && tableOk() ? arena.table.top : 0;
13849
+ const restingTops = [];
13850
+ const restingTopAt = (x, z) => {
13851
+ let top = 0;
13852
+ for (const c of restingTops) if (x > c.minX && x < c.maxX && z > c.minZ && z < c.maxZ && world.has(c.objectId)) top = Math.max(top, c.top);
13853
+ return top;
13854
+ };
13855
+ const groundAt = (x, z) => Math.max(x > arena.table.minX && x < arena.table.maxX && z > arena.table.minZ && z < arena.table.maxZ && tableOk() ? arena.table.top : 0, restingTopAt(x, z));
13812
13856
  const ownFluids = !options.fluids;
13813
13857
  const fluids = options.fluids ?? createFluids({ seed: 1, locale, liquids: options.liquids, groundAt });
13814
13858
  const pristineFluids = ownFluids ? fluids.serialize() : null;
@@ -13941,16 +13985,16 @@ void main() {
13941
13985
  const puncture = (ev) => {
13942
13986
  const e = entryOf(ev.object);
13943
13987
  if (!e?.containerId || e.fluidPart !== ev.part) return;
13944
- const host = hostOf(e);
13945
- host.root.updateMatrixWorld(true);
13946
- tmpM.copy(host.root.matrixWorld).invert();
13988
+ const host2 = hostOf(e);
13989
+ host2.root.updateMatrixWorld(true);
13990
+ tmpM.copy(host2.root.matrixWorld).invert();
13947
13991
  const local = tmpV2.set(ev.point[0], ev.point[1], ev.point[2]).applyMatrix4(tmpM);
13948
13992
  local.x -= e.fluidOffset[0];
13949
13993
  local.y -= e.fluidOffset[1];
13950
13994
  local.z -= e.fluidOffset[2];
13951
- host.root.getWorldQuaternion(tmpQ).invert();
13995
+ host2.root.getWorldQuaternion(tmpQ).invert();
13952
13996
  const n = tmpV22.set(ev.direction[0], ev.direction[1], ev.direction[2]).applyQuaternion(tmpQ);
13953
- const part = world.part(host.objectId, ev.part);
13997
+ const part = world.part(host2.objectId, ev.part);
13954
13998
  const hole = part?.holes[part.holes.length - 1];
13955
13999
  fluids.puncture(e.containerId, [local.x, local.y, local.z], [n.x, n.y, n.z], hole?.diameter ?? 9e-3);
13956
14000
  };
@@ -14156,6 +14200,7 @@ void main() {
14156
14200
  };
14157
14201
  const clear = () => {
14158
14202
  clearEverything();
14203
+ restingTops.length = 0;
14159
14204
  pending = [];
14160
14205
  action("clear", t("act.clear"));
14161
14206
  };
@@ -14375,6 +14420,7 @@ void main() {
14375
14420
  }
14376
14421
  });
14377
14422
  player.kick(recoil[0], recoil[1]);
14423
+ emitHost({ type: "shot", weapon: toolId, shots: shots.length, recoil });
14378
14424
  const f = first;
14379
14425
  const d = f ? num(f.distance, 1) : "";
14380
14426
  let text;
@@ -14614,7 +14660,8 @@ void main() {
14614
14660
  if (cat === "hand") return;
14615
14661
  const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
14616
14662
  if (res.status === "empty") {
14617
- if (!tool.reloading && tool.reserve > 0) tool.reload();
14663
+ emitHost({ type: "empty", weapon: toolId });
14664
+ if (!tool.reloading && tool.reserve > 0) startReload();
14618
14665
  say(t(tool.reloading ? "hud.reloading" : "hud.empty"));
14619
14666
  return;
14620
14667
  }
@@ -14666,6 +14713,7 @@ void main() {
14666
14713
  progress = null;
14667
14714
  action("tool", t("act.tool", { weapon: toolName(id) }), { data: { weapon: id } });
14668
14715
  fire("tool", id);
14716
+ emitHost({ type: "tool", weapon: id });
14669
14717
  };
14670
14718
  const cycleTool = (dir) => {
14671
14719
  if (!loadout.length) return;
@@ -14893,7 +14941,7 @@ void main() {
14893
14941
  case "KeyR":
14894
14942
  if (heldPiece) heldYaw += Math.PI / 8;
14895
14943
  else {
14896
- tool.reload();
14944
+ startReload();
14897
14945
  say(t("hud.reloading"));
14898
14946
  }
14899
14947
  break;
@@ -14991,10 +15039,10 @@ void main() {
14991
15039
  const syncContainers = (sdt) => {
14992
15040
  for (const e of entries.values()) {
14993
15041
  if (!e.containerId) continue;
14994
- const host = hostOf(e);
14995
- host.root.updateMatrixWorld(true);
14996
- const q = host.root.getWorldQuaternion(tmpQ);
14997
- const p = tmpV2.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(host.root.matrixWorld);
15042
+ const host2 = hostOf(e);
15043
+ host2.root.updateMatrixWorld(true);
15044
+ const q = host2.root.getWorldQuaternion(tmpQ);
15045
+ const p = tmpV2.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(host2.root.matrixWorld);
14998
15046
  const vel = sdt > 0 ? [(p.x - e.last[0]) / sdt, (p.y - e.last[1]) / sdt, (p.z - e.last[2]) / sdt] : void 0;
14999
15047
  e.last = [p.x, p.y, p.z];
15000
15048
  fluids.setTransform(e.containerId, [p.x, p.y, p.z], [q.x, q.y, q.z, q.w], vel);
@@ -15005,9 +15053,9 @@ void main() {
15005
15053
  if (!e.containerId || !e.fluidPart) continue;
15006
15054
  const c = fluids.container(e.containerId);
15007
15055
  if (!c) continue;
15008
- const host = hostOf(e);
15009
- const part = world.part(host.objectId, e.fluidPart);
15010
- if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(host.objectId, e.fluidPart, c.liters);
15056
+ const host2 = hostOf(e);
15057
+ const part = world.part(host2.objectId, e.fluidPart);
15058
+ if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(host2.objectId, e.fluidPart, c.liters);
15011
15059
  }
15012
15060
  };
15013
15061
  let paused = false;
@@ -15038,6 +15086,9 @@ void main() {
15038
15086
  updateImpacts();
15039
15087
  fluidView.update(dt);
15040
15088
  for (const s of tools.values()) s.update(dt);
15089
+ if (reloadingSeen && !tool.reloading) emitHost({ type: "reload", weapon: toolId, phase: "end" });
15090
+ reloadingSeen = tool.reloading;
15091
+ for (const update2 of frameListeners) update2(dt);
15041
15092
  for (const tr of tracers) {
15042
15093
  if (tr.life > 0) {
15043
15094
  tr.life -= dt;
@@ -15179,6 +15230,36 @@ void main() {
15179
15230
  const moduleCheck = (ok, text, data, itemId) => {
15180
15231
  log.add({ channel: "check", type: ok ? "pass" : "fail", text: `${ok ? "\u2713" : "\u2717"} ${text}`, itemId, data: { ok, ...data } });
15181
15232
  };
15233
+ const modules = /* @__PURE__ */ new Map();
15234
+ const detachers = [];
15235
+ const hostListeners = /* @__PURE__ */ new Set();
15236
+ const frameListeners = /* @__PURE__ */ new Set();
15237
+ let reloadingSeen = false;
15238
+ function emitHost(event) {
15239
+ for (const listener of hostListeners) listener(event);
15240
+ }
15241
+ function startReload() {
15242
+ if (tool.reload()) emitHost({ type: "reload", weapon: toolId, phase: "start" });
15243
+ }
15244
+ const host = {
15245
+ THREE,
15246
+ scene,
15247
+ camera,
15248
+ locale,
15249
+ groundAt,
15250
+ tool: () => ({ weapon: toolId, category: tool.def.category, ammo: tool.ammo, capacity: tool.capacity, reserve: tool.reserve, reloading: tool.reloading }),
15251
+ on(listener) {
15252
+ hostListeners.add(listener);
15253
+ return () => hostListeners.delete(listener);
15254
+ },
15255
+ onFrame(update2) {
15256
+ frameListeners.add(update2);
15257
+ return () => frameListeners.delete(update2);
15258
+ },
15259
+ log(text, data) {
15260
+ action("module_note", text, { data });
15261
+ }
15262
+ };
15182
15263
  const loadModule = (mod) => {
15183
15264
  const report = { module: mod.id, items: [], scenarios: [], ok: true, problems: [] };
15184
15265
  const fail = (problem) => {
@@ -15243,6 +15324,8 @@ void main() {
15243
15324
  report.scenarios.push(sc.id);
15244
15325
  }
15245
15326
  action("module", t("act.module_loaded", { name: mod.name, items: report.items.length, scenarios: report.scenarios.length, result: t(report.ok ? "mod.all_ok" : "mod.some_failed") }), { data: { module: mod.id, items: report.items, scenarios: report.scenarios, ok: report.ok } });
15327
+ modules.set(mod.id, mod);
15328
+ if (mod.attach) detachers.push(mod.attach(host));
15246
15329
  ui?.refresh();
15247
15330
  return report;
15248
15331
  };
@@ -15298,6 +15381,37 @@ void main() {
15298
15381
  }
15299
15382
  return report;
15300
15383
  };
15384
+ const layArea = (layout) => {
15385
+ const holders = layout.placements.filter((p) => p.surface);
15386
+ const rest = layout.placements.filter((p) => !p.surface);
15387
+ const placed = [...holders, ...rest].map((p) => {
15388
+ const so = spawn(p.item, p.position, p.rotationY ?? 0);
15389
+ if (p.surface) {
15390
+ const [hx, hz] = p.surface.halfSize;
15391
+ restingTops.push({ objectId: so.objectId, minX: p.position[0] - hx, maxX: p.position[0] + hx, minZ: p.position[2] - hz, maxZ: p.position[2] + hz, top: p.surface.height });
15392
+ }
15393
+ return { so, asleep: p.asleep === true };
15394
+ });
15395
+ for (const { so, asleep } of placed) {
15396
+ const piece = asleep ? rendererPieces.get(so.objectId) : void 0;
15397
+ if (piece) piece.sleeping = true;
15398
+ }
15399
+ if (layout.view) room.look(layout.view.eye, layout.view.target);
15400
+ return placed.map(({ so }) => so);
15401
+ };
15402
+ const openArea = (area) => {
15403
+ clear();
15404
+ const mod = [...modules.values()].find((m) => m.id === area || m.group === area);
15405
+ const label = mod?.group ?? S.groups[area] ?? area;
15406
+ ui?.showGroup(label);
15407
+ const layout = mod?.area?.(locale);
15408
+ if (layout) return layArea(layout);
15409
+ const items = catalog.filter((item) => item.group === label);
15410
+ const front = items.filter((item) => item.movable !== false);
15411
+ const back = items.filter((item) => item.movable === false);
15412
+ const row = (list, gap, z) => list.map((item, index) => spawn(item.id, [(index - (list.length - 1) / 2) * gap, 0, z], 0));
15413
+ return [...row(front, 1.6, 1.5), ...row(back, 4.5, -4)];
15414
+ };
15301
15415
  const room = {
15302
15416
  world,
15303
15417
  renderer,
@@ -15377,10 +15491,12 @@ void main() {
15377
15491
  fastForward = on;
15378
15492
  },
15379
15493
  equip,
15494
+ openArea,
15380
15495
  loadModule,
15381
15496
  animations: animationsOf,
15382
15497
  playAnimation,
15383
15498
  dispose() {
15499
+ for (const detach of detachers.splice(0)) detach();
15384
15500
  cancelAnimationFrame?.(raf);
15385
15501
  window.removeEventListener("resize", onResize);
15386
15502
  player.dispose();
@@ -15470,6 +15586,7 @@ void main() {
15470
15586
  if (options.initial) {
15471
15587
  for (const [i, id] of options.initial.entries()) spawn(id, [(i - (options.initial.length - 1) / 2) * 1.3, 0, 0.5]);
15472
15588
  }
15589
+ if (options.area) openArea(options.area);
15473
15590
  return room;
15474
15591
  }
15475
15592
  function structuredCloneSpec(spec) {