@adatechnology/game-test-room 0.13.0 → 0.15.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.js CHANGED
@@ -9,6 +9,9 @@ import { createGroundField, createVfx } from "@adatechnology/game-vfx";
9
9
  import { createGroundPatch, createVfxRenderer } from "@adatechnology/game-vfx-three";
10
10
  import { checkAnimation, checkBench, checkDestructible, checkSpec, createEnvironmentBus, shotExtras, useToHit } from "@adatechnology/game-contracts";
11
11
 
12
+ // src/arena.ts
13
+ import { markKitOrigin } from "@adatechnology/game-contracts";
14
+
12
15
  // src/items.ts
13
16
  var box = (id, material, size, position, extra = {}) => ({ id, material, shape: { kind: "box", size }, position, ...extra });
14
17
  var joint = (a, b, kind, extra = {}) => ({ a, b, kind, ...extra });
@@ -110,7 +113,7 @@ function resolveScenery(scenery) {
110
113
  return [...merged.entries()].map(([id, o]) => ({ id, spec: o.spec, position: o.position, rotationY: o.rotationY ?? 0, colors: o.colors, build: o.build, label: o.label, top: o.top, spout: o.spout }));
111
114
  }
112
115
  function createArena(THREE, size, scene, scenery, tapOption) {
113
- const group = new THREE.Group();
116
+ const group = markKitOrigin(new THREE.Group(), { owner: "game-test-room", kind: "room" });
114
117
  group.name = "test-room-arena";
115
118
  scene.background = new THREE.Color(10466504);
116
119
  group.add(new THREE.HemisphereLight(13624319, 7037527, 0.75));
@@ -468,7 +471,7 @@ function buildFromSpec(THREE, spec, options = {}) {
468
471
  }
469
472
 
470
473
  // src/explosives.ts
471
- import { backblastEffect, blastEffect, createExplosiveBody, tripwireSegment } from "@adatechnology/game-weapons";
474
+ import { ExplosiveBody, backblastEffect, blastEffect, createExplosiveBody, tripwireSegment } from "@adatechnology/game-weapons";
472
475
 
473
476
  // src/segment-box.ts
474
477
  var segmentTouchesBox = (from, to, min, max) => {
@@ -711,6 +714,14 @@ function createExplosives(context) {
711
714
  ...projectile.fire ? { fire: projectile.fire } : {}
712
715
  });
713
716
  };
717
+ const blastAt = (weapon, explosive, point) => {
718
+ const def = arsenal.explosives.get(explosive);
719
+ if (!def) return false;
720
+ const event = new ExplosiveBody({ explosive: def, origin: point, triggers: ["remote"], rules: arsenal.rules.explosives }).trigger("remote");
721
+ if (event?.type !== "detonate") return false;
722
+ detonate({ weapon }, event.detonation);
723
+ return true;
724
+ };
714
725
  const detonateCharges = () => {
715
726
  let count = 0;
716
727
  for (const projectile of [...projectiles]) {
@@ -802,7 +813,172 @@ function createExplosives(context) {
802
813
  }
803
814
  }
804
815
  };
805
- return { throwUse, placeUse, backblastUse, detonateCharges, update, clear };
816
+ return { throwUse, placeUse, backblastUse, detonateCharges, blastAt, update, clear };
817
+ }
818
+
819
+ // src/test-table.ts
820
+ var TRIAL_KINDS = ["shot", "punch", "stab", "fire", "blast"];
821
+ var DEFAULT_TRIALS = {
822
+ shot: { weapon: "pistol_9mm", distance: 2.5, lift: 0.5 },
823
+ punch: { weapon: "fists", distance: 0.3, lift: 0.15 },
824
+ stab: { weapon: "knife", distance: 0.22, lift: 0.1 },
825
+ fire: { weapon: "flamethrower", distance: 1.6, lift: 0.4, hold: 0.8 },
826
+ blast: { weapon: "frag_grenade", distance: 0.3, lift: 1, watch: 6 }
827
+ };
828
+ var resolveTrials = (overrides = {}) => Object.fromEntries(TRIAL_KINDS.map((kind) => [kind, { ...DEFAULT_TRIALS[kind], ...overrides[kind] }]));
829
+ function fitRow(width, start, total, length, gap) {
830
+ let used = 0;
831
+ let count = 0;
832
+ for (let i = start; i < total; i++) {
833
+ const w = width(i);
834
+ const next = count === 0 ? w : used + gap + w;
835
+ if (count > 0 && next > length) break;
836
+ used = next;
837
+ count++;
838
+ }
839
+ return count;
840
+ }
841
+ function rowCentres(widths, centreX, gap) {
842
+ const length = widths.reduce((sum, w) => sum + w, 0) + gap * Math.max(0, widths.length - 1);
843
+ let x = centreX - length / 2;
844
+ return widths.map((w) => {
845
+ const centre = x + w / 2;
846
+ x += w + gap;
847
+ return centre;
848
+ });
849
+ }
850
+ function trialView(tool, kind, min, max) {
851
+ const target = [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2];
852
+ const away = kind === "blast" ? tool.watch ?? 6 : tool.distance;
853
+ return { eye: [target[0], target[1] + tool.lift, max[2] + away], target };
854
+ }
855
+ function createTestTable(context, options = {}) {
856
+ const gap = options.gap ?? 0.12;
857
+ const margin = options.margin ?? 0.1;
858
+ const trials = resolveTrials(options.trials);
859
+ const listeners = /* @__PURE__ */ new Set();
860
+ let current = null;
861
+ const itemsOf = (group) => context.groups().get(group) ?? [];
862
+ const emit = () => {
863
+ const page = state();
864
+ for (const listener of [...listeners]) listener(page);
865
+ return page;
866
+ };
867
+ const state = () => {
868
+ if (!current) return null;
869
+ const total = itemsOf(current.group).length;
870
+ const live = current.objects.filter((o) => context.box(o.objectId));
871
+ return {
872
+ group: current.group,
873
+ page: current.page,
874
+ first: current.first,
875
+ total,
876
+ objects: live,
877
+ selected: current.selected,
878
+ hasPrev: current.page > 0,
879
+ hasNext: current.first + current.objects.length < total
880
+ };
881
+ };
882
+ const bounds = /* @__PURE__ */ new Map();
883
+ const widthOf = (itemId) => context.measure(itemId).w;
884
+ const boundsUntil = (group, length, until) => {
885
+ const items = itemsOf(group);
886
+ const known = bounds.get(group) ?? [0];
887
+ bounds.set(group, known);
888
+ while (known[known.length - 1] < items.length && !until(known)) {
889
+ const first = known[known.length - 1];
890
+ known.push(first + fitRow((i) => widthOf(items[i].id), first, items.length, length, gap));
891
+ }
892
+ return known;
893
+ };
894
+ const lengthOf = (top) => top.maxX - top.minX - 2 * margin;
895
+ const lay = (group, page, top) => {
896
+ const items = itemsOf(group);
897
+ const known = boundsUntil(group, lengthOf(top), (b) => b.length > page + 1);
898
+ const index = Math.max(0, Math.min(page, known.length - 2));
899
+ const first = known[index];
900
+ const laid = items.slice(first, known[index + 1]);
901
+ context.clear();
902
+ const sizes = laid.map((item) => context.measure(item.id));
903
+ const centres = rowCentres(sizes.map((size) => size.w), (top.minX + top.maxX) / 2, gap);
904
+ const objects = laid.map((item, i) => {
905
+ const d = Math.min(sizes[i].d, top.maxZ - top.minZ);
906
+ const { objectId } = context.spawn(item.id, [centres[i], top.top, top.maxZ - margin / 2 - d / 2]);
907
+ return { objectId, itemId: item.id, name: item.name };
908
+ });
909
+ current = { group, page: index, first, objects, selected: objects[0]?.objectId ?? null };
910
+ context.log("test_table", context.translate("act.test_table", { group, from: first + 1, to: first + laid.length, total: items.length }), { group, page: index, items: laid.map((i) => i.id) });
911
+ return emit();
912
+ };
913
+ const open = (group, page = 0) => {
914
+ const top = context.top();
915
+ if (!itemsOf(group).length || !top) return null;
916
+ return lay(group, page, top);
917
+ };
918
+ const find = (itemId) => {
919
+ const top = context.top();
920
+ const group = [...context.groups()].find(([, items]) => items.some((i) => i.id === itemId))?.[0];
921
+ if (group === void 0 || !top) return null;
922
+ const index = itemsOf(group).findIndex((i) => i.id === itemId);
923
+ const known = boundsUntil(group, lengthOf(top), (b) => b[b.length - 1] > index);
924
+ lay(group, known.findIndex((bound, i) => bound <= index && index < known[i + 1]), top);
925
+ const target = current?.objects.find((o) => o.itemId === itemId);
926
+ if (current && target) current.selected = target.objectId;
927
+ return emit();
928
+ };
929
+ const selected = (objectId) => {
930
+ const id = objectId ?? current?.selected ?? null;
931
+ return id && context.box(id) ? id : null;
932
+ };
933
+ const trial = (kind, objectId) => {
934
+ const id = selected(objectId);
935
+ const box2 = id ? context.box(id) : null;
936
+ if (!id || !box2) return false;
937
+ const tool = trials[kind];
938
+ if (!context.equip(tool.weapon)) return false;
939
+ const view = trialView(tool, kind, box2.min, box2.max);
940
+ context.look(view.eye, view.target);
941
+ const name = current?.objects.find((o) => o.objectId === id)?.name ?? id;
942
+ context.log("trial", context.translate(`act.trial.${kind}`, { name }), { kind, objectId: id, weapon: tool.weapon });
943
+ if (kind === "blast") {
944
+ const point = [view.target[0], box2.min[1] + 0.05, box2.min[2] - tool.distance];
945
+ return context.blastAt(tool.weapon, point);
946
+ }
947
+ context.use(tool.hold ?? 0);
948
+ emit();
949
+ return true;
950
+ };
951
+ return {
952
+ groups: () => [...context.groups()].filter(([, items]) => items.length > 0).map(([group]) => group),
953
+ items: () => [...context.groups()].flatMap(([group, items]) => items.map((item) => ({ id: item.id, name: item.name, group }))),
954
+ open,
955
+ find,
956
+ next: () => {
957
+ const page = state();
958
+ return page?.hasNext ? open(page.group, page.page + 1) : page;
959
+ },
960
+ prev: () => {
961
+ const page = state();
962
+ return page?.hasPrev ? open(page.group, page.page - 1) : page;
963
+ },
964
+ relay: () => {
965
+ if (!current) return null;
966
+ context.resetAll();
967
+ return open(current.group, current.page);
968
+ },
969
+ state,
970
+ select: (objectId) => {
971
+ if (!current || !current.objects.some((o) => o.objectId === objectId) || !context.box(objectId)) return false;
972
+ current.selected = objectId;
973
+ emit();
974
+ return true;
975
+ },
976
+ trial,
977
+ on: (listener) => {
978
+ listeners.add(listener);
979
+ return () => listeners.delete(listener);
980
+ }
981
+ };
806
982
  }
807
983
 
808
984
  // src/host-events.ts
@@ -2234,7 +2410,7 @@ var en = {
2234
2410
  "hud.throw": "throw",
2235
2411
  "hud.tilt": "tilt (hold)",
2236
2412
  "hud.equip": "equip {weapon}",
2237
- "ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 C: crouch \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use \xB7 E: grab/drop \xB7 G or RMB: throw \xB7 R: reload \xB7 Q: fire mode \xB7 X: clear a jam \xB7 B: safety \xB7 Y: detonate charges set down \xB7 Tab: panels \xB7 Esc: free the mouse",
2413
+ "ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 C: crouch \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use (bare hands: punch) \xB7 E: grab/drop \xB7 G or RMB: throw \xB7 R: reload \xB7 Q: fire mode \xB7 X: clear a jam \xB7 B: safety \xB7 Y: detonate charges set down \xB7 Tab: panels \xB7 Esc: free the mouse",
2238
2414
  "ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
2239
2415
  "camera.first": "1st person",
2240
2416
  "camera.third": "3rd person",
@@ -2248,6 +2424,32 @@ var en = {
2248
2424
  "ui.copy_json": "Copy JSON",
2249
2425
  "ui.download_ndjson": "Download NDJSON",
2250
2426
  "ui.download_txt": "Download TXT",
2427
+ "ui.test_table": "Test table",
2428
+ "ui.test_table.group": "Group to lay on the table",
2429
+ "ui.test_table.lay": "Lay",
2430
+ "ui.test_table.prev": "Previous items",
2431
+ "ui.test_table.next": "Next items",
2432
+ "ui.test_table.relay": "Put back",
2433
+ "ui.test_table.search": "Find an item to put on the table",
2434
+ "ui.test_table.hint": "Pick a group and lay it on the table, then pick an item and try it.",
2435
+ "ui.test_table.page": "Items {from}\u2013{to} of {total}. Pick one, then a trial:",
2436
+ "ui.trial.shot": "Shot",
2437
+ "ui.trial.punch": "Punch",
2438
+ "ui.trial.stab": "Knife",
2439
+ "ui.trial.fire": "Fire",
2440
+ "ui.trial.blast": "Blast",
2441
+ "ui.trial.shot.hint": "A 9 mm round from 2.5 m",
2442
+ "ui.trial.punch.hint": "A punch, up close",
2443
+ "ui.trial.stab.hint": "A knife blow, up close",
2444
+ "ui.trial.fire.hint": "A flamethrower burst from 1.6 m",
2445
+ "ui.trial.blast.hint": "A fragmentation grenade goes off just behind it (it reaches the neighbours too)",
2446
+ "act.test_table": "Test table: {group}, items {from}\u2013{to} of {total}",
2447
+ "act.trial.shot": "Trial: shooting {name}",
2448
+ "act.trial.punch": "Trial: punching {name}",
2449
+ "act.trial.stab": "Trial: stabbing {name}",
2450
+ "act.trial.fire": "Trial: setting fire to {name}",
2451
+ "act.trial.blast": "Trial: a blast beside {name}",
2452
+ "arena.test_table": "Test table",
2251
2453
  "ui.clear_log": "Clear",
2252
2454
  "ui.catalog": "Catalogue",
2253
2455
  "ui.clear": "Clear",
@@ -2515,7 +2717,7 @@ var ptBR = {
2515
2717
  "hud.throw": "arremessar",
2516
2718
  "hud.tilt": "inclinar (segurar)",
2517
2719
  "hud.equip": "equipar {weapon}",
2518
- "ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 C: agachar \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar \xB7 E: pegar/soltar \xB7 G ou bot\xE3o direito: arremessar \xB7 R: recarregar \xB7 Q: modo de tiro \xB7 X: destravar \xB7 B: trava \xB7 Y: detonar cargas postas \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
2720
+ "ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 C: agachar \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar (m\xE3o livre: soco) \xB7 E: pegar/soltar \xB7 G ou bot\xE3o direito: arremessar \xB7 R: recarregar \xB7 Q: modo de tiro \xB7 X: destravar \xB7 B: trava \xB7 Y: detonar cargas postas \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
2519
2721
  "ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
2520
2722
  "camera.first": "1\xAA pessoa",
2521
2723
  "camera.third": "3\xAA pessoa",
@@ -2529,6 +2731,32 @@ var ptBR = {
2529
2731
  "ui.copy_json": "Copiar JSON",
2530
2732
  "ui.download_ndjson": "Baixar NDJSON",
2531
2733
  "ui.download_txt": "Baixar TXT",
2734
+ "ui.test_table": "Mesa de testes",
2735
+ "ui.test_table.group": "Grupo para p\xF4r na mesa",
2736
+ "ui.test_table.lay": "P\xF4r na mesa",
2737
+ "ui.test_table.prev": "Itens anteriores",
2738
+ "ui.test_table.next": "Pr\xF3ximos itens",
2739
+ "ui.test_table.relay": "Repor",
2740
+ "ui.test_table.search": "Buscar item para p\xF4r na mesa",
2741
+ "ui.test_table.hint": "Escolha um grupo e ponha na mesa; depois escolha um item e teste.",
2742
+ "ui.test_table.page": "Itens {from}\u2013{to} de {total}. Escolha um e um teste:",
2743
+ "ui.trial.shot": "Tiro",
2744
+ "ui.trial.punch": "Soco",
2745
+ "ui.trial.stab": "Faca",
2746
+ "ui.trial.fire": "Fogo",
2747
+ "ui.trial.blast": "Explos\xE3o",
2748
+ "ui.trial.shot.hint": "Um tiro de 9 mm a 2,5 m",
2749
+ "ui.trial.punch.hint": "Um soco, de perto",
2750
+ "ui.trial.stab.hint": "Um golpe de faca, de perto",
2751
+ "ui.trial.fire.hint": "Uma rajada de lan\xE7a-chamas a 1,6 m",
2752
+ "ui.trial.blast.hint": "Uma granada de fragmenta\xE7\xE3o explode logo atr\xE1s dele (pega os vizinhos tamb\xE9m)",
2753
+ "act.test_table": "Mesa de testes: {group}, itens {from}\u2013{to} de {total}",
2754
+ "act.trial.shot": "Teste: tiro em {name}",
2755
+ "act.trial.punch": "Teste: soco em {name}",
2756
+ "act.trial.stab": "Teste: faca em {name}",
2757
+ "act.trial.fire": "Teste: fogo em {name}",
2758
+ "act.trial.blast": "Teste: explos\xE3o ao lado de {name}",
2759
+ "arena.test_table": "Mesa de testes",
2532
2760
  "ui.clear_log": "Limpar",
2533
2761
  "ui.catalog": "Cat\xE1logo",
2534
2762
  "ui.clear": "Limpar",
@@ -2870,6 +3098,122 @@ function createBenchPanel(o) {
2870
3098
  };
2871
3099
  }
2872
3100
 
3101
+ // src/test-table-panel.ts
3102
+ var TEST_TABLE_CSS = `
3103
+ .tr-table{flex:0 0 auto;max-height:42%}
3104
+ .tr-table .sub{color:#9aa3b2;margin:4px 0}
3105
+ .tr-table .list{display:flex;flex-wrap:wrap;gap:3px}
3106
+ .tr-table .list .tr-btn{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3107
+ .tr-table select{flex:1;min-width:0;max-width:55%}
3108
+ .tr-table .tr-search{margin:6px 0 0}
3109
+ .tr-root .tr-table button.tr-btn.on{background:#3b3a14;border-color:#ffd23f;color:#fff}
3110
+ `;
3111
+ var mk2 = (tag, cls, text) => {
3112
+ const e = document.createElement(tag);
3113
+ if (cls) e.className = cls;
3114
+ if (text !== void 0) e.textContent = text;
3115
+ return e;
3116
+ };
3117
+ function createTestTablePanel(table, t) {
3118
+ const element = mk2("div", "tr-panel tr-table");
3119
+ element.appendChild(mk2("h3", void 0, t("ui.test_table")));
3120
+ const head = mk2("div", "tr-row");
3121
+ const select = mk2("select");
3122
+ select.setAttribute("aria-label", t("ui.test_table.group"));
3123
+ const lay = mk2("button", "tr-btn", t("ui.test_table.lay"));
3124
+ const prev = mk2("button", "tr-btn", "\u25C0");
3125
+ const next = mk2("button", "tr-btn", "\u25B6");
3126
+ prev.title = t("ui.test_table.prev");
3127
+ next.title = t("ui.test_table.next");
3128
+ for (const b of [lay, prev, next]) b.type = "button";
3129
+ head.append(select, lay, prev, next);
3130
+ const search = mk2("input", "tr-search");
3131
+ search.type = "search";
3132
+ search.placeholder = t("ui.test_table.search");
3133
+ const found = mk2("div", "list");
3134
+ const sub = mk2("div", "sub", t("ui.test_table.hint"));
3135
+ const list = mk2("div", "list");
3136
+ const trials = mk2("div", "tr-row");
3137
+ const trialButtons = TRIAL_KINDS.map((kind) => {
3138
+ const b = mk2("button", "tr-btn", t(`ui.trial.${kind}`));
3139
+ b.type = "button";
3140
+ b.dataset.trial = kind;
3141
+ b.title = t(`ui.trial.${kind}.hint`);
3142
+ b.onclick = () => table.trial(kind);
3143
+ trials.appendChild(b);
3144
+ return b;
3145
+ });
3146
+ const relay = mk2("button", "tr-btn", t("ui.test_table.relay"));
3147
+ relay.type = "button";
3148
+ relay.onclick = () => table.relay();
3149
+ trials.appendChild(relay);
3150
+ element.append(head, search, found, sub, list, trials);
3151
+ const groups = () => {
3152
+ const keep = select.value;
3153
+ select.textContent = "";
3154
+ for (const group of table.groups()) {
3155
+ const op = mk2("option", void 0, group);
3156
+ op.value = group;
3157
+ select.appendChild(op);
3158
+ }
3159
+ if ([...select.options].some((o) => o.value === keep)) select.value = keep;
3160
+ };
3161
+ const render = (page) => {
3162
+ list.textContent = "";
3163
+ prev.disabled = !page?.hasPrev;
3164
+ next.disabled = !page?.hasNext;
3165
+ for (const b of [...trialButtons, relay]) b.disabled = !page?.selected;
3166
+ if (!page) {
3167
+ sub.textContent = t("ui.test_table.hint");
3168
+ return;
3169
+ }
3170
+ if (select.value !== page.group) select.value = page.group;
3171
+ sub.textContent = t("ui.test_table.page", { from: page.first + 1, to: page.first + page.objects.length, total: page.total });
3172
+ for (const o of page.objects) {
3173
+ const b = mk2("button", o.objectId === page.selected ? "tr-btn on" : "tr-btn", o.name);
3174
+ b.type = "button";
3175
+ b.title = o.itemId;
3176
+ b.onclick = () => table.select(o.objectId);
3177
+ list.appendChild(b);
3178
+ }
3179
+ };
3180
+ const fold2 = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
3181
+ search.oninput = () => {
3182
+ found.textContent = "";
3183
+ const term = fold2(search.value.trim());
3184
+ if (!term) return;
3185
+ const hits = table.items().filter((item) => fold2(`${item.name} ${item.id}`).includes(term)).slice(0, 8);
3186
+ for (const item of hits) {
3187
+ const b = mk2("button", "tr-btn", item.name);
3188
+ b.type = "button";
3189
+ b.title = `${item.group} \xB7 ${item.id}`;
3190
+ b.onclick = () => {
3191
+ search.value = "";
3192
+ found.textContent = "";
3193
+ table.find(item.id);
3194
+ };
3195
+ found.appendChild(b);
3196
+ }
3197
+ if (!hits.length) found.appendChild(mk2("div", "sub", t("ui.no_results")));
3198
+ };
3199
+ lay.onclick = () => table.open(select.value);
3200
+ prev.onclick = () => table.prev();
3201
+ next.onclick = () => table.next();
3202
+ select.onchange = () => table.open(select.value);
3203
+ for (const box2 of [select, search]) box2.addEventListener("keydown", (e) => e.stopPropagation());
3204
+ const off = table.on(render);
3205
+ groups();
3206
+ render(table.state());
3207
+ return {
3208
+ element,
3209
+ refresh: () => {
3210
+ groups();
3211
+ render(table.state());
3212
+ },
3213
+ dispose: off
3214
+ };
3215
+ }
3216
+
2873
3217
  // src/ui.ts
2874
3218
  var CSS = `
2875
3219
  .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}
@@ -2948,7 +3292,7 @@ var CSS = `
2948
3292
  @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}}
2949
3293
  `;
2950
3294
  var fold = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
2951
- var mk2 = (tag, cls, text) => {
3295
+ var mk3 = (tag, cls, text) => {
2952
3296
  const e = document.createElement(tag);
2953
3297
  if (cls) e.className = cls;
2954
3298
  if (text !== void 0) e.textContent = text;
@@ -2959,7 +3303,7 @@ var thumbSource = (thumbnail) => {
2959
3303
  return source.replace(/["\\\n\r]/g, (char) => encodeURIComponent(char));
2960
3304
  };
2961
3305
  var thumb = (thumbnail, credit) => {
2962
- const e = mk2("span", "tr-thumb");
3306
+ const e = mk3("span", "tr-thumb");
2963
3307
  if (credit) e.title = credit;
2964
3308
  else e.setAttribute("aria-hidden", "true");
2965
3309
  e.style.setProperty("--tr-thumb", `url("${thumbSource(thumbnail)}")`);
@@ -2980,42 +3324,43 @@ async function toggleFullscreen() {
2980
3324
  }
2981
3325
  function createUi(o) {
2982
3326
  const { t } = o;
2983
- const style = mk2("style");
2984
- style.textContent = CSS;
2985
- const root = mk2("div", "tr-root");
3327
+ const style = mk3("style");
3328
+ style.textContent = CSS + (o.testTable ? TEST_TABLE_CSS : "");
3329
+ const root = mk3("div", "tr-root");
2986
3330
  const pos = getComputedStyle?.(o.container)?.position;
2987
3331
  if (!pos || pos === "static") o.container.style.position = "relative";
2988
3332
  root.appendChild(style);
2989
3333
  o.container.appendChild(root);
2990
- const cross = mk2("div", "tr-cross");
2991
- cross.appendChild(mk2("i"));
2992
- const top = mk2("div", "tr-top");
2993
- const toolBox = mk2("div", "tr-tool");
2994
- const status = mk2("div", "tr-status");
2995
- const prog = mk2("div", "tr-prog");
2996
- const progLabel = mk2("b");
2997
- const progBar = mk2("div", "tr-bar");
2998
- const progFill = mk2("i");
3334
+ const cross = mk3("div", "tr-cross");
3335
+ cross.appendChild(mk3("i"));
3336
+ const top = mk3("div", "tr-top");
3337
+ const toolBox = mk3("div", "tr-tool");
3338
+ const status = mk3("div", "tr-status");
3339
+ const prog = mk3("div", "tr-prog");
3340
+ const progLabel = mk3("b");
3341
+ const progBar = mk3("div", "tr-bar");
3342
+ const progFill = mk3("i");
2999
3343
  progBar.appendChild(progFill);
3000
3344
  prog.append(progLabel, progBar);
3001
3345
  top.append(toolBox, status, prog);
3002
- const bottom = mk2("div", "tr-bottom");
3003
- const held = mk2("div", "tr-held");
3004
- const hotbar = mk2("div", "tr-hotbar");
3346
+ const bottom = mk3("div", "tr-bottom");
3347
+ const held = mk3("div", "tr-held");
3348
+ const hotbar = mk3("div", "tr-hotbar");
3005
3349
  bottom.append(held, hotbar);
3006
- const help = mk2("div", "tr-help");
3350
+ const help = mk3("div", "tr-help");
3007
3351
  help.textContent = t("ui.help");
3008
- const left = mk2("div", "tr-left");
3009
- const insp = mk2("div", "tr-panel tr-insp");
3010
- const logPanel = mk2("div", "tr-panel tr-log");
3011
- left.append(insp, logPanel);
3012
- const logTitle = mk2("h3", void 0, t("ui.log"));
3013
- const logBar = mk2("div", "tr-logbar");
3014
- const logBody = mk2("div", "tr-logbody");
3352
+ const left = mk3("div", "tr-left");
3353
+ const insp = mk3("div", "tr-panel tr-insp");
3354
+ const logPanel = mk3("div", "tr-panel tr-log");
3355
+ const tablePanel = o.testTable ? createTestTablePanel(o.testTable, t) : null;
3356
+ left.append(insp, ...tablePanel ? [tablePanel.element] : [], logPanel);
3357
+ const logTitle = mk3("h3", void 0, t("ui.log"));
3358
+ const logBar = mk3("div", "tr-logbar");
3359
+ const logBody = mk3("div", "tr-logbody");
3015
3360
  const filters = { action: true, event: true, check: true };
3016
3361
  const filterBtns = [];
3017
3362
  for (const ch of ["action", "event", "check"]) {
3018
- const b = mk2("button", "tr-btn on", t(`ui.filter.${ch}`));
3363
+ const b = mk3("button", "tr-btn on", t(`ui.filter.${ch}`));
3019
3364
  b.type = "button";
3020
3365
  b.onclick = () => {
3021
3366
  filters[ch] = !filters[ch];
@@ -3026,8 +3371,8 @@ function createUi(o) {
3026
3371
  logBar.appendChild(b);
3027
3372
  }
3028
3373
  let autoscroll = true;
3029
- const asLabel = mk2("label");
3030
- const asBox = mk2("input");
3374
+ const asLabel = mk3("label");
3375
+ const asBox = mk3("input");
3031
3376
  asBox.type = "checkbox";
3032
3377
  asBox.checked = true;
3033
3378
  asBox.onchange = () => {
@@ -3036,7 +3381,7 @@ function createUi(o) {
3036
3381
  asLabel.append(asBox, document.createTextNode(" " + t("ui.autoscroll")));
3037
3382
  logBar.appendChild(asLabel);
3038
3383
  const logBtn = (label, fn) => {
3039
- const b = mk2("button", "tr-btn", label);
3384
+ const b = mk3("button", "tr-btn", label);
3040
3385
  b.type = "button";
3041
3386
  b.onclick = fn;
3042
3387
  logBar.appendChild(b);
@@ -3044,7 +3389,7 @@ function createUi(o) {
3044
3389
  logBtn(t("ui.copy_json"), () => {
3045
3390
  const text = o.log.toJSON();
3046
3391
  const select = () => {
3047
- const ta = mk2("textarea");
3392
+ const ta = mk3("textarea");
3048
3393
  ta.value = text;
3049
3394
  ta.setAttribute("readonly", "");
3050
3395
  ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
@@ -3078,12 +3423,12 @@ function createUi(o) {
3078
3423
  const fill = (n, e) => {
3079
3424
  n.className = lineClass(e);
3080
3425
  n.textContent = "";
3081
- const tm = mk2("time", void 0, fmtTime(e.t));
3426
+ const tm = mk3("time", void 0, fmtTime(e.t));
3082
3427
  n.append(tm, document.createTextNode(e.text));
3083
3428
  };
3084
3429
  const addNode = (e) => {
3085
3430
  if (!filters[e.channel]) return;
3086
- const n = mk2("div");
3431
+ const n = mk3("div");
3087
3432
  fill(n, e);
3088
3433
  nodes.set(e.seq, n);
3089
3434
  logBody.appendChild(n);
@@ -3105,18 +3450,18 @@ function createUi(o) {
3105
3450
  if (n) fill(n, e);
3106
3451
  } else addNode(e);
3107
3452
  });
3108
- const right = mk2("div", "tr-right");
3109
- const cat = mk2("div", "tr-panel tr-cat");
3110
- const anim = mk2("div", "tr-panel tr-anim");
3111
- const tests = mk2("div", "tr-panel tr-tests");
3453
+ const right = mk3("div", "tr-right");
3454
+ const cat = mk3("div", "tr-panel tr-cat");
3455
+ const anim = mk3("div", "tr-panel tr-anim");
3456
+ const tests = mk3("div", "tr-panel tr-tests");
3112
3457
  const benchPanel = createBenchPanel({ t, num: (n, d) => num(n, d), onControl: o.onControl, onLayer: o.onLayer, onAction: o.onAction });
3113
3458
  right.append(cat, anim, benchPanel.element, tests);
3114
- cat.appendChild(mk2("h3", void 0, t("ui.catalog")));
3115
- const itemSearch = mk2("input", "tr-search");
3459
+ cat.appendChild(mk3("h3", void 0, t("ui.catalog")));
3460
+ const itemSearch = mk3("input", "tr-search");
3116
3461
  itemSearch.type = "search";
3117
3462
  itemSearch.placeholder = t("ui.search_items");
3118
- const tabs = mk2("div", "tr-tabs");
3119
- const items = mk2("div", "tr-items");
3463
+ const tabs = mk3("div", "tr-tabs");
3464
+ const items = mk3("div", "tr-items");
3120
3465
  cat.append(itemSearch, tabs, items);
3121
3466
  let activeGroup = "";
3122
3467
  const visibleItems = () => {
@@ -3128,18 +3473,18 @@ function createUi(o) {
3128
3473
  items.textContent = "";
3129
3474
  const searching = itemSearch.value.trim() !== "";
3130
3475
  const found = visibleItems();
3131
- if (!found.length) items.appendChild(mk2("div", "tr-empty", t("ui.no_results")));
3476
+ if (!found.length) items.appendChild(mk3("div", "tr-empty", t("ui.no_results")));
3132
3477
  for (const { item: it, group } of found) {
3133
- const wrap = mk2("div", "tr-item");
3134
- const b = mk2("button", it.thumbnail ? "tr-btn has-thumb" : "tr-btn", it.thumbnail ? void 0 : it.name);
3135
- if (it.thumbnail) b.append(thumb(it.thumbnail), mk2("span", "tr-name", it.name));
3136
- if (searching) b.appendChild(mk2("small", void 0, group));
3478
+ const wrap = mk3("div", "tr-item");
3479
+ const b = mk3("button", it.thumbnail ? "tr-btn has-thumb" : "tr-btn", it.thumbnail ? void 0 : it.name);
3480
+ if (it.thumbnail) b.append(thumb(it.thumbnail), mk3("span", "tr-name", it.name));
3481
+ if (searching) b.appendChild(mk3("small", void 0, group));
3137
3482
  b.type = "button";
3138
3483
  b.title = it.note ?? it.name;
3139
3484
  b.onclick = () => o.onSpawn(it.id);
3140
3485
  wrap.appendChild(b);
3141
3486
  if (it.weapon) {
3142
- const w = mk2("button", "tr-btn", "\u2694");
3487
+ const w = mk3("button", "tr-btn", "\u2694");
3143
3488
  w.type = "button";
3144
3489
  w.title = t("ui.equip");
3145
3490
  w.onclick = () => o.onEquip(it.id);
@@ -3153,7 +3498,7 @@ function createUi(o) {
3153
3498
  const groupNames = [...o.groups().keys()];
3154
3499
  if (!groupNames.includes(activeGroup)) activeGroup = groupNames[0] ?? "";
3155
3500
  for (const g of groupNames) {
3156
- const b = mk2("button", g === activeGroup ? "tr-btn on" : "tr-btn", g);
3501
+ const b = mk3("button", g === activeGroup ? "tr-btn on" : "tr-btn", g);
3157
3502
  b.type = "button";
3158
3503
  b.onclick = () => {
3159
3504
  activeGroup = g;
@@ -3166,9 +3511,9 @@ function createUi(o) {
3166
3511
  renderTabs();
3167
3512
  itemSearch.oninput = renderItems;
3168
3513
  for (const box2 of [itemSearch]) box2.addEventListener("keydown", (e) => e.stopPropagation());
3169
- const controls = mk2("div", "tr-row");
3514
+ const controls = mk3("div", "tr-row");
3170
3515
  const ctl = (label, fn) => {
3171
- const b = mk2("button", "tr-btn", label);
3516
+ const b = mk3("button", "tr-btn", label);
3172
3517
  b.type = "button";
3173
3518
  b.onclick = () => fn(b);
3174
3519
  controls.appendChild(b);
@@ -3193,29 +3538,29 @@ function createUi(o) {
3193
3538
  fullscreenBtn.title = t("ui.fullscreen.hint");
3194
3539
  const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
3195
3540
  for (const { key, b } of blastBtns) b.title = t(`ui.blast.${key}.hint`);
3196
- const qSel = mk2("select");
3541
+ const qSel = mk3("select");
3197
3542
  for (const q of ["low", "medium", "high"]) {
3198
- const op = mk2("option", void 0, t(`ui.quality.${q}`));
3543
+ const op = mk3("option", void 0, t(`ui.quality.${q}`));
3199
3544
  op.value = q;
3200
3545
  qSel.appendChild(op);
3201
3546
  }
3202
3547
  qSel.value = o.quality;
3203
3548
  qSel.onchange = () => o.onQuality(qSel.value);
3204
- const qLabel = mk2("label", void 0, t("ui.quality") + " ");
3549
+ const qLabel = mk3("label", void 0, t("ui.quality") + " ");
3205
3550
  qLabel.appendChild(qSel);
3206
3551
  controls.appendChild(qLabel);
3207
- const count = mk2("span");
3552
+ const count = mk3("span");
3208
3553
  controls.appendChild(count);
3209
3554
  cat.appendChild(controls);
3210
- tests.appendChild(mk2("h3", void 0, t("ui.tests")));
3211
- const runAll = mk2("button", "tr-btn", t("ui.run_all"));
3555
+ tests.appendChild(mk3("h3", void 0, t("ui.tests")));
3556
+ const runAll = mk3("button", "tr-btn", t("ui.run_all"));
3212
3557
  runAll.type = "button";
3213
- const summary = mk2("div", "tr-summary");
3214
- const testSearch = mk2("input", "tr-search");
3558
+ const summary = mk3("div", "tr-summary");
3559
+ const testSearch = mk3("input", "tr-search");
3215
3560
  testSearch.type = "search";
3216
3561
  testSearch.placeholder = t("ui.search_tests");
3217
3562
  testSearch.addEventListener("keydown", (e) => e.stopPropagation());
3218
- const list = mk2("div", "list");
3563
+ const list = mk3("div", "list");
3219
3564
  tests.append(runAll, summary, testSearch, list);
3220
3565
  testSearch.oninput = () => {
3221
3566
  const term = fold(testSearch.value.trim());
@@ -3238,7 +3583,7 @@ function createUi(o) {
3238
3583
  list.textContent = "";
3239
3584
  scBtns.clear();
3240
3585
  for (const s of o.scenarios()) {
3241
- const b = mk2("button", "tr-btn", s.name);
3586
+ const b = mk3("button", "tr-btn", s.name);
3242
3587
  b.type = "button";
3243
3588
  b.onclick = async () => {
3244
3589
  if (busy) return;
@@ -3264,7 +3609,7 @@ function createUi(o) {
3264
3609
  busy = false;
3265
3610
  }
3266
3611
  };
3267
- const toggle = mk2("button", "tr-btn tr-toggle", t("ui.panel"));
3612
+ const toggle = mk3("button", "tr-btn tr-toggle", t("ui.panel"));
3268
3613
  toggle.type = "button";
3269
3614
  const togglePanels = () => {
3270
3615
  root.classList.toggle("open");
@@ -3273,15 +3618,15 @@ function createUi(o) {
3273
3618
  toggle.onclick = togglePanels;
3274
3619
  root.append(cross, top, bottom, help, left, right, toggle);
3275
3620
  const bar = (value, max, cls) => {
3276
- const b = mk2("div", `tr-bar ${cls}`);
3277
- const i = mk2("i");
3621
+ const b = mk3("div", `tr-bar ${cls}`);
3622
+ const i = mk3("i");
3278
3623
  i.style.width = `${Math.max(0, Math.min(100, value / Math.max(1e-9, max) * 100))}%`;
3279
3624
  b.appendChild(i);
3280
3625
  return b;
3281
3626
  };
3282
3627
  const row = (k, v) => {
3283
- const r = mk2("div", "r");
3284
- r.append(mk2("span", void 0, k), mk2("span", void 0, v));
3628
+ const r = mk3("div", "r");
3629
+ r.append(mk3("span", void 0, k), mk3("span", void 0, v));
3285
3630
  return r;
3286
3631
  };
3287
3632
  const num = (n, d = 1) => {
@@ -3290,13 +3635,13 @@ function createUi(o) {
3290
3635
  };
3291
3636
  const renderInspector = (i) => {
3292
3637
  insp.textContent = "";
3293
- insp.appendChild(mk2("h3", void 0, t("ui.inspector")));
3638
+ insp.appendChild(mk3("h3", void 0, t("ui.inspector")));
3294
3639
  if (!i) {
3295
- insp.appendChild(mk2("div", void 0, t("ui.aim_at_something")));
3640
+ insp.appendChild(mk3("div", void 0, t("ui.aim_at_something")));
3296
3641
  return;
3297
3642
  }
3298
3643
  if (i.thumbnail) insp.appendChild(thumb(i.thumbnail, i.thumbnailCredit));
3299
- insp.appendChild(mk2("div", "t", `${i.object} \u203A ${i.part}`));
3644
+ insp.appendChild(mk3("div", "t", `${i.object} \u203A ${i.part}`));
3300
3645
  insp.appendChild(row(t("ins.material"), i.material));
3301
3646
  insp.appendChild(row(t("ins.status"), i.status + (i.burning ? ` \xB7 ${t("ins.burning")}` : "")));
3302
3647
  insp.appendChild(row(t("ins.hp"), `${num(i.hp, 0)} / ${num(i.maxHp, 0)}`));
@@ -3315,30 +3660,30 @@ function createUi(o) {
3315
3660
  insp.appendChild(bar(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
3316
3661
  }
3317
3662
  if (i.joints.length) {
3318
- insp.appendChild(mk2("div", void 0, t("ins.joints") + ":"));
3663
+ insp.appendChild(mk3("div", void 0, t("ins.joints") + ":"));
3319
3664
  for (const j of i.joints) {
3320
3665
  const tools = j.tools.length ? j.tools.join(", ") : t("tool.none");
3321
- insp.appendChild(mk2("div", void 0, `\u2022 ${j.kind} (${j.id})${j.open ? ` \xB7 ${t("ins.open")}` : ` \u2192 ${tools}`}`));
3666
+ insp.appendChild(mk3("div", void 0, `\u2022 ${j.kind} (${j.id})${j.open ? ` \xB7 ${t("ins.open")}` : ` \u2192 ${tools}`}`));
3322
3667
  }
3323
3668
  }
3324
- if (i.note) insp.appendChild(mk2("div", void 0, i.note));
3669
+ if (i.note) insp.appendChild(mk3("div", void 0, i.note));
3325
3670
  };
3326
3671
  let lastAnim = "";
3327
3672
  const renderAnimations = (a) => {
3328
3673
  anim.textContent = "";
3329
- anim.appendChild(mk2("h3", void 0, t("ui.animations")));
3674
+ anim.appendChild(mk3("h3", void 0, t("ui.animations")));
3330
3675
  if (!a) {
3331
- anim.appendChild(mk2("div", "sub", t("ui.animations_aim")));
3676
+ anim.appendChild(mk3("div", "sub", t("ui.animations_aim")));
3332
3677
  return;
3333
3678
  }
3334
- anim.appendChild(mk2("div", "sub", a.name));
3679
+ anim.appendChild(mk3("div", "sub", a.name));
3335
3680
  if (a.clips.length === 0) {
3336
- anim.appendChild(mk2("div", "sub", t("ui.animations_none")));
3681
+ anim.appendChild(mk3("div", "sub", t("ui.animations_none")));
3337
3682
  return;
3338
3683
  }
3339
- const l = mk2("div", "list");
3684
+ const l = mk3("div", "list");
3340
3685
  for (const c of a.clips) {
3341
- const b = mk2("button", a.playing === c.id ? "tr-btn on" : "tr-btn", `\u25B6 ${t(`anim.${c.id}`) === `anim.${c.id}` ? c.id : t(`anim.${c.id}`)} (${num(c.duration, 1)} s)`);
3686
+ const b = mk3("button", a.playing === c.id ? "tr-btn on" : "tr-btn", `\u25B6 ${t(`anim.${c.id}`) === `anim.${c.id}` ? c.id : t(`anim.${c.id}`)} (${num(c.duration, 1)} s)`);
3342
3687
  b.type = "button";
3343
3688
  b.onclick = () => o.onPlay(a.objectId, c.id);
3344
3689
  l.appendChild(b);
@@ -3355,8 +3700,8 @@ function createUi(o) {
3355
3700
  hud(s) {
3356
3701
  toolBox.textContent = "";
3357
3702
  toolBox.append(document.createTextNode(s.toolName));
3358
- if (s.ammo) toolBox.append(mk2("small", void 0, s.ammo));
3359
- if (s.camera) toolBox.append(mk2("small", void 0, t(`camera.${s.camera}`)));
3703
+ if (s.ammo) toolBox.append(mk3("small", void 0, s.ammo));
3704
+ if (s.camera) toolBox.append(mk3("small", void 0, t(`camera.${s.camera}`)));
3360
3705
  const helpText = s.camera ? `${t("ui.help")} \xB7 ${t("ui.help.camera")}` : t("ui.help");
3361
3706
  if (help.textContent !== helpText) help.textContent = helpText;
3362
3707
  if (s.status) status.textContent = s.status;
@@ -3379,7 +3724,7 @@ function createUi(o) {
3379
3724
  ];
3380
3725
  if (s.held.weapon) keys.push(["H", t("hud.equip", { weapon: s.held.weapon })]);
3381
3726
  for (const [k, label] of keys) {
3382
- held.append(mk2("kbd", void 0, k), document.createTextNode(` ${label} `));
3727
+ held.append(mk3("kbd", void 0, k), document.createTextNode(` ${label} `));
3383
3728
  }
3384
3729
  } else held.style.display = "none";
3385
3730
  const key = s.slots.join("|") + s.page;
@@ -3389,9 +3734,9 @@ function createUi(o) {
3389
3734
  slotsEl.length = 0;
3390
3735
  hudPage = s.page - 1;
3391
3736
  s.slots.forEach((name, i) => {
3392
- const b = mk2("button", "tr-slot");
3737
+ const b = mk3("button", "tr-slot");
3393
3738
  b.type = "button";
3394
- b.append(mk2("b", void 0, String((i + 1) % 10)), document.createTextNode(name));
3739
+ b.append(mk3("b", void 0, String((i + 1) % 10)), document.createTextNode(name));
3395
3740
  b.title = name;
3396
3741
  b.onclick = () => o.onTool(hudPage * 10 + i);
3397
3742
  hotbar.appendChild(b);
@@ -3423,6 +3768,7 @@ function createUi(o) {
3423
3768
  refresh() {
3424
3769
  renderTabs();
3425
3770
  renderScenarios();
3771
+ tablePanel?.refresh();
3426
3772
  },
3427
3773
  showGroup(group) {
3428
3774
  activeGroup = group;
@@ -3437,6 +3783,7 @@ function createUi(o) {
3437
3783
  },
3438
3784
  dispose() {
3439
3785
  offLog();
3786
+ tablePanel?.dispose();
3440
3787
  if (toastTimer) clearTimeout(toastTimer);
3441
3788
  root.remove();
3442
3789
  }
@@ -4626,6 +4973,10 @@ function createTestRoom(options) {
4626
4973
  heldPiece = null;
4627
4974
  heldTilt = 0;
4628
4975
  };
4976
+ const handUse = () => {
4977
+ if (heldPiece) grab();
4978
+ else useTool(FIXED_DT, false);
4979
+ };
4629
4980
  const grab = () => {
4630
4981
  if (heldPiece) {
4631
4982
  const name = heldName;
@@ -5094,10 +5445,10 @@ function createTestRoom(options) {
5094
5445
  if (tiltKey || heldTilt > 0) {
5095
5446
  heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
5096
5447
  }
5097
- if (useHeld && !paused) {
5448
+ if ((useHeld || useFirst) && !paused) {
5098
5449
  if (tool.def.category === "hand") {
5099
- if (useFirst) grab();
5100
- } else useTool(dt, !useFirst);
5450
+ if (useFirst) handUse();
5451
+ } else if (useHeld) useTool(dt, !useFirst);
5101
5452
  useFirst = false;
5102
5453
  }
5103
5454
  if (!paused) followHeld(dt);
@@ -5652,6 +6003,63 @@ function createTestRoom(options) {
5652
6003
  const row = (list, gap, z) => list.map((item, index) => spawn(item.id, [(index - (list.length - 1) / 2) * gap, 0, z], 0));
5653
6004
  return [...row(front, 1.6, 1.5), ...row(back, 4.5, -4)];
5654
6005
  };
6006
+ const catalogGroups = () => {
6007
+ const m = /* @__PURE__ */ new Map();
6008
+ for (const i of catalog) {
6009
+ const g = m.get(i.group) ?? [];
6010
+ g.push(i);
6011
+ m.set(i.group, g);
6012
+ }
6013
+ return m;
6014
+ };
6015
+ const tableOptions = options.testTable === false ? {} : options.testTable ?? {};
6016
+ const tableScenery = tableOptions.scenery ?? "test_table";
6017
+ const measured = /* @__PURE__ */ new Map();
6018
+ const testTable = createTestTable(
6019
+ {
6020
+ groups: catalogGroups,
6021
+ top: () => {
6022
+ const top = arena.tops.find((x) => x.id === tableScenery);
6023
+ return top && topOk(top) ? top : null;
6024
+ },
6025
+ measure: (itemId) => {
6026
+ let size = measured.get(itemId);
6027
+ if (!size) {
6028
+ const item = catalogMap.get(itemId);
6029
+ const box2 = item ? new THREE.Box3().setFromObject(buildRoot(item)) : null;
6030
+ size = box2 && !box2.isEmpty() ? { w: box2.max.x - box2.min.x, d: box2.max.z - box2.min.z } : { w: 0.3, d: 0.3 };
6031
+ measured.set(itemId, size);
6032
+ }
6033
+ return size;
6034
+ },
6035
+ spawn: (itemId, position) => spawn(itemId, position, 0),
6036
+ clear,
6037
+ resetAll,
6038
+ box: (objectId) => {
6039
+ const e = entries.get(objectId);
6040
+ if (!e || !world.has(objectId)) return null;
6041
+ const box2 = new THREE.Box3().setFromObject(e.so.root);
6042
+ return box2.isEmpty() ? null : { min: [box2.min.x, box2.min.y, box2.min.z], max: [box2.max.x, box2.max.y, box2.max.z] };
6043
+ },
6044
+ equip: (weapon) => {
6045
+ if (!arsenal.weapons.has(weapon)) return false;
6046
+ if (heldEntry) releaseHeld();
6047
+ equip(weapon);
6048
+ room.waitUntilReady();
6049
+ return true;
6050
+ },
6051
+ look: (eye, target) => player.look(eye, target),
6052
+ use: (hold) => room.use(hold),
6053
+ blastAt: (weapon, point) => {
6054
+ const def = arsenal.weapons.get(weapon);
6055
+ const explosive = def?.action.kind === "throwable" ? def.action.explosive : def?.action.kind === "launcher" ? def.action.projectile : void 0;
6056
+ return explosive !== void 0 && explosives.blastAt(weapon, explosive, point);
6057
+ },
6058
+ log: (type, text, data) => action(type, text, { data }),
6059
+ translate: t
6060
+ },
6061
+ tableOptions
6062
+ );
5655
6063
  const room = {
5656
6064
  world,
5657
6065
  renderer,
@@ -5671,6 +6079,7 @@ function createTestRoom(options) {
5671
6079
  get scenarios() {
5672
6080
  return allScenarios();
5673
6081
  },
6082
+ testTable,
5674
6083
  spawn,
5675
6084
  clear,
5676
6085
  resetAll,
@@ -5678,7 +6087,8 @@ function createTestRoom(options) {
5678
6087
  useItem,
5679
6088
  use(hold = 0) {
5680
6089
  if (tool.def.category === "hand") {
5681
- grab();
6090
+ handUse();
6091
+ flush();
5682
6092
  return;
5683
6093
  }
5684
6094
  useTool(FIXED_DT, false);
@@ -5749,6 +6159,7 @@ function createTestRoom(options) {
5749
6159
  },
5750
6160
  equip,
5751
6161
  openArea,
6162
+ modules: () => [...modules.values()].map((m) => ({ id: m.id, name: m.name, group: m.group, hasArea: typeof m.area === "function" })),
5752
6163
  openAreaAsync: (area) => loading.busy(t("loading.open_area", { area: areaLabel(area) }), () => openArea(area)),
5753
6164
  loading,
5754
6165
  busy: (label, work, busyOptions) => loading.busy(label, work, busyOptions),
@@ -5910,7 +6321,8 @@ function createTestRoom(options) {
5910
6321
  },
5911
6322
  onAction: (id) => {
5912
6323
  if (activeBench) slowly(activeBench, id, () => benches.get(activeBench)?.act(id));
5913
- }
6324
+ },
6325
+ ...options.testTable !== false && arena.tops.some((x) => x.id === tableScenery) ? { testTable } : {}
5914
6326
  });
5915
6327
  ui.hud(hudState());
5916
6328
  showBench();
@@ -5963,19 +6375,25 @@ function structuredCloneSpec(spec) {
5963
6375
  export {
5964
6376
  DEFAULT_HAND_STANCES,
5965
6377
  DEFAULT_STRIKE_CLIPS,
6378
+ DEFAULT_TRIALS,
5966
6379
  LOADING_CLASS,
5967
6380
  LOADING_CSS,
5968
6381
  MATERIAL_COLORS,
6382
+ TRIAL_KINDS,
5969
6383
  brickWallSpec,
5970
6384
  buildFromSpec,
5971
6385
  createLoading,
5972
6386
  createLog,
5973
6387
  createTestRoom,
6388
+ createTestTable,
5974
6389
  decodeBenchState,
5975
6390
  encodeBenchState,
6391
+ fitRow,
5976
6392
  loadingMarkup,
5977
6393
  locales,
5978
6394
  nextPaint,
5979
- resolveStrings
6395
+ resolveStrings,
6396
+ rowCentres,
6397
+ trialView
5980
6398
  };
5981
6399
  //# sourceMappingURL=index.js.map