@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.
@@ -23,20 +23,26 @@ var GameTestRoom = (() => {
23
23
  __export(index_exports, {
24
24
  DEFAULT_HAND_STANCES: () => DEFAULT_HAND_STANCES,
25
25
  DEFAULT_STRIKE_CLIPS: () => DEFAULT_STRIKE_CLIPS,
26
+ DEFAULT_TRIALS: () => DEFAULT_TRIALS,
26
27
  LOADING_CLASS: () => LOADING_CLASS,
27
28
  LOADING_CSS: () => LOADING_CSS,
28
29
  MATERIAL_COLORS: () => MATERIAL_COLORS,
30
+ TRIAL_KINDS: () => TRIAL_KINDS,
29
31
  brickWallSpec: () => brickWallSpec,
30
32
  buildFromSpec: () => buildFromSpec,
31
33
  createLoading: () => createLoading,
32
34
  createLog: () => createLog,
33
35
  createTestRoom: () => createTestRoom,
36
+ createTestTable: () => createTestTable,
34
37
  decodeBenchState: () => decodeBenchState,
35
38
  encodeBenchState: () => encodeBenchState,
39
+ fitRow: () => fitRow,
36
40
  loadingMarkup: () => loadingMarkup,
37
41
  locales: () => locales4,
38
42
  nextPaint: () => nextPaint,
39
- resolveStrings: () => resolveStrings5
43
+ resolveStrings: () => resolveStrings5,
44
+ rowCentres: () => rowCentres,
45
+ trialView: () => trialView
40
46
  });
41
47
 
42
48
  // ../game-contracts/dist/index.js
@@ -469,6 +475,13 @@ var GameTestRoom = (() => {
469
475
  }
470
476
  };
471
477
  }
478
+ var KIT_ORIGIN_KEY = "kitOrigin";
479
+ function markKitOrigin(object, origin) {
480
+ const node = object;
481
+ node.userData = node.userData ?? {};
482
+ node.userData[KIT_ORIGIN_KEY] = { ...origin };
483
+ return object;
484
+ }
472
485
 
473
486
  // ../game-destruction/dist/index.js
474
487
  var FIRE_PRIME_C = 300;
@@ -799,6 +812,23 @@ var GameTestRoom = (() => {
799
812
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
800
813
  };
801
814
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
815
+ var tearStack = (inp, root, t, u, v, n, sheet) => {
816
+ const { dims, rnd: rnd2, maxFragments } = inp;
817
+ const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
818
+ const per = clamp(n, 3, Math.max(3, Math.floor(maxFragments / layers)));
819
+ const out = [];
820
+ for (let k = 0; k < layers; k++) {
821
+ const off = -dims[t] / 2 + (k + 0.5) * dims[t] / layers;
822
+ for (const b of bsp(root, per, [u, v], rnd2)) {
823
+ if (out.length >= maxFragments) return out;
824
+ const c = [b.c[0], b.c[1], b.c[2]];
825
+ c[t] = off;
826
+ out.push({ center: c, size: [b.s[u], b.s[v], sheet], q: frame(u, v, t, (rnd2() - 0.5) * 0.8), shape: "scrap", weight: b.s[u] * b.s[v], inner: out.length % 4 !== 0 });
827
+ }
828
+ }
829
+ return out;
830
+ };
831
+ var STACK_LAYER = 0.012;
802
832
  var planFragments = (inp) => {
803
833
  const { style, dims, rnd: rnd2, maxFragments } = inp;
804
834
  const [fmin, fmax] = inp.fragmentSize;
@@ -841,6 +871,8 @@ var GameTestRoom = (() => {
841
871
  }
842
872
  if (style === "tear") {
843
873
  const n2 = clamp(Math.round(dims[u] * dims[v] / (mean * mean)), 3, maxFragments);
874
+ const sheet = inp.sheet ?? 0;
875
+ if (sheet > 0 && dims[t] > sheet * 4) return tearStack(inp, root, t, u, v, n2, sheet);
844
876
  const th2 = Math.min(inp.thickness, dims[t]);
845
877
  for (const b of bsp(root, n2, [u, v], rnd2)) {
846
878
  out.push({ center: [b.c[0], b.c[1], b.c[2]], size: [b.s[u], b.s[v], th2], q: frame(u, v, t), shape: "scrap", weight: b.s[u] * b.s[v] });
@@ -2413,6 +2445,7 @@ var GameTestRoom = (() => {
2413
2445
  0.03,
2414
2446
  0.15
2415
2447
  ],
2448
+ sheet: 1e-3,
2416
2449
  damageScale: {
2417
2450
  slash: 3
2418
2451
  },
@@ -2428,7 +2461,8 @@ var GameTestRoom = (() => {
2428
2461
  smoke: "grey"
2429
2462
  },
2430
2463
  visual: {
2431
- charColor: 1709072
2464
+ charColor: 1709072,
2465
+ innerColor: 15524048
2432
2466
  },
2433
2467
  tags: [
2434
2468
  "fabric",
@@ -4146,7 +4180,8 @@ var GameTestRoom = (() => {
4146
4180
  hit: hitLocal,
4147
4181
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4148
4182
  maxFragments,
4149
- rnd: rnd2
4183
+ rnd: rnd2,
4184
+ sheet: p.mat.sheet
4150
4185
  });
4151
4186
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
4152
4187
  const mass = p.st.mass;
@@ -4170,7 +4205,8 @@ var GameTestRoom = (() => {
4170
4205
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd2(), dv[2] * k],
4171
4206
  angularVelocity: [(rnd2() - 0.5) * 12, (rnd2() - 0.5) * 12, (rnd2() - 0.5) * 12],
4172
4207
  mass: mass * f.weight / totW,
4173
- ...marks.length > 0 ? { marks } : {}
4208
+ ...marks.length > 0 ? { marks } : {},
4209
+ ...f.inner ? { inner: true } : {}
4174
4210
  };
4175
4211
  });
4176
4212
  const reinforcement = p.spec.reinforcement;
@@ -15510,7 +15546,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15510
15546
  for (const mesh2 of anchors.meshes) mesh2.material.dispose();
15511
15547
  anchors = void 0;
15512
15548
  };
15513
- const root = new THREE.Group();
15549
+ const root = markKitOrigin(new THREE.Group(), { owner: "game-vfx-three", kind: "effect" });
15514
15550
  options.scene.add(root);
15515
15551
  const direction = new THREE.Vector3(...options.lightDirection ?? LAB_LIGHT_DIRECTION).normalize();
15516
15552
  const sky = hexToLinearRgb(SKY_COLOR);
@@ -15735,6 +15771,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15735
15771
  const bl = Math.round(ab + ((b & 255) - ab) * t);
15736
15772
  return r << 16 | g << 8 | bl;
15737
15773
  }
15774
+ var SHEET_MAX = 4e-3;
15775
+ var SHEET_AIR = 2.5;
15776
+ var SHEET_SWAY = 3;
15738
15777
  var FragmentSystem = class {
15739
15778
  constructor(THREE, o) {
15740
15779
  __publicField2(this, "THREE", THREE);
@@ -15840,7 +15879,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15840
15879
  asleep: false,
15841
15880
  rest: 0,
15842
15881
  fade: 1,
15843
- dying: false
15882
+ dying: false,
15883
+ air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0
15844
15884
  };
15845
15885
  this.list.push(live);
15846
15886
  let alive = this.list.reduce((n, l) => n + (l.dying ? 0 : 1), 0);
@@ -15924,6 +15964,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15924
15964
  }
15925
15965
  } else {
15926
15966
  f.ang.multiplyScalar(Math.exp(-0.15 * dt));
15967
+ if (f.air > 0) this.flutter(f, dt);
15927
15968
  }
15928
15969
  if (contact && f.vel.lengthSq() < 4) this.settle(f, dt);
15929
15970
  const slow = f.vel.lengthSq() < 0.012 && f.ang.lengthSq() < 0.2;
@@ -15938,6 +15979,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15938
15979
  f.still = 0;
15939
15980
  }
15940
15981
  }
15982
+ /** A sheet in the air: drag slows it to a slow fall, and it sways and turns over as it goes. */
15983
+ flutter(f, dt) {
15984
+ f.vel.multiplyScalar(Math.exp(-f.air * dt));
15985
+ f.vel.x += (this.rng() - 0.5) * SHEET_SWAY * dt;
15986
+ f.vel.z += (this.rng() - 0.5) * SHEET_SWAY * dt;
15987
+ f.ang.x += (this.rng() - 0.5) * SHEET_SWAY * 4 * dt;
15988
+ f.ang.z += (this.rng() - 0.5) * SHEET_SWAY * 4 * dt;
15989
+ }
15941
15990
  /** Tip the fragment towards lying on its flattest face. */
15942
15991
  settle(f, dt) {
15943
15992
  const mesh2 = f.mesh;
@@ -18218,6 +18267,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18218
18267
  const def = pv ? this.world.materials.get(pv.material) : void 0;
18219
18268
  if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
18220
18269
  if (kind === "fibers" || kind === "leaves") {
18270
+ if (def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
18221
18271
  const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
18222
18272
  return c >= 0 ? { color: c } : void 0;
18223
18273
  }
@@ -18366,7 +18416,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18366
18416
  for (let i = 0; i < ev.fragments.length; i += step) {
18367
18417
  const fr = ev.fragments[i];
18368
18418
  const fdef = this.world.materials.get(fr.material);
18369
- const mat = this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), transparent || fdef?.visual?.transparent === true, opacity);
18419
+ const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def || !fdef ? color : fallbackColor(fr.material, fdef);
18420
+ const mat = this.fragments.materialFor(fr.material, tone2, transparent || fdef?.visual?.transparent === true, opacity);
18370
18421
  this.fragments.spawn(fr, mat, ev.object, maxAlive);
18371
18422
  }
18372
18423
  const up = [0, 1, 0];
@@ -18384,7 +18435,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18384
18435
  this.vfx.burst("dust", ev.point, up, 5, 0.8, { color: 11901556 });
18385
18436
  break;
18386
18437
  case "tear":
18387
- this.vfx.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color });
18438
+ this.vfx.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color: def?.visual?.innerColor ?? color });
18388
18439
  break;
18389
18440
  case "snap":
18390
18441
  this.vfx.burst("chips", ev.point, up, 8 * sizeK, 3);
@@ -38143,7 +38194,7 @@ void main(){
38143
38194
  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 }));
38144
38195
  }
38145
38196
  function createArena(THREE, size, scene2, scenery, tapOption) {
38146
- const group = new THREE.Group();
38197
+ const group = markKitOrigin(new THREE.Group(), { owner: "game-test-room", kind: "room" });
38147
38198
  group.name = "test-room-arena";
38148
38199
  scene2.background = new THREE.Color(10466504);
38149
38200
  group.add(new THREE.HemisphereLight(13624319, 7037527, 0.75));
@@ -38741,6 +38792,14 @@ void main(){
38741
38792
  ...projectile.fire ? { fire: projectile.fire } : {}
38742
38793
  });
38743
38794
  };
38795
+ const blastAt = (weapon, explosive, point) => {
38796
+ const def = arsenal.explosives.get(explosive);
38797
+ if (!def) return false;
38798
+ const event = new ExplosiveBody({ explosive: def, origin: point, triggers: ["remote"], rules: arsenal.rules.explosives }).trigger("remote");
38799
+ if (event?.type !== "detonate") return false;
38800
+ detonate({ weapon }, event.detonation);
38801
+ return true;
38802
+ };
38744
38803
  const detonateCharges = () => {
38745
38804
  let count = 0;
38746
38805
  for (const projectile of [...projectiles]) {
@@ -38832,7 +38891,172 @@ void main(){
38832
38891
  }
38833
38892
  }
38834
38893
  };
38835
- return { throwUse, placeUse, backblastUse, detonateCharges, update, clear };
38894
+ return { throwUse, placeUse, backblastUse, detonateCharges, blastAt, update, clear };
38895
+ }
38896
+
38897
+ // src/test-table.ts
38898
+ var TRIAL_KINDS = ["shot", "punch", "stab", "fire", "blast"];
38899
+ var DEFAULT_TRIALS = {
38900
+ shot: { weapon: "pistol_9mm", distance: 2.5, lift: 0.5 },
38901
+ punch: { weapon: "fists", distance: 0.3, lift: 0.15 },
38902
+ stab: { weapon: "knife", distance: 0.22, lift: 0.1 },
38903
+ fire: { weapon: "flamethrower", distance: 1.6, lift: 0.4, hold: 0.8 },
38904
+ blast: { weapon: "frag_grenade", distance: 0.3, lift: 1, watch: 6 }
38905
+ };
38906
+ var resolveTrials = (overrides = {}) => Object.fromEntries(TRIAL_KINDS.map((kind) => [kind, { ...DEFAULT_TRIALS[kind], ...overrides[kind] }]));
38907
+ function fitRow(width, start, total, length3, gap) {
38908
+ let used = 0;
38909
+ let count = 0;
38910
+ for (let i = start; i < total; i++) {
38911
+ const w = width(i);
38912
+ const next = count === 0 ? w : used + gap + w;
38913
+ if (count > 0 && next > length3) break;
38914
+ used = next;
38915
+ count++;
38916
+ }
38917
+ return count;
38918
+ }
38919
+ function rowCentres(widths, centreX, gap) {
38920
+ const length3 = widths.reduce((sum2, w) => sum2 + w, 0) + gap * Math.max(0, widths.length - 1);
38921
+ let x = centreX - length3 / 2;
38922
+ return widths.map((w) => {
38923
+ const centre = x + w / 2;
38924
+ x += w + gap;
38925
+ return centre;
38926
+ });
38927
+ }
38928
+ function trialView(tool, kind, min2, max2) {
38929
+ const target = [(min2[0] + max2[0]) / 2, (min2[1] + max2[1]) / 2, (min2[2] + max2[2]) / 2];
38930
+ const away = kind === "blast" ? tool.watch ?? 6 : tool.distance;
38931
+ return { eye: [target[0], target[1] + tool.lift, max2[2] + away], target };
38932
+ }
38933
+ function createTestTable(context, options = {}) {
38934
+ const gap = options.gap ?? 0.12;
38935
+ const margin = options.margin ?? 0.1;
38936
+ const trials = resolveTrials(options.trials);
38937
+ const listeners = /* @__PURE__ */ new Set();
38938
+ let current = null;
38939
+ const itemsOf = (group) => context.groups().get(group) ?? [];
38940
+ const emit2 = () => {
38941
+ const page = state();
38942
+ for (const listener of [...listeners]) listener(page);
38943
+ return page;
38944
+ };
38945
+ const state = () => {
38946
+ if (!current) return null;
38947
+ const total = itemsOf(current.group).length;
38948
+ const live = current.objects.filter((o) => context.box(o.objectId));
38949
+ return {
38950
+ group: current.group,
38951
+ page: current.page,
38952
+ first: current.first,
38953
+ total,
38954
+ objects: live,
38955
+ selected: current.selected,
38956
+ hasPrev: current.page > 0,
38957
+ hasNext: current.first + current.objects.length < total
38958
+ };
38959
+ };
38960
+ const bounds = /* @__PURE__ */ new Map();
38961
+ const widthOf = (itemId) => context.measure(itemId).w;
38962
+ const boundsUntil = (group, length3, until) => {
38963
+ const items = itemsOf(group);
38964
+ const known = bounds.get(group) ?? [0];
38965
+ bounds.set(group, known);
38966
+ while (known[known.length - 1] < items.length && !until(known)) {
38967
+ const first = known[known.length - 1];
38968
+ known.push(first + fitRow((i) => widthOf(items[i].id), first, items.length, length3, gap));
38969
+ }
38970
+ return known;
38971
+ };
38972
+ const lengthOf2 = (top) => top.maxX - top.minX - 2 * margin;
38973
+ const lay = (group, page, top) => {
38974
+ const items = itemsOf(group);
38975
+ const known = boundsUntil(group, lengthOf2(top), (b) => b.length > page + 1);
38976
+ const index = Math.max(0, Math.min(page, known.length - 2));
38977
+ const first = known[index];
38978
+ const laid = items.slice(first, known[index + 1]);
38979
+ context.clear();
38980
+ const sizes = laid.map((item) => context.measure(item.id));
38981
+ const centres = rowCentres(sizes.map((size) => size.w), (top.minX + top.maxX) / 2, gap);
38982
+ const objects = laid.map((item, i) => {
38983
+ const d = Math.min(sizes[i].d, top.maxZ - top.minZ);
38984
+ const { objectId } = context.spawn(item.id, [centres[i], top.top, top.maxZ - margin / 2 - d / 2]);
38985
+ return { objectId, itemId: item.id, name: item.name };
38986
+ });
38987
+ current = { group, page: index, first, objects, selected: objects[0]?.objectId ?? null };
38988
+ 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) });
38989
+ return emit2();
38990
+ };
38991
+ const open = (group, page = 0) => {
38992
+ const top = context.top();
38993
+ if (!itemsOf(group).length || !top) return null;
38994
+ return lay(group, page, top);
38995
+ };
38996
+ const find = (itemId) => {
38997
+ const top = context.top();
38998
+ const group = [...context.groups()].find(([, items]) => items.some((i) => i.id === itemId))?.[0];
38999
+ if (group === void 0 || !top) return null;
39000
+ const index = itemsOf(group).findIndex((i) => i.id === itemId);
39001
+ const known = boundsUntil(group, lengthOf2(top), (b) => b[b.length - 1] > index);
39002
+ lay(group, known.findIndex((bound, i) => bound <= index && index < known[i + 1]), top);
39003
+ const target = current?.objects.find((o) => o.itemId === itemId);
39004
+ if (current && target) current.selected = target.objectId;
39005
+ return emit2();
39006
+ };
39007
+ const selected = (objectId) => {
39008
+ const id = objectId ?? current?.selected ?? null;
39009
+ return id && context.box(id) ? id : null;
39010
+ };
39011
+ const trial = (kind, objectId) => {
39012
+ const id = selected(objectId);
39013
+ const box2 = id ? context.box(id) : null;
39014
+ if (!id || !box2) return false;
39015
+ const tool = trials[kind];
39016
+ if (!context.equip(tool.weapon)) return false;
39017
+ const view = trialView(tool, kind, box2.min, box2.max);
39018
+ context.look(view.eye, view.target);
39019
+ const name = current?.objects.find((o) => o.objectId === id)?.name ?? id;
39020
+ context.log("trial", context.translate(`act.trial.${kind}`, { name }), { kind, objectId: id, weapon: tool.weapon });
39021
+ if (kind === "blast") {
39022
+ const point = [view.target[0], box2.min[1] + 0.05, box2.min[2] - tool.distance];
39023
+ return context.blastAt(tool.weapon, point);
39024
+ }
39025
+ context.use(tool.hold ?? 0);
39026
+ emit2();
39027
+ return true;
39028
+ };
39029
+ return {
39030
+ groups: () => [...context.groups()].filter(([, items]) => items.length > 0).map(([group]) => group),
39031
+ items: () => [...context.groups()].flatMap(([group, items]) => items.map((item) => ({ id: item.id, name: item.name, group }))),
39032
+ open,
39033
+ find,
39034
+ next: () => {
39035
+ const page = state();
39036
+ return page?.hasNext ? open(page.group, page.page + 1) : page;
39037
+ },
39038
+ prev: () => {
39039
+ const page = state();
39040
+ return page?.hasPrev ? open(page.group, page.page - 1) : page;
39041
+ },
39042
+ relay: () => {
39043
+ if (!current) return null;
39044
+ context.resetAll();
39045
+ return open(current.group, current.page);
39046
+ },
39047
+ state,
39048
+ select: (objectId) => {
39049
+ if (!current || !current.objects.some((o) => o.objectId === objectId) || !context.box(objectId)) return false;
39050
+ current.selected = objectId;
39051
+ emit2();
39052
+ return true;
39053
+ },
39054
+ trial,
39055
+ on: (listener) => {
39056
+ listeners.add(listener);
39057
+ return () => listeners.delete(listener);
39058
+ }
39059
+ };
38836
39060
  }
38837
39061
 
38838
39062
  // src/host-events.ts
@@ -40264,7 +40488,7 @@ void main(){
40264
40488
  "hud.throw": "throw",
40265
40489
  "hud.tilt": "tilt (hold)",
40266
40490
  "hud.equip": "equip {weapon}",
40267
- "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",
40491
+ "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",
40268
40492
  "ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
40269
40493
  "camera.first": "1st person",
40270
40494
  "camera.third": "3rd person",
@@ -40278,6 +40502,32 @@ void main(){
40278
40502
  "ui.copy_json": "Copy JSON",
40279
40503
  "ui.download_ndjson": "Download NDJSON",
40280
40504
  "ui.download_txt": "Download TXT",
40505
+ "ui.test_table": "Test table",
40506
+ "ui.test_table.group": "Group to lay on the table",
40507
+ "ui.test_table.lay": "Lay",
40508
+ "ui.test_table.prev": "Previous items",
40509
+ "ui.test_table.next": "Next items",
40510
+ "ui.test_table.relay": "Put back",
40511
+ "ui.test_table.search": "Find an item to put on the table",
40512
+ "ui.test_table.hint": "Pick a group and lay it on the table, then pick an item and try it.",
40513
+ "ui.test_table.page": "Items {from}\u2013{to} of {total}. Pick one, then a trial:",
40514
+ "ui.trial.shot": "Shot",
40515
+ "ui.trial.punch": "Punch",
40516
+ "ui.trial.stab": "Knife",
40517
+ "ui.trial.fire": "Fire",
40518
+ "ui.trial.blast": "Blast",
40519
+ "ui.trial.shot.hint": "A 9 mm round from 2.5 m",
40520
+ "ui.trial.punch.hint": "A punch, up close",
40521
+ "ui.trial.stab.hint": "A knife blow, up close",
40522
+ "ui.trial.fire.hint": "A flamethrower burst from 1.6 m",
40523
+ "ui.trial.blast.hint": "A fragmentation grenade goes off just behind it (it reaches the neighbours too)",
40524
+ "act.test_table": "Test table: {group}, items {from}\u2013{to} of {total}",
40525
+ "act.trial.shot": "Trial: shooting {name}",
40526
+ "act.trial.punch": "Trial: punching {name}",
40527
+ "act.trial.stab": "Trial: stabbing {name}",
40528
+ "act.trial.fire": "Trial: setting fire to {name}",
40529
+ "act.trial.blast": "Trial: a blast beside {name}",
40530
+ "arena.test_table": "Test table",
40281
40531
  "ui.clear_log": "Clear",
40282
40532
  "ui.catalog": "Catalogue",
40283
40533
  "ui.clear": "Clear",
@@ -40545,7 +40795,7 @@ void main(){
40545
40795
  "hud.throw": "arremessar",
40546
40796
  "hud.tilt": "inclinar (segurar)",
40547
40797
  "hud.equip": "equipar {weapon}",
40548
- "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",
40798
+ "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",
40549
40799
  "ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
40550
40800
  "camera.first": "1\xAA pessoa",
40551
40801
  "camera.third": "3\xAA pessoa",
@@ -40559,6 +40809,32 @@ void main(){
40559
40809
  "ui.copy_json": "Copiar JSON",
40560
40810
  "ui.download_ndjson": "Baixar NDJSON",
40561
40811
  "ui.download_txt": "Baixar TXT",
40812
+ "ui.test_table": "Mesa de testes",
40813
+ "ui.test_table.group": "Grupo para p\xF4r na mesa",
40814
+ "ui.test_table.lay": "P\xF4r na mesa",
40815
+ "ui.test_table.prev": "Itens anteriores",
40816
+ "ui.test_table.next": "Pr\xF3ximos itens",
40817
+ "ui.test_table.relay": "Repor",
40818
+ "ui.test_table.search": "Buscar item para p\xF4r na mesa",
40819
+ "ui.test_table.hint": "Escolha um grupo e ponha na mesa; depois escolha um item e teste.",
40820
+ "ui.test_table.page": "Itens {from}\u2013{to} de {total}. Escolha um e um teste:",
40821
+ "ui.trial.shot": "Tiro",
40822
+ "ui.trial.punch": "Soco",
40823
+ "ui.trial.stab": "Faca",
40824
+ "ui.trial.fire": "Fogo",
40825
+ "ui.trial.blast": "Explos\xE3o",
40826
+ "ui.trial.shot.hint": "Um tiro de 9 mm a 2,5 m",
40827
+ "ui.trial.punch.hint": "Um soco, de perto",
40828
+ "ui.trial.stab.hint": "Um golpe de faca, de perto",
40829
+ "ui.trial.fire.hint": "Uma rajada de lan\xE7a-chamas a 1,6 m",
40830
+ "ui.trial.blast.hint": "Uma granada de fragmenta\xE7\xE3o explode logo atr\xE1s dele (pega os vizinhos tamb\xE9m)",
40831
+ "act.test_table": "Mesa de testes: {group}, itens {from}\u2013{to} de {total}",
40832
+ "act.trial.shot": "Teste: tiro em {name}",
40833
+ "act.trial.punch": "Teste: soco em {name}",
40834
+ "act.trial.stab": "Teste: faca em {name}",
40835
+ "act.trial.fire": "Teste: fogo em {name}",
40836
+ "act.trial.blast": "Teste: explos\xE3o ao lado de {name}",
40837
+ "arena.test_table": "Mesa de testes",
40562
40838
  "ui.clear_log": "Limpar",
40563
40839
  "ui.catalog": "Cat\xE1logo",
40564
40840
  "ui.clear": "Limpar",
@@ -40900,6 +41176,122 @@ void main(){
40900
41176
  };
40901
41177
  }
40902
41178
 
41179
+ // src/test-table-panel.ts
41180
+ var TEST_TABLE_CSS = `
41181
+ .tr-table{flex:0 0 auto;max-height:42%}
41182
+ .tr-table .sub{color:#9aa3b2;margin:4px 0}
41183
+ .tr-table .list{display:flex;flex-wrap:wrap;gap:3px}
41184
+ .tr-table .list .tr-btn{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
41185
+ .tr-table select{flex:1;min-width:0;max-width:55%}
41186
+ .tr-table .tr-search{margin:6px 0 0}
41187
+ .tr-root .tr-table button.tr-btn.on{background:#3b3a14;border-color:#ffd23f;color:#fff}
41188
+ `;
41189
+ var mk2 = (tag, cls, text) => {
41190
+ const e = document.createElement(tag);
41191
+ if (cls) e.className = cls;
41192
+ if (text !== void 0) e.textContent = text;
41193
+ return e;
41194
+ };
41195
+ function createTestTablePanel(table, t) {
41196
+ const element = mk2("div", "tr-panel tr-table");
41197
+ element.appendChild(mk2("h3", void 0, t("ui.test_table")));
41198
+ const head = mk2("div", "tr-row");
41199
+ const select = mk2("select");
41200
+ select.setAttribute("aria-label", t("ui.test_table.group"));
41201
+ const lay = mk2("button", "tr-btn", t("ui.test_table.lay"));
41202
+ const prev = mk2("button", "tr-btn", "\u25C0");
41203
+ const next = mk2("button", "tr-btn", "\u25B6");
41204
+ prev.title = t("ui.test_table.prev");
41205
+ next.title = t("ui.test_table.next");
41206
+ for (const b of [lay, prev, next]) b.type = "button";
41207
+ head.append(select, lay, prev, next);
41208
+ const search = mk2("input", "tr-search");
41209
+ search.type = "search";
41210
+ search.placeholder = t("ui.test_table.search");
41211
+ const found = mk2("div", "list");
41212
+ const sub4 = mk2("div", "sub", t("ui.test_table.hint"));
41213
+ const list = mk2("div", "list");
41214
+ const trials = mk2("div", "tr-row");
41215
+ const trialButtons = TRIAL_KINDS.map((kind) => {
41216
+ const b = mk2("button", "tr-btn", t(`ui.trial.${kind}`));
41217
+ b.type = "button";
41218
+ b.dataset.trial = kind;
41219
+ b.title = t(`ui.trial.${kind}.hint`);
41220
+ b.onclick = () => table.trial(kind);
41221
+ trials.appendChild(b);
41222
+ return b;
41223
+ });
41224
+ const relay = mk2("button", "tr-btn", t("ui.test_table.relay"));
41225
+ relay.type = "button";
41226
+ relay.onclick = () => table.relay();
41227
+ trials.appendChild(relay);
41228
+ element.append(head, search, found, sub4, list, trials);
41229
+ const groups = () => {
41230
+ const keep = select.value;
41231
+ select.textContent = "";
41232
+ for (const group of table.groups()) {
41233
+ const op = mk2("option", void 0, group);
41234
+ op.value = group;
41235
+ select.appendChild(op);
41236
+ }
41237
+ if ([...select.options].some((o) => o.value === keep)) select.value = keep;
41238
+ };
41239
+ const render = (page) => {
41240
+ list.textContent = "";
41241
+ prev.disabled = !page?.hasPrev;
41242
+ next.disabled = !page?.hasNext;
41243
+ for (const b of [...trialButtons, relay]) b.disabled = !page?.selected;
41244
+ if (!page) {
41245
+ sub4.textContent = t("ui.test_table.hint");
41246
+ return;
41247
+ }
41248
+ if (select.value !== page.group) select.value = page.group;
41249
+ sub4.textContent = t("ui.test_table.page", { from: page.first + 1, to: page.first + page.objects.length, total: page.total });
41250
+ for (const o of page.objects) {
41251
+ const b = mk2("button", o.objectId === page.selected ? "tr-btn on" : "tr-btn", o.name);
41252
+ b.type = "button";
41253
+ b.title = o.itemId;
41254
+ b.onclick = () => table.select(o.objectId);
41255
+ list.appendChild(b);
41256
+ }
41257
+ };
41258
+ const fold2 = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
41259
+ search.oninput = () => {
41260
+ found.textContent = "";
41261
+ const term = fold2(search.value.trim());
41262
+ if (!term) return;
41263
+ const hits = table.items().filter((item) => fold2(`${item.name} ${item.id}`).includes(term)).slice(0, 8);
41264
+ for (const item of hits) {
41265
+ const b = mk2("button", "tr-btn", item.name);
41266
+ b.type = "button";
41267
+ b.title = `${item.group} \xB7 ${item.id}`;
41268
+ b.onclick = () => {
41269
+ search.value = "";
41270
+ found.textContent = "";
41271
+ table.find(item.id);
41272
+ };
41273
+ found.appendChild(b);
41274
+ }
41275
+ if (!hits.length) found.appendChild(mk2("div", "sub", t("ui.no_results")));
41276
+ };
41277
+ lay.onclick = () => table.open(select.value);
41278
+ prev.onclick = () => table.prev();
41279
+ next.onclick = () => table.next();
41280
+ select.onchange = () => table.open(select.value);
41281
+ for (const box2 of [select, search]) box2.addEventListener("keydown", (e) => e.stopPropagation());
41282
+ const off = table.on(render);
41283
+ groups();
41284
+ render(table.state());
41285
+ return {
41286
+ element,
41287
+ refresh: () => {
41288
+ groups();
41289
+ render(table.state());
41290
+ },
41291
+ dispose: off
41292
+ };
41293
+ }
41294
+
40903
41295
  // src/ui.ts
40904
41296
  var CSS = `
40905
41297
  .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}
@@ -40978,7 +41370,7 @@ void main(){
40978
41370
  @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}}
40979
41371
  `;
40980
41372
  var fold = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
40981
- var mk2 = (tag, cls, text) => {
41373
+ var mk3 = (tag, cls, text) => {
40982
41374
  const e = document.createElement(tag);
40983
41375
  if (cls) e.className = cls;
40984
41376
  if (text !== void 0) e.textContent = text;
@@ -40989,7 +41381,7 @@ void main(){
40989
41381
  return source.replace(/["\\\n\r]/g, (char) => encodeURIComponent(char));
40990
41382
  };
40991
41383
  var thumb = (thumbnail, credit) => {
40992
- const e = mk2("span", "tr-thumb");
41384
+ const e = mk3("span", "tr-thumb");
40993
41385
  if (credit) e.title = credit;
40994
41386
  else e.setAttribute("aria-hidden", "true");
40995
41387
  e.style.setProperty("--tr-thumb", `url("${thumbSource(thumbnail)}")`);
@@ -41010,42 +41402,43 @@ void main(){
41010
41402
  }
41011
41403
  function createUi(o) {
41012
41404
  const { t } = o;
41013
- const style = mk2("style");
41014
- style.textContent = CSS;
41015
- const root = mk2("div", "tr-root");
41405
+ const style = mk3("style");
41406
+ style.textContent = CSS + (o.testTable ? TEST_TABLE_CSS : "");
41407
+ const root = mk3("div", "tr-root");
41016
41408
  const pos = getComputedStyle?.(o.container)?.position;
41017
41409
  if (!pos || pos === "static") o.container.style.position = "relative";
41018
41410
  root.appendChild(style);
41019
41411
  o.container.appendChild(root);
41020
- const cross5 = mk2("div", "tr-cross");
41021
- cross5.appendChild(mk2("i"));
41022
- const top = mk2("div", "tr-top");
41023
- const toolBox = mk2("div", "tr-tool");
41024
- const status = mk2("div", "tr-status");
41025
- const prog = mk2("div", "tr-prog");
41026
- const progLabel = mk2("b");
41027
- const progBar = mk2("div", "tr-bar");
41028
- const progFill = mk2("i");
41412
+ const cross5 = mk3("div", "tr-cross");
41413
+ cross5.appendChild(mk3("i"));
41414
+ const top = mk3("div", "tr-top");
41415
+ const toolBox = mk3("div", "tr-tool");
41416
+ const status = mk3("div", "tr-status");
41417
+ const prog = mk3("div", "tr-prog");
41418
+ const progLabel = mk3("b");
41419
+ const progBar = mk3("div", "tr-bar");
41420
+ const progFill = mk3("i");
41029
41421
  progBar.appendChild(progFill);
41030
41422
  prog.append(progLabel, progBar);
41031
41423
  top.append(toolBox, status, prog);
41032
- const bottom = mk2("div", "tr-bottom");
41033
- const held = mk2("div", "tr-held");
41034
- const hotbar = mk2("div", "tr-hotbar");
41424
+ const bottom = mk3("div", "tr-bottom");
41425
+ const held = mk3("div", "tr-held");
41426
+ const hotbar = mk3("div", "tr-hotbar");
41035
41427
  bottom.append(held, hotbar);
41036
- const help = mk2("div", "tr-help");
41428
+ const help = mk3("div", "tr-help");
41037
41429
  help.textContent = t("ui.help");
41038
- const left = mk2("div", "tr-left");
41039
- const insp = mk2("div", "tr-panel tr-insp");
41040
- const logPanel = mk2("div", "tr-panel tr-log");
41041
- left.append(insp, logPanel);
41042
- const logTitle = mk2("h3", void 0, t("ui.log"));
41043
- const logBar = mk2("div", "tr-logbar");
41044
- const logBody = mk2("div", "tr-logbody");
41430
+ const left = mk3("div", "tr-left");
41431
+ const insp = mk3("div", "tr-panel tr-insp");
41432
+ const logPanel = mk3("div", "tr-panel tr-log");
41433
+ const tablePanel = o.testTable ? createTestTablePanel(o.testTable, t) : null;
41434
+ left.append(insp, ...tablePanel ? [tablePanel.element] : [], logPanel);
41435
+ const logTitle = mk3("h3", void 0, t("ui.log"));
41436
+ const logBar = mk3("div", "tr-logbar");
41437
+ const logBody = mk3("div", "tr-logbody");
41045
41438
  const filters = { action: true, event: true, check: true };
41046
41439
  const filterBtns = [];
41047
41440
  for (const ch of ["action", "event", "check"]) {
41048
- const b = mk2("button", "tr-btn on", t(`ui.filter.${ch}`));
41441
+ const b = mk3("button", "tr-btn on", t(`ui.filter.${ch}`));
41049
41442
  b.type = "button";
41050
41443
  b.onclick = () => {
41051
41444
  filters[ch] = !filters[ch];
@@ -41056,8 +41449,8 @@ void main(){
41056
41449
  logBar.appendChild(b);
41057
41450
  }
41058
41451
  let autoscroll = true;
41059
- const asLabel = mk2("label");
41060
- const asBox = mk2("input");
41452
+ const asLabel = mk3("label");
41453
+ const asBox = mk3("input");
41061
41454
  asBox.type = "checkbox";
41062
41455
  asBox.checked = true;
41063
41456
  asBox.onchange = () => {
@@ -41066,7 +41459,7 @@ void main(){
41066
41459
  asLabel.append(asBox, document.createTextNode(" " + t("ui.autoscroll")));
41067
41460
  logBar.appendChild(asLabel);
41068
41461
  const logBtn = (label, fn) => {
41069
- const b = mk2("button", "tr-btn", label);
41462
+ const b = mk3("button", "tr-btn", label);
41070
41463
  b.type = "button";
41071
41464
  b.onclick = fn;
41072
41465
  logBar.appendChild(b);
@@ -41074,7 +41467,7 @@ void main(){
41074
41467
  logBtn(t("ui.copy_json"), () => {
41075
41468
  const text = o.log.toJSON();
41076
41469
  const select = () => {
41077
- const ta = mk2("textarea");
41470
+ const ta = mk3("textarea");
41078
41471
  ta.value = text;
41079
41472
  ta.setAttribute("readonly", "");
41080
41473
  ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
@@ -41108,12 +41501,12 @@ void main(){
41108
41501
  const fill = (n, e) => {
41109
41502
  n.className = lineClass(e);
41110
41503
  n.textContent = "";
41111
- const tm = mk2("time", void 0, fmtTime(e.t));
41504
+ const tm = mk3("time", void 0, fmtTime(e.t));
41112
41505
  n.append(tm, document.createTextNode(e.text));
41113
41506
  };
41114
41507
  const addNode = (e) => {
41115
41508
  if (!filters[e.channel]) return;
41116
- const n = mk2("div");
41509
+ const n = mk3("div");
41117
41510
  fill(n, e);
41118
41511
  nodes.set(e.seq, n);
41119
41512
  logBody.appendChild(n);
@@ -41135,18 +41528,18 @@ void main(){
41135
41528
  if (n) fill(n, e);
41136
41529
  } else addNode(e);
41137
41530
  });
41138
- const right = mk2("div", "tr-right");
41139
- const cat = mk2("div", "tr-panel tr-cat");
41140
- const anim = mk2("div", "tr-panel tr-anim");
41141
- const tests = mk2("div", "tr-panel tr-tests");
41531
+ const right = mk3("div", "tr-right");
41532
+ const cat = mk3("div", "tr-panel tr-cat");
41533
+ const anim = mk3("div", "tr-panel tr-anim");
41534
+ const tests = mk3("div", "tr-panel tr-tests");
41142
41535
  const benchPanel = createBenchPanel({ t, num: (n, d) => num(n, d), onControl: o.onControl, onLayer: o.onLayer, onAction: o.onAction });
41143
41536
  right.append(cat, anim, benchPanel.element, tests);
41144
- cat.appendChild(mk2("h3", void 0, t("ui.catalog")));
41145
- const itemSearch = mk2("input", "tr-search");
41537
+ cat.appendChild(mk3("h3", void 0, t("ui.catalog")));
41538
+ const itemSearch = mk3("input", "tr-search");
41146
41539
  itemSearch.type = "search";
41147
41540
  itemSearch.placeholder = t("ui.search_items");
41148
- const tabs = mk2("div", "tr-tabs");
41149
- const items = mk2("div", "tr-items");
41541
+ const tabs = mk3("div", "tr-tabs");
41542
+ const items = mk3("div", "tr-items");
41150
41543
  cat.append(itemSearch, tabs, items);
41151
41544
  let activeGroup = "";
41152
41545
  const visibleItems = () => {
@@ -41158,18 +41551,18 @@ void main(){
41158
41551
  items.textContent = "";
41159
41552
  const searching = itemSearch.value.trim() !== "";
41160
41553
  const found = visibleItems();
41161
- if (!found.length) items.appendChild(mk2("div", "tr-empty", t("ui.no_results")));
41554
+ if (!found.length) items.appendChild(mk3("div", "tr-empty", t("ui.no_results")));
41162
41555
  for (const { item: it, group } of found) {
41163
- const wrap = mk2("div", "tr-item");
41164
- const b = mk2("button", it.thumbnail ? "tr-btn has-thumb" : "tr-btn", it.thumbnail ? void 0 : it.name);
41165
- if (it.thumbnail) b.append(thumb(it.thumbnail), mk2("span", "tr-name", it.name));
41166
- if (searching) b.appendChild(mk2("small", void 0, group));
41556
+ const wrap = mk3("div", "tr-item");
41557
+ const b = mk3("button", it.thumbnail ? "tr-btn has-thumb" : "tr-btn", it.thumbnail ? void 0 : it.name);
41558
+ if (it.thumbnail) b.append(thumb(it.thumbnail), mk3("span", "tr-name", it.name));
41559
+ if (searching) b.appendChild(mk3("small", void 0, group));
41167
41560
  b.type = "button";
41168
41561
  b.title = it.note ?? it.name;
41169
41562
  b.onclick = () => o.onSpawn(it.id);
41170
41563
  wrap.appendChild(b);
41171
41564
  if (it.weapon) {
41172
- const w = mk2("button", "tr-btn", "\u2694");
41565
+ const w = mk3("button", "tr-btn", "\u2694");
41173
41566
  w.type = "button";
41174
41567
  w.title = t("ui.equip");
41175
41568
  w.onclick = () => o.onEquip(it.id);
@@ -41183,7 +41576,7 @@ void main(){
41183
41576
  const groupNames = [...o.groups().keys()];
41184
41577
  if (!groupNames.includes(activeGroup)) activeGroup = groupNames[0] ?? "";
41185
41578
  for (const g of groupNames) {
41186
- const b = mk2("button", g === activeGroup ? "tr-btn on" : "tr-btn", g);
41579
+ const b = mk3("button", g === activeGroup ? "tr-btn on" : "tr-btn", g);
41187
41580
  b.type = "button";
41188
41581
  b.onclick = () => {
41189
41582
  activeGroup = g;
@@ -41196,9 +41589,9 @@ void main(){
41196
41589
  renderTabs();
41197
41590
  itemSearch.oninput = renderItems;
41198
41591
  for (const box2 of [itemSearch]) box2.addEventListener("keydown", (e) => e.stopPropagation());
41199
- const controls = mk2("div", "tr-row");
41592
+ const controls = mk3("div", "tr-row");
41200
41593
  const ctl = (label, fn) => {
41201
- const b = mk2("button", "tr-btn", label);
41594
+ const b = mk3("button", "tr-btn", label);
41202
41595
  b.type = "button";
41203
41596
  b.onclick = () => fn(b);
41204
41597
  controls.appendChild(b);
@@ -41223,29 +41616,29 @@ void main(){
41223
41616
  fullscreenBtn.title = t("ui.fullscreen.hint");
41224
41617
  const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
41225
41618
  for (const { key, b } of blastBtns) b.title = t(`ui.blast.${key}.hint`);
41226
- const qSel = mk2("select");
41619
+ const qSel = mk3("select");
41227
41620
  for (const q of ["low", "medium", "high"]) {
41228
- const op = mk2("option", void 0, t(`ui.quality.${q}`));
41621
+ const op = mk3("option", void 0, t(`ui.quality.${q}`));
41229
41622
  op.value = q;
41230
41623
  qSel.appendChild(op);
41231
41624
  }
41232
41625
  qSel.value = o.quality;
41233
41626
  qSel.onchange = () => o.onQuality(qSel.value);
41234
- const qLabel = mk2("label", void 0, t("ui.quality") + " ");
41627
+ const qLabel = mk3("label", void 0, t("ui.quality") + " ");
41235
41628
  qLabel.appendChild(qSel);
41236
41629
  controls.appendChild(qLabel);
41237
- const count = mk2("span");
41630
+ const count = mk3("span");
41238
41631
  controls.appendChild(count);
41239
41632
  cat.appendChild(controls);
41240
- tests.appendChild(mk2("h3", void 0, t("ui.tests")));
41241
- const runAll = mk2("button", "tr-btn", t("ui.run_all"));
41633
+ tests.appendChild(mk3("h3", void 0, t("ui.tests")));
41634
+ const runAll = mk3("button", "tr-btn", t("ui.run_all"));
41242
41635
  runAll.type = "button";
41243
- const summary = mk2("div", "tr-summary");
41244
- const testSearch = mk2("input", "tr-search");
41636
+ const summary = mk3("div", "tr-summary");
41637
+ const testSearch = mk3("input", "tr-search");
41245
41638
  testSearch.type = "search";
41246
41639
  testSearch.placeholder = t("ui.search_tests");
41247
41640
  testSearch.addEventListener("keydown", (e) => e.stopPropagation());
41248
- const list = mk2("div", "list");
41641
+ const list = mk3("div", "list");
41249
41642
  tests.append(runAll, summary, testSearch, list);
41250
41643
  testSearch.oninput = () => {
41251
41644
  const term = fold(testSearch.value.trim());
@@ -41268,7 +41661,7 @@ void main(){
41268
41661
  list.textContent = "";
41269
41662
  scBtns.clear();
41270
41663
  for (const s of o.scenarios()) {
41271
- const b = mk2("button", "tr-btn", s.name);
41664
+ const b = mk3("button", "tr-btn", s.name);
41272
41665
  b.type = "button";
41273
41666
  b.onclick = async () => {
41274
41667
  if (busy) return;
@@ -41294,7 +41687,7 @@ void main(){
41294
41687
  busy = false;
41295
41688
  }
41296
41689
  };
41297
- const toggle = mk2("button", "tr-btn tr-toggle", t("ui.panel"));
41690
+ const toggle = mk3("button", "tr-btn tr-toggle", t("ui.panel"));
41298
41691
  toggle.type = "button";
41299
41692
  const togglePanels = () => {
41300
41693
  root.classList.toggle("open");
@@ -41303,15 +41696,15 @@ void main(){
41303
41696
  toggle.onclick = togglePanels;
41304
41697
  root.append(cross5, top, bottom, help, left, right, toggle);
41305
41698
  const bar2 = (value, max2, cls) => {
41306
- const b = mk2("div", `tr-bar ${cls}`);
41307
- const i = mk2("i");
41699
+ const b = mk3("div", `tr-bar ${cls}`);
41700
+ const i = mk3("i");
41308
41701
  i.style.width = `${Math.max(0, Math.min(100, value / Math.max(1e-9, max2) * 100))}%`;
41309
41702
  b.appendChild(i);
41310
41703
  return b;
41311
41704
  };
41312
41705
  const row = (k, v) => {
41313
- const r = mk2("div", "r");
41314
- r.append(mk2("span", void 0, k), mk2("span", void 0, v));
41706
+ const r = mk3("div", "r");
41707
+ r.append(mk3("span", void 0, k), mk3("span", void 0, v));
41315
41708
  return r;
41316
41709
  };
41317
41710
  const num = (n, d = 1) => {
@@ -41320,13 +41713,13 @@ void main(){
41320
41713
  };
41321
41714
  const renderInspector = (i) => {
41322
41715
  insp.textContent = "";
41323
- insp.appendChild(mk2("h3", void 0, t("ui.inspector")));
41716
+ insp.appendChild(mk3("h3", void 0, t("ui.inspector")));
41324
41717
  if (!i) {
41325
- insp.appendChild(mk2("div", void 0, t("ui.aim_at_something")));
41718
+ insp.appendChild(mk3("div", void 0, t("ui.aim_at_something")));
41326
41719
  return;
41327
41720
  }
41328
41721
  if (i.thumbnail) insp.appendChild(thumb(i.thumbnail, i.thumbnailCredit));
41329
- insp.appendChild(mk2("div", "t", `${i.object} \u203A ${i.part}`));
41722
+ insp.appendChild(mk3("div", "t", `${i.object} \u203A ${i.part}`));
41330
41723
  insp.appendChild(row(t("ins.material"), i.material));
41331
41724
  insp.appendChild(row(t("ins.status"), i.status + (i.burning ? ` \xB7 ${t("ins.burning")}` : "")));
41332
41725
  insp.appendChild(row(t("ins.hp"), `${num(i.hp, 0)} / ${num(i.maxHp, 0)}`));
@@ -41345,30 +41738,30 @@ void main(){
41345
41738
  insp.appendChild(bar2(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
41346
41739
  }
41347
41740
  if (i.joints.length) {
41348
- insp.appendChild(mk2("div", void 0, t("ins.joints") + ":"));
41741
+ insp.appendChild(mk3("div", void 0, t("ins.joints") + ":"));
41349
41742
  for (const j of i.joints) {
41350
41743
  const tools = j.tools.length ? j.tools.join(", ") : t("tool.none");
41351
- insp.appendChild(mk2("div", void 0, `\u2022 ${j.kind} (${j.id})${j.open ? ` \xB7 ${t("ins.open")}` : ` \u2192 ${tools}`}`));
41744
+ insp.appendChild(mk3("div", void 0, `\u2022 ${j.kind} (${j.id})${j.open ? ` \xB7 ${t("ins.open")}` : ` \u2192 ${tools}`}`));
41352
41745
  }
41353
41746
  }
41354
- if (i.note) insp.appendChild(mk2("div", void 0, i.note));
41747
+ if (i.note) insp.appendChild(mk3("div", void 0, i.note));
41355
41748
  };
41356
41749
  let lastAnim = "";
41357
41750
  const renderAnimations = (a) => {
41358
41751
  anim.textContent = "";
41359
- anim.appendChild(mk2("h3", void 0, t("ui.animations")));
41752
+ anim.appendChild(mk3("h3", void 0, t("ui.animations")));
41360
41753
  if (!a) {
41361
- anim.appendChild(mk2("div", "sub", t("ui.animations_aim")));
41754
+ anim.appendChild(mk3("div", "sub", t("ui.animations_aim")));
41362
41755
  return;
41363
41756
  }
41364
- anim.appendChild(mk2("div", "sub", a.name));
41757
+ anim.appendChild(mk3("div", "sub", a.name));
41365
41758
  if (a.clips.length === 0) {
41366
- anim.appendChild(mk2("div", "sub", t("ui.animations_none")));
41759
+ anim.appendChild(mk3("div", "sub", t("ui.animations_none")));
41367
41760
  return;
41368
41761
  }
41369
- const l = mk2("div", "list");
41762
+ const l = mk3("div", "list");
41370
41763
  for (const c of a.clips) {
41371
- 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)`);
41764
+ 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)`);
41372
41765
  b.type = "button";
41373
41766
  b.onclick = () => o.onPlay(a.objectId, c.id);
41374
41767
  l.appendChild(b);
@@ -41385,8 +41778,8 @@ void main(){
41385
41778
  hud(s) {
41386
41779
  toolBox.textContent = "";
41387
41780
  toolBox.append(document.createTextNode(s.toolName));
41388
- if (s.ammo) toolBox.append(mk2("small", void 0, s.ammo));
41389
- if (s.camera) toolBox.append(mk2("small", void 0, t(`camera.${s.camera}`)));
41781
+ if (s.ammo) toolBox.append(mk3("small", void 0, s.ammo));
41782
+ if (s.camera) toolBox.append(mk3("small", void 0, t(`camera.${s.camera}`)));
41390
41783
  const helpText = s.camera ? `${t("ui.help")} \xB7 ${t("ui.help.camera")}` : t("ui.help");
41391
41784
  if (help.textContent !== helpText) help.textContent = helpText;
41392
41785
  if (s.status) status.textContent = s.status;
@@ -41409,7 +41802,7 @@ void main(){
41409
41802
  ];
41410
41803
  if (s.held.weapon) keys.push(["H", t("hud.equip", { weapon: s.held.weapon })]);
41411
41804
  for (const [k, label] of keys) {
41412
- held.append(mk2("kbd", void 0, k), document.createTextNode(` ${label} `));
41805
+ held.append(mk3("kbd", void 0, k), document.createTextNode(` ${label} `));
41413
41806
  }
41414
41807
  } else held.style.display = "none";
41415
41808
  const key = s.slots.join("|") + s.page;
@@ -41419,9 +41812,9 @@ void main(){
41419
41812
  slotsEl.length = 0;
41420
41813
  hudPage = s.page - 1;
41421
41814
  s.slots.forEach((name, i) => {
41422
- const b = mk2("button", "tr-slot");
41815
+ const b = mk3("button", "tr-slot");
41423
41816
  b.type = "button";
41424
- b.append(mk2("b", void 0, String((i + 1) % 10)), document.createTextNode(name));
41817
+ b.append(mk3("b", void 0, String((i + 1) % 10)), document.createTextNode(name));
41425
41818
  b.title = name;
41426
41819
  b.onclick = () => o.onTool(hudPage * 10 + i);
41427
41820
  hotbar.appendChild(b);
@@ -41453,6 +41846,7 @@ void main(){
41453
41846
  refresh() {
41454
41847
  renderTabs();
41455
41848
  renderScenarios();
41849
+ tablePanel?.refresh();
41456
41850
  },
41457
41851
  showGroup(group) {
41458
41852
  activeGroup = group;
@@ -41467,6 +41861,7 @@ void main(){
41467
41861
  },
41468
41862
  dispose() {
41469
41863
  offLog();
41864
+ tablePanel?.dispose();
41470
41865
  if (toastTimer) clearTimeout(toastTimer);
41471
41866
  root.remove();
41472
41867
  }
@@ -42656,6 +43051,10 @@ void main(){
42656
43051
  heldPiece = null;
42657
43052
  heldTilt = 0;
42658
43053
  };
43054
+ const handUse = () => {
43055
+ if (heldPiece) grab();
43056
+ else useTool(FIXED_DT, false);
43057
+ };
42659
43058
  const grab = () => {
42660
43059
  if (heldPiece) {
42661
43060
  const name = heldName;
@@ -43124,10 +43523,10 @@ void main(){
43124
43523
  if (tiltKey || heldTilt > 0) {
43125
43524
  heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
43126
43525
  }
43127
- if (useHeld && !paused) {
43526
+ if ((useHeld || useFirst) && !paused) {
43128
43527
  if (tool.def.category === "hand") {
43129
- if (useFirst) grab();
43130
- } else useTool(dt, !useFirst);
43528
+ if (useFirst) handUse();
43529
+ } else if (useHeld) useTool(dt, !useFirst);
43131
43530
  useFirst = false;
43132
43531
  }
43133
43532
  if (!paused) followHeld(dt);
@@ -43682,6 +44081,63 @@ void main(){
43682
44081
  const row = (list, gap, z) => list.map((item, index) => spawn2(item.id, [(index - (list.length - 1) / 2) * gap, 0, z], 0));
43683
44082
  return [...row(front, 1.6, 1.5), ...row(back, 4.5, -4)];
43684
44083
  };
44084
+ const catalogGroups = () => {
44085
+ const m = /* @__PURE__ */ new Map();
44086
+ for (const i of catalog) {
44087
+ const g = m.get(i.group) ?? [];
44088
+ g.push(i);
44089
+ m.set(i.group, g);
44090
+ }
44091
+ return m;
44092
+ };
44093
+ const tableOptions = options.testTable === false ? {} : options.testTable ?? {};
44094
+ const tableScenery = tableOptions.scenery ?? "test_table";
44095
+ const measured = /* @__PURE__ */ new Map();
44096
+ const testTable = createTestTable(
44097
+ {
44098
+ groups: catalogGroups,
44099
+ top: () => {
44100
+ const top = arena.tops.find((x) => x.id === tableScenery);
44101
+ return top && topOk(top) ? top : null;
44102
+ },
44103
+ measure: (itemId) => {
44104
+ let size = measured.get(itemId);
44105
+ if (!size) {
44106
+ const item = catalogMap.get(itemId);
44107
+ const box2 = item ? new THREE.Box3().setFromObject(buildRoot(item)) : null;
44108
+ size = box2 && !box2.isEmpty() ? { w: box2.max.x - box2.min.x, d: box2.max.z - box2.min.z } : { w: 0.3, d: 0.3 };
44109
+ measured.set(itemId, size);
44110
+ }
44111
+ return size;
44112
+ },
44113
+ spawn: (itemId, position) => spawn2(itemId, position, 0),
44114
+ clear,
44115
+ resetAll,
44116
+ box: (objectId) => {
44117
+ const e = entries.get(objectId);
44118
+ if (!e || !world.has(objectId)) return null;
44119
+ const box2 = new THREE.Box3().setFromObject(e.so.root);
44120
+ return box2.isEmpty() ? null : { min: [box2.min.x, box2.min.y, box2.min.z], max: [box2.max.x, box2.max.y, box2.max.z] };
44121
+ },
44122
+ equip: (weapon) => {
44123
+ if (!arsenal.weapons.has(weapon)) return false;
44124
+ if (heldEntry) releaseHeld();
44125
+ equip(weapon);
44126
+ room.waitUntilReady();
44127
+ return true;
44128
+ },
44129
+ look: (eye, target) => player.look(eye, target),
44130
+ use: (hold) => room.use(hold),
44131
+ blastAt: (weapon, point) => {
44132
+ const def = arsenal.weapons.get(weapon);
44133
+ const explosive = def?.action.kind === "throwable" ? def.action.explosive : def?.action.kind === "launcher" ? def.action.projectile : void 0;
44134
+ return explosive !== void 0 && explosives.blastAt(weapon, explosive, point);
44135
+ },
44136
+ log: (type, text, data) => action(type, text, { data }),
44137
+ translate: t
44138
+ },
44139
+ tableOptions
44140
+ );
43685
44141
  const room = {
43686
44142
  world,
43687
44143
  renderer,
@@ -43701,6 +44157,7 @@ void main(){
43701
44157
  get scenarios() {
43702
44158
  return allScenarios();
43703
44159
  },
44160
+ testTable,
43704
44161
  spawn: spawn2,
43705
44162
  clear,
43706
44163
  resetAll,
@@ -43708,7 +44165,8 @@ void main(){
43708
44165
  useItem,
43709
44166
  use(hold = 0) {
43710
44167
  if (tool.def.category === "hand") {
43711
- grab();
44168
+ handUse();
44169
+ flush();
43712
44170
  return;
43713
44171
  }
43714
44172
  useTool(FIXED_DT, false);
@@ -43779,6 +44237,7 @@ void main(){
43779
44237
  },
43780
44238
  equip,
43781
44239
  openArea,
44240
+ modules: () => [...modules.values()].map((m) => ({ id: m.id, name: m.name, group: m.group, hasArea: typeof m.area === "function" })),
43782
44241
  openAreaAsync: (area) => loading.busy(t("loading.open_area", { area: areaLabel(area) }), () => openArea(area)),
43783
44242
  loading,
43784
44243
  busy: (label, work, busyOptions) => loading.busy(label, work, busyOptions),
@@ -43940,7 +44399,8 @@ void main(){
43940
44399
  },
43941
44400
  onAction: (id) => {
43942
44401
  if (activeBench) slowly(activeBench, id, () => benches.get(activeBench)?.act(id));
43943
- }
44402
+ },
44403
+ ...options.testTable !== false && arena.tops.some((x) => x.id === tableScenery) ? { testTable } : {}
43944
44404
  });
43945
44405
  ui.hud(hudState());
43946
44406
  showBench();