@adatechnology/game-destruction-three 0.15.4 → 0.17.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.
@@ -55,6 +55,7 @@ var GameDestructionThree = (() => {
55
55
  heatColor: () => heatColor,
56
56
  impactFamily: () => impactFamily,
57
57
  impactKey: () => impactKey,
58
+ impactSoundIds: () => impactSoundIds,
58
59
  impactSoundKeys: () => impactSoundKeys,
59
60
  ricochetSoundKeys: () => ricochetSoundKeys,
60
61
  shotKey: () => shotKey,
@@ -100,6 +101,7 @@ var GameDestructionThree = (() => {
100
101
 
101
102
  // ../game-contracts/dist/index.js
102
103
  var DEFAULT_BLAST_SHOVE = { coupling: 0.02, maxSpeed: 20, lift: 0.3, minSpeed: 0.3, falloffPower: 2 };
104
+ var clampShare = (share) => Math.min(1, Math.max(0, share));
103
105
  function blastShove(blast3, target, options = {}) {
104
106
  const o = { ...DEFAULT_BLAST_SHOVE, ...options };
105
107
  const dx = target.position[0] - blast3.point[0];
@@ -109,7 +111,8 @@ var GameDestructionThree = (() => {
109
111
  if (!(blast3.radius > 0) || distance > blast3.radius) return void 0;
110
112
  const falloff = (1 - distance / blast3.radius) ** o.falloffPower;
111
113
  const mass = Math.max(target.mass, 0.05);
112
- const speed = Math.min(o.maxSpeed, Math.sqrt(2 * blast3.energy * (blast3.reach ?? 1) * o.coupling / mass)) * falloff;
114
+ const reach = clampShare(blast3.reach ?? 1);
115
+ const speed = Math.sqrt(reach) * Math.min(o.maxSpeed, Math.sqrt(2 * blast3.energy * o.coupling / mass)) * falloff;
113
116
  if (!(speed >= o.minSpeed)) return void 0;
114
117
  const direction = distance > 1e-4 ? [dx / distance, dy / distance + o.lift, dz / distance] : [0, 1, 0];
115
118
  const length = Math.hypot(direction[0], direction[1], direction[2]);
@@ -1572,16 +1575,16 @@ var GameDestructionThree = (() => {
1572
1575
  if (this.spaces.delete(id)) this.list = [...this.spaces.values()];
1573
1576
  return;
1574
1577
  }
1575
- const [hx, hy, hz] = enclosure.halfSize;
1578
+ const [hx, hy, hz2] = enclosure.halfSize;
1576
1579
  const space = {
1577
1580
  cx: enclosure.center[0],
1578
1581
  cy: enclosure.center[1],
1579
1582
  cz: enclosure.center[2],
1580
1583
  hx: Math.abs(hx),
1581
1584
  hy: Math.abs(hy),
1582
- hz: Math.abs(hz),
1585
+ hz: Math.abs(hz2),
1583
1586
  r: rotation(enclosure.orientation),
1584
- radius: Math.hypot(hx, hy, hz),
1587
+ radius: Math.hypot(hx, hy, hz2),
1585
1588
  openings: enclosure.openings ?? [],
1586
1589
  leak: Math.max(0, enclosure.leak ?? 0)
1587
1590
  };
@@ -5764,13 +5767,14 @@ var GameDestructionThree = (() => {
5764
5767
  function composeAfterEffects(context) {
5765
5768
  const { core, def, random, x, y, z } = context;
5766
5769
  const mark = def.mark;
5767
- if (mark.crater && mark.dent && y < 1.2) {
5770
+ const keepsMarks = !core.marklessAt?.(x, z);
5771
+ if (keepsMarks && mark.crater && mark.dent && y < 1.2) {
5768
5772
  core.emit({ type: "crater", x, z, radius: mark.crater, depth: mark.dent });
5769
5773
  core.trace?.crater?.(x, z, mark.crater, mark.dent);
5770
5774
  addCrater(core.marks, core.ground, random, { x, z, radius: mark.crater, depth: mark.dent, marks: core.layers.mark });
5771
5775
  if (core.layers.shrapnel) composeRimDebris(context);
5772
5776
  }
5773
- if (mark.scorch && y < 1.2 && core.layers.mark) {
5777
+ if (keepsMarks && mark.scorch && y < 1.2 && core.layers.mark) {
5774
5778
  core.trace?.scorch?.(x, z, mark.scorch);
5775
5779
  addScorch(core.marks, random, x, z, mark.scorch);
5776
5780
  }
@@ -7479,7 +7483,7 @@ var GameDestructionThree = (() => {
7479
7483
  const wind = { x: DEFAULT_WIND.x, y: DEFAULT_WIND.y, z: DEFAULT_WIND.z };
7480
7484
  if (options.wind) [wind.x, wind.y, wind.z] = options.wind;
7481
7485
  const camera2 = { x: 0, y: 1.6, z: 0 };
7482
- const layers = Object.fromEntries(VFX_LAYERS.map((layer2) => [layer2, true]));
7486
+ const layers = Object.fromEntries(VFX_LAYERS.map((layer3) => [layer3, true]));
7483
7487
  const ground = options.ground;
7484
7488
  const groundAt = options.groundAt ?? (ground ? (x, z) => ground.heightAt(x, z) : () => 0);
7485
7489
  const handles = { next: 0, events: 0 };
@@ -7521,6 +7525,7 @@ var GameDestructionThree = (() => {
7521
7525
  spawned: { puffs: 0, sparks: 0, chunks: 0, lights: 0 },
7522
7526
  clock: { time: 0, lastBlast: -1, lod: 1, forcedLod: null },
7523
7527
  groundAt,
7528
+ ...options.marklessAt ? { marklessAt: options.marklessAt } : {},
7524
7529
  emit: (request) => options.onRequest?.(request)
7525
7530
  };
7526
7531
  function definitionOf(id) {
@@ -7547,6 +7552,7 @@ var GameDestructionThree = (() => {
7547
7552
  return player ?? (player = createEffectPlayer({ engine: workshop(), effects, view }));
7548
7553
  }
7549
7554
  return {
7555
+ hostGroundAt: options.groundAt,
7550
7556
  play(id, point, playOptions = {}) {
7551
7557
  if (!(id in effects)) return null;
7552
7558
  const seed = playOptions.seed ?? (randomMode === "shared" ? void 0 : Math.imul(++handles.events, AUTO_SEED) + (options.seed ?? 0));
@@ -7609,10 +7615,10 @@ var GameDestructionThree = (() => {
7609
7615
  const fires = core.fires;
7610
7616
  for (let index = fires.items.length - 1; index >= 0; index--) if (fires.items[index].handle === handle) fires.removeAt(index);
7611
7617
  },
7612
- setLayer(layer2, on) {
7613
- layers[layer2] = on;
7618
+ setLayer(layer3, on) {
7619
+ layers[layer3] = on;
7614
7620
  },
7615
- layer: (layer2) => layers[layer2],
7621
+ layer: (layer3) => layers[layer3],
7616
7622
  setCamera(position) {
7617
7623
  camera2.x = position[0];
7618
7624
  camera2.y = position[1];
@@ -7730,7 +7736,7 @@ var GameDestructionThree = (() => {
7730
7736
  };
7731
7737
  var EFFECT_STEP_SECONDS = 1 / 50;
7732
7738
 
7733
- // ../game-vfx-three/dist/chunk-2MZDWTHX.js
7739
+ // ../game-vfx-three/dist/chunk-CPRCWTSQ.js
7734
7740
  var GLOW_VERTEX = `attribute vec4 aP;attribute vec4 aC;varying vec2 vU;varying vec4 vC;
7735
7741
  void main(){vec3 R=vec3(viewMatrix[0][0],viewMatrix[1][0],viewMatrix[2][0]);vec3 Up=vec3(viewMatrix[0][1],viewMatrix[1][1],viewMatrix[2][1]);
7736
7742
  vec3 wp=aP.xyz+(R*position.x+Up*position.y)*aP.w;vU=position.xy;vC=aC;gl_Position=projectionMatrix*viewMatrix*vec4(wp,1.);}`;
@@ -8170,7 +8176,11 @@ void main(){
8170
8176
  const dirt = texture(paint(make, (context, size) => drawDirt(context, size, random)));
8171
8177
  return { scorch, dirt };
8172
8178
  }
8173
- var MAX_CONFORM_STEPS = 48;
8179
+ var FLAT_STEPS_PER_METRE = 8;
8180
+ var MAX_FLAT_STEPS = 48;
8181
+ var DUG_STEPS_PER_METRE = 20;
8182
+ var MAX_DUG_STEPS = 64;
8183
+ var FLAT_TOLERANCE = 2e-3;
8174
8184
  var MIN_CONFORM_STEPS = 16;
8175
8185
  var BLOOD_GROW_RATE = 1.2;
8176
8186
  var BLOOD_START_GROWTH = 0.05;
@@ -8253,9 +8263,21 @@ void main(){
8253
8263
  spareGeometries.push(oldest.mesh.geometry);
8254
8264
  if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
8255
8265
  }
8266
+ const heightAt = options.heightAt ?? ((x, z) => options.ground?.heightAt(x, z) ?? 0);
8267
+ function groundDipsUnder(x, z, radius) {
8268
+ const centre = heightAt(x, z);
8269
+ for (const share of [0.25, 0.5, 0.75]) {
8270
+ for (let ray = 0; ray < 8; ray++) {
8271
+ const angle = ray / 8 * Math.PI * 2;
8272
+ if (Math.abs(heightAt(x + Math.cos(angle) * radius * share, z + Math.sin(angle) * radius * share) - centre) > FLAT_TOLERANCE) return true;
8273
+ }
8274
+ }
8275
+ return false;
8276
+ }
8256
8277
  function conform(mark, material) {
8257
8278
  const radius = mark.radius;
8258
- const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
8279
+ const dug = options.heightAt !== void 0 && groundDipsUnder(mark.x, mark.z, radius);
8280
+ const steps = Math.max(MIN_CONFORM_STEPS, Math.min(dug ? MAX_DUG_STEPS : MAX_FLAT_STEPS, Math.round(radius * (dug ? DUG_STEPS_PER_METRE : FLAT_STEPS_PER_METRE))));
8259
8281
  if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
8260
8282
  const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
8261
8283
  const grid = geometry.attributes.position;
@@ -8267,7 +8289,7 @@ void main(){
8267
8289
  const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
8268
8290
  const worldX = mark.x + localX * cos2 - localZ * sin2;
8269
8291
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
8270
- grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
8292
+ grid.setXYZ(index, worldX, heightAt(worldX, worldZ) + mark.lift, worldZ);
8271
8293
  }
8272
8294
  grid.needsUpdate = true;
8273
8295
  geometry.computeVertexNormals();
@@ -8939,7 +8961,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8939
8961
  decalLayer(THREE, engine.decals),
8940
8962
  ringLayer(THREE, engine.rings)
8941
8963
  ];
8942
- for (const layer2 of layers) group.add(layer2.object);
8964
+ for (const layer22 of layers) group.add(layer22.object);
8943
8965
  const meshes = createMeshLayer(THREE, engine.meshes);
8944
8966
  group.add(meshes.group);
8945
8967
  const lights = budget ? budgetedLights(engine, lightScale, budget) : legacyLights(THREE, engine, lightScale, group);
@@ -8947,13 +8969,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8947
8969
  group,
8948
8970
  sync() {
8949
8971
  let drawCalls = 0;
8950
- for (const layer2 of layers) if (layer2.sync()) drawCalls++;
8972
+ for (const layer22 of layers) if (layer22.sync()) drawCalls++;
8951
8973
  drawCalls += meshes.sync();
8952
8974
  lights.sync();
8953
8975
  return drawCalls;
8954
8976
  },
8955
8977
  dispose() {
8956
- for (const layer2 of layers) layer2.dispose();
8978
+ for (const layer22 of layers) layer22.dispose();
8957
8979
  meshes.dispose();
8958
8980
  lights.dispose();
8959
8981
  group.clear();
@@ -9057,7 +9079,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9057
9079
  const shocks = createShockLayer(THREE, pools.shocks);
9058
9080
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
9059
9081
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI, options.lights);
9060
- const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
9082
+ const marks = createMarkLayer(THREE, { ground: pools.ground, ...world.hostGroundAt ? { heightAt: world.hostGroundAt } : {}, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
9061
9083
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
9062
9084
  const stopMarks = world.onMarks((event) => marks.handle(event));
9063
9085
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -9190,7 +9212,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9190
9212
  stopMarks();
9191
9213
  overlay?.dispose();
9192
9214
  workshopLayers?.dispose();
9193
- for (const layer2 of [puffs, sparks, glows, chunks, shocks, lights, marks, sprites]) layer2.dispose();
9215
+ for (const layer22 of [puffs, sparks, glows, chunks, shocks, lights, marks, sprites]) layer22.dispose();
9194
9216
  rockGeometry.dispose();
9195
9217
  plateGeometry.dispose();
9196
9218
  options.scene.remove(root);
@@ -9274,13 +9296,154 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9274
9296
  return r << 16 | g << 8 | bl;
9275
9297
  }
9276
9298
 
9299
+ // src/fragment-shapes.ts
9300
+ var FRAGMENT_SHAPE_VARIANTS = 6;
9301
+ var SLIVER_STATIONS = 6;
9302
+ var SLIVER_FRAY = 0.3;
9303
+ var SLIVER_SECTION = [[0.5, 0.5], [0, 0.5], [-0.5, 0.5], [-0.5, -0.5], [0, -0.5], [0.5, -0.5]];
9304
+ var sliverWidth = (t) => t < 0.25 ? 0.6 + 1.6 * t : t > 0.5 ? Math.max(0, (1 - t) / 0.5) ** 0.85 : 1;
9305
+ var pushTriangle = (out, a, b, c) => {
9306
+ out.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]);
9307
+ };
9308
+ var faceted = (THREE, positions) => {
9309
+ const geometry = new THREE.BufferGeometry();
9310
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
9311
+ geometry.computeVertexNormals();
9312
+ return geometry;
9313
+ };
9314
+ function buildSliverGeometry(THREE, seed) {
9315
+ const rnd2 = createRng(7e3 + seed);
9316
+ const rings = [];
9317
+ const bendY = (rnd2() - 0.5) * 0.3;
9318
+ const bendZ = (rnd2() - 0.5) * 0.3;
9319
+ for (let i = 0; i < SLIVER_STATIONS - 1; i++) {
9320
+ const t = i / (SLIVER_STATIONS - 1);
9321
+ const x = -0.5 + t + (i > 0 ? (rnd2() - 0.5) * 0.08 : 0);
9322
+ const width = sliverWidth(t);
9323
+ const lift = Math.sin(t * Math.PI);
9324
+ rings.push(SLIVER_SECTION.map(([y, z]) => {
9325
+ const fray = 1 - SLIVER_FRAY + SLIVER_FRAY * 1.4 * rnd2();
9326
+ return [x, Math.max(-0.5, Math.min(0.5, y * width * fray + bendY * lift)), Math.max(-0.5, Math.min(0.5, z * width * fray + bendZ * lift))];
9327
+ }));
9328
+ }
9329
+ const tip = [0.5, (rnd2() - 0.5) * 0.5, (rnd2() - 0.5) * 0.3];
9330
+ const torn = [-0.5 + rnd2() * 0.12, (rnd2() - 0.5) * 0.2, (rnd2() - 0.5) * 0.2];
9331
+ const sides = SLIVER_SECTION.length;
9332
+ const positions = [];
9333
+ for (let k = 0; k < sides; k++) {
9334
+ const next = (k + 1) % sides;
9335
+ pushTriangle(positions, torn, rings[0][next], rings[0][k]);
9336
+ for (let i = 0; i < rings.length - 1; i++) {
9337
+ const a = rings[i][k], b = rings[i][next], c = rings[i + 1][next], d = rings[i + 1][k];
9338
+ pushTriangle(positions, a, b, c);
9339
+ pushTriangle(positions, a, c, d);
9340
+ }
9341
+ const last = rings[rings.length - 1];
9342
+ pushTriangle(positions, last[k], last[next], tip);
9343
+ }
9344
+ return faceted(THREE, positions);
9345
+ }
9346
+ function buildLumpGeometry(THREE, seed) {
9347
+ const rnd2 = createRng(8e3 + seed);
9348
+ const base = new THREE.IcosahedronGeometry(0.5, 0);
9349
+ const source = base.getAttribute("position");
9350
+ const pushOf = /* @__PURE__ */ new Map();
9351
+ const positions = [];
9352
+ for (let i = 0; i < source.count; i++) {
9353
+ const x = source.getX(i), y = source.getY(i), z = source.getZ(i);
9354
+ const key = `${x.toFixed(3)},${y.toFixed(3)},${z.toFixed(3)}`;
9355
+ let push = pushOf.get(key);
9356
+ if (push === void 0) {
9357
+ push = 0.6 + 0.55 * rnd2();
9358
+ pushOf.set(key, push);
9359
+ }
9360
+ positions.push(x * push, y * push, z * push);
9361
+ }
9362
+ base.dispose();
9363
+ const extent = [0, 0, 0];
9364
+ for (let i = 0; i < positions.length; i++) extent[i % 3] = Math.max(extent[i % 3], Math.abs(positions[i]));
9365
+ for (let i = 0; i < positions.length; i++) positions[i] *= 0.5 / Math.max(extent[i % 3], 1e-3);
9366
+ return faceted(THREE, positions);
9367
+ }
9368
+ var VENEER_EDGE = 11;
9369
+ function buildVeneerGeometry(THREE, seed) {
9370
+ const rnd2 = createRng(9e3 + seed);
9371
+ const shape = new THREE.Shape();
9372
+ const outline = [];
9373
+ const edge = (y, inward) => y - inward * (rnd2() * 0.16 + (rnd2() < 0.15 ? 0.12 : 0));
9374
+ for (let i = 0; i <= VENEER_EDGE; i++) outline.push([-0.5 + i / VENEER_EDGE, edge(0.5, 1)]);
9375
+ outline.push([0.5 - rnd2() * 0.12, 0.2 * rnd2()], [0.5, -0.1 + 0.2 * rnd2()], [0.5 - rnd2() * 0.12, -0.25 * rnd2()]);
9376
+ for (let i = VENEER_EDGE; i >= 0; i--) outline.push([-0.5 + i / VENEER_EDGE, edge(-0.5, -1)]);
9377
+ outline.push([-0.5 + rnd2() * 0.12, -0.2 * rnd2()], [-0.5, 0.1 * rnd2()], [-0.5 + rnd2() * 0.12, 0.25 * rnd2()]);
9378
+ shape.moveTo(outline[0][0], outline[0][1]);
9379
+ for (let i = 1; i < outline.length; i++) shape.lineTo(outline[i][0], outline[i][1]);
9380
+ shape.closePath();
9381
+ const geometry = new THREE.ExtrudeGeometry(shape, { depth: 1, bevelEnabled: false });
9382
+ geometry.translate(0, 0, -0.5);
9383
+ return geometry;
9384
+ }
9385
+ var TORN_END = 0.24;
9386
+ var TORN_STRANDS = [4, 7];
9387
+ var pushQuad = (out, quad) => {
9388
+ pushTriangle(out, quad[0], quad[1], quad[2]);
9389
+ pushTriangle(out, quad[0], quad[2], quad[3]);
9390
+ };
9391
+ var unevenCuts = (count, rnd2) => {
9392
+ const shares = Array.from({ length: count }, () => 0.35 + 1.65 * rnd2());
9393
+ const total = shares.reduce((sum, share) => sum + share, 0);
9394
+ const cuts = [-0.5];
9395
+ for (const share of shares) cuts.push(cuts[cuts.length - 1] + share / total);
9396
+ cuts[cuts.length - 1] = 0.5;
9397
+ return cuts;
9398
+ };
9399
+ function buildBrokenBoardGeometry(THREE, seed) {
9400
+ const rnd2 = createRng(1e4 + seed);
9401
+ const end = 0.5 - TORN_END;
9402
+ const positions = [];
9403
+ const lo = -0.5, hi = 0.5;
9404
+ pushQuad(positions, [[lo, lo, hi], [end, lo, hi], [end, hi, hi], [lo, hi, hi]]);
9405
+ pushQuad(positions, [[lo, hi, lo], [end, hi, lo], [end, lo, lo], [lo, lo, lo]]);
9406
+ pushQuad(positions, [[lo, hi, hi], [end, hi, hi], [end, hi, lo], [lo, hi, lo]]);
9407
+ pushQuad(positions, [[lo, lo, lo], [end, lo, lo], [end, lo, hi], [lo, lo, hi]]);
9408
+ pushQuad(positions, [[lo, lo, lo], [lo, lo, hi], [lo, hi, hi], [lo, hi, lo]]);
9409
+ const count = TORN_STRANDS[0] + Math.floor(rnd2() * (TORN_STRANDS[1] - TORN_STRANDS[0] + 1));
9410
+ const cuts = unevenCuts(count, rnd2);
9411
+ for (let k = 0; k < count; k++) {
9412
+ const y0 = cuts[k], y1 = cuts[k + 1];
9413
+ const length = TORN_END * (0.15 + 0.85 * rnd2());
9414
+ const midX = end + length * (0.35 + 0.3 * rnd2());
9415
+ const narrow = 0.55 + 0.35 * rnd2();
9416
+ const shiftY = (rnd2() - 0.5) * (y1 - y0) * 0.6;
9417
+ const shiftZ = (rnd2() - 0.5) * 0.5;
9418
+ const cy = (y0 + y1) / 2 + shiftY, hy = (y1 - y0) / 2 * narrow, hz2 = 0.5 * (0.4 + 0.5 * rnd2());
9419
+ const base = [[end, y0, lo], [end, y1, lo], [end, y1, hi], [end, y0, hi]];
9420
+ const clampZ = (z) => Math.max(lo, Math.min(hi, z));
9421
+ const mid = [[midX, cy - hy, clampZ(shiftZ - hz2)], [midX, cy + hy, clampZ(shiftZ - hz2)], [midX, cy + hy, clampZ(shiftZ + hz2)], [midX, cy - hy, clampZ(shiftZ + hz2)]];
9422
+ const tip = [end + length, y0 + (y1 - y0) * rnd2(), clampZ(shiftZ + (rnd2() - 0.5) * 0.4)];
9423
+ for (let i = 0; i < 4; i++) {
9424
+ const next = (i + 1) % 4;
9425
+ pushQuad(positions, [base[i], base[next], mid[next], mid[i]]);
9426
+ pushTriangle(positions, mid[i], mid[next], tip);
9427
+ }
9428
+ }
9429
+ return faceted(THREE, positions);
9430
+ }
9431
+
9277
9432
  // src/fragments.ts
9433
+ var BUILDERS = {
9434
+ sliver: buildSliverGeometry,
9435
+ lump: buildLumpGeometry,
9436
+ veneer: buildVeneerGeometry,
9437
+ board: buildBrokenBoardGeometry
9438
+ };
9278
9439
  var SHEET_MAX = 4e-3;
9279
9440
  var SHEET_AIR = 2.5;
9280
9441
  var SHEET_SWAY = 3;
9281
9442
  var BIG_LENGTH = 0.2;
9282
9443
  var BIG_WIDTH = 0.03;
9283
9444
  var FRAGMENT_SEED = 1374496523;
9445
+ var SUPPORT_CHECKS = 32;
9446
+ var SUPPORT_GAP = 0.02;
9284
9447
  var SHARD_SHAPES = 6;
9285
9448
  var isBig = (size) => {
9286
9449
  const [, middle, longest] = [...size].sort((a, b) => a - b);
@@ -9295,6 +9458,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9295
9458
  /** The mesh box of a fragment before its scale. */
9296
9459
  __publicField(this, "unitBox", { min: { x: -0.5, y: -0.5, z: -0.5 }, max: { x: 0.5, y: 0.5, z: 0.5 } });
9297
9460
  __publicField(this, "shardGeos", []);
9461
+ __publicField(this, "shapeGeos", /* @__PURE__ */ new Map());
9298
9462
  __publicField(this, "materials", /* @__PURE__ */ new Map());
9299
9463
  __publicField(this, "q");
9300
9464
  __publicField(this, "dq");
@@ -9304,6 +9468,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9304
9468
  __publicField(this, "rng", createRng(FRAGMENT_SEED));
9305
9469
  /** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
9306
9470
  __publicField(this, "tick", 0);
9471
+ __publicField(this, "supportTurn", 0);
9307
9472
  __publicField(this, "markGeometry", null);
9308
9473
  __publicField(this, "markMaterial", null);
9309
9474
  this.box = new THREE.BoxGeometry(1, 1, 1);
@@ -9352,21 +9517,54 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9352
9517
  }
9353
9518
  return g;
9354
9519
  }
9520
+ sharedGeometry(shape, seed) {
9521
+ const idx = seed % FRAGMENT_SHAPE_VARIANTS;
9522
+ let variants = this.shapeGeos.get(shape);
9523
+ if (!variants) {
9524
+ variants = [];
9525
+ this.shapeGeos.set(shape, variants);
9526
+ }
9527
+ let g = variants[idx];
9528
+ if (!g) {
9529
+ g = BUILDERS[shape](this.THREE, idx);
9530
+ variants[idx] = g;
9531
+ }
9532
+ return g;
9533
+ }
9534
+ /**
9535
+ * The geometry a fragment is drawn with: a splinter is a pointed, frayed sliver; MDF breaks in lumps, plywood peels in ragged veneer, the stub of a broken
9536
+ * board ends in torn strands at +x; the rest are boxes.
9537
+ */
9538
+ geometryOf(f) {
9539
+ const seed = hashString(f.id);
9540
+ if (f.shape === "shard") return this.shardGeometry(seed);
9541
+ if (f.shape === "splinter") return this.sharedGeometry("sliver", seed);
9542
+ const form = this.o.formOf?.(f.material);
9543
+ if (f.shape === "chunk" && form === "granules") return this.sharedGeometry("lump", seed);
9544
+ if (f.shape === "scrap" && form === "plies") return this.sharedGeometry("veneer", seed);
9545
+ if (f.shape === "slab" && (form === "slivers" || form === "plies")) return this.sharedGeometry("board", seed);
9546
+ return this.box;
9547
+ }
9355
9548
  markParts() {
9356
9549
  this.markGeometry ?? (this.markGeometry = new this.THREE.CircleGeometry(1, 10));
9357
9550
  this.markMaterial ?? (this.markMaterial = new this.THREE.MeshBasicMaterial({ color: 723465, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
9358
9551
  return { geometry: this.markGeometry, material: this.markMaterial };
9359
9552
  }
9360
9553
  /**
9361
- * Meshes, never put in a scene by this class, that draw every shared piece of a fragment once: the box, the six shards, the bullet-hole disc, and the opaque and
9554
+ * Meshes, never put in a scene by this class, that draw every shared piece of a fragment once: the box, the six shards, the wood splinters, MDF lumps and plywood veneer, the bullet-hole disc, and the opaque and
9362
9555
  * glass materials. A host draws them at load so the first blast does not upload these buffers in the middle of a frame.
9363
9556
  */
9364
9557
  warmMeshes() {
9365
9558
  const opaque = this.materialFor("warm", 8421504, false);
9366
9559
  const glass2 = this.materialFor("warm", 8421504, true);
9367
9560
  const geometries = [this.box, ...Array.from({ length: SHARD_SHAPES }, (_, index) => this.shardGeometry(index))];
9561
+ const wood = ["sliver", "lump", "veneer", "board"].flatMap((shape) => Array.from({ length: FRAGMENT_SHAPE_VARIANTS }, (_, index) => this.sharedGeometry(shape, index)));
9368
9562
  const { geometry, material } = this.markParts();
9369
- return [...geometries.flatMap((shared) => [new this.THREE.Mesh(shared, opaque), new this.THREE.Mesh(shared, glass2)]), new this.THREE.Mesh(geometry, material)];
9563
+ return [
9564
+ ...geometries.flatMap((shared) => [new this.THREE.Mesh(shared, opaque), new this.THREE.Mesh(shared, glass2)]),
9565
+ ...wood.map((shared) => new this.THREE.Mesh(shared, opaque)),
9566
+ new this.THREE.Mesh(geometry, material)
9567
+ ];
9370
9568
  }
9371
9569
  /**
9372
9570
  * Bullet holes that stay on a fragment (`Fragment.marks`): dark discs that are children of its mesh, so they fall, spin and settle with it. The mesh is
@@ -9391,7 +9589,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9391
9589
  * bigger than the budget (every live fragment was made in this same frame) does not make the rest at all, so a blast never builds a thousand meshes only to fade them
9392
9590
  * out. Returns null when the fragment was not made.
9393
9591
  */
9394
- spawn(f, material, home, maxAllowed) {
9592
+ /**
9593
+ * Throws `f` into the scene, within `maxAllowed` live fragments (the oldest small ones make room first). A `lasting` fragment (a steel bar) is debris that
9594
+ * stays: it has a body the player and the pieces run into, however thin it is, and it never fades.
9595
+ */
9596
+ spawn(f, material, home, maxAllowed, lasting = false) {
9395
9597
  if (this.list.length >= maxAllowed) {
9396
9598
  let alive = 0;
9397
9599
  let oldest;
@@ -9408,7 +9610,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9408
9610
  oldest.dying = true;
9409
9611
  }
9410
9612
  }
9411
- const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
9613
+ const geo = this.geometryOf(f);
9412
9614
  const mesh2 = new this.THREE.Mesh(geo, material);
9413
9615
  mesh2.position.set(f.center[0], f.center[1], f.center[2]);
9414
9616
  mesh2.quaternion.set(f.rotation[0], f.rotation[1], f.rotation[2], f.rotation[3]).normalize();
@@ -9436,9 +9638,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9436
9638
  air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
9437
9639
  body: null,
9438
9640
  born: this.tick,
9439
- life: Math.max(0, typeof this.o.life === "function" ? this.o.life(f, home) : this.o.life)
9641
+ life: lasting ? 0 : Math.max(0, typeof this.o.life === "function" ? this.o.life(f, home) : this.o.life)
9440
9642
  };
9441
- if (f.shape !== "shard" && isBig(f.size)) {
9643
+ if (lasting || f.shape !== "shard" && isBig(f.size)) {
9442
9644
  live.body = {
9443
9645
  mass: f.mass,
9444
9646
  push: (x, z) => {
@@ -9498,6 +9700,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9498
9700
  }
9499
9701
  update(dt) {
9500
9702
  this.tick++;
9703
+ this.wakeUnsupported();
9501
9704
  const g = this.o.gravity;
9502
9705
  for (let i = this.list.length - 1; i >= 0; i--) {
9503
9706
  const f = this.list[i];
@@ -9522,6 +9725,18 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9522
9725
  this.step(f, dt, g);
9523
9726
  }
9524
9727
  }
9728
+ /** A few fragments asleep each update (`SUPPORT_CHECKS`, in turn) look at the ground under them again: one left over air falls. */
9729
+ wakeUnsupported() {
9730
+ const n = this.list.length;
9731
+ for (let k = 0; k < Math.min(SUPPORT_CHECKS, n); k++) {
9732
+ this.supportTurn = (this.supportTurn + 1) % n;
9733
+ const f = this.list[this.supportTurn];
9734
+ if (!f.asleep || f.dying) continue;
9735
+ const e = this.m.makeRotationFromQuaternion(f.mesh.quaternion).elements;
9736
+ const ext = Math.abs(e[1]) * f.half.x + Math.abs(e[5]) * f.half.y + Math.abs(e[9]) * f.half.z;
9737
+ if (f.mesh.position.y - ext > this.o.groundAt(f.mesh.position.x, f.mesh.position.z) + SUPPORT_GAP) this.rouse(f);
9738
+ }
9739
+ }
9525
9740
  step(f, dt, g) {
9526
9741
  const mesh2 = f.mesh;
9527
9742
  f.vel.y -= g * dt;
@@ -9629,6 +9844,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9629
9844
  this.markMaterial?.dispose();
9630
9845
  for (const g of this.shardGeos) g?.dispose();
9631
9846
  this.shardGeos = [];
9847
+ for (const variants of this.shapeGeos.values()) for (const g of variants) g?.dispose();
9848
+ this.shapeGeos.clear();
9632
9849
  for (const m of this.materials.values()) m.dispose();
9633
9850
  this.materials.clear();
9634
9851
  }
@@ -9738,6 +9955,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9738
9955
  var SOLVER_PASSES = 10;
9739
9956
  var SIDE_BOUNCE = 0.1;
9740
9957
  var EDGE_POINTS = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
9958
+ var SUPPORT_CHECKS2 = 16;
9959
+ var SUPPORT_GAP2 = 0.02;
9741
9960
  var boxTopAt = (s, x, z) => {
9742
9961
  const e = s.e;
9743
9962
  const dx = x - s.c[0], dz = z - s.c[2];
@@ -9770,6 +9989,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9770
9989
  __publicField(this, "m");
9771
9990
  __publicField(this, "ax");
9772
9991
  __publicField(this, "solids", []);
9992
+ __publicField(this, "supportTurn", 0);
9773
9993
  this.p = new THREE.Vector3();
9774
9994
  this.q = new THREE.Quaternion();
9775
9995
  this.dq = new THREE.Quaternion();
@@ -9865,10 +10085,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9865
10085
  return this.mulInvInertiaWith(b, this.m.elements, x, y, z);
9866
10086
  }
9867
10087
  mulInvInertiaWith(b, e, x, y, z) {
9868
- const [hx, hy, hz] = b.halfExtents;
10088
+ const [hx, hy, hz2] = b.halfExtents;
9869
10089
  const k = 3 / b.mass;
9870
- const ix = k / (hy * hy + hz * hz);
9871
- const iy = k / (hx * hx + hz * hz);
10090
+ const ix = k / (hy * hy + hz2 * hz2);
10091
+ const iy = k / (hx * hx + hz2 * hz2);
9872
10092
  const iz = k / (hx * hx + hy * hy);
9873
10093
  const lx = (e[0] * x + e[1] * y + e[2] * z) * ix;
9874
10094
  const ly = (e[4] * x + e[5] * y + e[6] * z) * iy;
@@ -9877,11 +10097,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9877
10097
  }
9878
10098
  /** The points of the box that touch the ground, in its own frame: the 8 corners, then (with `edges`) `EDGE_POINTS` along each of its 12 edges. */
9879
10099
  samples(b, edges) {
9880
- const [hx, hy, hz] = b.halfExtents;
10100
+ const [hx, hy, hz2] = b.halfExtents;
9881
10101
  const out = [];
9882
- for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz : -hz]);
10102
+ for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz2 : -hz2]);
9883
10103
  if (!edges) return out;
9884
- const h = [hx, hy, hz];
10104
+ const h = [hx, hy, hz2];
9885
10105
  for (let axis2 = 0; axis2 < 3; axis2++) {
9886
10106
  const a1 = (axis2 + 1) % 3;
9887
10107
  const a2 = (axis2 + 2) % 3;
@@ -9929,6 +10149,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9929
10149
  const h = body.halfExtents;
9930
10150
  return { body, c: [c.x, c.y, c.z], e: [...new this.THREE.Matrix4().makeRotationFromQuaternion(body.root.quaternion).elements], half: h, r: Math.hypot(h[0], h[1], h[2]) };
9931
10151
  });
10152
+ this.wakeUnsupported();
9932
10153
  for (const b of this.list) {
9933
10154
  if (b.held) {
9934
10155
  b.velocity = [0, 0, 0];
@@ -9943,6 +10164,33 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9943
10164
  }
9944
10165
  this.separate(dt);
9945
10166
  }
10167
+ /** A few sleeping pieces each update (`SUPPORT_CHECKS`, in turn) look under them again: one whose table broke, or whose shelf fell over, falls. */
10168
+ wakeUnsupported() {
10169
+ const n = this.list.length;
10170
+ for (let k = 0; k < Math.min(SUPPORT_CHECKS2, n); k++) {
10171
+ this.supportTurn = (this.supportTurn + 1) % n;
10172
+ const b = this.list[this.supportTurn];
10173
+ if (!b.sleeping || b.held || this.supported(b, false) || this.supported(b, true)) continue;
10174
+ b.sleeping = false;
10175
+ b.still = 0;
10176
+ b.calmAt = null;
10177
+ }
10178
+ }
10179
+ /**
10180
+ * Whether a point of `b` (its corners, or with `edges` the points along its edges too) has the ground, a piece or a fixed top under it. Any point, not only the lowest:
10181
+ * a book propped on a fallen case rests by a corner high on it while its lowest one hangs beside it.
10182
+ */
10183
+ supported(b, edges) {
10184
+ const c = this.centerOf(b, this.p);
10185
+ const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
10186
+ for (const [sx, sy, sz] of this.samples(b, edges)) {
10187
+ const y = c.y + e[1] * sx + e[5] * sy + e[9] * sz;
10188
+ const x = c.x + e[0] * sx + e[4] * sy + e[8] * sz;
10189
+ const z = c.z + e[2] * sx + e[6] * sy + e[10] * sz;
10190
+ if (this.supportAt(b, x, y, z, STEP) >= y - SUPPORT_GAP2) return true;
10191
+ }
10192
+ return false;
10193
+ }
9946
10194
  /** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
9947
10195
  supportAt(self, x, y, z, step) {
9948
10196
  let top = this.o.groundAt(x, z);
@@ -10202,8 +10450,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10202
10450
  unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
10203
10451
  if (b.tipping) {
10204
10452
  b.tipping.seconds -= h;
10205
- const [hx, hy, hz] = b.halfExtents;
10206
- const upHalf = Math.abs(e[1]) * hx + Math.abs(e[5]) * hy + Math.abs(e[9]) * hz;
10453
+ const [hx, hy, hz2] = b.halfExtents;
10454
+ const upHalf = Math.abs(e[1]) * hx + Math.abs(e[5]) * hy + Math.abs(e[9]) * hz2;
10207
10455
  if (upHalf < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
10208
10456
  else {
10209
10457
  w[0] += b.tipping.x * TIP_ACCELERATION * h;
@@ -10316,6 +10564,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10316
10564
  constructor(THREE, scene2, radius = 4e-3) {
10317
10565
  __publicField(this, "THREE", THREE);
10318
10566
  __publicField(this, "scene", scene2);
10567
+ __publicField(this, "radius", radius);
10319
10568
  __publicField(this, "cages", /* @__PURE__ */ new Map());
10320
10569
  __publicField(this, "geometry");
10321
10570
  __publicField(this, "material");
@@ -10344,28 +10593,62 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10344
10593
  meshes() {
10345
10594
  return [...this.cages.values()];
10346
10595
  }
10347
- /** Draw `bars` (world space) as the cage of `key`; an empty list removes it. */
10348
- set(key, bars) {
10596
+ /**
10597
+ * Draw `bars` (world space) as the cage of `key`; an empty list removes it. The cage is a child of `holder` (the drawing of the part's object, the scene
10598
+ * by default), so it goes where the part goes: a beam that comes down takes its bare bars with it.
10599
+ */
10600
+ set(key, bars, holder) {
10349
10601
  this.remove(key);
10350
10602
  if (bars.length === 0) return;
10603
+ const parent = holder ?? this.scene;
10604
+ parent.updateWorldMatrix(true, false);
10605
+ const toParent = new this.THREE.Matrix4().copy(parent.matrixWorld).invert();
10351
10606
  const mesh2 = new this.THREE.InstancedMesh(this.geometry, this.material, bars.length);
10352
10607
  bars.forEach(([a, b], i) => {
10353
10608
  this.dir.set(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
10354
10609
  const length = this.dir.length();
10355
10610
  this.mid.set((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2);
10356
10611
  this.q.setFromUnitVectors(this.up, length > 1e-9 ? this.dir.divideScalar(length) : this.up);
10357
- this.m.compose(this.mid, this.q, this.scale.set(1, Math.max(length, 1e-4), 1));
10612
+ this.m.compose(this.mid, this.q, this.scale.set(1, Math.max(length, 1e-4), 1)).premultiply(toParent);
10358
10613
  mesh2.setMatrixAt(i, this.m);
10359
10614
  });
10360
10615
  mesh2.instanceMatrix.needsUpdate = true;
10361
10616
  mesh2.castShadow = true;
10362
- this.scene.add(mesh2);
10617
+ mesh2.name = "rebar-cage";
10618
+ parent.add(mesh2);
10363
10619
  this.cages.set(key, mesh2);
10364
10620
  }
10621
+ /** The cage of `from` now belongs to `to` (a part that left its object for one of its own) and hangs from `holder`, where it is in the world now. */
10622
+ move(from, to, holder) {
10623
+ const mesh2 = this.cages.get(from);
10624
+ if (!mesh2) return;
10625
+ this.cages.delete(from);
10626
+ this.remove(to);
10627
+ holder.attach(mesh2);
10628
+ this.cages.set(to, mesh2);
10629
+ }
10630
+ /** Takes the cage of `key` away and gives its bars, where they are in the world now: centre, turn (the bar along its y) and length. */
10631
+ take(key) {
10632
+ const mesh2 = this.cages.get(key);
10633
+ if (!mesh2) return [];
10634
+ mesh2.updateWorldMatrix(true, false);
10635
+ const bars = [];
10636
+ for (let i = 0; i < mesh2.count; i++) {
10637
+ mesh2.getMatrixAt(i, this.m);
10638
+ this.m.premultiply(mesh2.matrixWorld).decompose(this.mid, this.q, this.scale);
10639
+ bars.push({ center: [this.mid.x, this.mid.y, this.mid.z], rotation: [this.q.x, this.q.y, this.q.z, this.q.w], length: this.scale.y });
10640
+ }
10641
+ this.remove(key);
10642
+ return bars;
10643
+ }
10644
+ /** Radius of the bars drawn, metres. */
10645
+ get barRadius() {
10646
+ return this.radius;
10647
+ }
10365
10648
  remove(key) {
10366
10649
  const mesh2 = this.cages.get(key);
10367
10650
  if (!mesh2) return;
10368
- this.scene.remove(mesh2);
10651
+ mesh2.removeFromParent();
10369
10652
  mesh2.dispose();
10370
10653
  this.cages.delete(key);
10371
10654
  }
@@ -10445,26 +10728,26 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10445
10728
  return [s.radius * 2, s.radius * 2, s.radius * 2];
10446
10729
  };
10447
10730
  var aabbOf = (s, center, q) => {
10448
- let hx, hy, hz;
10731
+ let hx, hy, hz2;
10449
10732
  if (s.kind === "sphere") {
10450
- hx = hy = hz = s.radius;
10733
+ hx = hy = hz2 = s.radius;
10451
10734
  } else {
10452
10735
  const m = quat.toMatrix(q);
10453
10736
  if (s.kind === "box") {
10454
10737
  const a = s.size[0] / 2, b = s.size[1] / 2, c = s.size[2] / 2;
10455
10738
  hx = Math.abs(m[0]) * a + Math.abs(m[1]) * b + Math.abs(m[2]) * c;
10456
10739
  hy = Math.abs(m[3]) * a + Math.abs(m[4]) * b + Math.abs(m[5]) * c;
10457
- hz = Math.abs(m[6]) * a + Math.abs(m[7]) * b + Math.abs(m[8]) * c;
10740
+ hz2 = Math.abs(m[6]) * a + Math.abs(m[7]) * b + Math.abs(m[8]) * c;
10458
10741
  } else {
10459
10742
  const ax = [m[1], m[4], m[7]];
10460
10743
  const hh = s.height / 2;
10461
10744
  const e = (i) => s.radius * Math.sqrt(Math.max(0, 1 - ax[i] * ax[i])) + hh * Math.abs(ax[i]);
10462
10745
  hx = e(0);
10463
10746
  hy = e(1);
10464
- hz = e(2);
10747
+ hz2 = e(2);
10465
10748
  }
10466
10749
  }
10467
- return { min: [center[0] - hx, center[1] - hy, center[2] - hz], max: [center[0] + hx, center[1] + hy, center[2] + hz] };
10750
+ return { min: [center[0] - hx, center[1] - hy, center[2] - hz2], max: [center[0] + hx, center[1] + hy, center[2] + hz2] };
10468
10751
  };
10469
10752
  var FLEXURE_DEFAULTS = {
10470
10753
  enabled: true,
@@ -10649,6 +10932,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10649
10932
  },
10650
10933
  {
10651
10934
  id: "wood_soft",
10935
+ splitForm: "slivers",
10652
10936
  density: 500,
10653
10937
  hpPerKg: 90,
10654
10938
  penetration: 9,
@@ -10694,6 +10978,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10694
10978
  },
10695
10979
  {
10696
10980
  id: "wood_hard",
10981
+ splitForm: "slivers",
10697
10982
  density: 750,
10698
10983
  hpPerKg: 140,
10699
10984
  penetration: 14,
@@ -10738,6 +11023,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10738
11023
  },
10739
11024
  {
10740
11025
  id: "plywood",
11026
+ splitForm: "plies",
10741
11027
  density: 600,
10742
11028
  hpPerKg: 70,
10743
11029
  penetration: 7,
@@ -10785,6 +11071,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10785
11071
  },
10786
11072
  {
10787
11073
  id: "mdf",
11074
+ splitForm: "granules",
10788
11075
  density: 750,
10789
11076
  hpPerKg: 45,
10790
11077
  penetration: 6,
@@ -10951,6 +11238,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10951
11238
  floor: 60
10952
11239
  }
10953
11240
  },
11241
+ ricochetDeg: 12,
10954
11242
  hitDebris: "chips",
10955
11243
  breakDebris: "dust",
10956
11244
  visual: { innerColor: 10197652 },
@@ -10982,6 +11270,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10982
11270
  0.04,
10983
11271
  0.12
10984
11272
  ],
11273
+ ricochetDeg: 10,
10985
11274
  hitDebris: "chips",
10986
11275
  breakDebris: "dust",
10987
11276
  visual: { innerColor: 12085567 },
@@ -11039,6 +11328,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11039
11328
  0.06,
11040
11329
  0.3
11041
11330
  ],
11331
+ ricochetDeg: 15,
11042
11332
  hitDebris: "chips",
11043
11333
  breakDebris: "dust",
11044
11334
  visual: { innerColor: 9407364 },
@@ -11296,6 +11586,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11296
11586
  },
11297
11587
  {
11298
11588
  id: "green_wood",
11589
+ splitForm: "slivers",
11299
11590
  density: 650,
11300
11591
  hpPerKg: 5,
11301
11592
  penetration: 10,
@@ -11466,6 +11757,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11466
11757
  0.03,
11467
11758
  0.15
11468
11759
  ],
11760
+ ricochetDeg: 8,
11469
11761
  hitDebris: "chips",
11470
11762
  breakDebris: "dust",
11471
11763
  thermal: {
@@ -11565,6 +11857,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11565
11857
  0.02,
11566
11858
  0.08
11567
11859
  ],
11860
+ ricochetDeg: 8,
11568
11861
  hitDebris: "chips",
11569
11862
  breakDebris: "dust",
11570
11863
  thermal: {
@@ -11576,6 +11869,127 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11576
11869
  "brittle"
11577
11870
  ],
11578
11871
  compressive: 1e5
11872
+ },
11873
+ {
11874
+ id: "mud",
11875
+ density: 1700,
11876
+ hpPerKg: 1,
11877
+ penetration: 5,
11878
+ ductility: 0.8,
11879
+ fracture: "crumble",
11880
+ fragmentSize: [
11881
+ 0.02,
11882
+ 0.08
11883
+ ],
11884
+ hitDebris: "dust",
11885
+ breakDebris: "dust",
11886
+ thermal: {
11887
+ conductivity: 0.5,
11888
+ heatCapacity: 3.2
11889
+ },
11890
+ tags: [
11891
+ "soil",
11892
+ "wet"
11893
+ ],
11894
+ compressive: 5e3
11895
+ },
11896
+ {
11897
+ id: "clay",
11898
+ density: 1800,
11899
+ hpPerKg: 5,
11900
+ penetration: 7,
11901
+ ductility: 0.05,
11902
+ fracture: "crumble",
11903
+ fragmentSize: [
11904
+ 0.02,
11905
+ 0.1
11906
+ ],
11907
+ hitDebris: "chips",
11908
+ breakDebris: "dust",
11909
+ thermal: {
11910
+ conductivity: 0.15,
11911
+ heatCapacity: 2.2
11912
+ },
11913
+ tags: [
11914
+ "soil",
11915
+ "brittle"
11916
+ ],
11917
+ compressive: 6e4
11918
+ },
11919
+ {
11920
+ id: "snow",
11921
+ density: 300,
11922
+ hpPerKg: 1,
11923
+ penetration: 3,
11924
+ ductility: 0.6,
11925
+ fracture: "crumble",
11926
+ fragmentSize: [
11927
+ 0.01,
11928
+ 0.05
11929
+ ],
11930
+ hitDebris: "dust",
11931
+ breakDebris: "dust",
11932
+ thermal: {
11933
+ conductivity: 0.1,
11934
+ heatCapacity: 2.1
11935
+ },
11936
+ tags: [
11937
+ "soil",
11938
+ "frozen"
11939
+ ],
11940
+ compressive: 1e3
11941
+ },
11942
+ {
11943
+ id: "cobble",
11944
+ density: 2650,
11945
+ hpPerKg: 25,
11946
+ penetration: 25,
11947
+ penetrationClass: "concrete",
11948
+ penetrationScale: 0.9,
11949
+ ductility: 0.03,
11950
+ fracture: "crumble",
11951
+ fragmentSize: [
11952
+ 0.05,
11953
+ 0.15
11954
+ ],
11955
+ ricochetDeg: 15,
11956
+ hitDebris: "chips",
11957
+ breakDebris: "chips",
11958
+ thermal: {
11959
+ conductivity: 0.2,
11960
+ heatCapacity: 2.5
11961
+ },
11962
+ tags: [
11963
+ "masonry",
11964
+ "brittle"
11965
+ ],
11966
+ compressive: 3e5
11967
+ },
11968
+ {
11969
+ id: "paver",
11970
+ density: 2300,
11971
+ hpPerKg: 14,
11972
+ penetration: 18,
11973
+ penetrationClass: "concrete",
11974
+ penetrationScale: 1,
11975
+ ductility: 0.03,
11976
+ fracture: "crumble",
11977
+ fragmentSize: [
11978
+ 0.04,
11979
+ 0.12
11980
+ ],
11981
+ ricochetDeg: 12,
11982
+ hitDebris: "chips",
11983
+ breakDebris: "chips",
11984
+ thermal: {
11985
+ conductivity: 0.15,
11986
+ heatCapacity: 2.5
11987
+ },
11988
+ tags: [
11989
+ "masonry",
11990
+ "brittle"
11991
+ ],
11992
+ compressive: 35e4
11579
11993
  }
11580
11994
  ];
11581
11995
  var material_hints_default = {
@@ -11714,6 +12128,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11714
12128
  return out;
11715
12129
  };
11716
12130
  var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
12131
+ var BLAST_REFERENCE_RESISTANCE = 200 * 1900 * 0.13;
11717
12132
  var assemblies_default = [
11718
12133
  {
11719
12134
  id: "brick_wall_plastered",
@@ -12099,6 +12514,23 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12099
12514
  }
12100
12515
  };
12101
12516
  var tmpV = (THREE) => new THREE.Vector3();
12517
+ function plainPositions(mesh2, v0, v1) {
12518
+ const { collapsedRanges, plainPositions: plain } = mesh2.userData;
12519
+ if (!plain || !collapsedRanges?.some(([a, b]) => a < v1 && b > v0)) return null;
12520
+ return plain;
12521
+ }
12522
+ function showCollapsedRanges(mesh2) {
12523
+ const data = mesh2.userData;
12524
+ const plain = data.plainPositions;
12525
+ const position = mesh2.geometry?.getAttribute(POS_ATTR);
12526
+ if (!plain || !data.collapsedRanges?.length || !position) return;
12527
+ const array = position.array;
12528
+ for (const [v0, v1] of data.collapsedRanges) array.set(plain.subarray(v0 * 3, v1 * 3), v0 * 3);
12529
+ data.collapsedRanges = [];
12530
+ position.needsUpdate = true;
12531
+ mesh2.geometry.computeBoundingBox();
12532
+ mesh2.geometry.computeBoundingSphere();
12533
+ }
12102
12534
  var MIN_DENT_VERTICES = 10;
12103
12535
  var WIDEN_MARGIN = 1.1;
12104
12536
  var SKIN_FOLLOW = 0.85;
@@ -12864,7 +13296,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12864
13296
  const a = src.getAttribute(name);
12865
13297
  if (a.isInterleavedBufferAttribute) continue;
12866
13298
  const size = a.itemSize;
12867
- const from = (name === POS_ATTR ? host.work : null) ?? a.array;
13299
+ const from = (name === POS_ATTR ? plainPositions(host.mesh, v0, v1) ?? host.work : null) ?? a.array;
12868
13300
  const slice = from.slice(v0 * size, v1 * size);
12869
13301
  const Ctor = a.array.constructor;
12870
13302
  geo.setAttribute(name, new this.THREE.BufferAttribute(new Ctor(slice), size, a.normalized));
@@ -13022,11 +13454,29 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13022
13454
  // src/renderer.ts
13023
13455
  var rendererCount = 0;
13024
13456
  var COLLAPSE_CLOUD_KG = 1e3;
13457
+ var REBAR_COLOR = 7628380;
13458
+ var STEEL_DENSITY = 7850;
13025
13459
  var CHIP_DECAL_EDGE = 0.05;
13026
13460
  var WOOD_RIM = 1.5;
13027
13461
  var IMPACT_TAGS = ["metal", "glass", "wood"];
13028
13462
  var GLOW_FROM_C = 430;
13029
13463
  var QUEUE_CAP = 4e3;
13464
+ var STRUCTURAL_EVENTS = /* @__PURE__ */ new Set([
13465
+ "fracture",
13466
+ "detach",
13467
+ "collapse",
13468
+ "settle",
13469
+ "destroyed",
13470
+ "expose",
13471
+ "stage",
13472
+ "hole",
13473
+ "ignite",
13474
+ "primed",
13475
+ "extinguish",
13476
+ "burnout",
13477
+ "leak",
13478
+ "explode"
13479
+ ]);
13030
13480
  var MIN_FRAGMENTS_PER_SLICE = 8;
13031
13481
  var STATIC_REFRESH = 0.5;
13032
13482
  var STATIC_REFRESH_SLICE_MS = 1;
@@ -13135,6 +13585,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13135
13585
  __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
13136
13586
  __publicField(this, "staticAt", 0);
13137
13587
  __publicField(this, "queue", []);
13588
+ /** The cover of each body around a part of the world that went off, read as the event is emitted: before the blast tore or moved anything. */
13589
+ __publicField(this, "coverWhenWentOff", /* @__PURE__ */ new WeakMap());
13138
13590
  /** Presentation work an event left for later frames (fragments still to spawn, loose groups still to split), run in order within the frame budget. */
13139
13591
  __publicField(this, "slices", []);
13140
13592
  /** When the work of this frame must stop; `Infinity` outside a drain, where a call finishes what it started. */
@@ -13214,7 +13666,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13214
13666
  groundAt: this.groundAt,
13215
13667
  gravity: this.gravity,
13216
13668
  max: this.maxFragments,
13217
- life: o.fragmentLife ?? 12
13669
+ life: o.fragmentLife ?? 12,
13670
+ formOf: (id) => this.world.materials.get(id)?.splitForm
13218
13671
  });
13219
13672
  this.cages = new RebarCages(THREE, o.scene);
13220
13673
  this.pieceSys = new PieceSystem(THREE, {
@@ -13227,7 +13680,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13227
13680
  onSlam: (p, hit, speed) => this.slam(p, hit, speed)
13228
13681
  });
13229
13682
  this.unsubscribe = this.world.on("*", (ev) => {
13230
- if (this.queue.length < QUEUE_CAP) this.queue.push(ev);
13683
+ if (ev.type === "explode") this.coverWhenWentOff.set(ev, this.coverAround(ev));
13684
+ if (this.queue.length < QUEUE_CAP || STRUCTURAL_EVENTS.has(ev.type)) this.queue.push(ev);
13231
13685
  });
13232
13686
  }
13233
13687
  // ----------------------------------------------------------------------------------------------------------------------------------
@@ -13750,7 +14204,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13750
14204
  this.onChip(ev);
13751
14205
  break;
13752
14206
  case "expose":
13753
- this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
14207
+ if (ev.bars.length > 0) this.cages.set(`${ev.object}|${ev.part}`, ev.bars, this.bounds.get(ev.object)?.root);
14208
+ else this.dropCage(ev.object, ev.part);
13754
14209
  if (ev.bars.length > 0) {
13755
14210
  const [a, b] = ev.bars[0];
13756
14211
  this.playEvent({ kind: "expose" }, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2]);
@@ -13784,8 +14239,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13784
14239
  }
13785
14240
  case "melt":
13786
14241
  this.activate(ev.object, ev.part);
13787
- if (this.time - (this.throttles.get(`melt:${ev.object}|${ev.part}`) ?? -9) > 1.5) {
13788
- this.throttles.set(`melt:${ev.object}|${ev.part}`, this.time);
14242
+ if (this.time - (this.throttles.get(`melt:${ev.object}`) ?? -9) > 1.5) {
14243
+ this.throttles.set(`melt:${ev.object}`, this.time);
13789
14244
  this.sound("melt_sizzle", this.rootPoint(ev.object), 0.4);
13790
14245
  }
13791
14246
  break;
@@ -14048,7 +14503,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14048
14503
  const rotation2 = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
14049
14504
  for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation2);
14050
14505
  this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
14051
- this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
14506
+ this.vfx.burst("dust", ev.center, [0, 1, 0], ev.units ? Math.min(40, 8 + ev.units.length) : 8, ev.units ? 1.6 : 0.8, { color });
14052
14507
  this.impactSound(pv?.material ?? "", ev.center, 0.7, ev.blow);
14053
14508
  }
14054
14509
  onFracture(ev) {
@@ -14067,6 +14522,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14067
14522
  this.visuals.hide(pv, "broken");
14068
14523
  }
14069
14524
  this.spawnFragments(ev, 0, { def, color, opacity, transparent });
14525
+ this.loosenIfFootless(ev.object);
14070
14526
  const up = [0, 1, 0];
14071
14527
  const sizeK = clamp6(ev.fragments.length / 10, 0.5, 2);
14072
14528
  switch (ev.style) {
@@ -14112,6 +14568,33 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14112
14568
  this.sound("detach", this.rootPoint(ev.object), 0.5);
14113
14569
  this.splitLoose(ev.object);
14114
14570
  }
14571
+ /**
14572
+ * The cage of a part gave way (`expose` with no bars): nothing holds its bars any more, so each comes down where it is, as steel debris that stays on the
14573
+ * ground with a body (`FragmentSystem.spawn` with `lasting`), not as bars left standing in the air.
14574
+ */
14575
+ dropCage(objectId, partId) {
14576
+ const bars = this.cages.take(`${objectId}|${partId}`);
14577
+ if (bars.length === 0) return;
14578
+ const radius = this.cages.barRadius;
14579
+ const density = this.world.materials.get("steel")?.density ?? STEEL_DENSITY;
14580
+ const material = this.fragments.materialFor("steel", REBAR_COLOR, false);
14581
+ const rnd2 = createRng(hashString(`${objectId}|${partId}`));
14582
+ bars.forEach((bar, i) => {
14583
+ const fragment = {
14584
+ id: `${partId}#bar${i}`,
14585
+ part: partId,
14586
+ material: "steel",
14587
+ shape: "chunk",
14588
+ center: bar.center,
14589
+ size: [2 * radius, bar.length, 2 * radius],
14590
+ rotation: bar.rotation,
14591
+ velocity: [(rnd2() - 0.5) * 0.4, 0, (rnd2() - 0.5) * 0.4],
14592
+ angularVelocity: [(rnd2() - 0.5) * 1.5, (rnd2() - 0.5) * 0.5, (rnd2() - 0.5) * 1.5],
14593
+ mass: density * Math.PI * radius * radius * bar.length
14594
+ };
14595
+ this.fragments.spawn(fragment, material, objectId, this.maxFragments, true);
14596
+ });
14597
+ }
14115
14598
  /** Turn every loose group of the object (a detach, a collapse) into a piece of its own. */
14116
14599
  splitLoose(objectId) {
14117
14600
  if (!this.autoSplit) return;
@@ -14166,10 +14649,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14166
14649
  if (!pv) continue;
14167
14650
  from.parts.delete(p.id);
14168
14651
  nb.parts.set(p.id, pv);
14652
+ this.cages.move(`${from.id}|${p.id}`, `${newId}|${p.id}`, group);
14169
14653
  for (const mv of pv.meshes) {
14170
14654
  if (mv.host && !mv.loose) this.visuals.extract(mv, group);
14171
14655
  else {
14172
14656
  group.attach(mv.mesh);
14657
+ showCollapsedRanges(mv.mesh);
14173
14658
  mv.moved = true;
14174
14659
  }
14175
14660
  }
@@ -14196,6 +14681,45 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14196
14681
  if (kitOriginOf(from.root)?.kind === "furniture") this.tipIfTopHeavy(nb);
14197
14682
  this.o.onPiece?.(piece);
14198
14683
  }
14684
+ /**
14685
+ * Furniture that lost what it stood on falls: with every leg of a table broken the top is loose, and with the legs of one side gone what is left topples. Feet are the
14686
+ * meshes at the bottom of the object as it was whole; while the ones left still span the same floor (a cupboard with a broken door, a table missing one leg) it stands.
14687
+ */
14688
+ loosenIfFootless(objectId) {
14689
+ const b = this.bounds.get(objectId);
14690
+ if (!b || !this.autoSplit) return;
14691
+ const left = this.meshesOf(b).filter((mv) => this.visuals.shown(mv));
14692
+ if (left.length === 0) return;
14693
+ const body = b.piece ?? this.pieceSys.get(objectId);
14694
+ if (body) {
14695
+ const { half, center } = this.measure(b.root, left);
14696
+ body.halfExtents = half;
14697
+ this.pieceSys.setCenter(body, center);
14698
+ body.mass = Math.max(0.05, this.massOf(objectId));
14699
+ body.sleeping = false;
14700
+ return;
14701
+ }
14702
+ if (kitOriginOf(b.root)?.kind !== "furniture") return;
14703
+ const boxOf = (mv) => this.visuals.worldBox(mv, new this.THREE.Box3());
14704
+ const whole = [...b.parts.values()].flatMap((pv) => pv.meshes).map(boxOf).filter((one) => !one.isEmpty());
14705
+ const floor2 = Math.min(...whole.map((one) => one.min.y));
14706
+ const feetOf = (boxes) => boxes.filter((one) => one.min.y < floor2 + FEET_BAND).reduce((feet, one) => feet.union(one), new this.THREE.Box3());
14707
+ const was = feetOf(whole);
14708
+ const now = feetOf(left.map(boxOf).filter((one) => !one.isEmpty()));
14709
+ const same = !now.isEmpty() && now.min.x <= was.min.x + FEET_BAND && now.max.x >= was.max.x - FEET_BAND && now.min.z <= was.min.z + FEET_BAND && now.max.z >= was.max.z - FEET_BAND;
14710
+ if (same) return;
14711
+ if (!now.isEmpty()) {
14712
+ const box = left.map(boxOf).reduce((all, one) => all.union(one), new this.THREE.Box3());
14713
+ const centreX = (box.min.x + box.max.x) / 2;
14714
+ const centreZ = (box.min.z + box.max.z) / 2;
14715
+ const height = box.max.y - box.min.y;
14716
+ const narrow = height >= TIP_MIN_HEIGHT && Math.min(now.max.x - now.min.x, now.max.z - now.min.z) < TIP_NARROW * height;
14717
+ if (!narrow && centreX > now.min.x && centreX < now.max.x && centreZ > now.min.z && centreZ < now.max.z) return;
14718
+ }
14719
+ const piece = this.makePiece(objectId);
14720
+ this.tipIfTopHeavy(b);
14721
+ this.o.onLoosened?.(objectId, piece, "apart");
14722
+ }
14199
14723
  /**
14200
14724
  * A piece of furniture standing on feet too close for its height (a board on two legs in a line, a lone leg) does not stay up: the box of a piece hides that its weight is up
14201
14725
  * high and its feet are thin, so it gets the start of the fall, across the line of its feet.
@@ -14227,7 +14751,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14227
14751
  const report = options.report;
14228
14752
  const confinement = report?.find((r) => r.blast !== void 0)?.blast?.confinement ?? 1;
14229
14753
  const energy = ev.energy * confinement;
14230
- const reachOf = (objectId, at3) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? this.world.blastReach?.(p, at3, { ignore: ev.object ? [ev.object, objectId] : [objectId] }) ?? 1;
14754
+ const wentOff = this.coverWhenWentOff.get(ev);
14755
+ const reachOf = (objectId, at3) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? wentOff?.get(objectId) ?? this.world.blastReach?.(p, at3, { ignore: ev.object ? [ev.object, objectId] : [objectId] }) ?? 1;
14231
14756
  const shove = (pc) => {
14232
14757
  this.pieceSys.centerOf(pc, c);
14233
14758
  if (c.distanceTo(this.v2.set(p[0], p[1], p[2])) > ev.radius) return;
@@ -14247,6 +14772,29 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14247
14772
  ...report ? { confinement } : {}
14248
14773
  });
14249
14774
  }
14775
+ /** The cover between a part of the world that goes off and the centre of each piece and each shoveable body within its radius, as the world stands at that moment. */
14776
+ coverAround(ev) {
14777
+ const cover = /* @__PURE__ */ new Map();
14778
+ const reach = this.world.blastReach;
14779
+ if (!reach) return cover;
14780
+ const point = new this.THREE.Vector3(ev.point[0], ev.point[1], ev.point[2]);
14781
+ const centre = new this.THREE.Vector3();
14782
+ const read = (objectId) => {
14783
+ if (centre.distanceTo(point) <= ev.radius) cover.set(objectId, reach.call(this.world, ev.point, [centre.x, centre.y, centre.z], { ignore: [ev.object, objectId] }));
14784
+ };
14785
+ for (const pc of this.pieceSys.list) {
14786
+ this.pieceSys.centerOf(pc, centre);
14787
+ read(pc.objectId);
14788
+ }
14789
+ const shoveable = this.o.shoveable;
14790
+ if (!shoveable) return cover;
14791
+ for (const b of this.bounds.values()) {
14792
+ if (b.piece || cover.has(b.id) || !shoveable(b.id) || !this.world.has(b.id)) continue;
14793
+ centre.copy(this.measure(b.root, this.meshesOf(b)).center).applyQuaternion(b.root.quaternion).add(b.root.position);
14794
+ read(b.id);
14795
+ }
14796
+ return cover;
14797
+ }
14250
14798
  /** The bound objects without a piece that the host lets a blast move (`shoveable`): the ones it reaches with enough speed become pieces and get the shove. */
14251
14799
  loosen(point, radius, energy, reachOf) {
14252
14800
  const shoveable = this.o.shoveable;
@@ -14542,7 +15090,63 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14542
15090
  }
14543
15091
  };
14544
15092
 
14545
- // ../game-audio/dist/chunk-BXUGSOPY.js
15093
+ // ../game-audio/dist/chunk-JV5XXCQE.js
15094
+ var SFX_TIERS = { light: 0, medium: 0.34, heavy: 0.67 };
15095
+ function splitTier(id) {
15096
+ const match = /^(.*)_(light|medium|heavy)$/.exec(id);
15097
+ return match ? { base: match[1], tier: match[2] } : { base: id };
15098
+ }
15099
+ var refsOf = (entries2, baseUrl) => (entries2 ?? []).map((entry) => ({ url: `${baseUrl}${entry.file}` }));
15100
+ function sampleBankFromManifest(manifest, options) {
15101
+ const baseUrl = options.baseUrl ?? "";
15102
+ const samples = [];
15103
+ const tiersOf = /* @__PURE__ */ new Map();
15104
+ for (const sound of manifest.sounds) {
15105
+ const { base, tier } = splitTier(sound.id);
15106
+ if (tier) tiersOf.set(base, [...tiersOf.get(base) ?? [], sound]);
15107
+ }
15108
+ const bases = /* @__PURE__ */ new Map();
15109
+ for (const sound of manifest.sounds) {
15110
+ if (!splitTier(sound.id).tier) bases.set(sound.id, sound);
15111
+ }
15112
+ for (const [base, tiers] of tiersOf) {
15113
+ if (!bases.has(base)) bases.set(base, { id: base, category: tiers[0]?.category ?? "", layers: { close: tiers[0]?.layers.close ?? [] } });
15114
+ }
15115
+ for (const sound of bases.values()) {
15116
+ const numbers = options.sounds?.[sound.id] ?? options.defaults?.(sound);
15117
+ const close = refsOf(sound.layers.close, baseUrl);
15118
+ if (!numbers || close.length === 0) continue;
15119
+ const tiers = (tiersOf.get(sound.id) ?? []).map((tier) => {
15120
+ const name = splitTier(tier.id).tier;
15121
+ const tierFar = refsOf(tier.layers.far, baseUrl);
15122
+ return { name, from: SFX_TIERS[name] ?? 0, close: refsOf(tier.layers.close, baseUrl), ...tierFar.length > 0 ? { far: tierFar } : {} };
15123
+ }).sort((a, b) => a.from - b.from);
15124
+ const far = refsOf(sound.layers.far, baseUrl);
15125
+ const indoor = refsOf(sound.layers.tail_in, baseUrl);
15126
+ const outdoor = refsOf(sound.layers.tail_out, baseUrl);
15127
+ const files = [sound, ...tiersOf.get(sound.id) ?? []].flatMap((entry) => Object.values(entry.layers).flatMap((entries2) => entries2 ?? []));
15128
+ samples.push({
15129
+ id: sound.id,
15130
+ bus: numbers.bus,
15131
+ close,
15132
+ ...far.length > 0 ? { far } : {},
15133
+ ...tiers.length > 0 ? { tiers } : {},
15134
+ ...numbers.crossover ? { crossover: numbers.crossover } : {},
15135
+ ...indoor.length > 0 || outdoor.length > 0 ? { tail: { ...indoor.length > 0 ? { indoor } : {}, ...outdoor.length > 0 ? { outdoor } : {}, ...numbers.tailGain === void 0 ? {} : { gain: numbers.tailGain } } } : {},
15136
+ ...numbers.pitch === void 0 ? {} : { pitch: numbers.pitch },
15137
+ ...numbers.gain === void 0 ? {} : { gain: numbers.gain },
15138
+ ...numbers.falloff ? { falloff: numbers.falloff } : {},
15139
+ ...numbers.cutoffK === void 0 ? {} : { cutoffK: numbers.cutoffK },
15140
+ ...numbers.group ? { group: numbers.group } : {},
15141
+ ...numbers.priority === void 0 ? {} : { priority: numbers.priority },
15142
+ ...numbers.fallback ? { fallback: numbers.fallback } : {},
15143
+ sources: files.map((file) => `${file.file}: ${file.source} ${file.author} ${file.license}`)
15144
+ });
15145
+ }
15146
+ return { id: options.id, samples };
15147
+ }
15148
+
15149
+ // ../game-audio/dist/chunk-UUWKSG4J.js
14546
15150
  var DISTANCE_CURVES = {
14547
15151
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
14548
15152
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -14977,60 +15581,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14977
15581
  burst: 20,
14978
15582
  burstSpacing: 0.06
14979
15583
  };
14980
- var SFX_TIERS = { light: 0, medium: 0.34, heavy: 0.67 };
14981
- function splitTier(id) {
14982
- const match = /^(.*)_(light|medium|heavy)$/.exec(id);
14983
- return match ? { base: match[1], tier: match[2] } : { base: id };
14984
- }
14985
- var refsOf = (entries2, baseUrl) => (entries2 ?? []).map((entry) => ({ url: `${baseUrl}${entry.file}` }));
14986
- function sampleBankFromManifest(manifest, options) {
14987
- const baseUrl = options.baseUrl ?? "";
14988
- const samples = [];
14989
- const tiersOf = /* @__PURE__ */ new Map();
14990
- for (const sound of manifest.sounds) {
14991
- const { base, tier } = splitTier(sound.id);
14992
- if (tier) tiersOf.set(base, [...tiersOf.get(base) ?? [], sound]);
14993
- }
14994
- const bases = /* @__PURE__ */ new Map();
14995
- for (const sound of manifest.sounds) {
14996
- if (!splitTier(sound.id).tier) bases.set(sound.id, sound);
14997
- }
14998
- for (const [base, tiers] of tiersOf) {
14999
- if (!bases.has(base)) bases.set(base, { id: base, category: tiers[0]?.category ?? "", layers: { close: tiers[0]?.layers.close ?? [] } });
15000
- }
15001
- for (const sound of bases.values()) {
15002
- const numbers = options.sounds?.[sound.id] ?? options.defaults?.(sound);
15003
- const close = refsOf(sound.layers.close, baseUrl);
15004
- if (!numbers || close.length === 0) continue;
15005
- const tiers = (tiersOf.get(sound.id) ?? []).map((tier) => {
15006
- const name = splitTier(tier.id).tier;
15007
- const tierFar = refsOf(tier.layers.far, baseUrl);
15008
- return { name, from: SFX_TIERS[name] ?? 0, close: refsOf(tier.layers.close, baseUrl), ...tierFar.length > 0 ? { far: tierFar } : {} };
15009
- }).sort((a, b) => a.from - b.from);
15010
- const far = refsOf(sound.layers.far, baseUrl);
15011
- const indoor = refsOf(sound.layers.tail_in, baseUrl);
15012
- const outdoor = refsOf(sound.layers.tail_out, baseUrl);
15013
- const files = [sound, ...tiersOf.get(sound.id) ?? []].flatMap((entry) => Object.values(entry.layers).flatMap((entries2) => entries2 ?? []));
15014
- samples.push({
15015
- id: sound.id,
15016
- bus: numbers.bus,
15017
- close,
15018
- ...far.length > 0 ? { far } : {},
15019
- ...tiers.length > 0 ? { tiers } : {},
15020
- ...numbers.crossover ? { crossover: numbers.crossover } : {},
15021
- ...indoor.length > 0 || outdoor.length > 0 ? { tail: { ...indoor.length > 0 ? { indoor } : {}, ...outdoor.length > 0 ? { outdoor } : {}, ...numbers.tailGain === void 0 ? {} : { gain: numbers.tailGain } } } : {},
15022
- ...numbers.pitch === void 0 ? {} : { pitch: numbers.pitch },
15023
- ...numbers.gain === void 0 ? {} : { gain: numbers.gain },
15024
- ...numbers.falloff ? { falloff: numbers.falloff } : {},
15025
- ...numbers.cutoffK === void 0 ? {} : { cutoffK: numbers.cutoffK },
15026
- ...numbers.group ? { group: numbers.group } : {},
15027
- ...numbers.priority === void 0 ? {} : { priority: numbers.priority },
15028
- ...numbers.fallback ? { fallback: numbers.fallback } : {},
15029
- sources: files.map((file) => `${file.file}: ${file.source} ${file.author} ${file.license}`)
15030
- });
15031
- }
15032
- return { id: options.id, samples };
15033
- }
15584
+ var CATALOG = new Map([...SFX_CATALOG, ...SFX_IMPACTS].map((entry) => [entry.id, entry]));
15034
15585
  var PT = {
15035
15586
  group: "Som",
15036
15587
  name: "Painel de som (arquivos e C1\u2013C10)",
@@ -15105,9 +15656,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15105
15656
  strengths: { light: "leve", medium: "m\xE9dio", heavy: "forte" },
15106
15657
  states: { file: "A", synth: "S", missing: "F" },
15107
15658
  criteria: { C1: "C1 Sincronia", C2: "C2 Certo p/ a\xE7\xE3o", C3: "C3 Dist\xE2ncia", C4: "C4 Posi\xE7\xE3o", C5: "C5 Varia\xE7\xE3o", C6: "C6 Volume", C7: "C7 Intensidade", C8: "C8 Cauda", C9: "C9 Rajada", C10: "C10 Escuta" },
15108
- metrics: { source: "1 \xB7 Fonte do que tocou", chosen: "1 \xB7 Escolhido", matrix: "2 \xB7 Matriz", last: "\xDAltimo tocado", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Prefer\xEAncias", state: "5 \xB7 Medi\xE7\xE3o", count: "1 \xB7 Cat\xE1logo" },
15659
+ metrics: { source: "1 \xB7 Fonte do que tocou", chosen: "1 \xB7 Escolhido", matrix: "2 \xB7 Matriz", last: "\xDAltimo tocado", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Prefer\xEAncias", state: "5 \xB7 Medi\xE7\xE3o", count: "1 \xB7 Cat\xE1logo", unlicensed: "1 \xB7 Arquivo sem licen\xE7a" },
15109
15660
  run: { idle: "parada", measuring: "medindo\u2026", done: "pronta", failed: "falhou", recording: "gravando\u2026", recorded: "grava\xE7\xE3o baixada" },
15110
- words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono" },
15661
+ words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono", local_file: "arquivo do pacote local", sdk_file: "grava\xE7\xE3o do SDK", synth: "s\xEDntese do SDK" },
15111
15662
  sources: { sdk: "SDK", user: "seu som", synth: "sintetizado" },
15112
15663
  probe: "Fonte do som",
15113
15664
  wall: "Parede (oclus\xE3o)",
@@ -15178,9 +15729,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15178
15729
  environments: { indoor: "indoor", outdoor: "outdoor" },
15179
15730
  strengths: { light: "light", medium: "medium", heavy: "heavy" },
15180
15731
  criteria: { C1: "C1 Sync", C2: "C2 Right sound", C3: "C3 Distance", C4: "C4 Position", C5: "C5 Variation", C6: "C6 Level", C7: "C7 Strength", C8: "C8 Tail", C9: "C9 Burst", C10: "C10 Listening" },
15181
- metrics: { source: "1 \xB7 Source of what played", chosen: "1 \xB7 Chosen", matrix: "2 \xB7 Matrix", last: "Last played", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Preferences", state: "5 \xB7 Measurement", count: "1 \xB7 Catalogue" },
15732
+ metrics: { source: "1 \xB7 Source of what played", chosen: "1 \xB7 Chosen", matrix: "2 \xB7 Matrix", last: "Last played", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Preferences", state: "5 \xB7 Measurement", count: "1 \xB7 Catalogue", unlicensed: "1 \xB7 File without licence" },
15182
15733
  run: { idle: "idle", measuring: "measuring\u2026", done: "done", failed: "failed", recording: "recording\u2026", recorded: "recording downloaded" },
15183
- words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner" },
15734
+ words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner", local_file: "local pack file", sdk_file: "SDK recording", synth: "SDK synthesis" },
15184
15735
  sources: { sdk: "SDK", user: "your sound", synth: "synthesized" },
15185
15736
  probe: "Sound source",
15186
15737
  wall: "Wall (occlusion)",
@@ -15263,6 +15814,66 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15263
15814
  sources: [`cena:${line}`]
15264
15815
  };
15265
15816
  }
15817
+ var hz = (midi) => 440 * 2 ** ((midi - 69) / 12);
15818
+ var layer2 = (note, beat, wave, level) => {
15819
+ const [at3, midi, beats] = note;
15820
+ const length = beats * beat;
15821
+ return {
15822
+ at: at3 * beat,
15823
+ duration: length,
15824
+ source: { kind: "osc", wave, frequency: { set: hz(midi) } },
15825
+ envelope: [[0, 1e-4, "set"], [0.01, level, "linear"], [Math.max(0.02, length * 0.9), level * 0.35, "linear"], [length, 1e-4, "exp", "fixed"]]
15826
+ };
15827
+ };
15828
+ function radioTrack(spec) {
15829
+ const beat = 60 / spec.bpm;
15830
+ const recipes = Array.from({ length: spec.bars }, (_, bar) => {
15831
+ const inBar = (notes) => notes.filter((n) => n[0] >= bar * 4 && n[0] < bar * 4 + 4).map((n) => [n[0] - bar * 4, n[1], n[2]]);
15832
+ const layers = [
15833
+ ...inBar(spec.bass).map((n) => layer2(n, beat, "triangle", 0.16)),
15834
+ ...inBar(spec.chords).map((n) => layer2(n, beat, "sine", 0.05)),
15835
+ ...inBar(spec.melody).map((n) => layer2(n, beat, spec.wave, 0.07))
15836
+ ];
15837
+ return { id: `${spec.id}_${bar}`, bus: "world", duration: 4 * beat, layers, falloff: { model: "none" } };
15838
+ });
15839
+ return { song: { id: spec.id, barSeconds: 4 * beat, bars: recipes.map((r) => r.id) }, recipes };
15840
+ }
15841
+ var chord = (bar, root, minor = false) => [[bar * 4, root, 4], [bar * 4, root + (minor ? 3 : 4), 4], [bar * 4, root + 7, 4]];
15842
+ var bassLine = (roots, pattern) => roots.flatMap((root, bar) => pattern.map((b) => [bar * 4 + b, root - 12, 0.9]));
15843
+ var TRACKS = [
15844
+ // a bright pop loop: I–V–vi–IV in C
15845
+ radioTrack({
15846
+ id: "radio_track_pop",
15847
+ bpm: 112,
15848
+ bars: 4,
15849
+ wave: "square",
15850
+ bass: bassLine([48, 55, 57, 53], [0, 1.5, 2, 3]),
15851
+ chords: [...chord(0, 60), ...chord(1, 55), ...chord(2, 57, true), ...chord(3, 53)],
15852
+ melody: [[0, 76, 1], [1, 74, 0.5], [1.5, 72, 1.5], [4, 74, 1], [5, 76, 1], [6, 79, 2], [8, 76, 1], [9, 74, 1], [10, 72, 2], [12, 72, 1], [13, 74, 1], [14, 72, 2]]
15853
+ }),
15854
+ // a syncopated loop in A minor, the bass on the off-beats
15855
+ radioTrack({
15856
+ id: "radio_track_groove",
15857
+ bpm: 100,
15858
+ bars: 4,
15859
+ wave: "triangle",
15860
+ bass: bassLine([57, 62, 64, 57], [0, 0.75, 1.5, 2.5, 3.25]),
15861
+ chords: [...chord(0, 57, true), ...chord(1, 62, true), ...chord(2, 64), ...chord(3, 57, true)],
15862
+ melody: [[0.5, 72, 0.5], [1.25, 76, 0.75], [2.5, 74, 1], [4.5, 77, 0.5], [5.25, 74, 0.75], [6.5, 72, 1.5], [8.5, 71, 0.5], [9.25, 74, 0.75], [10.5, 76, 1.5], [12.5, 72, 0.5], [13.25, 69, 2]]
15863
+ }),
15864
+ // a slow blues-like loop in F with a swung melody
15865
+ radioTrack({
15866
+ id: "radio_track_slow",
15867
+ bpm: 76,
15868
+ bars: 4,
15869
+ wave: "sine",
15870
+ bass: bassLine([53, 58, 53, 60], [0, 2]),
15871
+ chords: [...chord(0, 65), ...chord(1, 58), ...chord(2, 65), ...chord(3, 60)],
15872
+ melody: [[0, 72, 1.5], [1.67, 75, 0.33], [2, 77, 2], [4, 74, 1], [5, 72, 1], [6, 70, 2], [8, 72, 1.5], [9.67, 69, 0.33], [10, 65, 2], [12, 67, 1], [13, 70, 1], [14, 72, 2]]
15873
+ })
15874
+ ];
15875
+ var RADIO_TRACKS = TRACKS.map((t) => t.song);
15876
+ var RADIO_BANK = { id: "vehicle_radio", sounds: TRACKS.flatMap((t) => t.recipes), loops: [] };
15266
15877
  var FOLEY_FALLOFF = {
15267
15878
  model: "inverse_square",
15268
15879
  k: 0.02,
@@ -15869,11 +16480,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15869
16480
  ...s.splash ? [sceneNoise({ type: "bandpass", f: s.splash.f * take.f, f2: s.splash.f2 * take.f, q: 1.4, d: s.splash.d * take.t * weight ** 0.4, vol: s.splash.vol * gait.vol * take.v * weight ** 0.5 })] : []
15870
16481
  ];
15871
16482
  }
15872
- var quieter = (level, layers) => layers.map((layer2) => {
15873
- const gain = layer2.gain ?? 1;
15874
- if (typeof gain === "number") return { ...layer2, gain: gain * level };
15875
- if (Array.isArray(gain)) return { ...layer2, gain: [gain[0] * level, gain[1] * level] };
15876
- return layer2;
16483
+ var quieter = (level, layers) => layers.map((layer22) => {
16484
+ const gain = layer22.gain ?? 1;
16485
+ if (typeof gain === "number") return { ...layer22, gain: gain * level };
16486
+ if (Array.isArray(gain)) return { ...layer22, gain: [gain[0] * level, gain[1] * level] };
16487
+ return layer22;
15877
16488
  });
15878
16489
  var recipe = (id, layers, falloff, bus = "world") => ({ id, bus, pan: true, layers, falloff, sources: ["sdk"] });
15879
16490
  var takeId = (id, index) => `${id}_${String(index + 1).padStart(2, "0")}`;
@@ -15889,7 +16500,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15889
16500
  (take) => quieter(LANDING_LEVEL, [
15890
16501
  // both feet, a hair apart, then the knees taking the weight (a low body thump) and the clothes settling
15891
16502
  ...footLayers(surface, take, GAITS.run, LANDINGS[landing2].weight),
15892
- ...footLayers(surface, { ...take, v: take.v * 0.6 }, GAITS.run, LANDINGS[landing2].weight * 0.6).map((layer2) => ({ ...layer2, at: (typeof layer2.at === "number" ? layer2.at : 0) + 0.018 * take.t })),
16503
+ ...footLayers(surface, { ...take, v: take.v * 0.6 }, GAITS.run, LANDINGS[landing2].weight * 0.6).map((layer22) => ({ ...layer22, at: (typeof layer22.at === "number" ? layer22.at : 0) + 0.018 * take.t })),
15893
16504
  sceneTone({ f: 60 * take.f, f2: 38, d: 0.14 * LANDINGS[landing2].weight * take.t, vol: 0.22 * LANDINGS[landing2].weight ** 0.7 * take.v, delay: 0.02 }),
15894
16505
  sceneNoise({ type: "bandpass", f: [1800, 2600], q: 0.8, d: 0.18, vol: LANDINGS[landing2].cloth * take.v, delay: 0.04 })
15895
16506
  ]),
@@ -15935,7 +16546,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15935
16546
  fall_damage: (take) => [
15936
16547
  ...BODY.body_fall(take),
15937
16548
  sceneNoise({ type: "highpass", f: 2800, d: 0.02, vol: 0.35 * take.v, delay: 0.01 }),
15938
- ...voice({ f0: 170, f1: 110, d: 0.32, fA: 650, fB: 1150, noise: 0.25, vol: 0.32 })(take).map((layer2) => ({ ...layer2, at: 0.08 }))
16549
+ ...voice({ f0: 170, f1: 110, d: 0.32, fA: 650, fB: 1150, noise: 0.25, vol: 0.32 })(take).map((layer22) => ({ ...layer22, at: 0.08 }))
15939
16550
  ],
15940
16551
  player_hurt: voice({ f0: 150, f1: 105, d: 0.26, fA: 620, fB: 1100, noise: 0.3, vol: 0.35 }),
15941
16552
  player_breath_run: (take) => [
@@ -16231,7 +16842,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16231
16842
  };
16232
16843
 
16233
16844
  // src/audio/recipes.ts
16234
- var scene = (id, layers, line, vars) => sceneRecipe(id, layers, line, vars ? { vars } : {});
16845
+ var scene = (id, layers, line, vars, group) => sceneRecipe(id, layers, line, { ...vars ? { vars } : {}, ...group ? { group } : {} });
16235
16846
  var boomLayers = (big) => [
16236
16847
  sceneNoise({ f: 1200, f2: 120, d: 1.8, vol: 2, wet: 1.2 }),
16237
16848
  sceneTone({ f: 65, f2: 24, d: 1, vol: 1.8 }),
@@ -16250,7 +16861,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16250
16861
  [sceneNoise({ type: "bandpass", f: 850, q: 1, d: 0.25, vol: 0.9, wet: 0.3 }), sceneTone({ f: 210, f2: 90, d: 0.1, vol: 0.6 }), sceneNoise({ type: "highpass", f: 2e3, d: 0.04, vol: 0.5 })],
16251
16862
  "L1360 wood"
16252
16863
  ),
16253
- scene("thunk", [sceneNoise({ type: "bandpass", f: 700, q: 2, d: 0.07, vol: 0.4 })], "L1361 thunk"),
16864
+ // many parts coming loose at once share the `debris_knock` voices (game-audio voice-groups.json)
16865
+ scene("thunk", [sceneNoise({ type: "bandpass", f: 700, q: 2, d: 0.07, vol: 0.4 })], "L1361 thunk", void 0, "debris_knock"),
16254
16866
  scene("ping", [sceneTone({ f: [2600, 3500], f2: 1400, d: 0.14, vol: 0.18, wet: 0.3 })], "L1362 ping"),
16255
16867
  scene("clang", [sceneTone({ f: [520, 720], f2: 480, d: 0.5, vol: 0.35, wet: 0.4 }), sceneTone({ f: 1370, f2: 1300, d: 0.3, vol: 0.15 }), sceneNoise({ type: "highpass", f: 2e3, d: 0.05, vol: 0.5 })], "L1363 clang"),
16256
16868
  scene("chip", [sceneNoise({ type: "bandpass", f: 1600, q: 1.2, d: 0.06, vol: 0.5 })], "L1364 chip"),
@@ -16262,7 +16874,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16262
16874
  // `thud(x, z, v)`: `v` is the `scale` param (default 1)
16263
16875
  scene("thud", [sceneTone({ f: 75, f2: 38, d: 0.22, vol: { param: "scale", scale: 0.6 } }), sceneNoise({ f: 420, d: 0.16, vol: { param: "scale", scale: 0.45 } })], "L1358 thud", { scale: 1 }),
16264
16876
  // `hiss(x, z, d)`: `seconds` is the param `d` (default 1.2)
16265
- scene("hiss", [sceneNoise({ type: "highpass", f: 3500, a: 0.05, d: { param: "seconds" }, vol: 0.35 })], "L1366 hiss", { seconds: 1.2 }),
16877
+ // every melting part shares one `sizzle` voice
16878
+ scene("hiss", [sceneNoise({ type: "highpass", f: 3500, a: 0.05, d: { param: "seconds" }, vol: 0.35 })], "L1366 hiss", { seconds: 1.2 }, "sizzle"),
16266
16879
  scene("boom", boomLayers(false), "L1367 boom"),
16267
16880
  scene("boom_big", boomLayers(true), "L1367 boom (big)"),
16268
16881
  scene("burst", [sceneNoise({ f: 700, f2: 120, d: 0.7, vol: 1.4, wet: 0.5 }), sceneNoise({ type: "highpass", f: 3e3, a: 0.05, d: 1.2, vol: 0.4, delay: 0.1 })], "L1357 burst"),
@@ -16417,6 +17030,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16417
17030
  break_paper: { sound: "paper_tear" },
16418
17031
  break_plant: { sound: "branch_cut" }
16419
17032
  };
17033
+ var impactSoundIds = (rules = IMPACT_SOUND_KEYS) => {
17034
+ const ids = /* @__PURE__ */ new Set();
17035
+ for (const rule of Object.values(rules)) {
17036
+ ids.add(rule.sound);
17037
+ if (rule.above) ids.add(rule.above.sound);
17038
+ if (rule.fallback) ids.add(rule.fallback);
17039
+ }
17040
+ return [...ids];
17041
+ };
16420
17042
  var DESTRUCTION_SOUND_KEYS = {
16421
17043
  art_scene: {
16422
17044
  impact_metal: { sound: "clang" },