@adatechnology/game-test-room 0.19.0 → 0.20.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.
@@ -605,9 +605,9 @@ var GameTestRoom = (() => {
605
605
  const tz = 2 * (qx * v[1] - qy * v[0]);
606
606
  return [v[0] + qw * tx + (qy * tz - qz * ty), v[1] + qw * ty + (qz * tx - qx * tz), v[2] + qw * tz + (qx * ty - qy * tx)];
607
607
  };
608
- var toLocal = (position, rotation, p) => rotateVec([-rotation[0], -rotation[1], -rotation[2], rotation[3]], [p[0] - position[0], p[1] - position[1], p[2] - position[2]]);
609
- var toWorld = (position, rotation, p) => {
610
- const r = rotateVec(rotation, p);
608
+ var toLocal = (position, rotation2, p) => rotateVec([-rotation2[0], -rotation2[1], -rotation2[2], rotation2[3]], [p[0] - position[0], p[1] - position[1], p[2] - position[2]]);
609
+ var toWorld = (position, rotation2, p) => {
610
+ const r = rotateVec(rotation2, p);
611
611
  return [r[0] + position[0], r[1] + position[1], r[2] + position[2]];
612
612
  };
613
613
  var shapeDims = (s) => {
@@ -1012,8 +1012,8 @@ var GameTestRoom = (() => {
1012
1012
  };
1013
1013
  var cellsOutside = (cells, thin, point, radius) => cells.filter((c) => planeDistance(c, thin, point) > radius);
1014
1014
  var MAX_MARKS_PER_FRAGMENT = 12;
1015
- var marksInBox = (center, size, rotation, holes) => {
1016
- const inverse = rotation ? quat.conjugate(rotation) : void 0;
1015
+ var marksInBox = (center, size, rotation2, holes) => {
1016
+ const inverse = rotation2 ? quat.conjugate(rotation2) : void 0;
1017
1017
  const marks = [];
1018
1018
  for (const hole of holes) {
1019
1019
  const offset = [hole.point[0] - center[0], hole.point[1] - center[1], hole.point[2] - center[2]];
@@ -3150,6 +3150,7 @@ var GameTestRoom = (() => {
3150
3150
  var CHIP_ENERGY = 200;
3151
3151
  var CHIP_SPLASH = 0.5;
3152
3152
  var CRATER_SPENT = 0.6;
3153
+ var EDGE_REACH = 1.5;
3153
3154
  var BAR_BOW = 0.08;
3154
3155
  var BULLET_JOINT_SPAN = 0.3;
3155
3156
  var CUT_SLICE = 1;
@@ -3601,8 +3602,8 @@ var GameTestRoom = (() => {
3601
3602
  }
3602
3603
  solveStructure(o);
3603
3604
  if (peak <= sec.steel) return;
3604
- const rotation = (elasticEnergy(peak, L, EI, flexure.loadFactor) - elasticEnergy(sec.steel, L, EI, flexure.loadFactor)) / (2 * sec.steel);
3605
- p.rt.hinge = (p.rt.hinge ?? 0) + rotation;
3605
+ const rotation2 = (elasticEnergy(peak, L, EI, flexure.loadFactor) - elasticEnergy(sec.steel, L, EI, flexure.loadFactor)) / (2 * sec.steel);
3606
+ p.rt.hinge = (p.rt.hinge ?? 0) + rotation2;
3606
3607
  markDamaged(p);
3607
3608
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
3608
3609
  };
@@ -4304,8 +4305,8 @@ var GameTestRoom = (() => {
4304
4305
  const detachEvents = (o, opened, by, src) => {
4305
4306
  for (const j of opened) emit2({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
4306
4307
  };
4307
- const slideOffResting = (o, rotation) => {
4308
- const upY = 1 - 2 * (rotation[0] * rotation[0] + rotation[2] * rotation[2]);
4308
+ const slideOffResting = (o, rotation2) => {
4309
+ const upY = 1 - 2 * (rotation2[0] * rotation2[0] + rotation2[2] * rotation2[2]);
4309
4310
  if (upY >= RESTING_HOLD_COS) return;
4310
4311
  const opened = o.joints.filter((j) => j.spec.kind === "resting" && !j.st.open);
4311
4312
  if (opened.length === 0) return;
@@ -4523,6 +4524,9 @@ var GameTestRoom = (() => {
4523
4524
  const lo = cell.center[a] - cell.size[a] / 2 + w / 2, hi = cell.center[a] + cell.size[a] / 2 - w / 2;
4524
4525
  size[a] = w;
4525
4526
  center[a] = clamp2(lp[a], lo, hi);
4527
+ const [from, to] = tornToEdge({ p, cell, a, b: a === u ? v : u, at: lp[a], crater: [center[a] - w / 2, center[a] + w / 2], reach: w * EDGE_REACH, gone: (k) => worn[k] === -1 });
4528
+ size[a] = to - from;
4529
+ center[a] = (from + to) / 2;
4526
4530
  }
4527
4531
  const taken2 = (cut2[i] ?? 0) + size[u] * size[v];
4528
4532
  if (taken2 >= cell.size[u] * cell.size[v] * CRATER_SPENT) {
@@ -4560,6 +4564,19 @@ var GameTestRoom = (() => {
4560
4564
  collapseCheck(o, src);
4561
4565
  }
4562
4566
  };
4567
+ const tornToEdge = (o) => {
4568
+ const { p, cell, a, b, at: at3, crater: crater2, reach, gone } = o;
4569
+ const half = p.dims[a] / 2;
4570
+ const lo = cell.center[a] - cell.size[a] / 2, hi = cell.center[a] + cell.size[a] / 2;
4571
+ const free = (side, sign) => {
4572
+ if (sign > 0 ? side >= half - 1e-6 : side <= -half + 1e-6) return sign > 0 || a !== planeAxes(p.thin)[1];
4573
+ const probe = side + sign * 1e-3;
4574
+ return gridOf(p).some((c, k) => gone(k) && Math.abs(probe - c.center[a]) <= c.size[a] / 2 && Math.abs(cell.center[b] - c.center[b]) <= c.size[b] / 2);
4575
+ };
4576
+ if (at3 - lo <= reach && free(lo, -1)) return [lo, Math.max(crater2[1], lo)];
4577
+ if (hi - at3 <= reach && free(hi, 1)) return [Math.min(crater2[0], hi), hi];
4578
+ return crater2;
4579
+ };
4563
4580
  const craterAcross = (energy) => clamp2(0.03 + 12e-4 * Math.sqrt(Math.max(0, energy)), 0.03, 0.14);
4564
4581
  const craterFragments = (o, p, cell, dir, speed, mass, tag) => {
4565
4582
  const { c, q } = partXf(o, p);
@@ -5712,11 +5729,11 @@ var GameTestRoom = (() => {
5712
5729
  impls.set(id, { impl, position: opts.position ?? [0, 0, 0], rotation: opts.rotation ?? [0, 0, 0, 1], poses: /* @__PURE__ */ new Map() });
5713
5730
  return id;
5714
5731
  },
5715
- setPartPose(objectId, partId, position, rotation) {
5716
- const q = quatFromEuler(...rotation ?? [0, 0, 0]);
5732
+ setPartPose(objectId, partId, position, rotation2) {
5733
+ const q = quatFromEuler(...rotation2 ?? [0, 0, 0]);
5717
5734
  const im = impls.get(objectId);
5718
5735
  if (im) {
5719
- im.impl.setPartPose?.(partId, position, rotation);
5736
+ im.impl.setPartPose?.(partId, position, rotation2);
5720
5737
  im.poses.set(partId, { pos: [position[0], position[1], position[2]], q });
5721
5738
  return;
5722
5739
  }
@@ -5733,17 +5750,17 @@ var GameTestRoom = (() => {
5733
5750
  },
5734
5751
  has: (id) => taken(id),
5735
5752
  ids: () => [...objects.keys(), ...impls.keys()],
5736
- setTransform(id, position, rotation) {
5753
+ setTransform(id, position, rotation2) {
5737
5754
  const im = impls.get(id);
5738
5755
  if (im) {
5739
5756
  im.position = [position[0], position[1], position[2]];
5740
- im.rotation = [rotation[0], rotation[1], rotation[2], rotation[3]];
5757
+ im.rotation = [rotation2[0], rotation2[1], rotation2[2], rotation2[3]];
5741
5758
  return;
5742
5759
  }
5743
5760
  const o = getObj(id);
5744
- o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
5761
+ o.base = { pos: [position[0], position[1], position[2]], rot: [rotation2[0], rotation2[1], rotation2[2], rotation2[3]] };
5745
5762
  applySoilPose(o);
5746
- slideOffResting(o, rotation);
5763
+ slideOffResting(o, rotation2);
5747
5764
  },
5748
5765
  splitGroup(id, group, newId) {
5749
5766
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -7524,7 +7541,7 @@ var GameTestRoom = (() => {
7524
7541
  const registry = createSoundRegistry(options.banks);
7525
7542
  const voices = createVoiceManager({ ...options.groups ? { groups: options.groups } : {}, ...options.maxVoices === void 0 ? {} : { maxVoices: options.maxVoices } });
7526
7543
  const engineRandom = createRng2(seed);
7527
- const shared = createNoiseStore(engineRandom);
7544
+ const shared2 = createNoiseStore(engineRandom);
7528
7545
  const busSettings = /* @__PURE__ */ new Map();
7529
7546
  const busNodes = /* @__PURE__ */ new Map();
7530
7547
  const meters = /* @__PURE__ */ new Map();
@@ -7563,7 +7580,7 @@ var GameTestRoom = (() => {
7563
7580
  if (!created) return null;
7564
7581
  context = created;
7565
7582
  chain = buildChain(created, mix, engineRandom);
7566
- if (mix.noise.create === "eager") noiseBuffer(shared, created, mix.noise.seconds);
7583
+ if (mix.noise.create === "eager") noiseBuffer(shared2, created, mix.noise.seconds);
7567
7584
  for (const [bus, setting] of Object.entries(mix.buses ?? {})) {
7568
7585
  busSettings.set(bus, { ...setting, ...busSettings.get(bus) });
7569
7586
  }
@@ -7682,8 +7699,8 @@ var GameTestRoom = (() => {
7682
7699
  const loopHost = {
7683
7700
  ready,
7684
7701
  output: outputFor,
7685
- prepareNoise: () => context && prepareNoise(shared, context, mix.noise.seconds),
7686
- noise: () => noiseBuffer(shared, context, mix.noise.seconds),
7702
+ prepareNoise: () => context && prepareNoise(shared2, context, mix.noise.seconds),
7703
+ noise: () => noiseBuffer(shared2, context, mix.noise.seconds),
7687
7704
  register: (loop) => void loops.add(loop),
7688
7705
  unregister: (loop) => void loops.delete(loop)
7689
7706
  };
@@ -7691,7 +7708,7 @@ var GameTestRoom = (() => {
7691
7708
  registry,
7692
7709
  register: (bank) => registry.register(bank),
7693
7710
  plan: (id, playOptions = {}) => planWith(id, playOptions, createRng2(playOptions.seed ?? seed)),
7694
- play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared),
7711
+ play: (id, playOptions = {}) => playWith(id, playOptions, randomFor(playOptions), shared2),
7695
7712
  playPlan(plan, playOptions = {}) {
7696
7713
  const audio = ready();
7697
7714
  if (!audio) return null;
@@ -7699,13 +7716,13 @@ var GameTestRoom = (() => {
7699
7716
  const recipe = { id: plan.id, bus: plan.bus, layers: [], sources: [], priority: plan.priority, ...plan.group ? { group: plan.group } : {} };
7700
7717
  const voiceId = plan.silent ? void 0 : admit(recipe, plan.level, plan.distance);
7701
7718
  if (voiceId === null) return null;
7702
- if (planUsesNoise(plan)) prepareNoise(shared, audio, mix.noise.seconds);
7703
- return start(plan, shared, voiceId, playOptions.delay);
7719
+ if (planUsesNoise(plan)) prepareNoise(shared2, audio, mix.noise.seconds);
7720
+ return start(plan, shared2, voiceId, playOptions.delay);
7704
7721
  },
7705
7722
  loop: (id, loopOptions = {}) => createLoopHandle(registry.loop(id), loopHost, loopOptions.volume),
7706
7723
  channel(channelOptions) {
7707
7724
  const random = channelOptions.random ?? (channelOptions.seed === void 0 ? engineRandom : createRng2(channelOptions.seed));
7708
- const store = channelOptions.ownNoise ? createNoiseStore(random) : shared;
7725
+ const store = channelOptions.ownNoise ? createNoiseStore(random) : shared2;
7709
7726
  return {
7710
7727
  random,
7711
7728
  play: (id, playOptions = {}) => playWith(id, playOptions, random, store),
@@ -7727,7 +7744,7 @@ var GameTestRoom = (() => {
7727
7744
  if (!context || !chain) return;
7728
7745
  const old = chain;
7729
7746
  chain = buildChain(context, mix, engineRandom);
7730
- if (mix.noise.seconds !== previous.noise.seconds) shared.reset();
7747
+ if (mix.noise.seconds !== previous.noise.seconds) shared2.reset();
7731
7748
  for (const node of busNodes.values()) {
7732
7749
  node.disconnect();
7733
7750
  node.connect(chain.master);
@@ -7746,7 +7763,7 @@ var GameTestRoom = (() => {
7746
7763
  warmUp() {
7747
7764
  const audio = ready();
7748
7765
  if (!audio) return false;
7749
- noiseBuffer(shared, audio, mix.noise.seconds);
7766
+ noiseBuffer(shared2, audio, mix.noise.seconds);
7750
7767
  return true;
7751
7768
  },
7752
7769
  level(bus) {
@@ -7781,7 +7798,7 @@ var GameTestRoom = (() => {
7781
7798
  chain = null;
7782
7799
  busNodes.clear();
7783
7800
  meters.clear();
7784
- shared.reset();
7801
+ shared2.reset();
7785
7802
  }
7786
7803
  };
7787
7804
  }
@@ -9149,11 +9166,231 @@ var GameTestRoom = (() => {
9149
9166
  glow.outIntensity = glow.i * Math.pow(1 - age, glow.curve) * flicker;
9150
9167
  }
9151
9168
  }
9169
+ var LEHMER_MULTIPLIER = 16807;
9170
+ var LEHMER_MODULUS = 2147483647;
9171
+ var HASH_MIX = 2654435761;
9172
+ function normalizeLehmerSeed(seed) {
9173
+ const whole = Math.floor(Math.abs(seed)) % (LEHMER_MODULUS - 1);
9174
+ return whole === 0 ? 1 : whole;
9175
+ }
9176
+ function lehmer(seed) {
9177
+ let state = normalizeLehmerSeed(seed);
9178
+ const next = () => {
9179
+ state = state * LEHMER_MULTIPLIER % LEHMER_MODULUS;
9180
+ return (state - 1) / (LEHMER_MODULUS - 1);
9181
+ };
9182
+ return {
9183
+ next,
9184
+ between: (min2, max2) => min2 + (max2 - min2) * next(),
9185
+ direction(out) {
9186
+ const up = next() * 2 - 1;
9187
+ const angle = next() * Math.PI * 2;
9188
+ const ring2 = Math.sqrt(1 - up * up);
9189
+ out.x = ring2 * Math.cos(angle);
9190
+ out.y = up;
9191
+ out.z = ring2 * Math.sin(angle);
9192
+ return out;
9193
+ },
9194
+ get state() {
9195
+ return state;
9196
+ }
9197
+ };
9198
+ }
9199
+ function mixEventSeed(eventSeed) {
9200
+ let mixed = Math.imul((eventSeed | 0) ^ HASH_MIX, 2246822507);
9201
+ mixed ^= mixed >>> 13;
9202
+ mixed = Math.imul(mixed, 3266489909);
9203
+ mixed ^= mixed >>> 16;
9204
+ return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1;
9205
+ }
9206
+ function streamFor2(eventSeed) {
9207
+ return lehmer(mixEventSeed(eventSeed));
9208
+ }
9209
+ var SKIN = 5e-3;
9210
+ var MAX_PAD = 0.45;
9211
+ function rotation(q) {
9212
+ const [x, y, z, w] = q ?? [0, 0, 0, 1];
9213
+ const n = Math.hypot(x, y, z, w) || 1;
9214
+ const qx = x / n;
9215
+ const qy = y / n;
9216
+ const qz = z / n;
9217
+ const qw = w / n;
9218
+ return new Float64Array([
9219
+ 1 - 2 * (qy * qy + qz * qz),
9220
+ 2 * (qx * qy - qz * qw),
9221
+ 2 * (qx * qz + qy * qw),
9222
+ 2 * (qx * qy + qz * qw),
9223
+ 1 - 2 * (qx * qx + qz * qz),
9224
+ 2 * (qy * qz - qx * qw),
9225
+ 2 * (qx * qz - qy * qw),
9226
+ 2 * (qy * qz + qx * qw),
9227
+ 1 - 2 * (qx * qx + qy * qy)
9228
+ ]);
9229
+ }
9230
+ var Enclosures = class {
9231
+ constructor(seed = 20973) {
9232
+ __publicField(this, "spaces", /* @__PURE__ */ new Map());
9233
+ __publicField(this, "list", []);
9234
+ __publicField(this, "random");
9235
+ __publicField(this, "out", { x: 0, y: 0, z: 0, nx: 0, ny: 0, nz: 0 });
9236
+ __publicField(this, "l0", new Float64Array(3));
9237
+ __publicField(this, "l1", new Float64Array(3));
9238
+ __publicField(this, "q", new Float64Array(3));
9239
+ this.random = lehmer(seed);
9240
+ }
9241
+ get empty() {
9242
+ return this.list.length === 0;
9243
+ }
9244
+ get size() {
9245
+ return this.list.length;
9246
+ }
9247
+ /** Declares, moves or (with `null`) ends the space `id`. */
9248
+ set(id, enclosure) {
9249
+ if (!enclosure) {
9250
+ if (this.spaces.delete(id)) this.list = [...this.spaces.values()];
9251
+ return;
9252
+ }
9253
+ const [hx, hy, hz] = enclosure.halfSize;
9254
+ const space = {
9255
+ cx: enclosure.center[0],
9256
+ cy: enclosure.center[1],
9257
+ cz: enclosure.center[2],
9258
+ hx: Math.abs(hx),
9259
+ hy: Math.abs(hy),
9260
+ hz: Math.abs(hz),
9261
+ r: rotation(enclosure.orientation),
9262
+ radius: Math.hypot(hx, hy, hz),
9263
+ openings: enclosure.openings ?? [],
9264
+ leak: Math.max(0, enclosure.leak ?? 0)
9265
+ };
9266
+ const old = this.spaces.get(id);
9267
+ this.spaces.set(id, space);
9268
+ const at3 = old ? this.list.indexOf(old) : -1;
9269
+ if (at3 >= 0) this.list[at3] = space;
9270
+ else this.list.push(space);
9271
+ }
9272
+ get(id) {
9273
+ const s = this.spaces.get(id);
9274
+ if (!s) return void 0;
9275
+ return { center: [s.cx, s.cy, s.cz], halfSize: [s.hx, s.hy, s.hz], openings: s.openings, leak: s.leak };
9276
+ }
9277
+ ids() {
9278
+ return [...this.spaces.keys()];
9279
+ }
9280
+ clear() {
9281
+ this.spaces.clear();
9282
+ this.list = [];
9283
+ }
9284
+ /** Whether a point is inside any space (on the inside of its walls). */
9285
+ inside(x, y, z) {
9286
+ for (const s of this.list) {
9287
+ this.toLocal(s, x, y, z, this.l0);
9288
+ if (Math.abs(this.l0[0]) <= s.hx && Math.abs(this.l0[1]) <= s.hy && Math.abs(this.l0[2]) <= s.hz) return true;
9289
+ }
9290
+ return false;
9291
+ }
9292
+ toLocal(s, x, y, z, out) {
9293
+ const dx = x - s.cx;
9294
+ const dy = y - s.cy;
9295
+ const dz = z - s.cz;
9296
+ const r = s.r;
9297
+ out[0] = r[0] * dx + r[3] * dy + r[6] * dz;
9298
+ out[1] = r[1] * dx + r[4] * dy + r[7] * dz;
9299
+ out[2] = r[2] * dx + r[5] * dy + r[8] * dz;
9300
+ }
9301
+ /** Passes at `rate` per second. */
9302
+ chance(rate, dt) {
9303
+ return rate > 0 && this.random.next() < 1 - Math.exp(-rate * dt);
9304
+ }
9305
+ /** The opening the wall point `q` (local) is in lets it through now. */
9306
+ through(s, q, dt) {
9307
+ for (const o of s.openings) {
9308
+ if (Math.abs(q[0] - o.center[0]) > o.halfSize[0] || Math.abs(q[1] - o.center[1]) > o.halfSize[1] || Math.abs(q[2] - o.center[2]) > o.halfSize[2]) continue;
9309
+ if (o.rate === void 0 || this.chance(o.rate, dt)) return true;
9310
+ }
9311
+ return this.chance(s.leak, dt);
9312
+ }
9313
+ confine(x0, y0, z0, x1, y1, z1, pad2, dt) {
9314
+ const list = this.list;
9315
+ if (list.length === 0) return false;
9316
+ const p = Math.min(Math.max(0, pad2), MAX_PAD);
9317
+ const l0 = this.l0;
9318
+ const l1 = this.l1;
9319
+ const q = this.q;
9320
+ for (const s of list) {
9321
+ const reach = s.radius + p + 0.05;
9322
+ const dx = x1 - s.cx;
9323
+ const dy = y1 - s.cy;
9324
+ const dz = z1 - s.cz;
9325
+ if (dx * dx + dy * dy + dz * dz > reach * reach) {
9326
+ const ex = x0 - s.cx;
9327
+ const ey = y0 - s.cy;
9328
+ const ez = z0 - s.cz;
9329
+ if (ex * ex + ey * ey + ez * ez > reach * reach) continue;
9330
+ }
9331
+ this.toLocal(s, x0, y0, z0, l0);
9332
+ this.toLocal(s, x1, y1, z1, l1);
9333
+ const h = [s.hx, s.hy, s.hz];
9334
+ const in0 = Math.abs(l0[0]) <= s.hx && Math.abs(l0[1]) <= s.hy && Math.abs(l0[2]) <= s.hz;
9335
+ let axis2 = 0;
9336
+ let sign = 1;
9337
+ if (in0) {
9338
+ if (Math.abs(l1[0]) <= s.hx && Math.abs(l1[1]) <= s.hy && Math.abs(l1[2]) <= s.hz) continue;
9339
+ for (let a = 0; a < 3; a++) q[a] = Math.min(h[a], Math.max(-h[a], l1[a]));
9340
+ if (this.through(s, q, dt)) continue;
9341
+ let worst = -Infinity;
9342
+ for (let a = 0; a < 3; a++) {
9343
+ const over = Math.abs(l1[a]) - h[a];
9344
+ if (over > worst) {
9345
+ worst = over;
9346
+ axis2 = a;
9347
+ }
9348
+ }
9349
+ sign = l1[axis2] < 0 ? -1 : 1;
9350
+ for (let a = 0; a < 3; a++) l1[a] = Math.min(h[a] - SKIN, Math.max(-(h[a] - SKIN), l1[a]));
9351
+ sign = -sign;
9352
+ } else {
9353
+ const padded0 = Math.abs(l0[0]) <= s.hx + p && Math.abs(l0[1]) <= s.hy + p && Math.abs(l0[2]) <= s.hz + p;
9354
+ const bound = padded0 ? 0 : p;
9355
+ if (!(Math.abs(l1[0]) <= s.hx + bound && Math.abs(l1[1]) <= s.hy + bound && Math.abs(l1[2]) <= s.hz + bound)) continue;
9356
+ for (let a = 0; a < 3; a++) q[a] = Math.min(h[a], Math.max(-h[a], l1[a]));
9357
+ if (this.through(s, q, dt)) continue;
9358
+ let worst = -Infinity;
9359
+ for (let a = 0; a < 3; a++) {
9360
+ const over = Math.abs(l0[a]) - (h[a] + bound);
9361
+ if (over > worst) {
9362
+ worst = over;
9363
+ axis2 = a;
9364
+ }
9365
+ }
9366
+ sign = l0[axis2] < 0 ? -1 : 1;
9367
+ l1[axis2] = sign * (h[axis2] + bound + SKIN);
9368
+ }
9369
+ const r = s.r;
9370
+ const out = this.out;
9371
+ out.x = s.cx + r[0] * l1[0] + r[1] * l1[1] + r[2] * l1[2];
9372
+ out.y = s.cy + r[3] * l1[0] + r[4] * l1[1] + r[5] * l1[2];
9373
+ out.z = s.cz + r[6] * l1[0] + r[7] * l1[1] + r[8] * l1[2];
9374
+ out.nx = r[axis2] * sign;
9375
+ out.ny = r[3 + axis2] * sign;
9376
+ out.nz = r[6 + axis2] * sign;
9377
+ return true;
9378
+ }
9379
+ return false;
9380
+ }
9381
+ };
9382
+ function slideAlong(out, vx2, vy2, vz2) {
9383
+ const into = vx2 * out.nx + vy2 * out.ny + vz2 * out.nz;
9384
+ if (into >= 0) return [vx2, vy2, vz2];
9385
+ return [vx2 - into * out.nx, vy2 - into * out.ny, vz2 - into * out.nz];
9386
+ }
9152
9387
  var FLOOR_FRACTION = 0.4;
9153
9388
  var FLOOR_BOUNCE = -0.2;
9154
9389
  var ARRIVAL_FRAME = 30;
9155
9390
  var ARRIVAL_BASE = 0.3;
9156
9391
  var GROWTH_CAP_SECONDS = 3.5;
9392
+ var CARRIED_KINDS = [true, true, false, true, false];
9393
+ var PAD_OF_SIZE = 0.35;
9157
9394
  function createPuffPool(capacity) {
9158
9395
  const placeholder = { next: () => 0, between: (min2) => min2, direction: (out) => out, state: 0 };
9159
9396
  return new OrderedPool(capacity, () => ({
@@ -9248,13 +9485,24 @@ var GameTestRoom = (() => {
9248
9485
  if (puff3.vy < 0) puff3.vy *= FLOOR_BOUNCE;
9249
9486
  }
9250
9487
  }
9251
- function updatePuffs(pool, dt, wind) {
9488
+ function updatePuffs(pool, dt, wind, confiner) {
9252
9489
  pool.compact(ageOrKeep2, dt);
9490
+ const confining = confiner !== void 0 && !confiner.empty;
9253
9491
  for (const puff3 of pool.items) {
9254
9492
  const age = puff3.t / puff3.life;
9255
9493
  const heat = puff3.h0 > 0 ? puff3.h0 * Math.pow(Math.max(0, 1 - puff3.t / puff3.hl), 1.25) : 0;
9494
+ const x0 = puff3.x;
9495
+ const y0 = puff3.y;
9496
+ const z0 = puff3.z;
9256
9497
  if (puff3.ring) moveOnRing(puff3, puff3.ring, wind);
9257
9498
  else moveFree(puff3, dt, age, heat, wind);
9499
+ if (confining && CARRIED_KINDS[puff3.kind] && confiner.confine(x0, y0, z0, puff3.x, puff3.y, puff3.z, puff3.size * PAD_OF_SIZE, dt)) {
9500
+ const out = confiner.out;
9501
+ puff3.x = out.x;
9502
+ puff3.y = out.y;
9503
+ puff3.z = out.z;
9504
+ [puff3.vx, puff3.vy, puff3.vz] = slideAlong(out, puff3.vx, puff3.vy, puff3.vz);
9505
+ }
9258
9506
  const size = puff3.s0 + (puff3.s1 - puff3.s0) * (1 - Math.exp(-puff3.t * puff3.gk)) + puff3.sg * Math.min(puff3.t, GROWTH_CAP_SECONDS);
9259
9507
  puff3.size = size * Math.min(1, puff3.t * ARRIVAL_FRAME + ARRIVAL_BASE);
9260
9508
  puff3.age = age;
@@ -9780,10 +10028,10 @@ var GameTestRoom = (() => {
9780
10028
  ])
9781
10029
  ]
9782
10030
  };
9783
- function localToWorld(translation, rotation, local2) {
10031
+ function localToWorld(translation, rotation2, local2) {
9784
10032
  const [x, y, z] = local2;
9785
- const [a, c, e] = [Math.cos(rotation[0]), Math.cos(rotation[1]), Math.cos(rotation[2])];
9786
- const [b, d, f] = [Math.sin(rotation[0]), Math.sin(rotation[1]), Math.sin(rotation[2])];
10033
+ const [a, c, e] = [Math.cos(rotation2[0]), Math.cos(rotation2[1]), Math.cos(rotation2[2])];
10034
+ const [b, d, f] = [Math.sin(rotation2[0]), Math.sin(rotation2[1]), Math.sin(rotation2[2])];
9787
10035
  const [ae, af, be, bf] = [a * e, a * f, b * e, b * f];
9788
10036
  return [
9789
10037
  c * e * x + -c * f * y + d * z + translation[0],
@@ -11446,46 +11694,6 @@ var GameTestRoom = (() => {
11446
11694
  var ALL_EFFECTS = [BLOOD_HIT, HEADSHOT, DISMEMBER, BLOATER_CLOUD, SCREAMER_SHRIEK, CORPSE_DECAY, REANIMATION, EYES_IN_FOG, MUZZLE_FLASH, IMPACT_BY_MATERIAL, PENETRATION, CASINGS_AND_MAGAZINE, MOLOTOV, GRENADE, EXPLOSIVE_BARREL, SHOPFRONT_GLASS, DOOR_STAGES, BUILDING_COLLAPSE, BLOCK_WALL, METAL_DOOR_EFFECT, IRON_RAILING, WOODEN_FENCE, HIT_POINT_BREAKAGE, PRESSURE_CYLINDERS, BUILDING_FIRE, RAIN_AND_PUDDLES, FOG_AND_ASH, SCRAPING_SPARKS, ENGINE_SMOKE, FUEL_LEAK, CAR_EXPLOSION, WINDSHIELD, HEADLIGHTS_IN_RAIN, DAMAGE_AND_BITE, INFECTION_PHASES, PANIC, BLOATER_BLINDNESS, CAMPFIRE, COUGH_AND_BREATH];
11447
11695
  var WORKSHOP_EFFECTS = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]));
11448
11696
  var GAME_EFFECTS = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS, ...VEHICLE_EFFECTS].map((effect) => [effect.id, effect])) };
11449
- var LEHMER_MULTIPLIER = 16807;
11450
- var LEHMER_MODULUS = 2147483647;
11451
- var HASH_MIX = 2654435761;
11452
- function normalizeLehmerSeed(seed) {
11453
- const whole = Math.floor(Math.abs(seed)) % (LEHMER_MODULUS - 1);
11454
- return whole === 0 ? 1 : whole;
11455
- }
11456
- function lehmer(seed) {
11457
- let state = normalizeLehmerSeed(seed);
11458
- const next = () => {
11459
- state = state * LEHMER_MULTIPLIER % LEHMER_MODULUS;
11460
- return (state - 1) / (LEHMER_MODULUS - 1);
11461
- };
11462
- return {
11463
- next,
11464
- between: (min2, max2) => min2 + (max2 - min2) * next(),
11465
- direction(out) {
11466
- const up = next() * 2 - 1;
11467
- const angle = next() * Math.PI * 2;
11468
- const ring2 = Math.sqrt(1 - up * up);
11469
- out.x = ring2 * Math.cos(angle);
11470
- out.y = up;
11471
- out.z = ring2 * Math.sin(angle);
11472
- return out;
11473
- },
11474
- get state() {
11475
- return state;
11476
- }
11477
- };
11478
- }
11479
- function mixEventSeed(eventSeed) {
11480
- let mixed = Math.imul((eventSeed | 0) ^ HASH_MIX, 2246822507);
11481
- mixed ^= mixed >>> 13;
11482
- mixed = Math.imul(mixed, 3266489909);
11483
- mixed ^= mixed >>> 16;
11484
- return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1;
11485
- }
11486
- function streamFor2(eventSeed) {
11487
- return lehmer(mixEventSeed(eventSeed));
11488
- }
11489
11697
  var TABLE = {
11490
11698
  concrete: { dust: 9078138, chip: 7236195, spark: 2 },
11491
11699
  metal: { dust: 0, chip: 0, spark: 18 },
@@ -12588,12 +12796,12 @@ var GameTestRoom = (() => {
12588
12796
  }
12589
12797
  function drawing(entry, action) {
12590
12798
  const context = engine.context;
12591
- const shared = context.random;
12799
+ const shared2 = context.random;
12592
12800
  if (entry.random) context.random = entry.random;
12593
12801
  try {
12594
12802
  action();
12595
12803
  } finally {
12596
- context.random = shared;
12804
+ context.random = shared2;
12597
12805
  }
12598
12806
  }
12599
12807
  return {
@@ -12789,8 +12997,8 @@ var GameTestRoom = (() => {
12789
12997
  function addScorch(store, random, x, z, radius) {
12790
12998
  const variant = Math.floor(random.next() * SCORCH_VARIANTS);
12791
12999
  const lift = 0.03 + nextLift(store) * 1e-3;
12792
- const rotation = random.next() * 6;
12793
- store.add({ kind: "scorch", x, z, radius, variant, lift, rotation });
13000
+ const rotation2 = random.next() * 6;
13001
+ store.add({ kind: "scorch", x, z, radius, variant, lift, rotation: rotation2 });
12794
13002
  }
12795
13003
  function addRimRocks(store, ground, random, x, z, radius) {
12796
13004
  const count = Math.round(6 + radius * 7);
@@ -12819,8 +13027,8 @@ var GameTestRoom = (() => {
12819
13027
  function addCrater(store, ground, random, spec) {
12820
13028
  ground?.dent(random, spec.x, spec.z, spec.radius, spec.depth);
12821
13029
  if (!spec.marks) return;
12822
- const rotation = random.next() * 6;
12823
- store.add({ kind: "dirt", x: spec.x, z: spec.z, radius: spec.radius * 1.02, lift: 0.045, rotation });
13030
+ const rotation2 = random.next() * 6;
13031
+ store.add({ kind: "dirt", x: spec.x, z: spec.z, radius: spec.radius * 1.02, lift: 0.045, rotation: rotation2 });
12824
13032
  addRimRocks(store, ground, random, spec.x, spec.z, spec.radius);
12825
13033
  }
12826
13034
  function addBloodSplat(store, ground, random, spec) {
@@ -13376,6 +13584,7 @@ var GameTestRoom = (() => {
13376
13584
  state.gas = Math.max(0, state.gas - dt / DECAY.gasSeconds);
13377
13585
  state.beat *= Math.exp(-DECAY.beat * dt);
13378
13586
  }
13587
+ var PAD_OF_SIZE2 = 0.35;
13379
13588
  var clamp4 = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
13380
13589
  var lerp4 = (a, b, t) => a + (b - a) * t;
13381
13590
  function rgb(hex) {
@@ -13535,9 +13744,20 @@ var GameTestRoom = (() => {
13535
13744
  vy2 *= k;
13536
13745
  vz2 *= k;
13537
13746
  }
13538
- const x = this.position[i3] + vx2 * dt;
13747
+ let x = this.position[i3] + vx2 * dt;
13539
13748
  let y = this.position[i3 + 1] + vy2 * dt;
13540
- const z = this.position[i3 + 2] + vz2 * dt;
13749
+ let z = this.position[i3 + 2] + vz2 * dt;
13750
+ const confiner = host.confiner;
13751
+ if (def.carried && confiner && !confiner.empty) {
13752
+ const pad2 = this.size0[i] * lerp4(1, def.grow, t) * PAD_OF_SIZE2;
13753
+ if (confiner.confine(this.position[i3], this.position[i3 + 1], this.position[i3 + 2], x, y, z, pad2, dt)) {
13754
+ const out = confiner.out;
13755
+ x = out.x;
13756
+ y = out.y;
13757
+ z = out.z;
13758
+ [vx2, vy2, vz2] = slideAlong(out, vx2, vy2, vz2);
13759
+ }
13760
+ }
13541
13761
  if (def.ground !== "none") {
13542
13762
  const own = this.floor[i];
13543
13763
  const gy = Number.isNaN(own) ? host.groundAt(x, z) + 5e-3 : own;
@@ -13623,7 +13843,8 @@ var GameTestRoom = (() => {
13623
13843
  alpha: 0.35,
13624
13844
  fadeIn: 0.15,
13625
13845
  cone: 0.6,
13626
- ground: "none"
13846
+ ground: "none",
13847
+ carried: true
13627
13848
  },
13628
13849
  sparks: {
13629
13850
  id: "sparks",
@@ -13667,7 +13888,8 @@ var GameTestRoom = (() => {
13667
13888
  alpha: 0.4,
13668
13889
  fadeIn: 0.1,
13669
13890
  cone: 0.4,
13670
- ground: "none"
13891
+ ground: "none",
13892
+ carried: true
13671
13893
  },
13672
13894
  shards: {
13673
13895
  id: "shards",
@@ -13825,7 +14047,8 @@ var GameTestRoom = (() => {
13825
14047
  fadeIn: 0.1,
13826
14048
  cone: 0.5,
13827
14049
  ground: "stop",
13828
- flutter: 0.6
14050
+ flutter: 0.6,
14051
+ carried: true
13829
14052
  },
13830
14053
  steam: {
13831
14054
  id: "steam",
@@ -13847,7 +14070,8 @@ var GameTestRoom = (() => {
13847
14070
  alpha: 0.35,
13848
14071
  fadeIn: 0.15,
13849
14072
  cone: 0.6,
13850
- ground: "none"
14073
+ ground: "none",
14074
+ carried: true
13851
14075
  },
13852
14076
  flash: {
13853
14077
  id: "flash",
@@ -13988,7 +14212,8 @@ var GameTestRoom = (() => {
13988
14212
  const host = {
13989
14213
  groundAt: this.options.groundAt,
13990
14214
  gravity: this.options.gravity,
13991
- land: (owner, x, y, z, color, tag) => this.listeners.get(owner)?.(x, y, z, color, tag)
14215
+ land: (owner, x, y, z, color, tag) => this.listeners.get(owner)?.(x, y, z, color, tag),
14216
+ ...this.options.confiner ? { confiner: this.options.confiner } : {}
13992
14217
  };
13993
14218
  if (this.additive.live > 0 || this.additive.drawCount > 0) this.additive.update(dt, host);
13994
14219
  if (this.normal.live > 0 || this.normal.drawCount > 0) this.normal.update(dt, host);
@@ -14194,8 +14419,8 @@ var GameTestRoom = (() => {
14194
14419
  * Takes away the marks inside a box: world `center`, `half` extents along the axes of the world `rotation` (a quaternion, x y z w; none means the
14195
14420
  * world axes). The surface they were on is gone (a brick came out of a wall with its bullet holes). Returns how many went.
14196
14421
  */
14197
- clearInside(center, half, rotation = [0, 0, 0, 1]) {
14198
- const qx = -rotation[0], qy = -rotation[1], qz = -rotation[2], qw = rotation[3];
14422
+ clearInside(center, half, rotation2 = [0, 0, 0, 1]) {
14423
+ const qx = -rotation2[0], qy = -rotation2[1], qz = -rotation2[2], qw = rotation2[3];
14199
14424
  let gone = 0;
14200
14425
  for (const decal2 of this.decals) {
14201
14426
  if (!decal2.on) continue;
@@ -14316,10 +14541,14 @@ var GameTestRoom = (() => {
14316
14541
  var DEFAULT_FADE_IN = 0.1;
14317
14542
  var FLOATS_PER_VECTOR = 3;
14318
14543
  var BYTE_MAX2 = 255;
14544
+ var CARRIED_GRAVITY = 3;
14545
+ var PAD_OF_SIZE3 = 0.35;
14319
14546
  var SpritePool = class {
14320
- constructor(capacity, context) {
14547
+ /** `carried`: the normal-blend pool (smoke, dust, steam), whose light sprites go round the closed spaces; the additive one (flames, sparks) does not. */
14548
+ constructor(capacity, context, carried = false) {
14321
14549
  __publicField(this, "capacity", capacity);
14322
14550
  __publicField(this, "context", context);
14551
+ __publicField(this, "carried", carried);
14323
14552
  __publicField(this, "count", 0);
14324
14553
  __publicField(this, "position");
14325
14554
  __publicField(this, "velocity");
@@ -14419,6 +14648,7 @@ var GameTestRoom = (() => {
14419
14648
  }
14420
14649
  update(dt, leaveDecal) {
14421
14650
  const { position, velocity } = this;
14651
+ const confiner = this.context.confiner;
14422
14652
  for (let index = this.count - 1; index >= 0; index--) {
14423
14653
  this.life[index] -= dt;
14424
14654
  if (this.life[index] <= 0) {
@@ -14432,9 +14662,25 @@ var GameTestRoom = (() => {
14432
14662
  velocity[offset] *= factor;
14433
14663
  velocity[offset + 1] *= factor;
14434
14664
  velocity[offset + 2] *= factor;
14665
+ const x0 = position[offset];
14666
+ const y0 = position[offset + 1];
14667
+ const z0 = position[offset + 2];
14435
14668
  position[offset] += velocity[offset] * dt;
14436
14669
  position[offset + 1] += velocity[offset + 1] * dt;
14437
14670
  position[offset + 2] += velocity[offset + 2] * dt;
14671
+ if (this.carried && confiner && !confiner.empty && Math.abs(this.gravity[index]) < CARRIED_GRAVITY) {
14672
+ const size = this.startSize[index] * (1 + (this.growth[index] - 1) * age);
14673
+ if (confiner.confine(x0, y0, z0, position[offset], position[offset + 1], position[offset + 2], size * PAD_OF_SIZE3, dt)) {
14674
+ const out = confiner.out;
14675
+ position[offset] = out.x;
14676
+ position[offset + 1] = out.y;
14677
+ position[offset + 2] = out.z;
14678
+ const [vx2, vy2, vz2] = slideAlong(out, velocity[offset], velocity[offset + 1], velocity[offset + 2]);
14679
+ velocity[offset] = vx2;
14680
+ velocity[offset + 1] = vy2;
14681
+ velocity[offset + 2] = vz2;
14682
+ }
14683
+ }
14438
14684
  const mode = this.groundMode[index];
14439
14685
  if (mode) {
14440
14686
  const groundHeight = this.context.groundAt(position[offset], position[offset + 2]);
@@ -14711,9 +14957,9 @@ var GameTestRoom = (() => {
14711
14957
  __publicField(this, "placement");
14712
14958
  __publicField(this, "accumulators", /* @__PURE__ */ new Map());
14713
14959
  const profile = options.profile ?? PROFILES.high;
14714
- this.context = { random: options.random, groundAt: options.groundAt ?? workshopGround, quality: { level: 0, mult: 1 }, trace: options.trace };
14960
+ this.context = { random: options.random, groundAt: options.groundAt ?? workshopGround, quality: { level: 0, mult: 1 }, trace: options.trace, ...options.confiner ? { confiner: options.confiner } : {} };
14715
14961
  this.add = new SpritePool(profile.sprites.add, this.context);
14716
- this.normal = new SpritePool(profile.sprites.normal, this.context);
14962
+ this.normal = new SpritePool(profile.sprites.normal, this.context, true);
14717
14963
  this.streaks = new StreakPool(profile.streaks, this.context);
14718
14964
  this.debris = new DebrisPool(this.context);
14719
14965
  this.decals = new DecalPool(this.context);
@@ -14901,12 +15147,14 @@ var GameTestRoom = (() => {
14901
15147
  const groundAt = options.groundAt ?? (ground ? (x, z) => ground.heightAt(x, z) : () => 0);
14902
15148
  const handles = { next: 0, events: 0 };
14903
15149
  const low = options.profile === "low";
15150
+ const enclosures = new Enclosures(20973 + (options.seed ?? 0));
14904
15151
  const spriteSystem = new SpriteKindSystem({
14905
15152
  capacity: options.sprites?.capacity ?? (low ? 1250 : 2500),
14906
15153
  density: options.sprites?.density ?? (low ? 0.6 : 1),
14907
15154
  seed: 2654435761 + (options.seed ?? 0),
14908
15155
  groundAt,
14909
15156
  gravity: options.gravity ?? DEFAULT_GRAVITY,
15157
+ confiner: enclosures,
14910
15158
  ...options.sprites?.kinds ? { kinds: options.sprites.kinds } : {}
14911
15159
  });
14912
15160
  const core = {
@@ -14956,7 +15204,7 @@ var GameTestRoom = (() => {
14956
15204
  return [camera2.x, camera2.y, camera2.z];
14957
15205
  }, forward: (local2) => [local2[0] ?? 0, local2[1] ?? 0, local2[2] ?? 0] };
14958
15206
  function workshop() {
14959
- return engine ?? (engine = new WorkshopEngine({ random: sharedRandom, profile, groundAt }));
15207
+ return engine ?? (engine = new WorkshopEngine({ random: sharedRandom, profile, groundAt, confiner: enclosures }));
14960
15208
  }
14961
15209
  function effectPlayer() {
14962
15210
  return player ?? (player = createEffectPlayer({ engine: workshop(), effects, view }));
@@ -15015,6 +15263,10 @@ var GameTestRoom = (() => {
15015
15263
  puff3.vz += dz * strength;
15016
15264
  }
15017
15265
  },
15266
+ enclose(id, enclosure) {
15267
+ enclosures.set(id, enclosure);
15268
+ },
15269
+ enclosures: () => enclosures,
15018
15270
  stop(handle) {
15019
15271
  player?.stop(handle);
15020
15272
  const fires = core.fires;
@@ -15042,7 +15294,7 @@ var GameTestRoom = (() => {
15042
15294
  core.clock.time += dt;
15043
15295
  core.schedule.update(core.clock.time);
15044
15296
  updateRings(core.rings, dt);
15045
- updatePuffs(core.puffs, dt, wind);
15297
+ updatePuffs(core.puffs, dt, wind, enclosures);
15046
15298
  updateSparks(core.sparks, { dt, groundAt });
15047
15299
  updateGlows(core.glows, dt);
15048
15300
  updateShocks(core.shocks, core.puffs.items, dt);
@@ -16442,6 +16694,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16442
16694
  });
16443
16695
  },
16444
16696
  stop: (handle) => world.stop(handle),
16697
+ // a module's closed space (a shut car's cabin): the world's smoke, dust and steam go round it
16698
+ enclose: (id, enclosure) => world.enclose(id, enclosure),
16445
16699
  configure(settings) {
16446
16700
  const band = settings.lod_band;
16447
16701
  if (typeof band === "string" && band in LOD_BANDS) world.forceLod(LOD_BANDS[band] ?? null);
@@ -18372,8 +18626,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18372
18626
  * Takes away the decals of a mesh that sit inside a box (world `center`, `half` extents along the axes of the world `rotation`): the surface they
18373
18627
  * marked is gone (a unit came out of the wall with its bullet holes and spalls).
18374
18628
  */
18375
- clearDecals(mv, center, half, rotation) {
18376
- const inv = rotation.clone().invert();
18629
+ clearDecals(mv, center, half, rotation2) {
18630
+ const inv = rotation2.clone().invert();
18377
18631
  const p = new this.THREE.Vector3();
18378
18632
  for (let i = mv.decals.length - 1; i >= 0; i--) {
18379
18633
  const d = mv.decals[i];
@@ -19803,8 +20057,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
19803
20057
  const T = this.THREE;
19804
20058
  const center = new T.Vector3(ev.center[0], ev.center[1], ev.center[2]);
19805
20059
  const half = new T.Vector3(ev.size[0] / 2 + CHIP_DECAL_EDGE, ev.size[1] / 2 + CHIP_DECAL_EDGE, ev.size[2] / 2 + CHIP_DECAL_EDGE);
19806
- const rotation = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
19807
- for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation);
20060
+ const rotation2 = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
20061
+ for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation2);
19808
20062
  this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
19809
20063
  this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
19810
20064
  this.sound(impactKey(def), ev.center, 0.7);
@@ -36122,11 +36376,11 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
36122
36376
  for (const key2 of Object.keys(c.streams)) endStreamKey(c, key2);
36123
36377
  containers.delete(id);
36124
36378
  },
36125
- setTransform(id, position, rotation, velocity) {
36379
+ setTransform(id, position, rotation2, velocity) {
36126
36380
  const c = containers.get(id);
36127
36381
  if (!c) return;
36128
36382
  c.position = copy3(position);
36129
- c.rotation = [...rotation];
36383
+ c.rotation = [...rotation2];
36130
36384
  if (velocity) c.velocity = copy3(velocity);
36131
36385
  refreshLevel(c);
36132
36386
  },
@@ -37048,10 +37302,10 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
37048
37302
  const hand = getHandTransform();
37049
37303
  const p = pose();
37050
37304
  const position = add2(hand.position, quatRotate(hand.rotation, p.position));
37051
- const rotation = quatMultiply(hand.rotation, quatFromEuler2(p.rotation[0], p.rotation[1], p.rotation[2]));
37305
+ const rotation2 = quatMultiply(hand.rotation, quatFromEuler2(p.rotation[0], p.rotation[1], p.rotation[2]));
37052
37306
  const velocity = lastPos && dt > 0 ? [(position[0] - lastPos[0]) / dt, (position[1] - lastPos[1]) / dt, (position[2] - lastPos[2]) / dt] : void 0;
37053
37307
  lastPos = position;
37054
- world.setTransform(containerId, position, rotation, velocity);
37308
+ world.setTransform(containerId, position, rotation2, velocity);
37055
37309
  };
37056
37310
  const player = {
37057
37311
  def,
@@ -39080,7 +39334,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
39080
39334
  const direction = new THREE.Vector3();
39081
39335
  const location = new THREE.Vector3();
39082
39336
  const scale2 = new THREE.Vector3();
39083
- const rotation = new THREE.Quaternion();
39337
+ const rotation2 = new THREE.Quaternion();
39084
39338
  const matrix = new THREE.Matrix4();
39085
39339
  return {
39086
39340
  drops,
@@ -39100,9 +39354,9 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
39100
39354
  const o = i * 3;
39101
39355
  const speed = Math.hypot(velocity[o], velocity[o + 1], velocity[o + 2]);
39102
39356
  direction.set(velocity[o], velocity[o + 1], velocity[o + 2]).normalize();
39103
- rotation.setFromUnitVectors(up, direction);
39357
+ rotation2.setFromUnitVectors(up, direction);
39104
39358
  const size = radius[i] * METRES_PER_MM;
39105
- matrix.compose(location.set(position[o], position[o + 1], position[o + 2]), rotation, scale2.set(size, size * (1 + Math.min(MAX_STRETCH, speed * STRETCH_PER_SPEED)), size));
39359
+ matrix.compose(location.set(position[o], position[o + 1], position[o + 2]), rotation2, scale2.set(size, size * (1 + Math.min(MAX_STRETCH, speed * STRETCH_PER_SPEED)), size));
39106
39360
  drops.setMatrixAt(i, matrix);
39107
39361
  drawn++;
39108
39362
  }
@@ -40280,6 +40534,81 @@ void main(){
40280
40534
  return root;
40281
40535
  }
40282
40536
 
40537
+ // ../game-shapes-three/dist/index.js
40538
+ var isTexture = (v) => !!v && typeof v === "object" && v.isTexture === true;
40539
+ function texturesOf(material, out) {
40540
+ for (const value of Object.values(material)) if (isTexture(value)) out.add(value);
40541
+ const uniforms = material.uniforms;
40542
+ if (uniforms) {
40543
+ for (const u of Object.values(uniforms)) if (isTexture(u?.value)) out.add(u.value);
40544
+ }
40545
+ }
40546
+ function collect(roots) {
40547
+ const geometries = /* @__PURE__ */ new Set();
40548
+ const materials = /* @__PURE__ */ new Set();
40549
+ const textures = /* @__PURE__ */ new Set();
40550
+ const skeletons = /* @__PURE__ */ new Set();
40551
+ for (const root of roots) {
40552
+ root.traverse((object) => {
40553
+ const o = object;
40554
+ if (o.geometry && typeof o.geometry.dispose === "function") geometries.add(o.geometry);
40555
+ if (o.skeleton && typeof o.skeleton.dispose === "function") skeletons.add(o.skeleton);
40556
+ const list = Array.isArray(o.material) ? o.material : o.material ? [o.material] : [];
40557
+ for (const m of list) {
40558
+ if (!m || typeof m.dispose !== "function") continue;
40559
+ materials.add(m);
40560
+ texturesOf(m, textures);
40561
+ }
40562
+ });
40563
+ }
40564
+ return { geometries, materials, textures, skeletons };
40565
+ }
40566
+ var marked = /* @__PURE__ */ new WeakSet();
40567
+ var shared = (r) => r.userData?.shared === true || marked.has(r);
40568
+ function releaseObject3D(roots, options = {}) {
40569
+ const list = Array.isArray(roots) ? roots : [roots];
40570
+ const report = { geometries: 0, materials: 0, textures: 0 };
40571
+ if (list.length === 0) return report;
40572
+ const found = collect(list);
40573
+ const keepRoots = options.keep === void 0 ? [] : Array.isArray(options.keep) ? options.keep : [options.keep];
40574
+ const releasing = new Set(list);
40575
+ for (const keep of keepRoots) {
40576
+ const walk = (o) => {
40577
+ if (releasing.has(o)) return;
40578
+ const owner = o;
40579
+ if (owner.geometry) found.geometries.delete(owner.geometry);
40580
+ if (owner.skeleton) found.skeletons.delete(owner.skeleton);
40581
+ const mats = Array.isArray(owner.material) ? owner.material : owner.material ? [owner.material] : [];
40582
+ for (const m of mats) {
40583
+ if (!m) continue;
40584
+ found.materials.delete(m);
40585
+ const t = /* @__PURE__ */ new Set();
40586
+ texturesOf(m, t);
40587
+ for (const x of t) found.textures.delete(x);
40588
+ }
40589
+ for (const c of o.children) walk(c);
40590
+ };
40591
+ walk(keep);
40592
+ }
40593
+ for (const g of found.geometries) {
40594
+ if (shared(g)) continue;
40595
+ g.dispose();
40596
+ report.geometries++;
40597
+ }
40598
+ for (const m of found.materials) {
40599
+ if (shared(m)) continue;
40600
+ m.dispose();
40601
+ report.materials++;
40602
+ }
40603
+ for (const t of found.textures) {
40604
+ if (shared(t)) continue;
40605
+ t.dispose();
40606
+ report.textures++;
40607
+ }
40608
+ for (const s of found.skeletons) s.dispose();
40609
+ return report;
40610
+ }
40611
+
40283
40612
  // src/segment-box.ts
40284
40613
  var segmentTouchesBox = (from, to, min2, max2) => {
40285
40614
  let near = 0;
@@ -40320,9 +40649,13 @@ void main(){
40320
40649
  const projectiles = [];
40321
40650
  const meshes = [];
40322
40651
  const up = new THREE.Vector3(0, 1, 0);
40652
+ const drop = (objects) => {
40653
+ for (const object of objects) object.removeFromParent();
40654
+ releaseObject3D(objects, { keep: scene2 });
40655
+ };
40323
40656
  const clear = () => {
40324
40657
  projectiles.length = 0;
40325
- for (const mesh2 of meshes) mesh2.removeFromParent();
40658
+ drop(meshes);
40326
40659
  meshes.length = 0;
40327
40660
  };
40328
40661
  const addMesh = (geometry, color, metalness, at3) => {
@@ -40346,13 +40679,12 @@ void main(){
40346
40679
  };
40347
40680
  const remove = (projectile) => {
40348
40681
  for (const extra of projectile.attached ?? []) {
40349
- extra.removeFromParent();
40350
40682
  const extraAt = meshes.indexOf(extra);
40351
40683
  if (extraAt >= 0) meshes.splice(extraAt, 1);
40352
40684
  }
40353
- projectile.mesh.removeFromParent();
40354
40685
  const meshAt = meshes.indexOf(projectile.mesh);
40355
40686
  if (meshAt >= 0) meshes.splice(meshAt, 1);
40687
+ drop([...projectile.attached ?? [], projectile.mesh]);
40356
40688
  const at3 = projectiles.indexOf(projectile);
40357
40689
  if (at3 >= 0) projectiles.splice(at3, 1);
40358
40690
  };
@@ -42566,7 +42898,7 @@ void main(){
42566
42898
  var NEAR_OPEN_LEAF = 0.9;
42567
42899
  var BODY_HEIGHT = 1.8;
42568
42900
  var HAND_REST = [0.22, -0.42, -0.32];
42569
- var SKIN = 0.01;
42901
+ var SKIN2 = 0.01;
42570
42902
  var HANDLE_INSET = 0.06;
42571
42903
  var STANDOFF2 = 0.55;
42572
42904
  var DRAWER_TRAVEL = 0.3;
@@ -42710,7 +43042,7 @@ void main(){
42710
43042
  const o = outs[0];
42711
43043
  return [o[1] * (o[0] + PLAYER_RADIUS), o[2] * (o[0] + PLAYER_RADIUS)];
42712
43044
  }
42713
- const push = PLAYER_RADIUS - d + SKIN;
43045
+ const push = PLAYER_RADIUS - d + SKIN2;
42714
43046
  return [dx / d * push, dz / d * push];
42715
43047
  };
42716
43048
  const isFree = (u, from, to) => {
@@ -42720,7 +43052,7 @@ void main(){
42720
43052
  options.pose(objectId, leaf2.part, leaf2.clip, to);
42721
43053
  obj.updateMatrixWorld(true);
42722
43054
  box2.setFromObject(obj);
42723
- box2.expandByScalar(-SKIN);
43055
+ box2.expandByScalar(-SKIN2);
42724
43056
  let by = null;
42725
43057
  if (Math.max(Math.abs(box2.min.x), Math.abs(box2.max.x), Math.abs(box2.min.z), Math.abs(box2.max.z)) > arena.limit) by = "wall";
42726
43058
  else
@@ -42738,7 +43070,7 @@ void main(){
42738
43070
  const dir = [tmp.x - centre.x, tmp.y - centre.y, tmp.z - centre.z];
42739
43071
  const len = Math.hypot(dir[0], dir[1], dir[2]);
42740
43072
  if (len > 1e-5) {
42741
- const hit = world.sweepBox({ min: [before.min.x + SKIN, before.min.y + FLOOR_GAP, before.min.z + SKIN], max: [before.max.x - SKIN, before.max.y - SKIN, before.max.z - SKIN] }, dir, len * 2, { ignore: objectId, spacing: 0.12 });
43073
+ const hit = world.sweepBox({ min: [before.min.x + SKIN2, before.min.y + FLOOR_GAP, before.min.z + SKIN2], max: [before.max.x - SKIN2, before.max.y - SKIN2, before.max.z - SKIN2] }, dir, len * 2, { ignore: objectId, spacing: 0.12 });
42742
43074
  if (hit && hit.normal[1] < 0.7) by = hit.object;
42743
43075
  }
42744
43076
  }
@@ -43026,6 +43358,7 @@ void main(){
43026
43358
  debris: "chips",
43027
43359
  color: [0.05, 0.05, 0.055],
43028
43360
  pattern: "asphalt",
43361
+ crack: { reach: 0.6, width: 8e-3, thresholdJoules: 800 },
43029
43362
  hits: {
43030
43363
  ballistic: { specificEnergy: 3e7, aspect: 0.2, minRadius: 0.03 },
43031
43364
  blunt: { specificEnergy: 1e8, aspect: 0.1, thresholdJoules: 400 },
@@ -43039,6 +43372,8 @@ void main(){
43039
43372
  debris: "chips",
43040
43373
  color: [0.33, 0.33, 0.31],
43041
43374
  pattern: "concrete",
43375
+ crack: { reach: 1, width: 6e-3, thresholdJoules: 300 },
43376
+ breach: { joulesPerCubedThickness: 24e8, radiusOverThickness: 1.5 },
43042
43377
  hits: {
43043
43378
  ballistic: { specificEnergy: 15e7, aspect: 0.14, minRadius: 0.018 },
43044
43379
  blunt: { specificEnergy: 3e8, aspect: 0.08, thresholdJoules: 800 },
@@ -43065,6 +43400,7 @@ void main(){
43065
43400
  debris: "shards",
43066
43401
  color: [0.72, 0.7, 0.66],
43067
43402
  pattern: "tile",
43403
+ crack: { reach: 0.5, width: 2e-3, thresholdJoules: 40 },
43068
43404
  hits: {
43069
43405
  ballistic: { specificEnergy: 8e6, aspect: 0.1, minRadius: 0.05 },
43070
43406
  blunt: { specificEnergy: 1e7, aspect: 0.08, thresholdJoules: 80, minRadius: 0.06 },
@@ -43091,6 +43427,7 @@ void main(){
43091
43427
  debris: "chips",
43092
43428
  color: [0.23, 0.22, 0.2],
43093
43429
  pattern: "stone",
43430
+ crack: { reach: 0.5, width: 4e-3, thresholdJoules: 1e3 },
43094
43431
  hits: {
43095
43432
  ballistic: { specificEnergy: 25e7, aspect: 0.12, minRadius: 0.015 },
43096
43433
  blunt: { specificEnergy: 5e8, aspect: 0.08, thresholdJoules: 1500 },
@@ -43113,6 +43450,91 @@ void main(){
43113
43450
  ];
43114
43451
  var DEFAULT_BAR_YIELD_JOULES = 24e5;
43115
43452
  var DEFAULT_PROFILE = "concrete_slab";
43453
+ function createRandom(seed) {
43454
+ let state = seed | 0;
43455
+ return () => {
43456
+ state = state + 1831565813 | 0;
43457
+ let mixed = Math.imul(state ^ state >>> 15, 1 | state);
43458
+ mixed = mixed + Math.imul(mixed ^ mixed >>> 7, 61 | mixed) ^ mixed;
43459
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
43460
+ };
43461
+ }
43462
+ var CRACK_REFERENCE_JOULES = 1e6;
43463
+ var MERGE_SHARE = 0.35;
43464
+ var HAIRLINE = 15e-4;
43465
+ var MIN_RAYS = 4;
43466
+ var MAX_RAYS = 11;
43467
+ var SEGMENTS2 = 4;
43468
+ var BRANCH_CHANCE = 0.45;
43469
+ function createCrackStore() {
43470
+ const list = [];
43471
+ let version = 0;
43472
+ let nextId = 1;
43473
+ let born = 0;
43474
+ return {
43475
+ list,
43476
+ get version() {
43477
+ return version;
43478
+ },
43479
+ add(spec, max2) {
43480
+ version++;
43481
+ const near = list.find((crack2) => Math.hypot(crack2.x - spec.x, crack2.z - spec.z) < crack2.reach * MERGE_SHARE);
43482
+ if (near) {
43483
+ near.reach = Math.max(near.reach, spec.reach) + Math.min(near.reach, spec.reach) * 0.25;
43484
+ near.width = Math.max(near.width, spec.width) * 1.1;
43485
+ near.rays = Math.min(MAX_RAYS, near.rays + 1);
43486
+ return near;
43487
+ }
43488
+ if (list.length >= max2) list.shift();
43489
+ const rays = MIN_RAYS + Math.floor(createRandom(spec.seed)() * (MAX_RAYS - MIN_RAYS - 2));
43490
+ const crack = { id: nextId++, x: spec.x, z: spec.z, reach: spec.reach, width: Math.max(HAIRLINE, spec.width), rays, seed: spec.seed, material: spec.material, born: born++ };
43491
+ list.push(crack);
43492
+ return crack;
43493
+ },
43494
+ clear() {
43495
+ list.length = 0;
43496
+ version++;
43497
+ },
43498
+ load(cracks) {
43499
+ list.length = 0;
43500
+ for (const crack of cracks) {
43501
+ list.push({ ...crack });
43502
+ nextId = Math.max(nextId, crack.id + 1);
43503
+ born = Math.max(born, crack.born + 1);
43504
+ }
43505
+ version++;
43506
+ }
43507
+ };
43508
+ }
43509
+ function crackLines(crack) {
43510
+ const random = createRandom(crack.seed);
43511
+ const segments = [];
43512
+ const widthAt = (distance4) => Math.max(HAIRLINE, crack.width * (1 - distance4 / crack.reach));
43513
+ for (let ray = 0; ray < crack.rays; ray++) {
43514
+ let angle = ray / crack.rays * Math.PI * 2 + (random() - 0.5) * 0.6;
43515
+ const length3 = crack.reach * (0.45 + 0.55 * random());
43516
+ let x = crack.x;
43517
+ let z = crack.z;
43518
+ let run = 0;
43519
+ for (let segment = 0; segment < SEGMENTS2; segment++) {
43520
+ const step = length3 / SEGMENTS2 * (0.75 + 0.5 * random());
43521
+ angle += (random() - 0.5) * 0.5;
43522
+ const nx = x + Math.cos(angle) * step;
43523
+ const nz = z + Math.sin(angle) * step;
43524
+ segments.push({ x0: x, z0: z, x1: nx, z1: nz, w0: widthAt(run), w1: widthAt(run + step) });
43525
+ if (segment > 0 && random() < BRANCH_CHANCE) {
43526
+ const side = angle + (random() < 0.5 ? -1 : 1) * (0.45 + random() * 0.5);
43527
+ const branch = step * (0.5 + 0.6 * random());
43528
+ const w = widthAt(run + step) * 0.6;
43529
+ segments.push({ x0: nx, z0: nz, x1: nx + Math.cos(side) * branch, z1: nz + Math.sin(side) * branch, w0: Math.max(HAIRLINE, w), w1: HAIRLINE });
43530
+ }
43531
+ run += step;
43532
+ x = nx;
43533
+ z = nz;
43534
+ }
43535
+ }
43536
+ return segments;
43537
+ }
43116
43538
  var REST_SPEED2 = 0.6;
43117
43539
  var RESTITUTION2 = 0.25;
43118
43540
  var FRICTION = 0.55;
@@ -43290,15 +43712,6 @@ void main(){
43290
43712
  }
43291
43713
  };
43292
43714
  }
43293
- function createRandom(seed) {
43294
- let state = seed | 0;
43295
- return () => {
43296
- state = state + 1831565813 | 0;
43297
- let mixed = Math.imul(state ^ state >>> 15, 1 | state);
43298
- mixed = mixed + Math.imul(mixed ^ mixed >>> 7, 61 | mixed) ^ mixed;
43299
- return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
43300
- };
43301
- }
43302
43715
  var DEFAULT_TERRAIN_BUDGET = {
43303
43716
  markAreaSize: 2,
43304
43717
  maxMarksPerArea: 24,
@@ -43307,7 +43720,8 @@ void main(){
43307
43720
  maxDebris: 400,
43308
43721
  maxDebrisPerBlow: 36,
43309
43722
  maxSlideCellsPerFrame: 2500,
43310
- maxBurningCells: 300
43723
+ maxBurningCells: 300,
43724
+ maxCracks: 120
43311
43725
  };
43312
43726
  var GRAVITY5 = 9.81;
43313
43727
  var FINE_CELLS = 1.5;
@@ -43384,6 +43798,7 @@ void main(){
43384
43798
  const mixed = new Uint8Array(chunksX * chunksZ);
43385
43799
  const dirty = /* @__PURE__ */ new Set();
43386
43800
  const marks = createMarkStore2(budget);
43801
+ const cracks = createCrackStore();
43387
43802
  const debrisStore = createDebrisStore(() => budget.maxDebris);
43388
43803
  const sliding = /* @__PURE__ */ new Set();
43389
43804
  const burning = /* @__PURE__ */ new Map();
@@ -43560,6 +43975,7 @@ void main(){
43560
43975
  let depth = start.depth;
43561
43976
  let radius = 0;
43562
43977
  let carved = 0;
43978
+ let breached = false;
43563
43979
  const firstMaterial = materials.get(start.material);
43564
43980
  if (start.loose <= 0) {
43565
43981
  for (; ; ) {
@@ -43571,6 +43987,19 @@ void main(){
43571
43987
  const response = material.hits[blow3];
43572
43988
  if (!response || energy < (response.thresholdJoules ?? 0)) break;
43573
43989
  const bars = layer2.stratum.reinforcement;
43990
+ const breach = blow3 === "explosive" ? material.breach : void 0;
43991
+ const breachJoules = breach ? breach.joulesPerCubedThickness * layer2.stratum.thickness ** 3 : Infinity;
43992
+ if (breach && depth <= layer2.top + 1e-6 && energy >= breachJoules) {
43993
+ const takenRadius2 = layer2.stratum.thickness * breach.radiusOverThickness * Math.cbrt(energy / breachJoules);
43994
+ const takenVolume2 = Math.PI * takenRadius2 * takenRadius2 * room / 2;
43995
+ energy -= breachJoules;
43996
+ depth += room;
43997
+ carved += room;
43998
+ breached = true;
43999
+ radius = Math.max(radius, takenRadius2);
44000
+ removed.push({ material: material.id, volume: takenVolume2 });
44001
+ continue;
44002
+ }
43574
44003
  const pastCover = bars !== void 0 && depth - layer2.top >= bars.cover - 1e-6;
43575
44004
  const specificEnergy = response.specificEnergy * (pastCover ? bars.resistance : 1);
43576
44005
  const limit = bars && !pastCover ? Math.min(room, layer2.top + bars.cover - depth) : room;
@@ -43598,8 +44027,10 @@ void main(){
43598
44027
  removed.push({ material: firstMaterial.id, volume });
43599
44028
  }
43600
44029
  }
43601
- const collapsed = loadBars(blow3, spec.energy, x, z, profile);
43602
- if (radius <= 0) return { point, radius: 0, depth: 0, totalDepth: columnAt(x, z).depth, removed: [], exposed: start.material, atLimit: start.remaining <= 1e-6, rebarExposed: rebarShowing(profile, start.depth), collapsed, fine: true };
44030
+ let collapsed = loadBars(blow3, spec.energy, x, z, profile);
44031
+ const crack = crackAround(blow3, spec.energy, x, z, start.loose > 0 ? void 0 : firstMaterial);
44032
+ const extra = { ...crack ? { crack } : {}, ...breached ? { breached } : {} };
44033
+ if (radius <= 0) return { point, radius: 0, depth: 0, totalDepth: columnAt(x, z).depth, removed: [], exposed: start.material, atLimit: start.remaining <= 1e-6, rebarExposed: rebarShowing(profile, start.depth), collapsed, fine: true, ...extra };
43603
44034
  if (firstMaterial.reposeDegrees === void 0 && start.loose <= 0) radius *= 1 + RIGID_WIDENING * (start.depth / maxDepthOfProfile(profile));
43604
44035
  const volumeTotal = removed.reduce((sum2, entry) => sum2 + entry.volume, 0);
43605
44036
  const fine = radius < cellSize * FINE_CELLS;
@@ -43617,8 +44048,16 @@ void main(){
43617
44048
  throwPieces({ x, y: point[1], z, volume: thrown, material, energy: spec.energy * (entry.volume / volumeTotal), direction: spec.direction, quota: share, bowlRadius: radius, blow: blow3 });
43618
44049
  quota -= share;
43619
44050
  }
44051
+ if (breached) collapsed = loadBars(blow3, spec.energy, x, z, profile) || collapsed;
43620
44052
  const after2 = columnAt(x, z);
43621
- return { point, radius, depth: carved, totalDepth: after2.depth, removed, exposed: after2.material, atLimit: after2.remaining <= 1e-4, rebarExposed: rebarShowing(profile, after2.depth), collapsed, fine };
44053
+ return { point, radius, depth: carved, totalDepth: after2.depth, removed, exposed: after2.material, atLimit: after2.remaining <= 1e-4, rebarExposed: rebarShowing(profile, after2.depth), collapsed, fine, ...extra };
44054
+ }
44055
+ function crackAround(blow3, energy, x, z, material) {
44056
+ const response = material?.crack;
44057
+ if (!material || !response || energy < response.thresholdJoules) return void 0;
44058
+ if (blow3 === "slash") return void 0;
44059
+ const scale2 = Math.cbrt(energy / CRACK_REFERENCE_JOULES);
44060
+ return cracks.add({ x, z, reach: response.reach * scale2, width: response.width * scale2, material: material.id, seed: Math.floor(random() * 2 ** 31) }, budget.maxCracks);
43622
44061
  }
43623
44062
  function loadBars(blow3, energy, x, z, profile) {
43624
44063
  const bars = profile.strata.find((stratum) => stratum.reinforcement);
@@ -43859,6 +44298,7 @@ void main(){
43859
44298
  barBend.fill(0);
43860
44299
  touched.fill(0);
43861
44300
  marks.clear();
44301
+ cracks.clear();
43862
44302
  debrisStore.clear();
43863
44303
  sliding.clear();
43864
44304
  burning.clear();
@@ -43910,6 +44350,9 @@ void main(){
43910
44350
  updateSlides();
43911
44351
  updateFire(dt);
43912
44352
  },
44353
+ get dirtyChunkCount() {
44354
+ return dirty.size;
44355
+ },
43913
44356
  drainDirtyChunks(max2 = budget.maxDirtyChunksPerFrame) {
43914
44357
  const taken = [];
43915
44358
  for (const index of dirty) {
@@ -43921,6 +44364,10 @@ void main(){
43921
44364
  },
43922
44365
  chunk,
43923
44366
  marks: () => marks.list,
44367
+ cracks: () => cracks.list,
44368
+ get cracksVersion() {
44369
+ return cracks.version;
44370
+ },
43924
44371
  debris: () => debrisStore.list,
43925
44372
  burningPoints: () => [...burning.keys()].map((key2) => {
43926
44373
  const cellRow = Math.floor(key2 / 65536);
@@ -43933,7 +44380,7 @@ void main(){
43933
44380
  stats: () => ({ marks: marks.list.length, debris: debrisStore.list.length, debrisMoving: debrisStore.list.filter((piece) => !piece.atRest).length, dirtyChunks: dirty.size, burning: burning.size }),
43934
44381
  reset,
43935
44382
  serialize() {
43936
- return { layout: currentLayout, dug: sparse(dug), loose: sparse(loose), scorch: sparse(scorch), barBend: sparse(barBend), marks: marks.list.map((mark) => ({ ...mark })) };
44383
+ return { layout: currentLayout, dug: sparse(dug), loose: sparse(loose), scorch: sparse(scorch), barBend: sparse(barBend), marks: marks.list.map((mark) => ({ ...mark })), cracks: cracks.list.map((crack) => ({ ...crack })) };
43937
44384
  },
43938
44385
  load(snapshot) {
43939
44386
  currentLayout = snapshot.layout;
@@ -43943,6 +44390,7 @@ void main(){
43943
44390
  fillSparse(scorch, snapshot.scorch);
43944
44391
  fillSparse(barBend, snapshot.barBend ?? []);
43945
44392
  marks.load(snapshot.marks);
44393
+ cracks.load(snapshot.cracks ?? []);
43946
44394
  for (let chunkIndex = 0; chunkIndex < touched.length; chunkIndex++) markChunk(chunkIndex);
43947
44395
  }
43948
44396
  };
@@ -44287,7 +44735,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
44287
44735
  mesh2.renderOrder = 1;
44288
44736
  mesh2.setColorAt(0, new THREE.Color(1, 1, 1));
44289
44737
  const matrix = new THREE.Matrix4();
44290
- const rotation = new THREE.Matrix4();
44738
+ const rotation2 = new THREE.Matrix4();
44291
44739
  const scale2 = new THREE.Matrix4();
44292
44740
  const colour = new THREE.Color();
44293
44741
  let seen = "";
@@ -44308,9 +44756,9 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
44308
44756
  for (const mark of marks) {
44309
44757
  if (Math.hypot(mark.x - camera2.x, mark.z - camera2.z) > drawDistance) continue;
44310
44758
  const size = mark.radius * 2 * MARK_REACH;
44311
- rotation.makeRotationY(mark.id * 2.399);
44759
+ rotation2.makeRotationY(mark.id * 2.399);
44312
44760
  scale2.makeScale(size, 1, size);
44313
- matrix.multiplyMatrices(rotation, scale2);
44761
+ matrix.multiplyMatrices(rotation2, scale2);
44314
44762
  matrix.setPosition(mark.x, terrain.heightAt(mark.x, mark.z) + MARK_LIFT, mark.z);
44315
44763
  mesh2.setMatrixAt(count, matrix);
44316
44764
  const [r, g, b] = terrain.material(mark.material).color;
@@ -44374,6 +44822,92 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
44374
44822
  }
44375
44823
  };
44376
44824
  }
44825
+ var CRACK_LIFT = 4e-3;
44826
+ var MAX_CRACK_QUADS = 8192;
44827
+ var CRACK_SHADE = 0.22;
44828
+ var DARK_SURFACE = 0.1;
44829
+ var DUST_LIFT = 2;
44830
+ var DUST_GREY = 0.12;
44831
+ function createCrackLayer(THREE, terrain) {
44832
+ const positions = new Float32Array(MAX_CRACK_QUADS * 4 * 3);
44833
+ const colours = new Float32Array(MAX_CRACK_QUADS * 4 * 3);
44834
+ const indices = new Uint32Array(MAX_CRACK_QUADS * 6);
44835
+ for (let quad = 0; quad < MAX_CRACK_QUADS; quad++) {
44836
+ const first = quad * 4;
44837
+ indices.set([first, first + 2, first + 1, first + 1, first + 2, first + 3], quad * 6);
44838
+ }
44839
+ const geometry = new THREE.BufferGeometry();
44840
+ const position = new THREE.BufferAttribute(positions, 3);
44841
+ const colour = new THREE.BufferAttribute(colours, 3);
44842
+ position.setUsage(THREE.DynamicDrawUsage);
44843
+ colour.setUsage(THREE.DynamicDrawUsage);
44844
+ geometry.setAttribute("position", position);
44845
+ geometry.setAttribute("color", colour);
44846
+ geometry.setIndex(new THREE.BufferAttribute(indices, 1));
44847
+ geometry.setDrawRange(0, 0);
44848
+ const material = new THREE.MeshBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.9, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
44849
+ const mesh2 = new THREE.Mesh(geometry, material);
44850
+ mesh2.name = "terrain-cracks";
44851
+ mesh2.frustumCulled = false;
44852
+ mesh2.renderOrder = 2;
44853
+ let seen = -1;
44854
+ let groundMoved = false;
44855
+ const stopListening = terrain.onDeform(() => {
44856
+ groundMoved = true;
44857
+ });
44858
+ const lift = (x, z) => terrain.heightAt(x, z) + CRACK_LIFT;
44859
+ return {
44860
+ mesh: mesh2,
44861
+ sync() {
44862
+ const cracks = terrain.cracks();
44863
+ if (seen === terrain.cracksVersion && !(groundMoved && cracks.length > 0)) return;
44864
+ seen = terrain.cracksVersion;
44865
+ groundMoved = false;
44866
+ let quad = 0;
44867
+ for (let n = cracks.length - 1; n >= 0 && quad < MAX_CRACK_QUADS; n--) {
44868
+ const crack = cracks[n];
44869
+ const tone = terrain.material(crack.material).color;
44870
+ const dark = tone[0] * 0.3 + tone[1] * 0.59 + tone[2] * 0.11 < DARK_SURFACE;
44871
+ const k = dark ? DUST_LIFT : CRACK_SHADE;
44872
+ const r = dark ? Math.min(1, tone[0] * k + DUST_GREY) : tone[0] * k;
44873
+ const g = dark ? Math.min(1, tone[1] * k + DUST_GREY) : tone[1] * k;
44874
+ const b = dark ? Math.min(1, tone[2] * k + DUST_GREY) : tone[2] * k;
44875
+ for (const s of crackLines(crack)) {
44876
+ if (quad >= MAX_CRACK_QUADS) break;
44877
+ if (!terrain.contains(s.x0, s.z0) || !terrain.contains(s.x1, s.z1)) continue;
44878
+ const length3 = Math.hypot(s.x1 - s.x0, s.z1 - s.z0);
44879
+ if (length3 < 1e-5) continue;
44880
+ const ax = -(s.z1 - s.z0) / length3;
44881
+ const az = (s.x1 - s.x0) / length3;
44882
+ const corners = [
44883
+ [s.x0 + ax * s.w0 * 0.5, s.z0 + az * s.w0 * 0.5],
44884
+ [s.x0 - ax * s.w0 * 0.5, s.z0 - az * s.w0 * 0.5],
44885
+ [s.x1 + ax * s.w1 * 0.5, s.z1 + az * s.w1 * 0.5],
44886
+ [s.x1 - ax * s.w1 * 0.5, s.z1 - az * s.w1 * 0.5]
44887
+ ];
44888
+ corners.forEach(([x, z], corner) => {
44889
+ const at3 = (quad * 4 + corner) * 3;
44890
+ positions[at3] = x;
44891
+ positions[at3 + 1] = lift(x, z);
44892
+ positions[at3 + 2] = z;
44893
+ colours[at3] = r;
44894
+ colours[at3 + 1] = g;
44895
+ colours[at3 + 2] = b;
44896
+ });
44897
+ quad++;
44898
+ }
44899
+ }
44900
+ position.needsUpdate = true;
44901
+ colour.needsUpdate = true;
44902
+ geometry.setDrawRange(0, quad * 6);
44903
+ },
44904
+ dispose() {
44905
+ stopListening();
44906
+ geometry.dispose();
44907
+ material.dispose();
44908
+ }
44909
+ };
44910
+ }
44377
44911
  var BAR_RADIUS = 6e-3;
44378
44912
  var EXPOSED_MARGIN = 2e-3;
44379
44913
  var MAX_SAG = 0.07;
@@ -44488,7 +45022,8 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
44488
45022
  const drawn = /* @__PURE__ */ new Map();
44489
45023
  const marks = createMarkLayer2(THREE, terrain, options.markDistance ?? DEFAULT_MARK_DISTANCE);
44490
45024
  const debris2 = createDebrisLayer(THREE, terrain);
44491
- group.add(marks.mesh, debris2.mesh);
45025
+ const cracks = createCrackLayer(THREE, terrain);
45026
+ group.add(marks.mesh, cracks.mesh, debris2.mesh);
44492
45027
  let pristineStale = true;
44493
45028
  let pristineBuilt = false;
44494
45029
  let pristineCount = 0;
@@ -44574,7 +45109,8 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
44574
45109
  return {
44575
45110
  group,
44576
45111
  sync(camera2) {
44577
- const limit = firstSync ? Infinity : void 0;
45112
+ const laidAnew = terrain.dirtyChunkCount >= terrain.chunksX * terrain.chunksZ;
45113
+ const limit = firstSync || laidAnew ? Infinity : void 0;
44578
45114
  firstSync = false;
44579
45115
  for (const chunk of terrain.drainDirtyChunks(limit)) {
44580
45116
  if (chunk.pristine) {
@@ -44595,9 +45131,10 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
44595
45131
  }
44596
45132
  }
44597
45133
  marks.sync(camera2);
45134
+ cracks.sync();
44598
45135
  debris2.sync();
44599
45136
  },
44600
- stats: () => ({ chunkMeshes: drawn.size, pristineChunks: pristineCount, marksDrawn: marks.mesh.count, debrisDrawn: debris2.mesh.count }),
45137
+ stats: () => ({ chunkMeshes: drawn.size, pristineChunks: pristineCount, marksDrawn: marks.mesh.count, debrisDrawn: debris2.mesh.count, crackQuads: cracks.mesh.geometry.drawRange.count / 6 }),
44601
45138
  dispose() {
44602
45139
  for (const index of [...drawn.keys()]) drop(index);
44603
45140
  for (const geometry of spareGeometries) geometry.dispose();
@@ -44605,11 +45142,19 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
44605
45142
  surface.dispose();
44606
45143
  rebarMaterial.dispose();
44607
45144
  marks.dispose();
45145
+ cracks.dispose();
44608
45146
  debris2.dispose();
44609
45147
  group.removeFromParent();
44610
45148
  }
44611
45149
  };
44612
45150
  }
45151
+ var PROFILE_NAMES = {
45152
+ en: { lawn: "Lawn", dirt: "Dirt", sand: "Sand", road: "Asphalt road", concrete_slab: "Concrete slab", suspended_slab: "Slab over a void", tile_floor: "Tile floor", wood_floor: "Wood floor", stone_paving: "Stone paving" },
45153
+ "pt-BR": { lawn: "Grama", dirt: "Terra", sand: "Areia", road: "Asfalto", concrete_slab: "Laje de concreto", suspended_slab: "Laje sobre v\xE3o", tile_floor: "Piso cer\xE2mico", wood_floor: "Assoalho", stone_paving: "Cal\xE7amento de pedra" }
45154
+ };
45155
+ function terrainProfileName(profileId, localeId) {
45156
+ return (PROFILE_NAMES[localeId] ?? PROFILE_NAMES.en)[profileId] ?? profileId;
45157
+ }
44613
45158
  var TERRAIN_STRIPS = ["dirt", "lawn", "sand", "road", "concrete_slab", "suspended_slab", "wood_floor", "tile_floor", "stone_paving"];
44614
45159
  var STRIP_WIDTH = 3;
44615
45160
  var STRIP_Z0 = 4;
@@ -44992,6 +45537,8 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
44992
45537
  "ins.damage": "Marks",
44993
45538
  "ins.damage_counts": "{dents}, {holes}, {cracks}",
44994
45539
  "ins.mass": "Mass",
45540
+ "ins.dug": "Dug",
45541
+ "ins.dig_limit": "Ground left to dig",
44995
45542
  "ins.load": "Load",
44996
45543
  "ins.stress": "Stress",
44997
45544
  "ins.joints": "Joints",
@@ -45340,6 +45887,8 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
45340
45887
  "ins.damage": "Marcas",
45341
45888
  "ins.damage_counts": "{dents}, {holes}, {cracks}",
45342
45889
  "ins.mass": "Massa",
45890
+ "ins.dug": "Escavado",
45891
+ "ins.dig_limit": "Ch\xE3o at\xE9 o limite",
45343
45892
  "ins.load": "Carga",
45344
45893
  "ins.stress": "Tens\xE3o",
45345
45894
  "ins.joints": "Juntas",
@@ -46227,6 +46776,15 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
46227
46776
  insp.appendChild(mk3("div", void 0, t("ui.aim_at_something")));
46228
46777
  return;
46229
46778
  }
46779
+ if (i.ground) {
46780
+ const g = i.ground;
46781
+ insp.appendChild(mk3("div", "t", `${i.object} \u203A ${g.surface}`));
46782
+ insp.appendChild(row(t("ins.material"), g.material));
46783
+ insp.appendChild(row(t("ins.dug"), `${num(g.dug * 100, 0)} cm`));
46784
+ insp.appendChild(row(t("ins.dig_limit"), `${num(g.remaining, 2)} m`));
46785
+ if (g.scorch > 0.01) insp.appendChild(row(t("ins.char"), `${num(g.scorch * 100, 0)} %`));
46786
+ return;
46787
+ }
46230
46788
  if (i.thumbnail) insp.appendChild(thumb(i.thumbnail, i.thumbnailCredit));
46231
46789
  insp.appendChild(mk3("div", "t", `${i.object} \u203A ${i.part}`));
46232
46790
  insp.appendChild(row(t("ins.material"), i.material));
@@ -47207,6 +47765,23 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
47207
47765
  }
47208
47766
  return m;
47209
47767
  };
47768
+ let releaseBatch = null;
47769
+ const dropTree = (root) => {
47770
+ if (!root) return;
47771
+ if (releaseBatch) releaseBatch.push(root);
47772
+ else releaseObject3D(root, { keep: scene2 });
47773
+ };
47774
+ const releasingTogether = (fn) => {
47775
+ if (releaseBatch) return fn();
47776
+ releaseBatch = [];
47777
+ try {
47778
+ fn();
47779
+ } finally {
47780
+ const roots = releaseBatch;
47781
+ releaseBatch = null;
47782
+ releaseObject3D(roots, { keep: scene2 });
47783
+ }
47784
+ };
47210
47785
  const removeEntry = (id) => {
47211
47786
  const e = entries.get(id);
47212
47787
  if (!e) return;
@@ -47223,7 +47798,10 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
47223
47798
  e.follower?.dispose?.();
47224
47799
  if (world.has(id)) world.remove(id);
47225
47800
  e.so.root.removeFromParent();
47801
+ const inner = wrapped.get(e.so.root);
47226
47802
  letGo(e.so.root);
47803
+ dropTree(e.so.root);
47804
+ dropTree(inner);
47227
47805
  entries.delete(id);
47228
47806
  doors.forget(id);
47229
47807
  rendererPieces.delete(id);
@@ -47239,12 +47817,14 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
47239
47817
  }
47240
47818
  if (world.has(id)) world.remove(id);
47241
47819
  p?.root.removeFromParent();
47820
+ dropTree(p?.root);
47242
47821
  rendererPieces.delete(id);
47243
47822
  splitOf.delete(id);
47244
47823
  }
47245
47824
  loose.length = 0;
47246
47825
  };
47247
- const clearEverything = () => {
47826
+ const clearEverything = () => releasingTogether(clearEverythingNow);
47827
+ const clearEverythingNow = () => {
47248
47828
  if (heldEntry || heldPiece) releaseHeld();
47249
47829
  useItemPlayer = null;
47250
47830
  playing.length = 0;
@@ -47293,8 +47873,10 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
47293
47873
  seating.release();
47294
47874
  pushByKey = false;
47295
47875
  push.end();
47296
- clearEverything();
47297
- for (const id of [...entries.keys()]) removeEntry(id);
47876
+ releasingTogether(() => {
47877
+ clearEverythingNow();
47878
+ for (const id of [...entries.keys()]) removeEntry(id);
47879
+ });
47298
47880
  renderer.reset();
47299
47881
  for (const listener of [...resetListeners]) listener();
47300
47882
  tools.clear();
@@ -47447,11 +48029,11 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
47447
48029
  };
47448
48030
  const reachAvatar = () => {
47449
48031
  const doorHand = doors.handTargets();
47450
- const targets = push.active ? null : heldPiece ? twistHand : handSource?.() ?? null;
48032
+ const targets = push.active ? null : doorHand ?? (heldPiece ? twistHand : handSource?.() ?? null);
47451
48033
  lastHand = {
47452
48034
  stance: push.active || doorHand ? "hold" : handStances[toolId] ?? (targets ? "hold" : "relaxed"),
47453
48035
  triggerFinger: targets?.fingers?.index ?? null,
47454
- targets: push.handTargets() ?? doorHand ?? targets
48036
+ targets: push.handTargets() ?? targets
47455
48037
  };
47456
48038
  if (!avatar?.reach) return;
47457
48039
  avatar.reach(targets, { camera: camera2, keepOutside: { contact: (origin, direction, maxDistance) => rayWorld(origin, direction, maxDistance)?.distance ?? null } });
@@ -48561,9 +49143,36 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
48561
49143
  if (!leaf2) return void 0;
48562
49144
  return t(leaf2.locked ? "hud.door_hint_locked" : leaf2.open ? "hud.door_hint_close" : "hud.door_hint_open");
48563
49145
  };
49146
+ const groundInspect = (r) => {
49147
+ const onFloor = terrain.raycast(aimO, aimD, r.distance + 3);
49148
+ const [x, , z] = onFloor?.point ?? r.point;
49149
+ const column = terrain.columnAt(x, z);
49150
+ const materialName = S.text[`material.${column.material}`] ?? dStrings.materialNames[column.material] ?? column.material;
49151
+ return {
49152
+ object: t("ground.name"),
49153
+ objectId: GROUND_ID2,
49154
+ part: column.profile,
49155
+ material: materialName,
49156
+ status: "",
49157
+ hp: 0,
49158
+ maxHp: 0,
49159
+ temperature: 0,
49160
+ burning: false,
49161
+ char: column.scorch,
49162
+ melt: 0,
49163
+ dents: 0,
49164
+ holes: 0,
49165
+ cracks: 0,
49166
+ mass: 0,
49167
+ joints: [],
49168
+ distance: onFloor?.distance ?? r.distance,
49169
+ ground: { surface: terrainProfileName(column.profile, locale), material: materialName, dug: column.depth, remaining: column.remaining, scorch: column.scorch }
49170
+ };
49171
+ };
48564
49172
  const inspectInfo = () => {
48565
49173
  const r = inspectTarget();
48566
49174
  if (!r) return null;
49175
+ if (r.object === GROUND_ID2) return groundInspect(r);
48567
49176
  const st = world.state(r.object);
48568
49177
  const p = world.part(r.object, r.part);
48569
49178
  if (!st || !p) return null;