@adatechnology/game-test-room 0.14.0 → 0.16.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
@@ -476,6 +482,13 @@ var GameTestRoom = (() => {
476
482
  node.userData[KIT_ORIGIN_KEY] = { ...origin };
477
483
  return object;
478
484
  }
485
+ function kitOriginOf(object) {
486
+ for (let node = object; node; node = node.parent) {
487
+ const origin = node.userData?.[KIT_ORIGIN_KEY];
488
+ if (origin && typeof origin.owner === "string") return origin;
489
+ }
490
+ return null;
491
+ }
479
492
 
480
493
  // ../game-destruction/dist/index.js
481
494
  var FIRE_PRIME_C = 300;
@@ -806,6 +819,62 @@ var GameTestRoom = (() => {
806
819
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
807
820
  };
808
821
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
822
+ var splinterBox = (inp, box2, limit = inp.maxFragments) => {
823
+ const { rnd: rnd2 } = inp;
824
+ const dims = box2.s;
825
+ const [fmin, fmax] = inp.fragmentSize;
826
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
827
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
828
+ const w = Math.max(fmin * 1.5, 0.01);
829
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
830
+ const maxLen = fmax * 4;
831
+ const cells = bsp(box2, c, [a, b], rnd2);
832
+ const pieces = [];
833
+ for (const cell of cells) {
834
+ const extra = rnd2() < 0.4 ? 1 : 0;
835
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
836
+ for (const p of bsp(cell, k, [g], rnd2)) pieces.push(p);
837
+ }
838
+ return pieces.slice(0, limit).map((p) => ({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] }));
839
+ };
840
+ var severAt = (inp, root, at3, gap) => {
841
+ const dims = root.s;
842
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
843
+ const half = dims[L] / 2;
844
+ const lo = clamp(at3 - gap / 2, -half, half), hi = clamp(at3 + gap / 2, -half, half);
845
+ const out = [];
846
+ const stub = (from, to) => {
847
+ if (to - from < 0.02) return;
848
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
849
+ c[L] = (from + to) / 2;
850
+ sz[L] = to - from;
851
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
852
+ };
853
+ stub(-half, lo);
854
+ stub(hi, half);
855
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
856
+ band.c[L] = (lo + hi) / 2;
857
+ band.s[L] = Math.max(hi - lo, 0.01);
858
+ for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
859
+ return out;
860
+ };
861
+ var tearStack = (inp, root, t, u, v, n, sheet) => {
862
+ const { dims, rnd: rnd2, maxFragments } = inp;
863
+ const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
864
+ const per = clamp(n, 3, Math.max(3, Math.floor(maxFragments / layers)));
865
+ const out = [];
866
+ for (let k = 0; k < layers; k++) {
867
+ const off = -dims[t] / 2 + (k + 0.5) * dims[t] / layers;
868
+ for (const b of bsp(root, per, [u, v], rnd2)) {
869
+ if (out.length >= maxFragments) return out;
870
+ const c = [b.c[0], b.c[1], b.c[2]];
871
+ c[t] = off;
872
+ 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 });
873
+ }
874
+ }
875
+ return out;
876
+ };
877
+ var STACK_LAYER = 0.012;
809
878
  var planFragments = (inp) => {
810
879
  const { style, dims, rnd: rnd2, maxFragments } = inp;
811
880
  const [fmin, fmax] = inp.fragmentSize;
@@ -827,27 +896,14 @@ var GameTestRoom = (() => {
827
896
  return out;
828
897
  }
829
898
  if (style === "splinter") {
830
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
831
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
832
- const w = Math.max(fmin * 1.5, 0.01);
833
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
834
- const maxLen = fmax * 4;
835
- const cells = bsp(root, c, [a, b], rnd2);
836
- const pieces = [];
837
- for (const cell of cells) {
838
- let list = [cell];
839
- const extra = rnd2() < 0.4 ? 1 : 0;
840
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
841
- list = bsp(cell, k, [g], rnd2);
842
- for (const p of list) pieces.push(p);
843
- }
844
- for (const p of pieces.slice(0, maxFragments)) {
845
- out.push({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] });
846
- }
899
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
900
+ for (const p of splinterBox(inp, root)) out.push(p);
847
901
  return out;
848
902
  }
849
903
  if (style === "tear") {
850
904
  const n2 = clamp(Math.round(dims[u] * dims[v] / (mean * mean)), 3, maxFragments);
905
+ const sheet = inp.sheet ?? 0;
906
+ if (sheet > 0 && dims[t] > sheet * 4) return tearStack(inp, root, t, u, v, n2, sheet);
851
907
  const th2 = Math.min(inp.thickness, dims[t]);
852
908
  for (const b of bsp(root, n2, [u, v], rnd2)) {
853
909
  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 +2476,7 @@ var GameTestRoom = (() => {
2420
2476
  0.03,
2421
2477
  0.15
2422
2478
  ],
2479
+ sheet: 1e-3,
2423
2480
  damageScale: {
2424
2481
  slash: 3
2425
2482
  },
@@ -2435,7 +2492,8 @@ var GameTestRoom = (() => {
2435
2492
  smoke: "grey"
2436
2493
  },
2437
2494
  visual: {
2438
- charColor: 1709072
2495
+ charColor: 1709072,
2496
+ innerColor: 15524048
2439
2497
  },
2440
2498
  tags: [
2441
2499
  "fabric",
@@ -2822,9 +2880,14 @@ var GameTestRoom = (() => {
2822
2880
  var BREACH_AREA = 0.25;
2823
2881
  var BREACH_BLAST_REACH = 0.12;
2824
2882
  var RESIDUAL_CAPACITY = 0.15;
2883
+ var SEVER_BAND = 0.1;
2884
+ var SEVER_ASPECT = 3;
2885
+ var SEVER_BALLISTIC = 2;
2825
2886
  var SUPPORT_TOLERANCE = 0.05;
2826
2887
  var UNIT_FRAGMENT_FACTOR = 4;
2827
2888
  var BREACH_GRID_CAP = 4096;
2889
+ var CHIP_ENERGY = 200;
2890
+ var CHIP_SPLASH = 0.5;
2828
2891
  var CUT_SLICE = 1;
2829
2892
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2830
2893
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2868,6 +2931,7 @@ var GameTestRoom = (() => {
2868
2931
  let windCfg = windOpts ? resolveWind(windOpts) : null;
2869
2932
  const ignitionRules = resolveIgnitionRules(options.ignition);
2870
2933
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
2934
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
2871
2935
  const rng = createRng(options.seed ?? 1);
2872
2936
  const rnd2 = () => rng.next();
2873
2937
  let time = 0;
@@ -3241,41 +3305,68 @@ var GameTestRoom = (() => {
3241
3305
  markDamaged(p);
3242
3306
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
3243
3307
  };
3308
+ const freeSpan = (p, supports) => {
3309
+ if (supports.length === 0) return Infinity;
3310
+ const d = p.dims;
3311
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
3312
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
3313
+ let far = 0;
3314
+ for (let i = 0; i <= 16; i++) {
3315
+ const x = (i / 16 - 0.5) * d[L];
3316
+ let near = Infinity;
3317
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
3318
+ far = Math.max(far, near);
3319
+ }
3320
+ return 2 * far;
3321
+ };
3244
3322
  const solveStructure = (o) => {
3245
3323
  o.maxStress = 0;
3246
3324
  if (!o.hasStructure) return;
3247
3325
  const parts = o.parts;
3248
3326
  const n = parts.length;
3249
3327
  const dist = new Array(n).fill(-1);
3250
- const queue = [];
3251
3328
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
3252
3329
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
3253
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
3254
- dist[i] = 0;
3255
- queue.push(i);
3256
- }
3257
- for (let h = 0; h < queue.length; h++) {
3258
- const s = queue[h];
3259
- if (isPartition(s)) continue;
3260
- for (const j of o.byPart.get(parts[s].spec.id)) {
3261
- if (j.st.open || !j.bears) continue;
3262
- const r = j.ia === s ? j.ib : j.ia;
3263
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
3264
- dist[r] = dist[s] + 1;
3265
- queue.push(r);
3330
+ const out = /* @__PURE__ */ new Set();
3331
+ let queue = [];
3332
+ let lowerOf = /* @__PURE__ */ new Map();
3333
+ for (; ; ) {
3334
+ dist.fill(-1);
3335
+ queue = [];
3336
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
3337
+ dist[i] = 0;
3338
+ queue.push(i);
3339
+ }
3340
+ for (let h = 0; h < queue.length; h++) {
3341
+ const s = queue[h];
3342
+ if (isPartition(s)) continue;
3343
+ for (const j of o.byPart.get(parts[s].spec.id)) {
3344
+ if (j.st.open || !j.bears) continue;
3345
+ const r = j.ia === s ? j.ib : j.ia;
3346
+ if (dist[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
3347
+ dist[r] = dist[s] + 1;
3348
+ queue.push(r);
3349
+ }
3266
3350
  }
3267
- }
3268
- const incoming = new Array(n).fill(0);
3269
- const lowerOf = /* @__PURE__ */ new Map();
3270
- for (const i of queue) {
3271
- const lower = [];
3272
- for (const j of o.byPart.get(parts[i].spec.id)) {
3273
- if (j.st.open || !j.bears) continue;
3274
- const k = restsOn(o, j, i);
3275
- if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
3351
+ lowerOf = /* @__PURE__ */ new Map();
3352
+ let dropped = false;
3353
+ for (const i of queue) {
3354
+ const lower = [];
3355
+ for (const j of o.byPart.get(parts[i].spec.id)) {
3356
+ if (j.st.open || !j.bears) continue;
3357
+ const k = restsOn(o, j, i);
3358
+ if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
3359
+ }
3360
+ lowerOf.set(i, lower);
3361
+ const max2 = parts[i].spec.structure?.maxSpan;
3362
+ if (max2 !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max2) {
3363
+ out.add(i);
3364
+ dropped = true;
3365
+ }
3276
3366
  }
3277
- lowerOf.set(i, lower);
3367
+ if (!dropped) break;
3278
3368
  }
3369
+ const incoming = new Array(n).fill(0);
3279
3370
  const bending = new Array(n).fill(0);
3280
3371
  const through = parts.map(() => []);
3281
3372
  const pass = (k, at3, mass) => {
@@ -3787,6 +3878,7 @@ var GameTestRoom = (() => {
3787
3878
  const r = rayPart(o, p, origin, d);
3788
3879
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
3789
3880
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
3881
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
3790
3882
  if (!best || r.t0 < best.distance) best = { object: o.id, part: p.spec.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
3791
3883
  }
3792
3884
  }
@@ -4050,6 +4142,62 @@ var GameTestRoom = (() => {
4050
4142
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
4051
4143
  openBreach(o, p, [sum2[0] / total, sum2[1] / total, sum2[2] / total], total, energy, dir, src);
4052
4144
  };
4145
+ const unitGrids = /* @__PURE__ */ new WeakMap();
4146
+ const gridOf = (p) => {
4147
+ let grid = unitGrids.get(p);
4148
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP));
4149
+ return grid;
4150
+ };
4151
+ const insideChip = (p, objectPoint) => {
4152
+ const cells = p.st.chipped;
4153
+ if (!cells?.length) return false;
4154
+ const [u, v] = planeAxes(p.thin);
4155
+ const lp = toLocal(p.pos, p.q, objectPoint);
4156
+ return cells.some((c) => Math.abs(lp[u] - c.center[u]) <= c.size[u] / 2 && Math.abs(lp[v] - c.center[v]) <= c.size[v] / 2);
4157
+ };
4158
+ const chipUnits = (o, p, wp, energy, dir, radius, src) => {
4159
+ var _a, _b;
4160
+ if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4161
+ const [u, v] = planeAxes(p.thin);
4162
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4163
+ const grid = gridOf(p);
4164
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
4165
+ const out = [];
4166
+ for (let i = 0; i < grid.length; i++) {
4167
+ const cell = grid[i];
4168
+ if (worn[i] === -1) continue;
4169
+ const gap = Math.hypot(Math.max(0, Math.abs(lp[u] - cell.center[u]) - cell.size[u] / 2), Math.max(0, Math.abs(lp[v] - cell.center[v]) - cell.size[v] / 2));
4170
+ if (gap > radius) continue;
4171
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
4172
+ const total = (worn[i] ?? 0) + energy * share;
4173
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4174
+ if (total < chipEnergy * mass) {
4175
+ worn[i] = total;
4176
+ continue;
4177
+ }
4178
+ worn[i] = -1;
4179
+ out.push(cell);
4180
+ }
4181
+ if (out.length === 0) return;
4182
+ const { c, q } = partXf(o, p);
4183
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4184
+ const holes = localHolesOf(p);
4185
+ for (const cell of out) {
4186
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
4187
+ const center = toWorld(c, q, cell.center);
4188
+ const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
4189
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
4190
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
4191
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
4192
+ p.st.mass = Math.max(0, p.st.mass - mass);
4193
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
4194
+ markDamaged(p);
4195
+ setStage(o, p, 1, {}, src);
4196
+ emit2({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
4197
+ debris2(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4198
+ }
4199
+ for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4200
+ };
4053
4201
  const openBreach = (o, p, center, total, energy, dir, src) => {
4054
4202
  const [u, v] = planeAxes(p.thin);
4055
4203
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -4061,7 +4209,8 @@ var GameTestRoom = (() => {
4061
4209
  failPart(o, p, worldCenter, dir, energy, 12, src);
4062
4210
  return;
4063
4211
  }
4064
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP), p.thin, lc, radius);
4212
+ const worn = p.rt.chips;
4213
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
4065
4214
  const removed = p.st.mass * cells.reduce((sum2, cell) => sum2 + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
4066
4215
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
4067
4216
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -4145,6 +4294,8 @@ var GameTestRoom = (() => {
4145
4294
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
4146
4295
  const hitLocal = toLocal(c, q, hitW);
4147
4296
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4297
+ const axis2 = style === "splinter" ? severAxis(p) : -1;
4298
+ const cutAt = axis2 < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis2] : void 0);
4148
4299
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4149
4300
  style,
4150
4301
  dims: p.dims,
@@ -4153,7 +4304,9 @@ var GameTestRoom = (() => {
4153
4304
  hit: hitLocal,
4154
4305
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4155
4306
  maxFragments,
4156
- rnd: rnd2
4307
+ rnd: rnd2,
4308
+ sheet: p.mat.sheet,
4309
+ ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
4157
4310
  });
4158
4311
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
4159
4312
  const mass = p.st.mass;
@@ -4177,7 +4330,8 @@ var GameTestRoom = (() => {
4177
4330
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd2(), dv[2] * k],
4178
4331
  angularVelocity: [(rnd2() - 0.5) * 12, (rnd2() - 0.5) * 12, (rnd2() - 0.5) * 12],
4179
4332
  mass: mass * f.weight / totW,
4180
- ...marks.length > 0 ? { marks } : {}
4333
+ ...marks.length > 0 ? { marks } : {},
4334
+ ...f.inner ? { inner: true } : {}
4181
4335
  };
4182
4336
  });
4183
4337
  const reinforcement = p.spec.reinforcement;
@@ -4211,10 +4365,33 @@ var GameTestRoom = (() => {
4211
4365
  emit2({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
4212
4366
  };
4213
4367
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
4368
+ const severAxis = (p) => {
4369
+ if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
4370
+ const d = p.dims;
4371
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
4372
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
4373
+ };
4374
+ const bandOf = (o, p, axis2, origin) => {
4375
+ const { c, q } = partXf(o, p);
4376
+ const along = toLocal(c, q, origin)[axis2];
4377
+ const L = p.dims[axis2];
4378
+ return clamp2(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
4379
+ };
4214
4380
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
4381
+ var _a;
4215
4382
  if (isLost(p) || amount <= 0) return 0;
4216
4383
  const prev = p.st.hp;
4217
4384
  p.st.hp -= amount;
4385
+ const axis2 = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
4386
+ if (axis2 >= 0 && p.st.hp > 0) {
4387
+ const band = bandOf(o, p, axis2, origin);
4388
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
4389
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
4390
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis2]) {
4391
+ p.rt.cutAt = -p.dims[axis2] / 2 + (band + 0.5) * SEVER_BAND;
4392
+ p.st.hp = 0;
4393
+ }
4394
+ }
4218
4395
  markDamaged(p);
4219
4396
  const trigger = CHAIN_TRIGGER_OF[type];
4220
4397
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -4253,7 +4430,7 @@ var GameTestRoom = (() => {
4253
4430
  const ballistic2 = (o, p, entry, n, d, E, caliber, pathLen, src, round2 = {}) => {
4254
4431
  const mat = p.mat;
4255
4432
  const piercing = round2.piercing ?? 1;
4256
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
4433
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry)) || insideChip(p, objLocal(o, entry))) {
4257
4434
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
4258
4435
  }
4259
4436
  const sin2 = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -4312,6 +4489,7 @@ var GameTestRoom = (() => {
4312
4489
  debris2(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4313
4490
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
4314
4491
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale2, "ballistic", entry, d, E * 0.1, 12, src);
4492
+ chipUnits(o, p, entry, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
4315
4493
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4316
4494
  };
4317
4495
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -4339,6 +4517,7 @@ var GameTestRoom = (() => {
4339
4517
  const scale2 = mat.damageScale?.[kind] ?? 1;
4340
4518
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale2 * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
4341
4519
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
4520
+ if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
4342
4521
  return done;
4343
4522
  };
4344
4523
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -5488,6 +5667,7 @@ var GameTestRoom = (() => {
5488
5667
  sway: "sway",
5489
5668
  spall: "plaster came off",
5490
5669
  stage: "damage stage",
5670
+ chip: "brick knocked out",
5491
5671
  expose: "rebar bare",
5492
5672
  body: "body",
5493
5673
  blast: "blast",
@@ -5578,6 +5758,7 @@ var GameTestRoom = (() => {
5578
5758
  sway: "balan\xE7o",
5579
5759
  spall: "reboco soltou",
5580
5760
  stage: "est\xE1gio de dano",
5761
+ chip: "tijolo arrancado",
5581
5762
  expose: "vergalh\xE3o \xE0 mostra",
5582
5763
  body: "corpo",
5583
5764
  blast: "explos\xE3o",
@@ -15742,6 +15923,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15742
15923
  const bl = Math.round(ab + ((b & 255) - ab) * t);
15743
15924
  return r << 16 | g << 8 | bl;
15744
15925
  }
15926
+ var SHEET_MAX = 4e-3;
15927
+ var SHEET_AIR = 2.5;
15928
+ var SHEET_SWAY = 3;
15745
15929
  var FragmentSystem = class {
15746
15930
  constructor(THREE, o) {
15747
15931
  __publicField2(this, "THREE", THREE);
@@ -15847,7 +16031,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15847
16031
  asleep: false,
15848
16032
  rest: 0,
15849
16033
  fade: 1,
15850
- dying: false
16034
+ dying: false,
16035
+ air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0
15851
16036
  };
15852
16037
  this.list.push(live);
15853
16038
  let alive = this.list.reduce((n, l) => n + (l.dying ? 0 : 1), 0);
@@ -15931,6 +16116,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15931
16116
  }
15932
16117
  } else {
15933
16118
  f.ang.multiplyScalar(Math.exp(-0.15 * dt));
16119
+ if (f.air > 0) this.flutter(f, dt);
15934
16120
  }
15935
16121
  if (contact && f.vel.lengthSq() < 4) this.settle(f, dt);
15936
16122
  const slow = f.vel.lengthSq() < 0.012 && f.ang.lengthSq() < 0.2;
@@ -15945,6 +16131,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15945
16131
  f.still = 0;
15946
16132
  }
15947
16133
  }
16134
+ /** A sheet in the air: drag slows it to a slow fall, and it sways and turns over as it goes. */
16135
+ flutter(f, dt) {
16136
+ f.vel.multiplyScalar(Math.exp(-f.air * dt));
16137
+ f.vel.x += (this.rng() - 0.5) * SHEET_SWAY * dt;
16138
+ f.vel.z += (this.rng() - 0.5) * SHEET_SWAY * dt;
16139
+ f.ang.x += (this.rng() - 0.5) * SHEET_SWAY * 4 * dt;
16140
+ f.ang.z += (this.rng() - 0.5) * SHEET_SWAY * 4 * dt;
16141
+ }
15948
16142
  /** Tip the fragment towards lying on its flattest face. */
15949
16143
  settle(f, dt) {
15950
16144
  const mesh2 = f.mesh;
@@ -17606,6 +17800,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17606
17800
  }
17607
17801
  };
17608
17802
  var rendererCount = 0;
17803
+ var COLLAPSE_CLOUD_KG = 1e3;
17609
17804
  var IMPACT_TAGS = ["metal", "glass", "wood"];
17610
17805
  var GLOW_FROM_C = 430;
17611
17806
  var QUEUE_CAP = 4e3;
@@ -18065,10 +18260,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18065
18260
  case "settle":
18066
18261
  this.onSettle(ev.object);
18067
18262
  break;
18068
- case "collapse":
18263
+ case "collapse": {
18264
+ const root = this.bounds.get(ev.object)?.root;
18265
+ const furniture = !!root && kitOriginOf(root)?.kind === "furniture";
18069
18266
  this.splitLoose(ev.object);
18070
- this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
18267
+ if (!furniture || ev.mass >= COLLAPSE_CLOUD_KG) this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
18268
+ else this.vfx.burst("dust", this.rootPoint(ev.object), [0, 1, 0], clamp6(Math.round(ev.mass / 4), 4, 24), 0.8);
18071
18269
  break;
18270
+ }
18072
18271
  case "disassemble_progress": {
18073
18272
  const k = `unscrew:${ev.joint}`;
18074
18273
  if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
@@ -18098,6 +18297,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18098
18297
  case "stage":
18099
18298
  this.onStage(ev);
18100
18299
  break;
18300
+ case "chip":
18301
+ this.onChip(ev);
18302
+ break;
18101
18303
  case "expose":
18102
18304
  this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
18103
18305
  if (ev.bars.length > 0) {
@@ -18225,6 +18427,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18225
18427
  const def = pv ? this.world.materials.get(pv.material) : void 0;
18226
18428
  if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
18227
18429
  if (kind === "fibers" || kind === "leaves") {
18430
+ if (def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
18228
18431
  const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
18229
18432
  return c >= 0 ? { color: c } : void 0;
18230
18433
  }
@@ -18353,6 +18556,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18353
18556
  if (!played) this.vfx.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
18354
18557
  this.sound(impactKey(def), ev.opening.center, 0.8);
18355
18558
  }
18559
+ /**
18560
+ * A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust. The gap itself is the drawing's (the house renderer redraws
18561
+ * the wall as its courses without that unit); a part drawn as one box keeps its box.
18562
+ */
18563
+ onChip(ev) {
18564
+ const pv = this.partVis(ev.object, ev.part);
18565
+ const def = pv ? this.world.materials.get(pv.material) : void 0;
18566
+ const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
18567
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
18568
+ for (const fr of ev.fragments) this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fallbackColor(fr.material, def), false), ev.object, maxAlive);
18569
+ if (!this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.center)) this.vfx.burst("dust", ev.center, [0, 1, 0], 6, 0.8, { color });
18570
+ this.sound(impactKey(def), ev.center, 0.7);
18571
+ }
18356
18572
  onFracture(ev) {
18357
18573
  const b = this.bounds.get(ev.object);
18358
18574
  const pv = b?.parts.get(ev.part);
@@ -18373,7 +18589,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18373
18589
  for (let i = 0; i < ev.fragments.length; i += step) {
18374
18590
  const fr = ev.fragments[i];
18375
18591
  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);
18592
+ const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def || !fdef ? color : fallbackColor(fr.material, fdef);
18593
+ const mat = this.fragments.materialFor(fr.material, tone2, transparent || fdef?.visual?.transparent === true, opacity);
18377
18594
  this.fragments.spawn(fr, mat, ev.object, maxAlive);
18378
18595
  }
18379
18596
  const up = [0, 1, 0];
@@ -18391,7 +18608,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18391
18608
  this.vfx.burst("dust", ev.point, up, 5, 0.8, { color: 11901556 });
18392
18609
  break;
18393
18610
  case "tear":
18394
- this.vfx.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color });
18611
+ this.vfx.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color: def?.visual?.innerColor ?? color });
18395
18612
  break;
18396
18613
  case "snap":
18397
18614
  this.vfx.burst("chips", ev.point, up, 8 * sizeK, 3);
@@ -38147,7 +38364,7 @@ void main(){
38147
38364
  if (!position) throw new Error(`game-test-room: scenery "${id}" is new, so it needs a position`);
38148
38365
  merged.set(id, { ...obj, position });
38149
38366
  }
38150
- 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 }));
38367
+ return [...merged.entries()].map(([id, o]) => ({ id, spec: o.spec, position: o.position, rotationY: o.rotationY ?? 0, colors: o.colors, build: o.build, follow: o.follow, label: o.label, top: o.top, spout: o.spout, free: o.free }));
38151
38368
  }
38152
38369
  function createArena(THREE, size, scene2, scenery, tapOption) {
38153
38370
  const group = markKitOrigin(new THREE.Group(), { owner: "game-test-room", kind: "room" });
@@ -38748,6 +38965,14 @@ void main(){
38748
38965
  ...projectile.fire ? { fire: projectile.fire } : {}
38749
38966
  });
38750
38967
  };
38968
+ const blastAt = (weapon, explosive, point) => {
38969
+ const def = arsenal.explosives.get(explosive);
38970
+ if (!def) return false;
38971
+ const event = new ExplosiveBody({ explosive: def, origin: point, triggers: ["remote"], rules: arsenal.rules.explosives }).trigger("remote");
38972
+ if (event?.type !== "detonate") return false;
38973
+ detonate({ weapon }, event.detonation);
38974
+ return true;
38975
+ };
38751
38976
  const detonateCharges = () => {
38752
38977
  let count = 0;
38753
38978
  for (const projectile of [...projectiles]) {
@@ -38839,7 +39064,172 @@ void main(){
38839
39064
  }
38840
39065
  }
38841
39066
  };
38842
- return { throwUse, placeUse, backblastUse, detonateCharges, update, clear };
39067
+ return { throwUse, placeUse, backblastUse, detonateCharges, blastAt, update, clear };
39068
+ }
39069
+
39070
+ // src/test-table.ts
39071
+ var TRIAL_KINDS = ["shot", "punch", "stab", "fire", "blast"];
39072
+ var DEFAULT_TRIALS = {
39073
+ shot: { weapon: "pistol_9mm", distance: 2.5, lift: 0.5 },
39074
+ punch: { weapon: "fists", distance: 0.3, lift: 0.15 },
39075
+ stab: { weapon: "knife", distance: 0.22, lift: 0.1 },
39076
+ fire: { weapon: "flamethrower", distance: 1.6, lift: 0.4, hold: 0.8 },
39077
+ blast: { weapon: "frag_grenade", distance: 0.3, lift: 1, watch: 6 }
39078
+ };
39079
+ var resolveTrials = (overrides = {}) => Object.fromEntries(TRIAL_KINDS.map((kind) => [kind, { ...DEFAULT_TRIALS[kind], ...overrides[kind] }]));
39080
+ function fitRow(width, start, total, length3, gap) {
39081
+ let used = 0;
39082
+ let count = 0;
39083
+ for (let i = start; i < total; i++) {
39084
+ const w = width(i);
39085
+ const next = count === 0 ? w : used + gap + w;
39086
+ if (count > 0 && next > length3) break;
39087
+ used = next;
39088
+ count++;
39089
+ }
39090
+ return count;
39091
+ }
39092
+ function rowCentres(widths, centreX, gap) {
39093
+ const length3 = widths.reduce((sum2, w) => sum2 + w, 0) + gap * Math.max(0, widths.length - 1);
39094
+ let x = centreX - length3 / 2;
39095
+ return widths.map((w) => {
39096
+ const centre = x + w / 2;
39097
+ x += w + gap;
39098
+ return centre;
39099
+ });
39100
+ }
39101
+ function trialView(tool, kind, min2, max2) {
39102
+ const target = [(min2[0] + max2[0]) / 2, (min2[1] + max2[1]) / 2, (min2[2] + max2[2]) / 2];
39103
+ const away = kind === "blast" ? tool.watch ?? 6 : tool.distance;
39104
+ return { eye: [target[0], target[1] + tool.lift, max2[2] + away], target };
39105
+ }
39106
+ function createTestTable(context, options = {}) {
39107
+ const gap = options.gap ?? 0.12;
39108
+ const margin = options.margin ?? 0.1;
39109
+ const trials = resolveTrials(options.trials);
39110
+ const listeners = /* @__PURE__ */ new Set();
39111
+ let current = null;
39112
+ const itemsOf = (group) => context.groups().get(group) ?? [];
39113
+ const emit2 = () => {
39114
+ const page = state();
39115
+ for (const listener of [...listeners]) listener(page);
39116
+ return page;
39117
+ };
39118
+ const state = () => {
39119
+ if (!current) return null;
39120
+ const total = itemsOf(current.group).length;
39121
+ const live = current.objects.filter((o) => context.box(o.objectId));
39122
+ return {
39123
+ group: current.group,
39124
+ page: current.page,
39125
+ first: current.first,
39126
+ total,
39127
+ objects: live,
39128
+ selected: current.selected,
39129
+ hasPrev: current.page > 0,
39130
+ hasNext: current.first + current.objects.length < total
39131
+ };
39132
+ };
39133
+ const bounds = /* @__PURE__ */ new Map();
39134
+ const widthOf = (itemId) => context.measure(itemId).w;
39135
+ const boundsUntil = (group, length3, until) => {
39136
+ const items = itemsOf(group);
39137
+ const known = bounds.get(group) ?? [0];
39138
+ bounds.set(group, known);
39139
+ while (known[known.length - 1] < items.length && !until(known)) {
39140
+ const first = known[known.length - 1];
39141
+ known.push(first + fitRow((i) => widthOf(items[i].id), first, items.length, length3, gap));
39142
+ }
39143
+ return known;
39144
+ };
39145
+ const lengthOf2 = (top) => top.maxX - top.minX - 2 * margin;
39146
+ const lay = (group, page, top) => {
39147
+ const items = itemsOf(group);
39148
+ const known = boundsUntil(group, lengthOf2(top), (b) => b.length > page + 1);
39149
+ const index = Math.max(0, Math.min(page, known.length - 2));
39150
+ const first = known[index];
39151
+ const laid = items.slice(first, known[index + 1]);
39152
+ context.clear();
39153
+ const sizes = laid.map((item) => context.measure(item.id));
39154
+ const centres = rowCentres(sizes.map((size) => size.w), (top.minX + top.maxX) / 2, gap);
39155
+ const objects = laid.map((item, i) => {
39156
+ const d = Math.min(sizes[i].d, top.maxZ - top.minZ);
39157
+ const { objectId } = context.spawn(item.id, [centres[i], top.top, top.maxZ - margin / 2 - d / 2]);
39158
+ return { objectId, itemId: item.id, name: item.name };
39159
+ });
39160
+ current = { group, page: index, first, objects, selected: objects[0]?.objectId ?? null };
39161
+ 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) });
39162
+ return emit2();
39163
+ };
39164
+ const open = (group, page = 0) => {
39165
+ const top = context.top();
39166
+ if (!itemsOf(group).length || !top) return null;
39167
+ return lay(group, page, top);
39168
+ };
39169
+ const find = (itemId) => {
39170
+ const top = context.top();
39171
+ const group = [...context.groups()].find(([, items]) => items.some((i) => i.id === itemId))?.[0];
39172
+ if (group === void 0 || !top) return null;
39173
+ const index = itemsOf(group).findIndex((i) => i.id === itemId);
39174
+ const known = boundsUntil(group, lengthOf2(top), (b) => b[b.length - 1] > index);
39175
+ lay(group, known.findIndex((bound, i) => bound <= index && index < known[i + 1]), top);
39176
+ const target = current?.objects.find((o) => o.itemId === itemId);
39177
+ if (current && target) current.selected = target.objectId;
39178
+ return emit2();
39179
+ };
39180
+ const selected = (objectId) => {
39181
+ const id = objectId ?? current?.selected ?? null;
39182
+ return id && context.box(id) ? id : null;
39183
+ };
39184
+ const trial = (kind, objectId) => {
39185
+ const id = selected(objectId);
39186
+ const box2 = id ? context.box(id) : null;
39187
+ if (!id || !box2) return false;
39188
+ const tool = trials[kind];
39189
+ if (!context.equip(tool.weapon)) return false;
39190
+ const view = trialView(tool, kind, box2.min, box2.max);
39191
+ context.look(view.eye, view.target);
39192
+ const name = current?.objects.find((o) => o.objectId === id)?.name ?? id;
39193
+ context.log("trial", context.translate(`act.trial.${kind}`, { name }), { kind, objectId: id, weapon: tool.weapon });
39194
+ if (kind === "blast") {
39195
+ const point = [view.target[0], box2.min[1] + 0.05, box2.min[2] - tool.distance];
39196
+ return context.blastAt(tool.weapon, point);
39197
+ }
39198
+ context.use(tool.hold ?? 0);
39199
+ emit2();
39200
+ return true;
39201
+ };
39202
+ return {
39203
+ groups: () => [...context.groups()].filter(([, items]) => items.length > 0).map(([group]) => group),
39204
+ items: () => [...context.groups()].flatMap(([group, items]) => items.map((item) => ({ id: item.id, name: item.name, group }))),
39205
+ open,
39206
+ find,
39207
+ next: () => {
39208
+ const page = state();
39209
+ return page?.hasNext ? open(page.group, page.page + 1) : page;
39210
+ },
39211
+ prev: () => {
39212
+ const page = state();
39213
+ return page?.hasPrev ? open(page.group, page.page - 1) : page;
39214
+ },
39215
+ relay: () => {
39216
+ if (!current) return null;
39217
+ context.resetAll();
39218
+ return open(current.group, current.page);
39219
+ },
39220
+ state,
39221
+ select: (objectId) => {
39222
+ if (!current || !current.objects.some((o) => o.objectId === objectId) || !context.box(objectId)) return false;
39223
+ current.selected = objectId;
39224
+ emit2();
39225
+ return true;
39226
+ },
39227
+ trial,
39228
+ on: (listener) => {
39229
+ listeners.add(listener);
39230
+ return () => listeners.delete(listener);
39231
+ }
39232
+ };
38843
39233
  }
38844
39234
 
38845
39235
  // src/host-events.ts
@@ -40207,6 +40597,9 @@ void main(){
40207
40597
  "act.strike.leg": "{weapon}: {clip} with the {leg} leg begins, impact in {seconds} s",
40208
40598
  "leg.left": "left",
40209
40599
  "leg.right": "right",
40600
+ "act.strike.hand": "{weapon}: {clip} with the {hand} hand begins, impact in {seconds} s",
40601
+ "hand.left": "left",
40602
+ "hand.right": "right",
40210
40603
  "act.flame.hit": "{weapon}: flame at {temp} \xB0C on {where}",
40211
40604
  "act.flame.air": "{weapon}: flame, nothing in range",
40212
40605
  "act.heat.hit": "{weapon}: heating {where} at {temp} \xB0C",
@@ -40238,6 +40631,7 @@ void main(){
40238
40631
  "loading.open_area": "Opening {area}\u2026",
40239
40632
  "loading.spawn": "Building {name}\u2026",
40240
40633
  "act.loosened": "{name} was heavy but free: the blast moved it",
40634
+ "act.shoved": "{name} stands free: the shots moved it",
40241
40635
  "act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
40242
40636
  "act.use_item": "{name}: {use}",
40243
40637
  "act.tap.on": "Tap opened",
@@ -40271,7 +40665,7 @@ void main(){
40271
40665
  "hud.throw": "throw",
40272
40666
  "hud.tilt": "tilt (hold)",
40273
40667
  "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",
40668
+ "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
40669
  "ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
40276
40670
  "camera.first": "1st person",
40277
40671
  "camera.third": "3rd person",
@@ -40285,6 +40679,32 @@ void main(){
40285
40679
  "ui.copy_json": "Copy JSON",
40286
40680
  "ui.download_ndjson": "Download NDJSON",
40287
40681
  "ui.download_txt": "Download TXT",
40682
+ "ui.test_table": "Test table",
40683
+ "ui.test_table.group": "Group to lay on the table",
40684
+ "ui.test_table.lay": "Lay",
40685
+ "ui.test_table.prev": "Previous items",
40686
+ "ui.test_table.next": "Next items",
40687
+ "ui.test_table.relay": "Put back",
40688
+ "ui.test_table.search": "Find an item to put on the table",
40689
+ "ui.test_table.hint": "Pick a group and lay it on the table, then pick an item and try it.",
40690
+ "ui.test_table.page": "Items {from}\u2013{to} of {total}. Pick one, then a trial:",
40691
+ "ui.trial.shot": "Shot",
40692
+ "ui.trial.punch": "Punch",
40693
+ "ui.trial.stab": "Knife",
40694
+ "ui.trial.fire": "Fire",
40695
+ "ui.trial.blast": "Blast",
40696
+ "ui.trial.shot.hint": "A 9 mm round from 2.5 m",
40697
+ "ui.trial.punch.hint": "A punch, up close",
40698
+ "ui.trial.stab.hint": "A knife blow, up close",
40699
+ "ui.trial.fire.hint": "A flamethrower burst from 1.6 m",
40700
+ "ui.trial.blast.hint": "A fragmentation grenade goes off just behind it (it reaches the neighbours too)",
40701
+ "act.test_table": "Test table: {group}, items {from}\u2013{to} of {total}",
40702
+ "act.trial.shot": "Trial: shooting {name}",
40703
+ "act.trial.punch": "Trial: punching {name}",
40704
+ "act.trial.stab": "Trial: stabbing {name}",
40705
+ "act.trial.fire": "Trial: setting fire to {name}",
40706
+ "act.trial.blast": "Trial: a blast beside {name}",
40707
+ "arena.test_table": "Test table",
40288
40708
  "ui.clear_log": "Clear",
40289
40709
  "ui.catalog": "Catalogue",
40290
40710
  "ui.clear": "Clear",
@@ -40488,6 +40908,9 @@ void main(){
40488
40908
  "act.strike.leg": "{weapon}: {clip} com a perna {leg} come\xE7a, impacto em {seconds} s",
40489
40909
  "leg.left": "esquerda",
40490
40910
  "leg.right": "direita",
40911
+ "act.strike.hand": "{weapon}: {clip} com a m\xE3o {hand} come\xE7a, impacto em {seconds} s",
40912
+ "hand.left": "esquerda",
40913
+ "hand.right": "direita",
40491
40914
  "act.flame.hit": "{weapon}: chama a {temp} \xB0C em {where}",
40492
40915
  "act.flame.air": "{weapon}: chama, nada ao alcance",
40493
40916
  "act.heat.hit": "{weapon}: esquentando {where} a {temp} \xB0C",
@@ -40519,6 +40942,7 @@ void main(){
40519
40942
  "loading.open_area": "Abrindo {area}\u2026",
40520
40943
  "loading.spawn": "Montando {name}\u2026",
40521
40944
  "act.loosened": "{name} \xE9 pesado mas estava solto: a explos\xE3o o moveu",
40945
+ "act.shoved": "{name} estava solto no ch\xE3o: os tiros o moveram",
40522
40946
  "act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
40523
40947
  "act.use_item": "{name}: {use}",
40524
40948
  "act.tap.on": "Abriu a torneira",
@@ -40552,7 +40976,7 @@ void main(){
40552
40976
  "hud.throw": "arremessar",
40553
40977
  "hud.tilt": "inclinar (segurar)",
40554
40978
  "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",
40979
+ "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
40980
  "ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
40557
40981
  "camera.first": "1\xAA pessoa",
40558
40982
  "camera.third": "3\xAA pessoa",
@@ -40566,6 +40990,32 @@ void main(){
40566
40990
  "ui.copy_json": "Copiar JSON",
40567
40991
  "ui.download_ndjson": "Baixar NDJSON",
40568
40992
  "ui.download_txt": "Baixar TXT",
40993
+ "ui.test_table": "Mesa de testes",
40994
+ "ui.test_table.group": "Grupo para p\xF4r na mesa",
40995
+ "ui.test_table.lay": "P\xF4r na mesa",
40996
+ "ui.test_table.prev": "Itens anteriores",
40997
+ "ui.test_table.next": "Pr\xF3ximos itens",
40998
+ "ui.test_table.relay": "Repor",
40999
+ "ui.test_table.search": "Buscar item para p\xF4r na mesa",
41000
+ "ui.test_table.hint": "Escolha um grupo e ponha na mesa; depois escolha um item e teste.",
41001
+ "ui.test_table.page": "Itens {from}\u2013{to} de {total}. Escolha um e um teste:",
41002
+ "ui.trial.shot": "Tiro",
41003
+ "ui.trial.punch": "Soco",
41004
+ "ui.trial.stab": "Faca",
41005
+ "ui.trial.fire": "Fogo",
41006
+ "ui.trial.blast": "Explos\xE3o",
41007
+ "ui.trial.shot.hint": "Um tiro de 9 mm a 2,5 m",
41008
+ "ui.trial.punch.hint": "Um soco, de perto",
41009
+ "ui.trial.stab.hint": "Um golpe de faca, de perto",
41010
+ "ui.trial.fire.hint": "Uma rajada de lan\xE7a-chamas a 1,6 m",
41011
+ "ui.trial.blast.hint": "Uma granada de fragmenta\xE7\xE3o explode logo atr\xE1s dele (pega os vizinhos tamb\xE9m)",
41012
+ "act.test_table": "Mesa de testes: {group}, itens {from}\u2013{to} de {total}",
41013
+ "act.trial.shot": "Teste: tiro em {name}",
41014
+ "act.trial.punch": "Teste: soco em {name}",
41015
+ "act.trial.stab": "Teste: faca em {name}",
41016
+ "act.trial.fire": "Teste: fogo em {name}",
41017
+ "act.trial.blast": "Teste: explos\xE3o ao lado de {name}",
41018
+ "arena.test_table": "Mesa de testes",
40569
41019
  "ui.clear_log": "Limpar",
40570
41020
  "ui.catalog": "Cat\xE1logo",
40571
41021
  "ui.clear": "Limpar",
@@ -40907,6 +41357,122 @@ void main(){
40907
41357
  };
40908
41358
  }
40909
41359
 
41360
+ // src/test-table-panel.ts
41361
+ var TEST_TABLE_CSS = `
41362
+ .tr-table{flex:0 0 auto;max-height:42%}
41363
+ .tr-table .sub{color:#9aa3b2;margin:4px 0}
41364
+ .tr-table .list{display:flex;flex-wrap:wrap;gap:3px}
41365
+ .tr-table .list .tr-btn{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
41366
+ .tr-table select{flex:1;min-width:0;max-width:55%}
41367
+ .tr-table .tr-search{margin:6px 0 0}
41368
+ .tr-root .tr-table button.tr-btn.on{background:#3b3a14;border-color:#ffd23f;color:#fff}
41369
+ `;
41370
+ var mk2 = (tag, cls, text) => {
41371
+ const e = document.createElement(tag);
41372
+ if (cls) e.className = cls;
41373
+ if (text !== void 0) e.textContent = text;
41374
+ return e;
41375
+ };
41376
+ function createTestTablePanel(table, t) {
41377
+ const element = mk2("div", "tr-panel tr-table");
41378
+ element.appendChild(mk2("h3", void 0, t("ui.test_table")));
41379
+ const head = mk2("div", "tr-row");
41380
+ const select = mk2("select");
41381
+ select.setAttribute("aria-label", t("ui.test_table.group"));
41382
+ const lay = mk2("button", "tr-btn", t("ui.test_table.lay"));
41383
+ const prev = mk2("button", "tr-btn", "\u25C0");
41384
+ const next = mk2("button", "tr-btn", "\u25B6");
41385
+ prev.title = t("ui.test_table.prev");
41386
+ next.title = t("ui.test_table.next");
41387
+ for (const b of [lay, prev, next]) b.type = "button";
41388
+ head.append(select, lay, prev, next);
41389
+ const search = mk2("input", "tr-search");
41390
+ search.type = "search";
41391
+ search.placeholder = t("ui.test_table.search");
41392
+ const found = mk2("div", "list");
41393
+ const sub4 = mk2("div", "sub", t("ui.test_table.hint"));
41394
+ const list = mk2("div", "list");
41395
+ const trials = mk2("div", "tr-row");
41396
+ const trialButtons = TRIAL_KINDS.map((kind) => {
41397
+ const b = mk2("button", "tr-btn", t(`ui.trial.${kind}`));
41398
+ b.type = "button";
41399
+ b.dataset.trial = kind;
41400
+ b.title = t(`ui.trial.${kind}.hint`);
41401
+ b.onclick = () => table.trial(kind);
41402
+ trials.appendChild(b);
41403
+ return b;
41404
+ });
41405
+ const relay = mk2("button", "tr-btn", t("ui.test_table.relay"));
41406
+ relay.type = "button";
41407
+ relay.onclick = () => table.relay();
41408
+ trials.appendChild(relay);
41409
+ element.append(head, search, found, sub4, trials, list);
41410
+ const groups = () => {
41411
+ const keep = select.value;
41412
+ select.textContent = "";
41413
+ for (const group of table.groups()) {
41414
+ const op = mk2("option", void 0, group);
41415
+ op.value = group;
41416
+ select.appendChild(op);
41417
+ }
41418
+ if ([...select.options].some((o) => o.value === keep)) select.value = keep;
41419
+ };
41420
+ const render = (page) => {
41421
+ list.textContent = "";
41422
+ prev.disabled = !page?.hasPrev;
41423
+ next.disabled = !page?.hasNext;
41424
+ for (const b of [...trialButtons, relay]) b.disabled = !page?.selected;
41425
+ if (!page) {
41426
+ sub4.textContent = t("ui.test_table.hint");
41427
+ return;
41428
+ }
41429
+ if (select.value !== page.group) select.value = page.group;
41430
+ sub4.textContent = t("ui.test_table.page", { from: page.first + 1, to: page.first + page.objects.length, total: page.total });
41431
+ for (const o of page.objects) {
41432
+ const b = mk2("button", o.objectId === page.selected ? "tr-btn on" : "tr-btn", o.name);
41433
+ b.type = "button";
41434
+ b.title = o.itemId;
41435
+ b.onclick = () => table.select(o.objectId);
41436
+ list.appendChild(b);
41437
+ }
41438
+ };
41439
+ const fold2 = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
41440
+ search.oninput = () => {
41441
+ found.textContent = "";
41442
+ const term = fold2(search.value.trim());
41443
+ if (!term) return;
41444
+ const hits = table.items().filter((item) => fold2(`${item.name} ${item.id}`).includes(term)).slice(0, 8);
41445
+ for (const item of hits) {
41446
+ const b = mk2("button", "tr-btn", item.name);
41447
+ b.type = "button";
41448
+ b.title = `${item.group} \xB7 ${item.id}`;
41449
+ b.onclick = () => {
41450
+ search.value = "";
41451
+ found.textContent = "";
41452
+ table.find(item.id);
41453
+ };
41454
+ found.appendChild(b);
41455
+ }
41456
+ if (!hits.length) found.appendChild(mk2("div", "sub", t("ui.no_results")));
41457
+ };
41458
+ lay.onclick = () => table.open(select.value);
41459
+ prev.onclick = () => table.prev();
41460
+ next.onclick = () => table.next();
41461
+ select.onchange = () => table.open(select.value);
41462
+ for (const box2 of [select, search]) box2.addEventListener("keydown", (e) => e.stopPropagation());
41463
+ const off = table.on(render);
41464
+ groups();
41465
+ render(table.state());
41466
+ return {
41467
+ element,
41468
+ refresh: () => {
41469
+ groups();
41470
+ render(table.state());
41471
+ },
41472
+ dispose: off
41473
+ };
41474
+ }
41475
+
40910
41476
  // src/ui.ts
40911
41477
  var CSS = `
40912
41478
  .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 +41551,7 @@ void main(){
40985
41551
  @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
41552
  `;
40987
41553
  var fold = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
40988
- var mk2 = (tag, cls, text) => {
41554
+ var mk3 = (tag, cls, text) => {
40989
41555
  const e = document.createElement(tag);
40990
41556
  if (cls) e.className = cls;
40991
41557
  if (text !== void 0) e.textContent = text;
@@ -40996,7 +41562,7 @@ void main(){
40996
41562
  return source.replace(/["\\\n\r]/g, (char) => encodeURIComponent(char));
40997
41563
  };
40998
41564
  var thumb = (thumbnail, credit) => {
40999
- const e = mk2("span", "tr-thumb");
41565
+ const e = mk3("span", "tr-thumb");
41000
41566
  if (credit) e.title = credit;
41001
41567
  else e.setAttribute("aria-hidden", "true");
41002
41568
  e.style.setProperty("--tr-thumb", `url("${thumbSource(thumbnail)}")`);
@@ -41017,42 +41583,43 @@ void main(){
41017
41583
  }
41018
41584
  function createUi(o) {
41019
41585
  const { t } = o;
41020
- const style = mk2("style");
41021
- style.textContent = CSS;
41022
- const root = mk2("div", "tr-root");
41586
+ const style = mk3("style");
41587
+ style.textContent = CSS + (o.testTable ? TEST_TABLE_CSS : "");
41588
+ const root = mk3("div", "tr-root");
41023
41589
  const pos = getComputedStyle?.(o.container)?.position;
41024
41590
  if (!pos || pos === "static") o.container.style.position = "relative";
41025
41591
  root.appendChild(style);
41026
41592
  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");
41593
+ const cross5 = mk3("div", "tr-cross");
41594
+ cross5.appendChild(mk3("i"));
41595
+ const top = mk3("div", "tr-top");
41596
+ const toolBox = mk3("div", "tr-tool");
41597
+ const status = mk3("div", "tr-status");
41598
+ const prog = mk3("div", "tr-prog");
41599
+ const progLabel = mk3("b");
41600
+ const progBar = mk3("div", "tr-bar");
41601
+ const progFill = mk3("i");
41036
41602
  progBar.appendChild(progFill);
41037
41603
  prog.append(progLabel, progBar);
41038
41604
  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");
41605
+ const bottom = mk3("div", "tr-bottom");
41606
+ const held = mk3("div", "tr-held");
41607
+ const hotbar = mk3("div", "tr-hotbar");
41042
41608
  bottom.append(held, hotbar);
41043
- const help = mk2("div", "tr-help");
41609
+ const help = mk3("div", "tr-help");
41044
41610
  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");
41611
+ const left = mk3("div", "tr-left");
41612
+ const insp = mk3("div", "tr-panel tr-insp");
41613
+ const logPanel = mk3("div", "tr-panel tr-log");
41614
+ const tablePanel = o.testTable ? createTestTablePanel(o.testTable, t) : null;
41615
+ left.append(insp, ...tablePanel ? [tablePanel.element] : [], logPanel);
41616
+ const logTitle = mk3("h3", void 0, t("ui.log"));
41617
+ const logBar = mk3("div", "tr-logbar");
41618
+ const logBody = mk3("div", "tr-logbody");
41052
41619
  const filters = { action: true, event: true, check: true };
41053
41620
  const filterBtns = [];
41054
41621
  for (const ch of ["action", "event", "check"]) {
41055
- const b = mk2("button", "tr-btn on", t(`ui.filter.${ch}`));
41622
+ const b = mk3("button", "tr-btn on", t(`ui.filter.${ch}`));
41056
41623
  b.type = "button";
41057
41624
  b.onclick = () => {
41058
41625
  filters[ch] = !filters[ch];
@@ -41063,8 +41630,8 @@ void main(){
41063
41630
  logBar.appendChild(b);
41064
41631
  }
41065
41632
  let autoscroll = true;
41066
- const asLabel = mk2("label");
41067
- const asBox = mk2("input");
41633
+ const asLabel = mk3("label");
41634
+ const asBox = mk3("input");
41068
41635
  asBox.type = "checkbox";
41069
41636
  asBox.checked = true;
41070
41637
  asBox.onchange = () => {
@@ -41073,7 +41640,7 @@ void main(){
41073
41640
  asLabel.append(asBox, document.createTextNode(" " + t("ui.autoscroll")));
41074
41641
  logBar.appendChild(asLabel);
41075
41642
  const logBtn = (label, fn) => {
41076
- const b = mk2("button", "tr-btn", label);
41643
+ const b = mk3("button", "tr-btn", label);
41077
41644
  b.type = "button";
41078
41645
  b.onclick = fn;
41079
41646
  logBar.appendChild(b);
@@ -41081,7 +41648,7 @@ void main(){
41081
41648
  logBtn(t("ui.copy_json"), () => {
41082
41649
  const text = o.log.toJSON();
41083
41650
  const select = () => {
41084
- const ta = mk2("textarea");
41651
+ const ta = mk3("textarea");
41085
41652
  ta.value = text;
41086
41653
  ta.setAttribute("readonly", "");
41087
41654
  ta.style.cssText = "position:fixed;left:8px;bottom:8px;width:min(480px,80vw);height:120px;z-index:99999";
@@ -41115,12 +41682,12 @@ void main(){
41115
41682
  const fill = (n, e) => {
41116
41683
  n.className = lineClass(e);
41117
41684
  n.textContent = "";
41118
- const tm = mk2("time", void 0, fmtTime(e.t));
41685
+ const tm = mk3("time", void 0, fmtTime(e.t));
41119
41686
  n.append(tm, document.createTextNode(e.text));
41120
41687
  };
41121
41688
  const addNode = (e) => {
41122
41689
  if (!filters[e.channel]) return;
41123
- const n = mk2("div");
41690
+ const n = mk3("div");
41124
41691
  fill(n, e);
41125
41692
  nodes.set(e.seq, n);
41126
41693
  logBody.appendChild(n);
@@ -41142,18 +41709,18 @@ void main(){
41142
41709
  if (n) fill(n, e);
41143
41710
  } else addNode(e);
41144
41711
  });
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");
41712
+ const right = mk3("div", "tr-right");
41713
+ const cat = mk3("div", "tr-panel tr-cat");
41714
+ const anim = mk3("div", "tr-panel tr-anim");
41715
+ const tests = mk3("div", "tr-panel tr-tests");
41149
41716
  const benchPanel = createBenchPanel({ t, num: (n, d) => num(n, d), onControl: o.onControl, onLayer: o.onLayer, onAction: o.onAction });
41150
41717
  right.append(cat, anim, benchPanel.element, tests);
41151
- cat.appendChild(mk2("h3", void 0, t("ui.catalog")));
41152
- const itemSearch = mk2("input", "tr-search");
41718
+ cat.appendChild(mk3("h3", void 0, t("ui.catalog")));
41719
+ const itemSearch = mk3("input", "tr-search");
41153
41720
  itemSearch.type = "search";
41154
41721
  itemSearch.placeholder = t("ui.search_items");
41155
- const tabs = mk2("div", "tr-tabs");
41156
- const items = mk2("div", "tr-items");
41722
+ const tabs = mk3("div", "tr-tabs");
41723
+ const items = mk3("div", "tr-items");
41157
41724
  cat.append(itemSearch, tabs, items);
41158
41725
  let activeGroup = "";
41159
41726
  const visibleItems = () => {
@@ -41165,18 +41732,18 @@ void main(){
41165
41732
  items.textContent = "";
41166
41733
  const searching = itemSearch.value.trim() !== "";
41167
41734
  const found = visibleItems();
41168
- if (!found.length) items.appendChild(mk2("div", "tr-empty", t("ui.no_results")));
41735
+ if (!found.length) items.appendChild(mk3("div", "tr-empty", t("ui.no_results")));
41169
41736
  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));
41737
+ const wrap = mk3("div", "tr-item");
41738
+ const b = mk3("button", it.thumbnail ? "tr-btn has-thumb" : "tr-btn", it.thumbnail ? void 0 : it.name);
41739
+ if (it.thumbnail) b.append(thumb(it.thumbnail), mk3("span", "tr-name", it.name));
41740
+ if (searching) b.appendChild(mk3("small", void 0, group));
41174
41741
  b.type = "button";
41175
41742
  b.title = it.note ?? it.name;
41176
41743
  b.onclick = () => o.onSpawn(it.id);
41177
41744
  wrap.appendChild(b);
41178
41745
  if (it.weapon) {
41179
- const w = mk2("button", "tr-btn", "\u2694");
41746
+ const w = mk3("button", "tr-btn", "\u2694");
41180
41747
  w.type = "button";
41181
41748
  w.title = t("ui.equip");
41182
41749
  w.onclick = () => o.onEquip(it.id);
@@ -41190,7 +41757,7 @@ void main(){
41190
41757
  const groupNames = [...o.groups().keys()];
41191
41758
  if (!groupNames.includes(activeGroup)) activeGroup = groupNames[0] ?? "";
41192
41759
  for (const g of groupNames) {
41193
- const b = mk2("button", g === activeGroup ? "tr-btn on" : "tr-btn", g);
41760
+ const b = mk3("button", g === activeGroup ? "tr-btn on" : "tr-btn", g);
41194
41761
  b.type = "button";
41195
41762
  b.onclick = () => {
41196
41763
  activeGroup = g;
@@ -41203,9 +41770,9 @@ void main(){
41203
41770
  renderTabs();
41204
41771
  itemSearch.oninput = renderItems;
41205
41772
  for (const box2 of [itemSearch]) box2.addEventListener("keydown", (e) => e.stopPropagation());
41206
- const controls = mk2("div", "tr-row");
41773
+ const controls = mk3("div", "tr-row");
41207
41774
  const ctl = (label, fn) => {
41208
- const b = mk2("button", "tr-btn", label);
41775
+ const b = mk3("button", "tr-btn", label);
41209
41776
  b.type = "button";
41210
41777
  b.onclick = () => fn(b);
41211
41778
  controls.appendChild(b);
@@ -41230,29 +41797,29 @@ void main(){
41230
41797
  fullscreenBtn.title = t("ui.fullscreen.hint");
41231
41798
  const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
41232
41799
  for (const { key, b } of blastBtns) b.title = t(`ui.blast.${key}.hint`);
41233
- const qSel = mk2("select");
41800
+ const qSel = mk3("select");
41234
41801
  for (const q of ["low", "medium", "high"]) {
41235
- const op = mk2("option", void 0, t(`ui.quality.${q}`));
41802
+ const op = mk3("option", void 0, t(`ui.quality.${q}`));
41236
41803
  op.value = q;
41237
41804
  qSel.appendChild(op);
41238
41805
  }
41239
41806
  qSel.value = o.quality;
41240
41807
  qSel.onchange = () => o.onQuality(qSel.value);
41241
- const qLabel = mk2("label", void 0, t("ui.quality") + " ");
41808
+ const qLabel = mk3("label", void 0, t("ui.quality") + " ");
41242
41809
  qLabel.appendChild(qSel);
41243
41810
  controls.appendChild(qLabel);
41244
- const count = mk2("span");
41811
+ const count = mk3("span");
41245
41812
  controls.appendChild(count);
41246
41813
  cat.appendChild(controls);
41247
- tests.appendChild(mk2("h3", void 0, t("ui.tests")));
41248
- const runAll = mk2("button", "tr-btn", t("ui.run_all"));
41814
+ tests.appendChild(mk3("h3", void 0, t("ui.tests")));
41815
+ const runAll = mk3("button", "tr-btn", t("ui.run_all"));
41249
41816
  runAll.type = "button";
41250
- const summary = mk2("div", "tr-summary");
41251
- const testSearch = mk2("input", "tr-search");
41817
+ const summary = mk3("div", "tr-summary");
41818
+ const testSearch = mk3("input", "tr-search");
41252
41819
  testSearch.type = "search";
41253
41820
  testSearch.placeholder = t("ui.search_tests");
41254
41821
  testSearch.addEventListener("keydown", (e) => e.stopPropagation());
41255
- const list = mk2("div", "list");
41822
+ const list = mk3("div", "list");
41256
41823
  tests.append(runAll, summary, testSearch, list);
41257
41824
  testSearch.oninput = () => {
41258
41825
  const term = fold(testSearch.value.trim());
@@ -41275,7 +41842,7 @@ void main(){
41275
41842
  list.textContent = "";
41276
41843
  scBtns.clear();
41277
41844
  for (const s of o.scenarios()) {
41278
- const b = mk2("button", "tr-btn", s.name);
41845
+ const b = mk3("button", "tr-btn", s.name);
41279
41846
  b.type = "button";
41280
41847
  b.onclick = async () => {
41281
41848
  if (busy) return;
@@ -41301,7 +41868,7 @@ void main(){
41301
41868
  busy = false;
41302
41869
  }
41303
41870
  };
41304
- const toggle = mk2("button", "tr-btn tr-toggle", t("ui.panel"));
41871
+ const toggle = mk3("button", "tr-btn tr-toggle", t("ui.panel"));
41305
41872
  toggle.type = "button";
41306
41873
  const togglePanels = () => {
41307
41874
  root.classList.toggle("open");
@@ -41310,15 +41877,15 @@ void main(){
41310
41877
  toggle.onclick = togglePanels;
41311
41878
  root.append(cross5, top, bottom, help, left, right, toggle);
41312
41879
  const bar2 = (value, max2, cls) => {
41313
- const b = mk2("div", `tr-bar ${cls}`);
41314
- const i = mk2("i");
41880
+ const b = mk3("div", `tr-bar ${cls}`);
41881
+ const i = mk3("i");
41315
41882
  i.style.width = `${Math.max(0, Math.min(100, value / Math.max(1e-9, max2) * 100))}%`;
41316
41883
  b.appendChild(i);
41317
41884
  return b;
41318
41885
  };
41319
41886
  const row = (k, v) => {
41320
- const r = mk2("div", "r");
41321
- r.append(mk2("span", void 0, k), mk2("span", void 0, v));
41887
+ const r = mk3("div", "r");
41888
+ r.append(mk3("span", void 0, k), mk3("span", void 0, v));
41322
41889
  return r;
41323
41890
  };
41324
41891
  const num = (n, d = 1) => {
@@ -41327,13 +41894,13 @@ void main(){
41327
41894
  };
41328
41895
  const renderInspector = (i) => {
41329
41896
  insp.textContent = "";
41330
- insp.appendChild(mk2("h3", void 0, t("ui.inspector")));
41897
+ insp.appendChild(mk3("h3", void 0, t("ui.inspector")));
41331
41898
  if (!i) {
41332
- insp.appendChild(mk2("div", void 0, t("ui.aim_at_something")));
41899
+ insp.appendChild(mk3("div", void 0, t("ui.aim_at_something")));
41333
41900
  return;
41334
41901
  }
41335
41902
  if (i.thumbnail) insp.appendChild(thumb(i.thumbnail, i.thumbnailCredit));
41336
- insp.appendChild(mk2("div", "t", `${i.object} \u203A ${i.part}`));
41903
+ insp.appendChild(mk3("div", "t", `${i.object} \u203A ${i.part}`));
41337
41904
  insp.appendChild(row(t("ins.material"), i.material));
41338
41905
  insp.appendChild(row(t("ins.status"), i.status + (i.burning ? ` \xB7 ${t("ins.burning")}` : "")));
41339
41906
  insp.appendChild(row(t("ins.hp"), `${num(i.hp, 0)} / ${num(i.maxHp, 0)}`));
@@ -41352,30 +41919,30 @@ void main(){
41352
41919
  insp.appendChild(bar2(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
41353
41920
  }
41354
41921
  if (i.joints.length) {
41355
- insp.appendChild(mk2("div", void 0, t("ins.joints") + ":"));
41922
+ insp.appendChild(mk3("div", void 0, t("ins.joints") + ":"));
41356
41923
  for (const j of i.joints) {
41357
41924
  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}`}`));
41925
+ insp.appendChild(mk3("div", void 0, `\u2022 ${j.kind} (${j.id})${j.open ? ` \xB7 ${t("ins.open")}` : ` \u2192 ${tools}`}`));
41359
41926
  }
41360
41927
  }
41361
- if (i.note) insp.appendChild(mk2("div", void 0, i.note));
41928
+ if (i.note) insp.appendChild(mk3("div", void 0, i.note));
41362
41929
  };
41363
41930
  let lastAnim = "";
41364
41931
  const renderAnimations = (a) => {
41365
41932
  anim.textContent = "";
41366
- anim.appendChild(mk2("h3", void 0, t("ui.animations")));
41933
+ anim.appendChild(mk3("h3", void 0, t("ui.animations")));
41367
41934
  if (!a) {
41368
- anim.appendChild(mk2("div", "sub", t("ui.animations_aim")));
41935
+ anim.appendChild(mk3("div", "sub", t("ui.animations_aim")));
41369
41936
  return;
41370
41937
  }
41371
- anim.appendChild(mk2("div", "sub", a.name));
41938
+ anim.appendChild(mk3("div", "sub", a.name));
41372
41939
  if (a.clips.length === 0) {
41373
- anim.appendChild(mk2("div", "sub", t("ui.animations_none")));
41940
+ anim.appendChild(mk3("div", "sub", t("ui.animations_none")));
41374
41941
  return;
41375
41942
  }
41376
- const l = mk2("div", "list");
41943
+ const l = mk3("div", "list");
41377
41944
  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)`);
41945
+ 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
41946
  b.type = "button";
41380
41947
  b.onclick = () => o.onPlay(a.objectId, c.id);
41381
41948
  l.appendChild(b);
@@ -41392,8 +41959,8 @@ void main(){
41392
41959
  hud(s) {
41393
41960
  toolBox.textContent = "";
41394
41961
  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}`)));
41962
+ if (s.ammo) toolBox.append(mk3("small", void 0, s.ammo));
41963
+ if (s.camera) toolBox.append(mk3("small", void 0, t(`camera.${s.camera}`)));
41397
41964
  const helpText = s.camera ? `${t("ui.help")} \xB7 ${t("ui.help.camera")}` : t("ui.help");
41398
41965
  if (help.textContent !== helpText) help.textContent = helpText;
41399
41966
  if (s.status) status.textContent = s.status;
@@ -41416,7 +41983,7 @@ void main(){
41416
41983
  ];
41417
41984
  if (s.held.weapon) keys.push(["H", t("hud.equip", { weapon: s.held.weapon })]);
41418
41985
  for (const [k, label] of keys) {
41419
- held.append(mk2("kbd", void 0, k), document.createTextNode(` ${label} `));
41986
+ held.append(mk3("kbd", void 0, k), document.createTextNode(` ${label} `));
41420
41987
  }
41421
41988
  } else held.style.display = "none";
41422
41989
  const key = s.slots.join("|") + s.page;
@@ -41426,9 +41993,9 @@ void main(){
41426
41993
  slotsEl.length = 0;
41427
41994
  hudPage = s.page - 1;
41428
41995
  s.slots.forEach((name, i) => {
41429
- const b = mk2("button", "tr-slot");
41996
+ const b = mk3("button", "tr-slot");
41430
41997
  b.type = "button";
41431
- b.append(mk2("b", void 0, String((i + 1) % 10)), document.createTextNode(name));
41998
+ b.append(mk3("b", void 0, String((i + 1) % 10)), document.createTextNode(name));
41432
41999
  b.title = name;
41433
42000
  b.onclick = () => o.onTool(hudPage * 10 + i);
41434
42001
  hotbar.appendChild(b);
@@ -41460,6 +42027,7 @@ void main(){
41460
42027
  refresh() {
41461
42028
  renderTabs();
41462
42029
  renderScenarios();
42030
+ tablePanel?.refresh();
41463
42031
  },
41464
42032
  showGroup(group) {
41465
42033
  activeGroup = group;
@@ -41474,6 +42042,7 @@ void main(){
41474
42042
  },
41475
42043
  dispose() {
41476
42044
  offLog();
42045
+ tablePanel?.dispose();
41477
42046
  if (toastTimer) clearTimeout(toastTimer);
41478
42047
  root.remove();
41479
42048
  }
@@ -41484,6 +42053,7 @@ void main(){
41484
42053
 
41485
42054
  // src/room.ts
41486
42055
  var FIXED_DT = 1 / 60;
42056
+ var SHOVE_SPEED = 0.04;
41487
42057
  var METRICS_INTERVAL_S = 0.25;
41488
42058
  var CARRY_NEAR = 0.62;
41489
42059
  var CARRY_PALM = 0.012;
@@ -41846,7 +42416,9 @@ void main(){
41846
42416
  scene2.add(root);
41847
42417
  root.updateMatrixWorld(true);
41848
42418
  world.add(structuredCloneSpec(o.spec), { id: o.id, position: o.position, ...o.rotationY ? { rotation: [0, Math.sin(o.rotationY / 2), 0, Math.cos(o.rotationY / 2)] } : {} });
41849
- registerEntry(o.id, o.id, root, o.spec, o.label ?? S.text[`arena.${o.id}`] ?? o.id, void 0, void 0, true, false);
42419
+ const e = registerEntry(o.id, o.id, root, o.spec, o.label ?? S.text[`arena.${o.id}`] ?? o.id, void 0, void 0, true, false);
42420
+ if (o.free) e.free = true;
42421
+ e.follower = o.follow?.(root, world, o.id) || null;
41850
42422
  }
41851
42423
  };
41852
42424
  addArena();
@@ -41941,6 +42513,7 @@ void main(){
41941
42513
  renderer.unbind(id);
41942
42514
  } catch {
41943
42515
  }
42516
+ e.follower?.dispose?.();
41944
42517
  if (world.has(id)) world.remove(id);
41945
42518
  e.so.root.removeFromParent();
41946
42519
  letGo(e.so.root);
@@ -41975,6 +42548,7 @@ void main(){
41975
42548
  for (const e of entries.values()) {
41976
42549
  world.reset(e.so.objectId);
41977
42550
  renderer.restore(e.so.objectId);
42551
+ e.follower?.restore?.();
41978
42552
  }
41979
42553
  renderer.clear();
41980
42554
  vfx.world.clear();
@@ -41996,6 +42570,7 @@ void main(){
41996
42570
  if (heldEntry === e) continue;
41997
42571
  world.reset(e.so.objectId);
41998
42572
  renderer.restore(e.so.objectId);
42573
+ e.follower?.restore?.();
41999
42574
  if (e.containerId) {
42000
42575
  const c = fluids.container(e.containerId);
42001
42576
  const f = e.item?.fluid?.container;
@@ -42299,6 +42874,19 @@ void main(){
42299
42874
  if (!rendererPieces.has(objectId)) return;
42300
42875
  renderer.impulse(objectId, [dir[0] * momentum, dir[1] * momentum, dir[2] * momentum], point);
42301
42876
  };
42877
+ const shoveFree = (pushes) => {
42878
+ for (const [objectId, push] of pushes) {
42879
+ const e = entries.get(objectId);
42880
+ if (!e || e.piece || !e.free) continue;
42881
+ const mass = Math.max(0.2, massOfObject(objectId));
42882
+ if (Math.hypot(...push.impulse) / mass < SHOVE_SPEED) continue;
42883
+ const piece = renderer.makePiece(objectId, { mass });
42884
+ e.piece = piece;
42885
+ rendererPieces.set(objectId, piece);
42886
+ action("loosened", t("act.shoved", { name: e.name }), { objectId, itemId: e.so.itemId });
42887
+ renderer.impulse(objectId, push.impulse, push.point);
42888
+ }
42889
+ };
42302
42890
  const fireShots = (shots, recoil) => {
42303
42891
  if (shots.length === 0) return;
42304
42892
  const w = toolName();
@@ -42308,6 +42896,7 @@ void main(){
42308
42896
  let hits = 0;
42309
42897
  let first = null;
42310
42898
  let energy = 0;
42899
+ const pushes = /* @__PURE__ */ new Map();
42311
42900
  withoutHeld(() => {
42312
42901
  for (const s of shots) {
42313
42902
  energy = Math.max(energy, s.energy);
@@ -42319,11 +42908,16 @@ void main(){
42319
42908
  end3 = r0.point;
42320
42909
  if (!first) first = { object: r0.object, part: r0.part, point: r0.point, normal: [0, 1, 0], distance: dist3(s.origin, r0.point) };
42321
42910
  const speed = Math.max(80, s.speed);
42322
- pushPiece(r0.object, s.direction, 2 * s.energy / speed, r0.point);
42911
+ const momentum = 2 * s.energy / speed;
42912
+ pushPiece(r0.object, s.direction, momentum, r0.point);
42913
+ const push = pushes.get(r0.object) ?? { impulse: [0, 0, 0], point: r0.point };
42914
+ for (let k = 0; k < 3; k++) push.impulse[k] += s.direction[k] * momentum;
42915
+ pushes.set(r0.object, push);
42323
42916
  }
42324
42917
  if (shots.length <= 3 || rnd2() < 0.34) tracer(m, end3);
42325
42918
  }
42326
42919
  });
42920
+ shoveFree(pushes);
42327
42921
  player.kick(recoil[0], recoil[1]);
42328
42922
  const f = first;
42329
42923
  const d = f ? num(f.distance, 1) : "";
@@ -42415,16 +43009,25 @@ void main(){
42415
43009
  let strikes = [];
42416
43010
  const kickLeg = options.kickLeg ?? "alternate";
42417
43011
  let nextLeg = "right";
43012
+ const punchHand = options.punchHand ?? "alternate";
43013
+ const punchTools = new Set(options.punchTools ?? ["hand", "fists"]);
43014
+ let nextHand = "right";
42418
43015
  const strikeUse = (u, id = toolId) => {
42419
43016
  const clip = strikeClips[id];
42420
43017
  const leg = id === "kick" ? kickLeg === "alternate" ? nextLeg : kickLeg : void 0;
42421
- const seconds = clip === void 0 ? null : avatar?.strike?.(clip, leg ? { leg } : void 0) ?? null;
43018
+ const hand = punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
43019
+ const side = leg ? { leg } : hand ? { hand } : void 0;
43020
+ const seconds = clip === void 0 ? null : avatar?.strike?.(clip, side) ?? null;
42422
43021
  if (leg && seconds !== null && kickLeg === "alternate") nextLeg = leg === "right" ? "left" : "right";
43022
+ if (hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
42423
43023
  if (clip === void 0 || seconds === null) {
42424
43024
  meleeUse(u, id);
42425
43025
  return;
42426
43026
  }
42427
- action("strike", t(leg ? "act.strike.leg" : "act.strike", { weapon: toolName(id), clip, seconds: num(seconds, 2), leg: leg ? t(`leg.${leg}`) : "" }), { data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {} } });
43027
+ const text = leg ? "act.strike.leg" : hand ? "act.strike.hand" : "act.strike";
43028
+ action("strike", t(text, { weapon: toolName(id), clip, seconds: num(seconds, 2), leg: leg ? t(`leg.${leg}`) : "", hand: hand ? t(`hand.${hand}`) : "" }), {
43029
+ data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {}, ...hand ? { hand } : {} }
43030
+ });
42428
43031
  strikes.push({
42429
43032
  key: id,
42430
43033
  remaining: seconds,
@@ -42663,6 +43266,10 @@ void main(){
42663
43266
  heldPiece = null;
42664
43267
  heldTilt = 0;
42665
43268
  };
43269
+ const handUse = () => {
43270
+ if (heldPiece) grab();
43271
+ else useTool(FIXED_DT, false);
43272
+ };
42666
43273
  const grab = () => {
42667
43274
  if (heldPiece) {
42668
43275
  const name = heldName;
@@ -43131,10 +43738,10 @@ void main(){
43131
43738
  if (tiltKey || heldTilt > 0) {
43132
43739
  heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
43133
43740
  }
43134
- if (useHeld && !paused) {
43741
+ if ((useHeld || useFirst) && !paused) {
43135
43742
  if (tool.def.category === "hand") {
43136
- if (useFirst) grab();
43137
- } else useTool(dt, !useFirst);
43743
+ if (useFirst) handUse();
43744
+ } else if (useHeld) useTool(dt, !useFirst);
43138
43745
  useFirst = false;
43139
43746
  }
43140
43747
  if (!paused) followHeld(dt);
@@ -43689,6 +44296,63 @@ void main(){
43689
44296
  const row = (list, gap, z) => list.map((item, index) => spawn2(item.id, [(index - (list.length - 1) / 2) * gap, 0, z], 0));
43690
44297
  return [...row(front, 1.6, 1.5), ...row(back, 4.5, -4)];
43691
44298
  };
44299
+ const catalogGroups = () => {
44300
+ const m = /* @__PURE__ */ new Map();
44301
+ for (const i of catalog) {
44302
+ const g = m.get(i.group) ?? [];
44303
+ g.push(i);
44304
+ m.set(i.group, g);
44305
+ }
44306
+ return m;
44307
+ };
44308
+ const tableOptions = options.testTable === false ? {} : options.testTable ?? {};
44309
+ const tableScenery = tableOptions.scenery ?? "test_table";
44310
+ const measured = /* @__PURE__ */ new Map();
44311
+ const testTable = createTestTable(
44312
+ {
44313
+ groups: catalogGroups,
44314
+ top: () => {
44315
+ const top = arena.tops.find((x) => x.id === tableScenery);
44316
+ return top && topOk(top) ? top : null;
44317
+ },
44318
+ measure: (itemId) => {
44319
+ let size = measured.get(itemId);
44320
+ if (!size) {
44321
+ const item = catalogMap.get(itemId);
44322
+ const box2 = item ? new THREE.Box3().setFromObject(buildRoot(item)) : null;
44323
+ size = box2 && !box2.isEmpty() ? { w: box2.max.x - box2.min.x, d: box2.max.z - box2.min.z } : { w: 0.3, d: 0.3 };
44324
+ measured.set(itemId, size);
44325
+ }
44326
+ return size;
44327
+ },
44328
+ spawn: (itemId, position) => spawn2(itemId, position, 0),
44329
+ clear,
44330
+ resetAll,
44331
+ box: (objectId) => {
44332
+ const e = entries.get(objectId);
44333
+ if (!e || !world.has(objectId)) return null;
44334
+ const box2 = new THREE.Box3().setFromObject(e.so.root);
44335
+ return box2.isEmpty() ? null : { min: [box2.min.x, box2.min.y, box2.min.z], max: [box2.max.x, box2.max.y, box2.max.z] };
44336
+ },
44337
+ equip: (weapon) => {
44338
+ if (!arsenal.weapons.has(weapon)) return false;
44339
+ if (heldEntry) releaseHeld();
44340
+ equip(weapon);
44341
+ room.waitUntilReady();
44342
+ return true;
44343
+ },
44344
+ look: (eye, target) => player.look(eye, target),
44345
+ use: (hold) => room.use(hold),
44346
+ blastAt: (weapon, point) => {
44347
+ const def = arsenal.weapons.get(weapon);
44348
+ const explosive = def?.action.kind === "throwable" ? def.action.explosive : def?.action.kind === "launcher" ? def.action.projectile : void 0;
44349
+ return explosive !== void 0 && explosives.blastAt(weapon, explosive, point);
44350
+ },
44351
+ log: (type, text, data) => action(type, text, { data }),
44352
+ translate: t
44353
+ },
44354
+ tableOptions
44355
+ );
43692
44356
  const room = {
43693
44357
  world,
43694
44358
  renderer,
@@ -43708,6 +44372,7 @@ void main(){
43708
44372
  get scenarios() {
43709
44373
  return allScenarios();
43710
44374
  },
44375
+ testTable,
43711
44376
  spawn: spawn2,
43712
44377
  clear,
43713
44378
  resetAll,
@@ -43715,7 +44380,8 @@ void main(){
43715
44380
  useItem,
43716
44381
  use(hold = 0) {
43717
44382
  if (tool.def.category === "hand") {
43718
- grab();
44383
+ handUse();
44384
+ flush();
43719
44385
  return;
43720
44386
  }
43721
44387
  useTool(FIXED_DT, false);
@@ -43948,7 +44614,8 @@ void main(){
43948
44614
  },
43949
44615
  onAction: (id) => {
43950
44616
  if (activeBench) slowly(activeBench, id, () => benches.get(activeBench)?.act(id));
43951
- }
44617
+ },
44618
+ ...options.testTable !== false && arena.tops.some((x) => x.id === tableScenery) ? { testTable } : {}
43952
44619
  });
43953
44620
  ui.hud(hudState());
43954
44621
  showBench();