@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.
@@ -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
@@ -806,6 +812,23 @@ var GameTestRoom = (() => {
806
812
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
807
813
  };
808
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;
809
832
  var planFragments = (inp) => {
810
833
  const { style, dims, rnd: rnd2, maxFragments } = inp;
811
834
  const [fmin, fmax] = inp.fragmentSize;
@@ -848,6 +871,8 @@ var GameTestRoom = (() => {
848
871
  }
849
872
  if (style === "tear") {
850
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);
851
876
  const th2 = Math.min(inp.thickness, dims[t]);
852
877
  for (const b of bsp(root, n2, [u, v], rnd2)) {
853
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] });
@@ -2420,6 +2445,7 @@ var GameTestRoom = (() => {
2420
2445
  0.03,
2421
2446
  0.15
2422
2447
  ],
2448
+ sheet: 1e-3,
2423
2449
  damageScale: {
2424
2450
  slash: 3
2425
2451
  },
@@ -2435,7 +2461,8 @@ var GameTestRoom = (() => {
2435
2461
  smoke: "grey"
2436
2462
  },
2437
2463
  visual: {
2438
- charColor: 1709072
2464
+ charColor: 1709072,
2465
+ innerColor: 15524048
2439
2466
  },
2440
2467
  tags: [
2441
2468
  "fabric",
@@ -4153,7 +4180,8 @@ var GameTestRoom = (() => {
4153
4180
  hit: hitLocal,
4154
4181
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4155
4182
  maxFragments,
4156
- rnd: rnd2
4183
+ rnd: rnd2,
4184
+ sheet: p.mat.sheet
4157
4185
  });
4158
4186
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
4159
4187
  const mass = p.st.mass;
@@ -4177,7 +4205,8 @@ var GameTestRoom = (() => {
4177
4205
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd2(), dv[2] * k],
4178
4206
  angularVelocity: [(rnd2() - 0.5) * 12, (rnd2() - 0.5) * 12, (rnd2() - 0.5) * 12],
4179
4207
  mass: mass * f.weight / totW,
4180
- ...marks.length > 0 ? { marks } : {}
4208
+ ...marks.length > 0 ? { marks } : {},
4209
+ ...f.inner ? { inner: true } : {}
4181
4210
  };
4182
4211
  });
4183
4212
  const reinforcement = p.spec.reinforcement;
@@ -15742,6 +15771,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15742
15771
  const bl = Math.round(ab + ((b & 255) - ab) * t);
15743
15772
  return r << 16 | g << 8 | bl;
15744
15773
  }
15774
+ var SHEET_MAX = 4e-3;
15775
+ var SHEET_AIR = 2.5;
15776
+ var SHEET_SWAY = 3;
15745
15777
  var FragmentSystem = class {
15746
15778
  constructor(THREE, o) {
15747
15779
  __publicField2(this, "THREE", THREE);
@@ -15847,7 +15879,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15847
15879
  asleep: false,
15848
15880
  rest: 0,
15849
15881
  fade: 1,
15850
- dying: false
15882
+ dying: false,
15883
+ air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0
15851
15884
  };
15852
15885
  this.list.push(live);
15853
15886
  let alive = this.list.reduce((n, l) => n + (l.dying ? 0 : 1), 0);
@@ -15931,6 +15964,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15931
15964
  }
15932
15965
  } else {
15933
15966
  f.ang.multiplyScalar(Math.exp(-0.15 * dt));
15967
+ if (f.air > 0) this.flutter(f, dt);
15934
15968
  }
15935
15969
  if (contact && f.vel.lengthSq() < 4) this.settle(f, dt);
15936
15970
  const slow = f.vel.lengthSq() < 0.012 && f.ang.lengthSq() < 0.2;
@@ -15945,6 +15979,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15945
15979
  f.still = 0;
15946
15980
  }
15947
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
+ }
15948
15990
  /** Tip the fragment towards lying on its flattest face. */
15949
15991
  settle(f, dt) {
15950
15992
  const mesh2 = f.mesh;
@@ -18225,6 +18267,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18225
18267
  const def = pv ? this.world.materials.get(pv.material) : void 0;
18226
18268
  if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
18227
18269
  if (kind === "fibers" || kind === "leaves") {
18270
+ if (def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
18228
18271
  const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
18229
18272
  return c >= 0 ? { color: c } : void 0;
18230
18273
  }
@@ -18373,7 +18416,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18373
18416
  for (let i = 0; i < ev.fragments.length; i += step) {
18374
18417
  const fr = ev.fragments[i];
18375
18418
  const fdef = this.world.materials.get(fr.material);
18376
- 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);
18377
18421
  this.fragments.spawn(fr, mat, ev.object, maxAlive);
18378
18422
  }
18379
18423
  const up = [0, 1, 0];
@@ -18391,7 +18435,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18391
18435
  this.vfx.burst("dust", ev.point, up, 5, 0.8, { color: 11901556 });
18392
18436
  break;
18393
18437
  case "tear":
18394
- 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 });
18395
18439
  break;
18396
18440
  case "snap":
18397
18441
  this.vfx.burst("chips", ev.point, up, 8 * sizeK, 3);
@@ -38748,6 +38792,14 @@ void main(){
38748
38792
  ...projectile.fire ? { fire: projectile.fire } : {}
38749
38793
  });
38750
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
+ };
38751
38803
  const detonateCharges = () => {
38752
38804
  let count = 0;
38753
38805
  for (const projectile of [...projectiles]) {
@@ -38839,7 +38891,172 @@ void main(){
38839
38891
  }
38840
38892
  }
38841
38893
  };
38842
- 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
+ };
38843
39060
  }
38844
39061
 
38845
39062
  // src/host-events.ts
@@ -40271,7 +40488,7 @@ void main(){
40271
40488
  "hud.throw": "throw",
40272
40489
  "hud.tilt": "tilt (hold)",
40273
40490
  "hud.equip": "equip {weapon}",
40274
- "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",
40275
40492
  "ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
40276
40493
  "camera.first": "1st person",
40277
40494
  "camera.third": "3rd person",
@@ -40285,6 +40502,32 @@ void main(){
40285
40502
  "ui.copy_json": "Copy JSON",
40286
40503
  "ui.download_ndjson": "Download NDJSON",
40287
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",
40288
40531
  "ui.clear_log": "Clear",
40289
40532
  "ui.catalog": "Catalogue",
40290
40533
  "ui.clear": "Clear",
@@ -40552,7 +40795,7 @@ void main(){
40552
40795
  "hud.throw": "arremessar",
40553
40796
  "hud.tilt": "inclinar (segurar)",
40554
40797
  "hud.equip": "equipar {weapon}",
40555
- "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",
40556
40799
  "ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
40557
40800
  "camera.first": "1\xAA pessoa",
40558
40801
  "camera.third": "3\xAA pessoa",
@@ -40566,6 +40809,32 @@ void main(){
40566
40809
  "ui.copy_json": "Copiar JSON",
40567
40810
  "ui.download_ndjson": "Baixar NDJSON",
40568
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",
40569
40838
  "ui.clear_log": "Limpar",
40570
40839
  "ui.catalog": "Cat\xE1logo",
40571
40840
  "ui.clear": "Limpar",
@@ -40907,6 +41176,122 @@ void main(){
40907
41176
  };
40908
41177
  }
40909
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
+
40910
41295
  // src/ui.ts
40911
41296
  var CSS = `
40912
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}
@@ -40985,7 +41370,7 @@ void main(){
40985
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}}
40986
41371
  `;
40987
41372
  var fold = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
40988
- var mk2 = (tag, cls, text) => {
41373
+ var mk3 = (tag, cls, text) => {
40989
41374
  const e = document.createElement(tag);
40990
41375
  if (cls) e.className = cls;
40991
41376
  if (text !== void 0) e.textContent = text;
@@ -40996,7 +41381,7 @@ void main(){
40996
41381
  return source.replace(/["\\\n\r]/g, (char) => encodeURIComponent(char));
40997
41382
  };
40998
41383
  var thumb = (thumbnail, credit) => {
40999
- const e = mk2("span", "tr-thumb");
41384
+ const e = mk3("span", "tr-thumb");
41000
41385
  if (credit) e.title = credit;
41001
41386
  else e.setAttribute("aria-hidden", "true");
41002
41387
  e.style.setProperty("--tr-thumb", `url("${thumbSource(thumbnail)}")`);
@@ -41017,42 +41402,43 @@ void main(){
41017
41402
  }
41018
41403
  function createUi(o) {
41019
41404
  const { t } = o;
41020
- const style = mk2("style");
41021
- style.textContent = CSS;
41022
- 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");
41023
41408
  const pos = getComputedStyle?.(o.container)?.position;
41024
41409
  if (!pos || pos === "static") o.container.style.position = "relative";
41025
41410
  root.appendChild(style);
41026
41411
  o.container.appendChild(root);
41027
- const cross5 = mk2("div", "tr-cross");
41028
- cross5.appendChild(mk2("i"));
41029
- const top = mk2("div", "tr-top");
41030
- const toolBox = mk2("div", "tr-tool");
41031
- const status = mk2("div", "tr-status");
41032
- const prog = mk2("div", "tr-prog");
41033
- const progLabel = mk2("b");
41034
- const progBar = mk2("div", "tr-bar");
41035
- 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");
41036
41421
  progBar.appendChild(progFill);
41037
41422
  prog.append(progLabel, progBar);
41038
41423
  top.append(toolBox, status, prog);
41039
- const bottom = mk2("div", "tr-bottom");
41040
- const held = mk2("div", "tr-held");
41041
- 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");
41042
41427
  bottom.append(held, hotbar);
41043
- const help = mk2("div", "tr-help");
41428
+ const help = mk3("div", "tr-help");
41044
41429
  help.textContent = t("ui.help");
41045
- const left = mk2("div", "tr-left");
41046
- const insp = mk2("div", "tr-panel tr-insp");
41047
- const logPanel = mk2("div", "tr-panel tr-log");
41048
- left.append(insp, logPanel);
41049
- const logTitle = mk2("h3", void 0, t("ui.log"));
41050
- const logBar = mk2("div", "tr-logbar");
41051
- 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");
41052
41438
  const filters = { action: true, event: true, check: true };
41053
41439
  const filterBtns = [];
41054
41440
  for (const ch of ["action", "event", "check"]) {
41055
- const b = mk2("button", "tr-btn on", t(`ui.filter.${ch}`));
41441
+ const b = mk3("button", "tr-btn on", t(`ui.filter.${ch}`));
41056
41442
  b.type = "button";
41057
41443
  b.onclick = () => {
41058
41444
  filters[ch] = !filters[ch];
@@ -41063,8 +41449,8 @@ void main(){
41063
41449
  logBar.appendChild(b);
41064
41450
  }
41065
41451
  let autoscroll = true;
41066
- const asLabel = mk2("label");
41067
- const asBox = mk2("input");
41452
+ const asLabel = mk3("label");
41453
+ const asBox = mk3("input");
41068
41454
  asBox.type = "checkbox";
41069
41455
  asBox.checked = true;
41070
41456
  asBox.onchange = () => {
@@ -41073,7 +41459,7 @@ void main(){
41073
41459
  asLabel.append(asBox, document.createTextNode(" " + t("ui.autoscroll")));
41074
41460
  logBar.appendChild(asLabel);
41075
41461
  const logBtn = (label, fn) => {
41076
- const b = mk2("button", "tr-btn", label);
41462
+ const b = mk3("button", "tr-btn", label);
41077
41463
  b.type = "button";
41078
41464
  b.onclick = fn;
41079
41465
  logBar.appendChild(b);
@@ -41081,7 +41467,7 @@ void main(){
41081
41467
  logBtn(t("ui.copy_json"), () => {
41082
41468
  const text = o.log.toJSON();
41083
41469
  const select = () => {
41084
- const ta = mk2("textarea");
41470
+ const ta = mk3("textarea");
41085
41471
  ta.value = text;
41086
41472
  ta.setAttribute("readonly", "");
41087
41473
  ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
@@ -41115,12 +41501,12 @@ void main(){
41115
41501
  const fill = (n, e) => {
41116
41502
  n.className = lineClass(e);
41117
41503
  n.textContent = "";
41118
- const tm = mk2("time", void 0, fmtTime(e.t));
41504
+ const tm = mk3("time", void 0, fmtTime(e.t));
41119
41505
  n.append(tm, document.createTextNode(e.text));
41120
41506
  };
41121
41507
  const addNode = (e) => {
41122
41508
  if (!filters[e.channel]) return;
41123
- const n = mk2("div");
41509
+ const n = mk3("div");
41124
41510
  fill(n, e);
41125
41511
  nodes.set(e.seq, n);
41126
41512
  logBody.appendChild(n);
@@ -41142,18 +41528,18 @@ void main(){
41142
41528
  if (n) fill(n, e);
41143
41529
  } else addNode(e);
41144
41530
  });
41145
- const right = mk2("div", "tr-right");
41146
- const cat = mk2("div", "tr-panel tr-cat");
41147
- const anim = mk2("div", "tr-panel tr-anim");
41148
- 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");
41149
41535
  const benchPanel = createBenchPanel({ t, num: (n, d) => num(n, d), onControl: o.onControl, onLayer: o.onLayer, onAction: o.onAction });
41150
41536
  right.append(cat, anim, benchPanel.element, tests);
41151
- cat.appendChild(mk2("h3", void 0, t("ui.catalog")));
41152
- const itemSearch = mk2("input", "tr-search");
41537
+ cat.appendChild(mk3("h3", void 0, t("ui.catalog")));
41538
+ const itemSearch = mk3("input", "tr-search");
41153
41539
  itemSearch.type = "search";
41154
41540
  itemSearch.placeholder = t("ui.search_items");
41155
- const tabs = mk2("div", "tr-tabs");
41156
- const items = mk2("div", "tr-items");
41541
+ const tabs = mk3("div", "tr-tabs");
41542
+ const items = mk3("div", "tr-items");
41157
41543
  cat.append(itemSearch, tabs, items);
41158
41544
  let activeGroup = "";
41159
41545
  const visibleItems = () => {
@@ -41165,18 +41551,18 @@ void main(){
41165
41551
  items.textContent = "";
41166
41552
  const searching = itemSearch.value.trim() !== "";
41167
41553
  const found = visibleItems();
41168
- 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")));
41169
41555
  for (const { item: it, group } of found) {
41170
- const wrap = mk2("div", "tr-item");
41171
- const b = mk2("button", it.thumbnail ? "tr-btn has-thumb" : "tr-btn", it.thumbnail ? void 0 : it.name);
41172
- if (it.thumbnail) b.append(thumb(it.thumbnail), mk2("span", "tr-name", it.name));
41173
- 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));
41174
41560
  b.type = "button";
41175
41561
  b.title = it.note ?? it.name;
41176
41562
  b.onclick = () => o.onSpawn(it.id);
41177
41563
  wrap.appendChild(b);
41178
41564
  if (it.weapon) {
41179
- const w = mk2("button", "tr-btn", "\u2694");
41565
+ const w = mk3("button", "tr-btn", "\u2694");
41180
41566
  w.type = "button";
41181
41567
  w.title = t("ui.equip");
41182
41568
  w.onclick = () => o.onEquip(it.id);
@@ -41190,7 +41576,7 @@ void main(){
41190
41576
  const groupNames = [...o.groups().keys()];
41191
41577
  if (!groupNames.includes(activeGroup)) activeGroup = groupNames[0] ?? "";
41192
41578
  for (const g of groupNames) {
41193
- 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);
41194
41580
  b.type = "button";
41195
41581
  b.onclick = () => {
41196
41582
  activeGroup = g;
@@ -41203,9 +41589,9 @@ void main(){
41203
41589
  renderTabs();
41204
41590
  itemSearch.oninput = renderItems;
41205
41591
  for (const box2 of [itemSearch]) box2.addEventListener("keydown", (e) => e.stopPropagation());
41206
- const controls = mk2("div", "tr-row");
41592
+ const controls = mk3("div", "tr-row");
41207
41593
  const ctl = (label, fn) => {
41208
- const b = mk2("button", "tr-btn", label);
41594
+ const b = mk3("button", "tr-btn", label);
41209
41595
  b.type = "button";
41210
41596
  b.onclick = () => fn(b);
41211
41597
  controls.appendChild(b);
@@ -41230,29 +41616,29 @@ void main(){
41230
41616
  fullscreenBtn.title = t("ui.fullscreen.hint");
41231
41617
  const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
41232
41618
  for (const { key, b } of blastBtns) b.title = t(`ui.blast.${key}.hint`);
41233
- const qSel = mk2("select");
41619
+ const qSel = mk3("select");
41234
41620
  for (const q of ["low", "medium", "high"]) {
41235
- const op = mk2("option", void 0, t(`ui.quality.${q}`));
41621
+ const op = mk3("option", void 0, t(`ui.quality.${q}`));
41236
41622
  op.value = q;
41237
41623
  qSel.appendChild(op);
41238
41624
  }
41239
41625
  qSel.value = o.quality;
41240
41626
  qSel.onchange = () => o.onQuality(qSel.value);
41241
- const qLabel = mk2("label", void 0, t("ui.quality") + " ");
41627
+ const qLabel = mk3("label", void 0, t("ui.quality") + " ");
41242
41628
  qLabel.appendChild(qSel);
41243
41629
  controls.appendChild(qLabel);
41244
- const count = mk2("span");
41630
+ const count = mk3("span");
41245
41631
  controls.appendChild(count);
41246
41632
  cat.appendChild(controls);
41247
- tests.appendChild(mk2("h3", void 0, t("ui.tests")));
41248
- 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"));
41249
41635
  runAll.type = "button";
41250
- const summary = mk2("div", "tr-summary");
41251
- const testSearch = mk2("input", "tr-search");
41636
+ const summary = mk3("div", "tr-summary");
41637
+ const testSearch = mk3("input", "tr-search");
41252
41638
  testSearch.type = "search";
41253
41639
  testSearch.placeholder = t("ui.search_tests");
41254
41640
  testSearch.addEventListener("keydown", (e) => e.stopPropagation());
41255
- const list = mk2("div", "list");
41641
+ const list = mk3("div", "list");
41256
41642
  tests.append(runAll, summary, testSearch, list);
41257
41643
  testSearch.oninput = () => {
41258
41644
  const term = fold(testSearch.value.trim());
@@ -41275,7 +41661,7 @@ void main(){
41275
41661
  list.textContent = "";
41276
41662
  scBtns.clear();
41277
41663
  for (const s of o.scenarios()) {
41278
- const b = mk2("button", "tr-btn", s.name);
41664
+ const b = mk3("button", "tr-btn", s.name);
41279
41665
  b.type = "button";
41280
41666
  b.onclick = async () => {
41281
41667
  if (busy) return;
@@ -41301,7 +41687,7 @@ void main(){
41301
41687
  busy = false;
41302
41688
  }
41303
41689
  };
41304
- const toggle = mk2("button", "tr-btn tr-toggle", t("ui.panel"));
41690
+ const toggle = mk3("button", "tr-btn tr-toggle", t("ui.panel"));
41305
41691
  toggle.type = "button";
41306
41692
  const togglePanels = () => {
41307
41693
  root.classList.toggle("open");
@@ -41310,15 +41696,15 @@ void main(){
41310
41696
  toggle.onclick = togglePanels;
41311
41697
  root.append(cross5, top, bottom, help, left, right, toggle);
41312
41698
  const bar2 = (value, max2, cls) => {
41313
- const b = mk2("div", `tr-bar ${cls}`);
41314
- const i = mk2("i");
41699
+ const b = mk3("div", `tr-bar ${cls}`);
41700
+ const i = mk3("i");
41315
41701
  i.style.width = `${Math.max(0, Math.min(100, value / Math.max(1e-9, max2) * 100))}%`;
41316
41702
  b.appendChild(i);
41317
41703
  return b;
41318
41704
  };
41319
41705
  const row = (k, v) => {
41320
- const r = mk2("div", "r");
41321
- 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));
41322
41708
  return r;
41323
41709
  };
41324
41710
  const num = (n, d = 1) => {
@@ -41327,13 +41713,13 @@ void main(){
41327
41713
  };
41328
41714
  const renderInspector = (i) => {
41329
41715
  insp.textContent = "";
41330
- insp.appendChild(mk2("h3", void 0, t("ui.inspector")));
41716
+ insp.appendChild(mk3("h3", void 0, t("ui.inspector")));
41331
41717
  if (!i) {
41332
- insp.appendChild(mk2("div", void 0, t("ui.aim_at_something")));
41718
+ insp.appendChild(mk3("div", void 0, t("ui.aim_at_something")));
41333
41719
  return;
41334
41720
  }
41335
41721
  if (i.thumbnail) insp.appendChild(thumb(i.thumbnail, i.thumbnailCredit));
41336
- insp.appendChild(mk2("div", "t", `${i.object} \u203A ${i.part}`));
41722
+ insp.appendChild(mk3("div", "t", `${i.object} \u203A ${i.part}`));
41337
41723
  insp.appendChild(row(t("ins.material"), i.material));
41338
41724
  insp.appendChild(row(t("ins.status"), i.status + (i.burning ? ` \xB7 ${t("ins.burning")}` : "")));
41339
41725
  insp.appendChild(row(t("ins.hp"), `${num(i.hp, 0)} / ${num(i.maxHp, 0)}`));
@@ -41352,30 +41738,30 @@ void main(){
41352
41738
  insp.appendChild(bar2(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
41353
41739
  }
41354
41740
  if (i.joints.length) {
41355
- insp.appendChild(mk2("div", void 0, t("ins.joints") + ":"));
41741
+ insp.appendChild(mk3("div", void 0, t("ins.joints") + ":"));
41356
41742
  for (const j of i.joints) {
41357
41743
  const tools = j.tools.length ? j.tools.join(", ") : t("tool.none");
41358
- 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}`}`));
41359
41745
  }
41360
41746
  }
41361
- if (i.note) insp.appendChild(mk2("div", void 0, i.note));
41747
+ if (i.note) insp.appendChild(mk3("div", void 0, i.note));
41362
41748
  };
41363
41749
  let lastAnim = "";
41364
41750
  const renderAnimations = (a) => {
41365
41751
  anim.textContent = "";
41366
- anim.appendChild(mk2("h3", void 0, t("ui.animations")));
41752
+ anim.appendChild(mk3("h3", void 0, t("ui.animations")));
41367
41753
  if (!a) {
41368
- anim.appendChild(mk2("div", "sub", t("ui.animations_aim")));
41754
+ anim.appendChild(mk3("div", "sub", t("ui.animations_aim")));
41369
41755
  return;
41370
41756
  }
41371
- anim.appendChild(mk2("div", "sub", a.name));
41757
+ anim.appendChild(mk3("div", "sub", a.name));
41372
41758
  if (a.clips.length === 0) {
41373
- anim.appendChild(mk2("div", "sub", t("ui.animations_none")));
41759
+ anim.appendChild(mk3("div", "sub", t("ui.animations_none")));
41374
41760
  return;
41375
41761
  }
41376
- const l = mk2("div", "list");
41762
+ const l = mk3("div", "list");
41377
41763
  for (const c of a.clips) {
41378
- 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)`);
41379
41765
  b.type = "button";
41380
41766
  b.onclick = () => o.onPlay(a.objectId, c.id);
41381
41767
  l.appendChild(b);
@@ -41392,8 +41778,8 @@ void main(){
41392
41778
  hud(s) {
41393
41779
  toolBox.textContent = "";
41394
41780
  toolBox.append(document.createTextNode(s.toolName));
41395
- if (s.ammo) toolBox.append(mk2("small", void 0, s.ammo));
41396
- 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}`)));
41397
41783
  const helpText = s.camera ? `${t("ui.help")} \xB7 ${t("ui.help.camera")}` : t("ui.help");
41398
41784
  if (help.textContent !== helpText) help.textContent = helpText;
41399
41785
  if (s.status) status.textContent = s.status;
@@ -41416,7 +41802,7 @@ void main(){
41416
41802
  ];
41417
41803
  if (s.held.weapon) keys.push(["H", t("hud.equip", { weapon: s.held.weapon })]);
41418
41804
  for (const [k, label] of keys) {
41419
- held.append(mk2("kbd", void 0, k), document.createTextNode(` ${label} `));
41805
+ held.append(mk3("kbd", void 0, k), document.createTextNode(` ${label} `));
41420
41806
  }
41421
41807
  } else held.style.display = "none";
41422
41808
  const key = s.slots.join("|") + s.page;
@@ -41426,9 +41812,9 @@ void main(){
41426
41812
  slotsEl.length = 0;
41427
41813
  hudPage = s.page - 1;
41428
41814
  s.slots.forEach((name, i) => {
41429
- const b = mk2("button", "tr-slot");
41815
+ const b = mk3("button", "tr-slot");
41430
41816
  b.type = "button";
41431
- 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));
41432
41818
  b.title = name;
41433
41819
  b.onclick = () => o.onTool(hudPage * 10 + i);
41434
41820
  hotbar.appendChild(b);
@@ -41460,6 +41846,7 @@ void main(){
41460
41846
  refresh() {
41461
41847
  renderTabs();
41462
41848
  renderScenarios();
41849
+ tablePanel?.refresh();
41463
41850
  },
41464
41851
  showGroup(group) {
41465
41852
  activeGroup = group;
@@ -41474,6 +41861,7 @@ void main(){
41474
41861
  },
41475
41862
  dispose() {
41476
41863
  offLog();
41864
+ tablePanel?.dispose();
41477
41865
  if (toastTimer) clearTimeout(toastTimer);
41478
41866
  root.remove();
41479
41867
  }
@@ -42663,6 +43051,10 @@ void main(){
42663
43051
  heldPiece = null;
42664
43052
  heldTilt = 0;
42665
43053
  };
43054
+ const handUse = () => {
43055
+ if (heldPiece) grab();
43056
+ else useTool(FIXED_DT, false);
43057
+ };
42666
43058
  const grab = () => {
42667
43059
  if (heldPiece) {
42668
43060
  const name = heldName;
@@ -43131,10 +43523,10 @@ void main(){
43131
43523
  if (tiltKey || heldTilt > 0) {
43132
43524
  heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
43133
43525
  }
43134
- if (useHeld && !paused) {
43526
+ if ((useHeld || useFirst) && !paused) {
43135
43527
  if (tool.def.category === "hand") {
43136
- if (useFirst) grab();
43137
- } else useTool(dt, !useFirst);
43528
+ if (useFirst) handUse();
43529
+ } else if (useHeld) useTool(dt, !useFirst);
43138
43530
  useFirst = false;
43139
43531
  }
43140
43532
  if (!paused) followHeld(dt);
@@ -43689,6 +44081,63 @@ void main(){
43689
44081
  const row = (list, gap, z) => list.map((item, index) => spawn2(item.id, [(index - (list.length - 1) / 2) * gap, 0, z], 0));
43690
44082
  return [...row(front, 1.6, 1.5), ...row(back, 4.5, -4)];
43691
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
+ );
43692
44141
  const room = {
43693
44142
  world,
43694
44143
  renderer,
@@ -43708,6 +44157,7 @@ void main(){
43708
44157
  get scenarios() {
43709
44158
  return allScenarios();
43710
44159
  },
44160
+ testTable,
43711
44161
  spawn: spawn2,
43712
44162
  clear,
43713
44163
  resetAll,
@@ -43715,7 +44165,8 @@ void main(){
43715
44165
  useItem,
43716
44166
  use(hold = 0) {
43717
44167
  if (tool.def.category === "hand") {
43718
- grab();
44168
+ handUse();
44169
+ flush();
43719
44170
  return;
43720
44171
  }
43721
44172
  useTool(FIXED_DT, false);
@@ -43948,7 +44399,8 @@ void main(){
43948
44399
  },
43949
44400
  onAction: (id) => {
43950
44401
  if (activeBench) slowly(activeBench, id, () => benches.get(activeBench)?.act(id));
43951
- }
44402
+ },
44403
+ ...options.testTable !== false && arena.tops.some((x) => x.id === tableScenery) ? { testTable } : {}
43952
44404
  });
43953
44405
  ui.hud(hudState());
43954
44406
  showBench();