@adatechnology/game-test-room 0.15.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.
@@ -482,6 +482,13 @@ var GameTestRoom = (() => {
482
482
  node.userData[KIT_ORIGIN_KEY] = { ...origin };
483
483
  return object;
484
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
+ }
485
492
 
486
493
  // ../game-destruction/dist/index.js
487
494
  var FIRE_PRIME_C = 300;
@@ -812,6 +819,45 @@ var GameTestRoom = (() => {
812
819
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
813
820
  };
814
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
+ };
815
861
  var tearStack = (inp, root, t, u, v, n, sheet) => {
816
862
  const { dims, rnd: rnd2, maxFragments } = inp;
817
863
  const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
@@ -850,23 +896,8 @@ var GameTestRoom = (() => {
850
896
  return out;
851
897
  }
852
898
  if (style === "splinter") {
853
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
854
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
855
- const w = Math.max(fmin * 1.5, 0.01);
856
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
857
- const maxLen = fmax * 4;
858
- const cells = bsp(root, c, [a, b], rnd2);
859
- const pieces = [];
860
- for (const cell of cells) {
861
- let list = [cell];
862
- const extra = rnd2() < 0.4 ? 1 : 0;
863
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
864
- list = bsp(cell, k, [g], rnd2);
865
- for (const p of list) pieces.push(p);
866
- }
867
- for (const p of pieces.slice(0, maxFragments)) {
868
- 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] });
869
- }
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);
870
901
  return out;
871
902
  }
872
903
  if (style === "tear") {
@@ -2849,9 +2880,14 @@ var GameTestRoom = (() => {
2849
2880
  var BREACH_AREA = 0.25;
2850
2881
  var BREACH_BLAST_REACH = 0.12;
2851
2882
  var RESIDUAL_CAPACITY = 0.15;
2883
+ var SEVER_BAND = 0.1;
2884
+ var SEVER_ASPECT = 3;
2885
+ var SEVER_BALLISTIC = 2;
2852
2886
  var SUPPORT_TOLERANCE = 0.05;
2853
2887
  var UNIT_FRAGMENT_FACTOR = 4;
2854
2888
  var BREACH_GRID_CAP = 4096;
2889
+ var CHIP_ENERGY = 200;
2890
+ var CHIP_SPLASH = 0.5;
2855
2891
  var CUT_SLICE = 1;
2856
2892
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2857
2893
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2895,6 +2931,7 @@ var GameTestRoom = (() => {
2895
2931
  let windCfg = windOpts ? resolveWind(windOpts) : null;
2896
2932
  const ignitionRules = resolveIgnitionRules(options.ignition);
2897
2933
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
2934
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
2898
2935
  const rng = createRng(options.seed ?? 1);
2899
2936
  const rnd2 = () => rng.next();
2900
2937
  let time = 0;
@@ -3268,41 +3305,68 @@ var GameTestRoom = (() => {
3268
3305
  markDamaged(p);
3269
3306
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
3270
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
+ };
3271
3322
  const solveStructure = (o) => {
3272
3323
  o.maxStress = 0;
3273
3324
  if (!o.hasStructure) return;
3274
3325
  const parts = o.parts;
3275
3326
  const n = parts.length;
3276
3327
  const dist = new Array(n).fill(-1);
3277
- const queue = [];
3278
3328
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
3279
3329
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
3280
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
3281
- dist[i] = 0;
3282
- queue.push(i);
3283
- }
3284
- for (let h = 0; h < queue.length; h++) {
3285
- const s = queue[h];
3286
- if (isPartition(s)) continue;
3287
- for (const j of o.byPart.get(parts[s].spec.id)) {
3288
- if (j.st.open || !j.bears) continue;
3289
- const r = j.ia === s ? j.ib : j.ia;
3290
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
3291
- dist[r] = dist[s] + 1;
3292
- 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
+ }
3293
3350
  }
3294
- }
3295
- const incoming = new Array(n).fill(0);
3296
- const lowerOf = /* @__PURE__ */ new Map();
3297
- for (const i of queue) {
3298
- const lower = [];
3299
- for (const j of o.byPart.get(parts[i].spec.id)) {
3300
- if (j.st.open || !j.bears) continue;
3301
- const k = restsOn(o, j, i);
3302
- 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
+ }
3303
3366
  }
3304
- lowerOf.set(i, lower);
3367
+ if (!dropped) break;
3305
3368
  }
3369
+ const incoming = new Array(n).fill(0);
3306
3370
  const bending = new Array(n).fill(0);
3307
3371
  const through = parts.map(() => []);
3308
3372
  const pass = (k, at3, mass) => {
@@ -3814,6 +3878,7 @@ var GameTestRoom = (() => {
3814
3878
  const r = rayPart(o, p, origin, d);
3815
3879
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
3816
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;
3817
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 };
3818
3883
  }
3819
3884
  }
@@ -4077,6 +4142,62 @@ var GameTestRoom = (() => {
4077
4142
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
4078
4143
  openBreach(o, p, [sum2[0] / total, sum2[1] / total, sum2[2] / total], total, energy, dir, src);
4079
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
+ };
4080
4201
  const openBreach = (o, p, center, total, energy, dir, src) => {
4081
4202
  const [u, v] = planeAxes(p.thin);
4082
4203
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -4088,7 +4209,8 @@ var GameTestRoom = (() => {
4088
4209
  failPart(o, p, worldCenter, dir, energy, 12, src);
4089
4210
  return;
4090
4211
  }
4091
- 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);
4092
4214
  const removed = p.st.mass * cells.reduce((sum2, cell) => sum2 + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
4093
4215
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
4094
4216
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -4172,6 +4294,8 @@ var GameTestRoom = (() => {
4172
4294
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
4173
4295
  const hitLocal = toLocal(c, q, hitW);
4174
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);
4175
4299
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4176
4300
  style,
4177
4301
  dims: p.dims,
@@ -4181,7 +4305,8 @@ var GameTestRoom = (() => {
4181
4305
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4182
4306
  maxFragments,
4183
4307
  rnd: rnd2,
4184
- sheet: p.mat.sheet
4308
+ sheet: p.mat.sheet,
4309
+ ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
4185
4310
  });
4186
4311
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
4187
4312
  const mass = p.st.mass;
@@ -4240,10 +4365,33 @@ var GameTestRoom = (() => {
4240
4365
  emit2({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
4241
4366
  };
4242
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
+ };
4243
4380
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
4381
+ var _a;
4244
4382
  if (isLost(p) || amount <= 0) return 0;
4245
4383
  const prev = p.st.hp;
4246
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
+ }
4247
4395
  markDamaged(p);
4248
4396
  const trigger = CHAIN_TRIGGER_OF[type];
4249
4397
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -4282,7 +4430,7 @@ var GameTestRoom = (() => {
4282
4430
  const ballistic2 = (o, p, entry, n, d, E, caliber, pathLen, src, round2 = {}) => {
4283
4431
  const mat = p.mat;
4284
4432
  const piercing = round2.piercing ?? 1;
4285
- 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))) {
4286
4434
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
4287
4435
  }
4288
4436
  const sin2 = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -4341,6 +4489,7 @@ var GameTestRoom = (() => {
4341
4489
  debris2(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4342
4490
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
4343
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);
4344
4493
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4345
4494
  };
4346
4495
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -4368,6 +4517,7 @@ var GameTestRoom = (() => {
4368
4517
  const scale2 = mat.damageScale?.[kind] ?? 1;
4369
4518
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale2 * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
4370
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);
4371
4521
  return done;
4372
4522
  };
4373
4523
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -5517,6 +5667,7 @@ var GameTestRoom = (() => {
5517
5667
  sway: "sway",
5518
5668
  spall: "plaster came off",
5519
5669
  stage: "damage stage",
5670
+ chip: "brick knocked out",
5520
5671
  expose: "rebar bare",
5521
5672
  body: "body",
5522
5673
  blast: "blast",
@@ -5607,6 +5758,7 @@ var GameTestRoom = (() => {
5607
5758
  sway: "balan\xE7o",
5608
5759
  spall: "reboco soltou",
5609
5760
  stage: "est\xE1gio de dano",
5761
+ chip: "tijolo arrancado",
5610
5762
  expose: "vergalh\xE3o \xE0 mostra",
5611
5763
  body: "corpo",
5612
5764
  blast: "explos\xE3o",
@@ -17648,6 +17800,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17648
17800
  }
17649
17801
  };
17650
17802
  var rendererCount = 0;
17803
+ var COLLAPSE_CLOUD_KG = 1e3;
17651
17804
  var IMPACT_TAGS = ["metal", "glass", "wood"];
17652
17805
  var GLOW_FROM_C = 430;
17653
17806
  var QUEUE_CAP = 4e3;
@@ -18107,10 +18260,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18107
18260
  case "settle":
18108
18261
  this.onSettle(ev.object);
18109
18262
  break;
18110
- case "collapse":
18263
+ case "collapse": {
18264
+ const root = this.bounds.get(ev.object)?.root;
18265
+ const furniture = !!root && kitOriginOf(root)?.kind === "furniture";
18111
18266
  this.splitLoose(ev.object);
18112
- 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);
18113
18269
  break;
18270
+ }
18114
18271
  case "disassemble_progress": {
18115
18272
  const k = `unscrew:${ev.joint}`;
18116
18273
  if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
@@ -18140,6 +18297,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18140
18297
  case "stage":
18141
18298
  this.onStage(ev);
18142
18299
  break;
18300
+ case "chip":
18301
+ this.onChip(ev);
18302
+ break;
18143
18303
  case "expose":
18144
18304
  this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
18145
18305
  if (ev.bars.length > 0) {
@@ -18396,6 +18556,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18396
18556
  if (!played) this.vfx.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
18397
18557
  this.sound(impactKey(def), ev.opening.center, 0.8);
18398
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
+ }
18399
18572
  onFracture(ev) {
18400
18573
  const b = this.bounds.get(ev.object);
18401
18574
  const pv = b?.parts.get(ev.part);
@@ -38191,7 +38364,7 @@ void main(){
38191
38364
  if (!position) throw new Error(`game-test-room: scenery "${id}" is new, so it needs a position`);
38192
38365
  merged.set(id, { ...obj, position });
38193
38366
  }
38194
- return [...merged.entries()].map(([id, o]) => ({ id, spec: o.spec, position: o.position, rotationY: o.rotationY ?? 0, colors: o.colors, build: o.build, label: o.label, top: o.top, spout: o.spout }));
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 }));
38195
38368
  }
38196
38369
  function createArena(THREE, size, scene2, scenery, tapOption) {
38197
38370
  const group = markKitOrigin(new THREE.Group(), { owner: "game-test-room", kind: "room" });
@@ -40424,6 +40597,9 @@ void main(){
40424
40597
  "act.strike.leg": "{weapon}: {clip} with the {leg} leg begins, impact in {seconds} s",
40425
40598
  "leg.left": "left",
40426
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",
40427
40603
  "act.flame.hit": "{weapon}: flame at {temp} \xB0C on {where}",
40428
40604
  "act.flame.air": "{weapon}: flame, nothing in range",
40429
40605
  "act.heat.hit": "{weapon}: heating {where} at {temp} \xB0C",
@@ -40455,6 +40631,7 @@ void main(){
40455
40631
  "loading.open_area": "Opening {area}\u2026",
40456
40632
  "loading.spawn": "Building {name}\u2026",
40457
40633
  "act.loosened": "{name} was heavy but free: the blast moved it",
40634
+ "act.shoved": "{name} stands free: the shots moved it",
40458
40635
  "act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
40459
40636
  "act.use_item": "{name}: {use}",
40460
40637
  "act.tap.on": "Tap opened",
@@ -40731,6 +40908,9 @@ void main(){
40731
40908
  "act.strike.leg": "{weapon}: {clip} com a perna {leg} come\xE7a, impacto em {seconds} s",
40732
40909
  "leg.left": "esquerda",
40733
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",
40734
40914
  "act.flame.hit": "{weapon}: chama a {temp} \xB0C em {where}",
40735
40915
  "act.flame.air": "{weapon}: chama, nada ao alcance",
40736
40916
  "act.heat.hit": "{weapon}: esquentando {where} a {temp} \xB0C",
@@ -40762,6 +40942,7 @@ void main(){
40762
40942
  "loading.open_area": "Abrindo {area}\u2026",
40763
40943
  "loading.spawn": "Montando {name}\u2026",
40764
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",
40765
40946
  "act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
40766
40947
  "act.use_item": "{name}: {use}",
40767
40948
  "act.tap.on": "Abriu a torneira",
@@ -41225,7 +41406,7 @@ void main(){
41225
41406
  relay.type = "button";
41226
41407
  relay.onclick = () => table.relay();
41227
41408
  trials.appendChild(relay);
41228
- element.append(head, search, found, sub4, list, trials);
41409
+ element.append(head, search, found, sub4, trials, list);
41229
41410
  const groups = () => {
41230
41411
  const keep = select.value;
41231
41412
  select.textContent = "";
@@ -41872,6 +42053,7 @@ void main(){
41872
42053
 
41873
42054
  // src/room.ts
41874
42055
  var FIXED_DT = 1 / 60;
42056
+ var SHOVE_SPEED = 0.04;
41875
42057
  var METRICS_INTERVAL_S = 0.25;
41876
42058
  var CARRY_NEAR = 0.62;
41877
42059
  var CARRY_PALM = 0.012;
@@ -42234,7 +42416,9 @@ void main(){
42234
42416
  scene2.add(root);
42235
42417
  root.updateMatrixWorld(true);
42236
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)] } : {} });
42237
- 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;
42238
42422
  }
42239
42423
  };
42240
42424
  addArena();
@@ -42329,6 +42513,7 @@ void main(){
42329
42513
  renderer.unbind(id);
42330
42514
  } catch {
42331
42515
  }
42516
+ e.follower?.dispose?.();
42332
42517
  if (world.has(id)) world.remove(id);
42333
42518
  e.so.root.removeFromParent();
42334
42519
  letGo(e.so.root);
@@ -42363,6 +42548,7 @@ void main(){
42363
42548
  for (const e of entries.values()) {
42364
42549
  world.reset(e.so.objectId);
42365
42550
  renderer.restore(e.so.objectId);
42551
+ e.follower?.restore?.();
42366
42552
  }
42367
42553
  renderer.clear();
42368
42554
  vfx.world.clear();
@@ -42384,6 +42570,7 @@ void main(){
42384
42570
  if (heldEntry === e) continue;
42385
42571
  world.reset(e.so.objectId);
42386
42572
  renderer.restore(e.so.objectId);
42573
+ e.follower?.restore?.();
42387
42574
  if (e.containerId) {
42388
42575
  const c = fluids.container(e.containerId);
42389
42576
  const f = e.item?.fluid?.container;
@@ -42687,6 +42874,19 @@ void main(){
42687
42874
  if (!rendererPieces.has(objectId)) return;
42688
42875
  renderer.impulse(objectId, [dir[0] * momentum, dir[1] * momentum, dir[2] * momentum], point);
42689
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
+ };
42690
42890
  const fireShots = (shots, recoil) => {
42691
42891
  if (shots.length === 0) return;
42692
42892
  const w = toolName();
@@ -42696,6 +42896,7 @@ void main(){
42696
42896
  let hits = 0;
42697
42897
  let first = null;
42698
42898
  let energy = 0;
42899
+ const pushes = /* @__PURE__ */ new Map();
42699
42900
  withoutHeld(() => {
42700
42901
  for (const s of shots) {
42701
42902
  energy = Math.max(energy, s.energy);
@@ -42707,11 +42908,16 @@ void main(){
42707
42908
  end3 = r0.point;
42708
42909
  if (!first) first = { object: r0.object, part: r0.part, point: r0.point, normal: [0, 1, 0], distance: dist3(s.origin, r0.point) };
42709
42910
  const speed = Math.max(80, s.speed);
42710
- 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);
42711
42916
  }
42712
42917
  if (shots.length <= 3 || rnd2() < 0.34) tracer(m, end3);
42713
42918
  }
42714
42919
  });
42920
+ shoveFree(pushes);
42715
42921
  player.kick(recoil[0], recoil[1]);
42716
42922
  const f = first;
42717
42923
  const d = f ? num(f.distance, 1) : "";
@@ -42803,16 +43009,25 @@ void main(){
42803
43009
  let strikes = [];
42804
43010
  const kickLeg = options.kickLeg ?? "alternate";
42805
43011
  let nextLeg = "right";
43012
+ const punchHand = options.punchHand ?? "alternate";
43013
+ const punchTools = new Set(options.punchTools ?? ["hand", "fists"]);
43014
+ let nextHand = "right";
42806
43015
  const strikeUse = (u, id = toolId) => {
42807
43016
  const clip = strikeClips[id];
42808
43017
  const leg = id === "kick" ? kickLeg === "alternate" ? nextLeg : kickLeg : void 0;
42809
- 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;
42810
43021
  if (leg && seconds !== null && kickLeg === "alternate") nextLeg = leg === "right" ? "left" : "right";
43022
+ if (hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
42811
43023
  if (clip === void 0 || seconds === null) {
42812
43024
  meleeUse(u, id);
42813
43025
  return;
42814
43026
  }
42815
- 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
+ });
42816
43031
  strikes.push({
42817
43032
  key: id,
42818
43033
  remaining: seconds,