@adatechnology/game-test-room 0.14.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
@@ -471,7 +471,7 @@ function buildFromSpec(THREE, spec, options = {}) {
471
471
  }
472
472
 
473
473
  // src/explosives.ts
474
- import { backblastEffect, blastEffect, createExplosiveBody, tripwireSegment } from "@adatechnology/game-weapons";
474
+ import { ExplosiveBody, backblastEffect, blastEffect, createExplosiveBody, tripwireSegment } from "@adatechnology/game-weapons";
475
475
 
476
476
  // src/segment-box.ts
477
477
  var segmentTouchesBox = (from, to, min, max) => {
@@ -714,6 +714,14 @@ function createExplosives(context) {
714
714
  ...projectile.fire ? { fire: projectile.fire } : {}
715
715
  });
716
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
+ };
717
725
  const detonateCharges = () => {
718
726
  let count = 0;
719
727
  for (const projectile of [...projectiles]) {
@@ -805,7 +813,172 @@ function createExplosives(context) {
805
813
  }
806
814
  }
807
815
  };
808
- 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
+ };
809
982
  }
810
983
 
811
984
  // src/host-events.ts
@@ -2237,7 +2410,7 @@ var en = {
2237
2410
  "hud.throw": "throw",
2238
2411
  "hud.tilt": "tilt (hold)",
2239
2412
  "hud.equip": "equip {weapon}",
2240
- "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",
2241
2414
  "ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
2242
2415
  "camera.first": "1st person",
2243
2416
  "camera.third": "3rd person",
@@ -2251,6 +2424,32 @@ var en = {
2251
2424
  "ui.copy_json": "Copy JSON",
2252
2425
  "ui.download_ndjson": "Download NDJSON",
2253
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",
2254
2453
  "ui.clear_log": "Clear",
2255
2454
  "ui.catalog": "Catalogue",
2256
2455
  "ui.clear": "Clear",
@@ -2518,7 +2717,7 @@ var ptBR = {
2518
2717
  "hud.throw": "arremessar",
2519
2718
  "hud.tilt": "inclinar (segurar)",
2520
2719
  "hud.equip": "equipar {weapon}",
2521
- "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",
2522
2721
  "ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
2523
2722
  "camera.first": "1\xAA pessoa",
2524
2723
  "camera.third": "3\xAA pessoa",
@@ -2532,6 +2731,32 @@ var ptBR = {
2532
2731
  "ui.copy_json": "Copiar JSON",
2533
2732
  "ui.download_ndjson": "Baixar NDJSON",
2534
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",
2535
2760
  "ui.clear_log": "Limpar",
2536
2761
  "ui.catalog": "Cat\xE1logo",
2537
2762
  "ui.clear": "Limpar",
@@ -2873,6 +3098,122 @@ function createBenchPanel(o) {
2873
3098
  };
2874
3099
  }
2875
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
+
2876
3217
  // src/ui.ts
2877
3218
  var CSS = `
2878
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}
@@ -2951,7 +3292,7 @@ var CSS = `
2951
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}}
2952
3293
  `;
2953
3294
  var fold = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
2954
- var mk2 = (tag, cls, text) => {
3295
+ var mk3 = (tag, cls, text) => {
2955
3296
  const e = document.createElement(tag);
2956
3297
  if (cls) e.className = cls;
2957
3298
  if (text !== void 0) e.textContent = text;
@@ -2962,7 +3303,7 @@ var thumbSource = (thumbnail) => {
2962
3303
  return source.replace(/["\\\n\r]/g, (char) => encodeURIComponent(char));
2963
3304
  };
2964
3305
  var thumb = (thumbnail, credit) => {
2965
- const e = mk2("span", "tr-thumb");
3306
+ const e = mk3("span", "tr-thumb");
2966
3307
  if (credit) e.title = credit;
2967
3308
  else e.setAttribute("aria-hidden", "true");
2968
3309
  e.style.setProperty("--tr-thumb", `url("${thumbSource(thumbnail)}")`);
@@ -2983,42 +3324,43 @@ async function toggleFullscreen() {
2983
3324
  }
2984
3325
  function createUi(o) {
2985
3326
  const { t } = o;
2986
- const style = mk2("style");
2987
- style.textContent = CSS;
2988
- 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");
2989
3330
  const pos = getComputedStyle?.(o.container)?.position;
2990
3331
  if (!pos || pos === "static") o.container.style.position = "relative";
2991
3332
  root.appendChild(style);
2992
3333
  o.container.appendChild(root);
2993
- const cross = mk2("div", "tr-cross");
2994
- cross.appendChild(mk2("i"));
2995
- const top = mk2("div", "tr-top");
2996
- const toolBox = mk2("div", "tr-tool");
2997
- const status = mk2("div", "tr-status");
2998
- const prog = mk2("div", "tr-prog");
2999
- const progLabel = mk2("b");
3000
- const progBar = mk2("div", "tr-bar");
3001
- 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");
3002
3343
  progBar.appendChild(progFill);
3003
3344
  prog.append(progLabel, progBar);
3004
3345
  top.append(toolBox, status, prog);
3005
- const bottom = mk2("div", "tr-bottom");
3006
- const held = mk2("div", "tr-held");
3007
- 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");
3008
3349
  bottom.append(held, hotbar);
3009
- const help = mk2("div", "tr-help");
3350
+ const help = mk3("div", "tr-help");
3010
3351
  help.textContent = t("ui.help");
3011
- const left = mk2("div", "tr-left");
3012
- const insp = mk2("div", "tr-panel tr-insp");
3013
- const logPanel = mk2("div", "tr-panel tr-log");
3014
- left.append(insp, logPanel);
3015
- const logTitle = mk2("h3", void 0, t("ui.log"));
3016
- const logBar = mk2("div", "tr-logbar");
3017
- 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");
3018
3360
  const filters = { action: true, event: true, check: true };
3019
3361
  const filterBtns = [];
3020
3362
  for (const ch of ["action", "event", "check"]) {
3021
- const b = mk2("button", "tr-btn on", t(`ui.filter.${ch}`));
3363
+ const b = mk3("button", "tr-btn on", t(`ui.filter.${ch}`));
3022
3364
  b.type = "button";
3023
3365
  b.onclick = () => {
3024
3366
  filters[ch] = !filters[ch];
@@ -3029,8 +3371,8 @@ function createUi(o) {
3029
3371
  logBar.appendChild(b);
3030
3372
  }
3031
3373
  let autoscroll = true;
3032
- const asLabel = mk2("label");
3033
- const asBox = mk2("input");
3374
+ const asLabel = mk3("label");
3375
+ const asBox = mk3("input");
3034
3376
  asBox.type = "checkbox";
3035
3377
  asBox.checked = true;
3036
3378
  asBox.onchange = () => {
@@ -3039,7 +3381,7 @@ function createUi(o) {
3039
3381
  asLabel.append(asBox, document.createTextNode(" " + t("ui.autoscroll")));
3040
3382
  logBar.appendChild(asLabel);
3041
3383
  const logBtn = (label, fn) => {
3042
- const b = mk2("button", "tr-btn", label);
3384
+ const b = mk3("button", "tr-btn", label);
3043
3385
  b.type = "button";
3044
3386
  b.onclick = fn;
3045
3387
  logBar.appendChild(b);
@@ -3047,7 +3389,7 @@ function createUi(o) {
3047
3389
  logBtn(t("ui.copy_json"), () => {
3048
3390
  const text = o.log.toJSON();
3049
3391
  const select = () => {
3050
- const ta = mk2("textarea");
3392
+ const ta = mk3("textarea");
3051
3393
  ta.value = text;
3052
3394
  ta.setAttribute("readonly", "");
3053
3395
  ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
@@ -3081,12 +3423,12 @@ function createUi(o) {
3081
3423
  const fill = (n, e) => {
3082
3424
  n.className = lineClass(e);
3083
3425
  n.textContent = "";
3084
- const tm = mk2("time", void 0, fmtTime(e.t));
3426
+ const tm = mk3("time", void 0, fmtTime(e.t));
3085
3427
  n.append(tm, document.createTextNode(e.text));
3086
3428
  };
3087
3429
  const addNode = (e) => {
3088
3430
  if (!filters[e.channel]) return;
3089
- const n = mk2("div");
3431
+ const n = mk3("div");
3090
3432
  fill(n, e);
3091
3433
  nodes.set(e.seq, n);
3092
3434
  logBody.appendChild(n);
@@ -3108,18 +3450,18 @@ function createUi(o) {
3108
3450
  if (n) fill(n, e);
3109
3451
  } else addNode(e);
3110
3452
  });
3111
- const right = mk2("div", "tr-right");
3112
- const cat = mk2("div", "tr-panel tr-cat");
3113
- const anim = mk2("div", "tr-panel tr-anim");
3114
- 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");
3115
3457
  const benchPanel = createBenchPanel({ t, num: (n, d) => num(n, d), onControl: o.onControl, onLayer: o.onLayer, onAction: o.onAction });
3116
3458
  right.append(cat, anim, benchPanel.element, tests);
3117
- cat.appendChild(mk2("h3", void 0, t("ui.catalog")));
3118
- const itemSearch = mk2("input", "tr-search");
3459
+ cat.appendChild(mk3("h3", void 0, t("ui.catalog")));
3460
+ const itemSearch = mk3("input", "tr-search");
3119
3461
  itemSearch.type = "search";
3120
3462
  itemSearch.placeholder = t("ui.search_items");
3121
- const tabs = mk2("div", "tr-tabs");
3122
- const items = mk2("div", "tr-items");
3463
+ const tabs = mk3("div", "tr-tabs");
3464
+ const items = mk3("div", "tr-items");
3123
3465
  cat.append(itemSearch, tabs, items);
3124
3466
  let activeGroup = "";
3125
3467
  const visibleItems = () => {
@@ -3131,18 +3473,18 @@ function createUi(o) {
3131
3473
  items.textContent = "";
3132
3474
  const searching = itemSearch.value.trim() !== "";
3133
3475
  const found = visibleItems();
3134
- 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")));
3135
3477
  for (const { item: it, group } of found) {
3136
- const wrap = mk2("div", "tr-item");
3137
- const b = mk2("button", it.thumbnail ? "tr-btn has-thumb" : "tr-btn", it.thumbnail ? void 0 : it.name);
3138
- if (it.thumbnail) b.append(thumb(it.thumbnail), mk2("span", "tr-name", it.name));
3139
- 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));
3140
3482
  b.type = "button";
3141
3483
  b.title = it.note ?? it.name;
3142
3484
  b.onclick = () => o.onSpawn(it.id);
3143
3485
  wrap.appendChild(b);
3144
3486
  if (it.weapon) {
3145
- const w = mk2("button", "tr-btn", "\u2694");
3487
+ const w = mk3("button", "tr-btn", "\u2694");
3146
3488
  w.type = "button";
3147
3489
  w.title = t("ui.equip");
3148
3490
  w.onclick = () => o.onEquip(it.id);
@@ -3156,7 +3498,7 @@ function createUi(o) {
3156
3498
  const groupNames = [...o.groups().keys()];
3157
3499
  if (!groupNames.includes(activeGroup)) activeGroup = groupNames[0] ?? "";
3158
3500
  for (const g of groupNames) {
3159
- 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);
3160
3502
  b.type = "button";
3161
3503
  b.onclick = () => {
3162
3504
  activeGroup = g;
@@ -3169,9 +3511,9 @@ function createUi(o) {
3169
3511
  renderTabs();
3170
3512
  itemSearch.oninput = renderItems;
3171
3513
  for (const box2 of [itemSearch]) box2.addEventListener("keydown", (e) => e.stopPropagation());
3172
- const controls = mk2("div", "tr-row");
3514
+ const controls = mk3("div", "tr-row");
3173
3515
  const ctl = (label, fn) => {
3174
- const b = mk2("button", "tr-btn", label);
3516
+ const b = mk3("button", "tr-btn", label);
3175
3517
  b.type = "button";
3176
3518
  b.onclick = () => fn(b);
3177
3519
  controls.appendChild(b);
@@ -3196,29 +3538,29 @@ function createUi(o) {
3196
3538
  fullscreenBtn.title = t("ui.fullscreen.hint");
3197
3539
  const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
3198
3540
  for (const { key, b } of blastBtns) b.title = t(`ui.blast.${key}.hint`);
3199
- const qSel = mk2("select");
3541
+ const qSel = mk3("select");
3200
3542
  for (const q of ["low", "medium", "high"]) {
3201
- const op = mk2("option", void 0, t(`ui.quality.${q}`));
3543
+ const op = mk3("option", void 0, t(`ui.quality.${q}`));
3202
3544
  op.value = q;
3203
3545
  qSel.appendChild(op);
3204
3546
  }
3205
3547
  qSel.value = o.quality;
3206
3548
  qSel.onchange = () => o.onQuality(qSel.value);
3207
- const qLabel = mk2("label", void 0, t("ui.quality") + " ");
3549
+ const qLabel = mk3("label", void 0, t("ui.quality") + " ");
3208
3550
  qLabel.appendChild(qSel);
3209
3551
  controls.appendChild(qLabel);
3210
- const count = mk2("span");
3552
+ const count = mk3("span");
3211
3553
  controls.appendChild(count);
3212
3554
  cat.appendChild(controls);
3213
- tests.appendChild(mk2("h3", void 0, t("ui.tests")));
3214
- 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"));
3215
3557
  runAll.type = "button";
3216
- const summary = mk2("div", "tr-summary");
3217
- const testSearch = mk2("input", "tr-search");
3558
+ const summary = mk3("div", "tr-summary");
3559
+ const testSearch = mk3("input", "tr-search");
3218
3560
  testSearch.type = "search";
3219
3561
  testSearch.placeholder = t("ui.search_tests");
3220
3562
  testSearch.addEventListener("keydown", (e) => e.stopPropagation());
3221
- const list = mk2("div", "list");
3563
+ const list = mk3("div", "list");
3222
3564
  tests.append(runAll, summary, testSearch, list);
3223
3565
  testSearch.oninput = () => {
3224
3566
  const term = fold(testSearch.value.trim());
@@ -3241,7 +3583,7 @@ function createUi(o) {
3241
3583
  list.textContent = "";
3242
3584
  scBtns.clear();
3243
3585
  for (const s of o.scenarios()) {
3244
- const b = mk2("button", "tr-btn", s.name);
3586
+ const b = mk3("button", "tr-btn", s.name);
3245
3587
  b.type = "button";
3246
3588
  b.onclick = async () => {
3247
3589
  if (busy) return;
@@ -3267,7 +3609,7 @@ function createUi(o) {
3267
3609
  busy = false;
3268
3610
  }
3269
3611
  };
3270
- const toggle = mk2("button", "tr-btn tr-toggle", t("ui.panel"));
3612
+ const toggle = mk3("button", "tr-btn tr-toggle", t("ui.panel"));
3271
3613
  toggle.type = "button";
3272
3614
  const togglePanels = () => {
3273
3615
  root.classList.toggle("open");
@@ -3276,15 +3618,15 @@ function createUi(o) {
3276
3618
  toggle.onclick = togglePanels;
3277
3619
  root.append(cross, top, bottom, help, left, right, toggle);
3278
3620
  const bar = (value, max, cls) => {
3279
- const b = mk2("div", `tr-bar ${cls}`);
3280
- const i = mk2("i");
3621
+ const b = mk3("div", `tr-bar ${cls}`);
3622
+ const i = mk3("i");
3281
3623
  i.style.width = `${Math.max(0, Math.min(100, value / Math.max(1e-9, max) * 100))}%`;
3282
3624
  b.appendChild(i);
3283
3625
  return b;
3284
3626
  };
3285
3627
  const row = (k, v) => {
3286
- const r = mk2("div", "r");
3287
- 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));
3288
3630
  return r;
3289
3631
  };
3290
3632
  const num = (n, d = 1) => {
@@ -3293,13 +3635,13 @@ function createUi(o) {
3293
3635
  };
3294
3636
  const renderInspector = (i) => {
3295
3637
  insp.textContent = "";
3296
- insp.appendChild(mk2("h3", void 0, t("ui.inspector")));
3638
+ insp.appendChild(mk3("h3", void 0, t("ui.inspector")));
3297
3639
  if (!i) {
3298
- insp.appendChild(mk2("div", void 0, t("ui.aim_at_something")));
3640
+ insp.appendChild(mk3("div", void 0, t("ui.aim_at_something")));
3299
3641
  return;
3300
3642
  }
3301
3643
  if (i.thumbnail) insp.appendChild(thumb(i.thumbnail, i.thumbnailCredit));
3302
- insp.appendChild(mk2("div", "t", `${i.object} \u203A ${i.part}`));
3644
+ insp.appendChild(mk3("div", "t", `${i.object} \u203A ${i.part}`));
3303
3645
  insp.appendChild(row(t("ins.material"), i.material));
3304
3646
  insp.appendChild(row(t("ins.status"), i.status + (i.burning ? ` \xB7 ${t("ins.burning")}` : "")));
3305
3647
  insp.appendChild(row(t("ins.hp"), `${num(i.hp, 0)} / ${num(i.maxHp, 0)}`));
@@ -3318,30 +3660,30 @@ function createUi(o) {
3318
3660
  insp.appendChild(bar(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
3319
3661
  }
3320
3662
  if (i.joints.length) {
3321
- insp.appendChild(mk2("div", void 0, t("ins.joints") + ":"));
3663
+ insp.appendChild(mk3("div", void 0, t("ins.joints") + ":"));
3322
3664
  for (const j of i.joints) {
3323
3665
  const tools = j.tools.length ? j.tools.join(", ") : t("tool.none");
3324
- 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}`}`));
3325
3667
  }
3326
3668
  }
3327
- if (i.note) insp.appendChild(mk2("div", void 0, i.note));
3669
+ if (i.note) insp.appendChild(mk3("div", void 0, i.note));
3328
3670
  };
3329
3671
  let lastAnim = "";
3330
3672
  const renderAnimations = (a) => {
3331
3673
  anim.textContent = "";
3332
- anim.appendChild(mk2("h3", void 0, t("ui.animations")));
3674
+ anim.appendChild(mk3("h3", void 0, t("ui.animations")));
3333
3675
  if (!a) {
3334
- anim.appendChild(mk2("div", "sub", t("ui.animations_aim")));
3676
+ anim.appendChild(mk3("div", "sub", t("ui.animations_aim")));
3335
3677
  return;
3336
3678
  }
3337
- anim.appendChild(mk2("div", "sub", a.name));
3679
+ anim.appendChild(mk3("div", "sub", a.name));
3338
3680
  if (a.clips.length === 0) {
3339
- anim.appendChild(mk2("div", "sub", t("ui.animations_none")));
3681
+ anim.appendChild(mk3("div", "sub", t("ui.animations_none")));
3340
3682
  return;
3341
3683
  }
3342
- const l = mk2("div", "list");
3684
+ const l = mk3("div", "list");
3343
3685
  for (const c of a.clips) {
3344
- 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)`);
3345
3687
  b.type = "button";
3346
3688
  b.onclick = () => o.onPlay(a.objectId, c.id);
3347
3689
  l.appendChild(b);
@@ -3358,8 +3700,8 @@ function createUi(o) {
3358
3700
  hud(s) {
3359
3701
  toolBox.textContent = "";
3360
3702
  toolBox.append(document.createTextNode(s.toolName));
3361
- if (s.ammo) toolBox.append(mk2("small", void 0, s.ammo));
3362
- 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}`)));
3363
3705
  const helpText = s.camera ? `${t("ui.help")} \xB7 ${t("ui.help.camera")}` : t("ui.help");
3364
3706
  if (help.textContent !== helpText) help.textContent = helpText;
3365
3707
  if (s.status) status.textContent = s.status;
@@ -3382,7 +3724,7 @@ function createUi(o) {
3382
3724
  ];
3383
3725
  if (s.held.weapon) keys.push(["H", t("hud.equip", { weapon: s.held.weapon })]);
3384
3726
  for (const [k, label] of keys) {
3385
- held.append(mk2("kbd", void 0, k), document.createTextNode(` ${label} `));
3727
+ held.append(mk3("kbd", void 0, k), document.createTextNode(` ${label} `));
3386
3728
  }
3387
3729
  } else held.style.display = "none";
3388
3730
  const key = s.slots.join("|") + s.page;
@@ -3392,9 +3734,9 @@ function createUi(o) {
3392
3734
  slotsEl.length = 0;
3393
3735
  hudPage = s.page - 1;
3394
3736
  s.slots.forEach((name, i) => {
3395
- const b = mk2("button", "tr-slot");
3737
+ const b = mk3("button", "tr-slot");
3396
3738
  b.type = "button";
3397
- 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));
3398
3740
  b.title = name;
3399
3741
  b.onclick = () => o.onTool(hudPage * 10 + i);
3400
3742
  hotbar.appendChild(b);
@@ -3426,6 +3768,7 @@ function createUi(o) {
3426
3768
  refresh() {
3427
3769
  renderTabs();
3428
3770
  renderScenarios();
3771
+ tablePanel?.refresh();
3429
3772
  },
3430
3773
  showGroup(group) {
3431
3774
  activeGroup = group;
@@ -3440,6 +3783,7 @@ function createUi(o) {
3440
3783
  },
3441
3784
  dispose() {
3442
3785
  offLog();
3786
+ tablePanel?.dispose();
3443
3787
  if (toastTimer) clearTimeout(toastTimer);
3444
3788
  root.remove();
3445
3789
  }
@@ -4629,6 +4973,10 @@ function createTestRoom(options) {
4629
4973
  heldPiece = null;
4630
4974
  heldTilt = 0;
4631
4975
  };
4976
+ const handUse = () => {
4977
+ if (heldPiece) grab();
4978
+ else useTool(FIXED_DT, false);
4979
+ };
4632
4980
  const grab = () => {
4633
4981
  if (heldPiece) {
4634
4982
  const name = heldName;
@@ -5097,10 +5445,10 @@ function createTestRoom(options) {
5097
5445
  if (tiltKey || heldTilt > 0) {
5098
5446
  heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
5099
5447
  }
5100
- if (useHeld && !paused) {
5448
+ if ((useHeld || useFirst) && !paused) {
5101
5449
  if (tool.def.category === "hand") {
5102
- if (useFirst) grab();
5103
- } else useTool(dt, !useFirst);
5450
+ if (useFirst) handUse();
5451
+ } else if (useHeld) useTool(dt, !useFirst);
5104
5452
  useFirst = false;
5105
5453
  }
5106
5454
  if (!paused) followHeld(dt);
@@ -5655,6 +6003,63 @@ function createTestRoom(options) {
5655
6003
  const row = (list, gap, z) => list.map((item, index) => spawn(item.id, [(index - (list.length - 1) / 2) * gap, 0, z], 0));
5656
6004
  return [...row(front, 1.6, 1.5), ...row(back, 4.5, -4)];
5657
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
+ );
5658
6063
  const room = {
5659
6064
  world,
5660
6065
  renderer,
@@ -5674,6 +6079,7 @@ function createTestRoom(options) {
5674
6079
  get scenarios() {
5675
6080
  return allScenarios();
5676
6081
  },
6082
+ testTable,
5677
6083
  spawn,
5678
6084
  clear,
5679
6085
  resetAll,
@@ -5681,7 +6087,8 @@ function createTestRoom(options) {
5681
6087
  useItem,
5682
6088
  use(hold = 0) {
5683
6089
  if (tool.def.category === "hand") {
5684
- grab();
6090
+ handUse();
6091
+ flush();
5685
6092
  return;
5686
6093
  }
5687
6094
  useTool(FIXED_DT, false);
@@ -5914,7 +6321,8 @@ function createTestRoom(options) {
5914
6321
  },
5915
6322
  onAction: (id) => {
5916
6323
  if (activeBench) slowly(activeBench, id, () => benches.get(activeBench)?.act(id));
5917
- }
6324
+ },
6325
+ ...options.testTable !== false && arena.tops.some((x) => x.id === tableScenery) ? { testTable } : {}
5918
6326
  });
5919
6327
  ui.hud(hudState());
5920
6328
  showBench();
@@ -5967,19 +6375,25 @@ function structuredCloneSpec(spec) {
5967
6375
  export {
5968
6376
  DEFAULT_HAND_STANCES,
5969
6377
  DEFAULT_STRIKE_CLIPS,
6378
+ DEFAULT_TRIALS,
5970
6379
  LOADING_CLASS,
5971
6380
  LOADING_CSS,
5972
6381
  MATERIAL_COLORS,
6382
+ TRIAL_KINDS,
5973
6383
  brickWallSpec,
5974
6384
  buildFromSpec,
5975
6385
  createLoading,
5976
6386
  createLog,
5977
6387
  createTestRoom,
6388
+ createTestTable,
5978
6389
  decodeBenchState,
5979
6390
  encodeBenchState,
6391
+ fitRow,
5980
6392
  loadingMarkup,
5981
6393
  locales,
5982
6394
  nextPaint,
5983
- resolveStrings
6395
+ resolveStrings,
6396
+ rowCentres,
6397
+ trialView
5984
6398
  };
5985
6399
  //# sourceMappingURL=index.js.map