@adatechnology/game-destruction-three 0.15.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -100,6 +100,7 @@ var GameDestructionThree = (() => {
100
100
 
101
101
  // ../game-contracts/dist/index.js
102
102
  var DEFAULT_BLAST_SHOVE = { coupling: 0.02, maxSpeed: 20, lift: 0.3, minSpeed: 0.3, falloffPower: 2 };
103
+ var clampShare = (share) => Math.min(1, Math.max(0, share));
103
104
  function blastShove(blast3, target, options = {}) {
104
105
  const o = { ...DEFAULT_BLAST_SHOVE, ...options };
105
106
  const dx = target.position[0] - blast3.point[0];
@@ -109,7 +110,8 @@ var GameDestructionThree = (() => {
109
110
  if (!(blast3.radius > 0) || distance > blast3.radius) return void 0;
110
111
  const falloff = (1 - distance / blast3.radius) ** o.falloffPower;
111
112
  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;
113
+ const reach = clampShare(blast3.reach ?? 1);
114
+ const speed = Math.sqrt(reach) * Math.min(o.maxSpeed, Math.sqrt(2 * blast3.energy * o.coupling / mass)) * falloff;
113
115
  if (!(speed >= o.minSpeed)) return void 0;
114
116
  const direction = distance > 1e-4 ? [dx / distance, dy / distance + o.lift, dz / distance] : [0, 1, 0];
115
117
  const length = Math.hypot(direction[0], direction[1], direction[2]);
@@ -1572,16 +1574,16 @@ var GameDestructionThree = (() => {
1572
1574
  if (this.spaces.delete(id)) this.list = [...this.spaces.values()];
1573
1575
  return;
1574
1576
  }
1575
- const [hx, hy, hz] = enclosure.halfSize;
1577
+ const [hx, hy, hz2] = enclosure.halfSize;
1576
1578
  const space = {
1577
1579
  cx: enclosure.center[0],
1578
1580
  cy: enclosure.center[1],
1579
1581
  cz: enclosure.center[2],
1580
1582
  hx: Math.abs(hx),
1581
1583
  hy: Math.abs(hy),
1582
- hz: Math.abs(hz),
1584
+ hz: Math.abs(hz2),
1583
1585
  r: rotation(enclosure.orientation),
1584
- radius: Math.hypot(hx, hy, hz),
1586
+ radius: Math.hypot(hx, hy, hz2),
1585
1587
  openings: enclosure.openings ?? [],
1586
1588
  leak: Math.max(0, enclosure.leak ?? 0)
1587
1589
  };
@@ -5764,13 +5766,14 @@ var GameDestructionThree = (() => {
5764
5766
  function composeAfterEffects(context) {
5765
5767
  const { core, def, random, x, y, z } = context;
5766
5768
  const mark = def.mark;
5767
- if (mark.crater && mark.dent && y < 1.2) {
5769
+ const keepsMarks = !core.marklessAt?.(x, z);
5770
+ if (keepsMarks && mark.crater && mark.dent && y < 1.2) {
5768
5771
  core.emit({ type: "crater", x, z, radius: mark.crater, depth: mark.dent });
5769
5772
  core.trace?.crater?.(x, z, mark.crater, mark.dent);
5770
5773
  addCrater(core.marks, core.ground, random, { x, z, radius: mark.crater, depth: mark.dent, marks: core.layers.mark });
5771
5774
  if (core.layers.shrapnel) composeRimDebris(context);
5772
5775
  }
5773
- if (mark.scorch && y < 1.2 && core.layers.mark) {
5776
+ if (keepsMarks && mark.scorch && y < 1.2 && core.layers.mark) {
5774
5777
  core.trace?.scorch?.(x, z, mark.scorch);
5775
5778
  addScorch(core.marks, random, x, z, mark.scorch);
5776
5779
  }
@@ -7479,7 +7482,7 @@ var GameDestructionThree = (() => {
7479
7482
  const wind = { x: DEFAULT_WIND.x, y: DEFAULT_WIND.y, z: DEFAULT_WIND.z };
7480
7483
  if (options.wind) [wind.x, wind.y, wind.z] = options.wind;
7481
7484
  const camera2 = { x: 0, y: 1.6, z: 0 };
7482
- const layers = Object.fromEntries(VFX_LAYERS.map((layer2) => [layer2, true]));
7485
+ const layers = Object.fromEntries(VFX_LAYERS.map((layer3) => [layer3, true]));
7483
7486
  const ground = options.ground;
7484
7487
  const groundAt = options.groundAt ?? (ground ? (x, z) => ground.heightAt(x, z) : () => 0);
7485
7488
  const handles = { next: 0, events: 0 };
@@ -7521,6 +7524,7 @@ var GameDestructionThree = (() => {
7521
7524
  spawned: { puffs: 0, sparks: 0, chunks: 0, lights: 0 },
7522
7525
  clock: { time: 0, lastBlast: -1, lod: 1, forcedLod: null },
7523
7526
  groundAt,
7527
+ ...options.marklessAt ? { marklessAt: options.marklessAt } : {},
7524
7528
  emit: (request) => options.onRequest?.(request)
7525
7529
  };
7526
7530
  function definitionOf(id) {
@@ -7547,6 +7551,7 @@ var GameDestructionThree = (() => {
7547
7551
  return player ?? (player = createEffectPlayer({ engine: workshop(), effects, view }));
7548
7552
  }
7549
7553
  return {
7554
+ hostGroundAt: options.groundAt,
7550
7555
  play(id, point, playOptions = {}) {
7551
7556
  if (!(id in effects)) return null;
7552
7557
  const seed = playOptions.seed ?? (randomMode === "shared" ? void 0 : Math.imul(++handles.events, AUTO_SEED) + (options.seed ?? 0));
@@ -7609,10 +7614,10 @@ var GameDestructionThree = (() => {
7609
7614
  const fires = core.fires;
7610
7615
  for (let index = fires.items.length - 1; index >= 0; index--) if (fires.items[index].handle === handle) fires.removeAt(index);
7611
7616
  },
7612
- setLayer(layer2, on) {
7613
- layers[layer2] = on;
7617
+ setLayer(layer3, on) {
7618
+ layers[layer3] = on;
7614
7619
  },
7615
- layer: (layer2) => layers[layer2],
7620
+ layer: (layer3) => layers[layer3],
7616
7621
  setCamera(position) {
7617
7622
  camera2.x = position[0];
7618
7623
  camera2.y = position[1];
@@ -7730,7 +7735,7 @@ var GameDestructionThree = (() => {
7730
7735
  };
7731
7736
  var EFFECT_STEP_SECONDS = 1 / 50;
7732
7737
 
7733
- // ../game-vfx-three/dist/chunk-2MZDWTHX.js
7738
+ // ../game-vfx-three/dist/chunk-CPRCWTSQ.js
7734
7739
  var GLOW_VERTEX = `attribute vec4 aP;attribute vec4 aC;varying vec2 vU;varying vec4 vC;
7735
7740
  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
7741
  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 +8175,11 @@ void main(){
8170
8175
  const dirt = texture(paint(make, (context, size) => drawDirt(context, size, random)));
8171
8176
  return { scorch, dirt };
8172
8177
  }
8173
- var MAX_CONFORM_STEPS = 48;
8178
+ var FLAT_STEPS_PER_METRE = 8;
8179
+ var MAX_FLAT_STEPS = 48;
8180
+ var DUG_STEPS_PER_METRE = 20;
8181
+ var MAX_DUG_STEPS = 64;
8182
+ var FLAT_TOLERANCE = 2e-3;
8174
8183
  var MIN_CONFORM_STEPS = 16;
8175
8184
  var BLOOD_GROW_RATE = 1.2;
8176
8185
  var BLOOD_START_GROWTH = 0.05;
@@ -8253,9 +8262,21 @@ void main(){
8253
8262
  spareGeometries.push(oldest.mesh.geometry);
8254
8263
  if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
8255
8264
  }
8265
+ const heightAt = options.heightAt ?? ((x, z) => options.ground?.heightAt(x, z) ?? 0);
8266
+ function groundDipsUnder(x, z, radius) {
8267
+ const centre = heightAt(x, z);
8268
+ for (const share of [0.25, 0.5, 0.75]) {
8269
+ for (let ray = 0; ray < 8; ray++) {
8270
+ const angle = ray / 8 * Math.PI * 2;
8271
+ if (Math.abs(heightAt(x + Math.cos(angle) * radius * share, z + Math.sin(angle) * radius * share) - centre) > FLAT_TOLERANCE) return true;
8272
+ }
8273
+ }
8274
+ return false;
8275
+ }
8256
8276
  function conform(mark, material) {
8257
8277
  const radius = mark.radius;
8258
- const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
8278
+ const dug = options.heightAt !== void 0 && groundDipsUnder(mark.x, mark.z, radius);
8279
+ 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
8280
  if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
8260
8281
  const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
8261
8282
  const grid = geometry.attributes.position;
@@ -8267,7 +8288,7 @@ void main(){
8267
8288
  const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
8268
8289
  const worldX = mark.x + localX * cos2 - localZ * sin2;
8269
8290
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
8270
- grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
8291
+ grid.setXYZ(index, worldX, heightAt(worldX, worldZ) + mark.lift, worldZ);
8271
8292
  }
8272
8293
  grid.needsUpdate = true;
8273
8294
  geometry.computeVertexNormals();
@@ -8939,7 +8960,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8939
8960
  decalLayer(THREE, engine.decals),
8940
8961
  ringLayer(THREE, engine.rings)
8941
8962
  ];
8942
- for (const layer2 of layers) group.add(layer2.object);
8963
+ for (const layer22 of layers) group.add(layer22.object);
8943
8964
  const meshes = createMeshLayer(THREE, engine.meshes);
8944
8965
  group.add(meshes.group);
8945
8966
  const lights = budget ? budgetedLights(engine, lightScale, budget) : legacyLights(THREE, engine, lightScale, group);
@@ -8947,13 +8968,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8947
8968
  group,
8948
8969
  sync() {
8949
8970
  let drawCalls = 0;
8950
- for (const layer2 of layers) if (layer2.sync()) drawCalls++;
8971
+ for (const layer22 of layers) if (layer22.sync()) drawCalls++;
8951
8972
  drawCalls += meshes.sync();
8952
8973
  lights.sync();
8953
8974
  return drawCalls;
8954
8975
  },
8955
8976
  dispose() {
8956
- for (const layer2 of layers) layer2.dispose();
8977
+ for (const layer22 of layers) layer22.dispose();
8957
8978
  meshes.dispose();
8958
8979
  lights.dispose();
8959
8980
  group.clear();
@@ -9057,7 +9078,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9057
9078
  const shocks = createShockLayer(THREE, pools.shocks);
9058
9079
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
9059
9080
  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 });
9081
+ const marks = createMarkLayer(THREE, { ground: pools.ground, ...world.hostGroundAt ? { heightAt: world.hostGroundAt } : {}, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
9061
9082
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
9062
9083
  const stopMarks = world.onMarks((event) => marks.handle(event));
9063
9084
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -9190,7 +9211,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9190
9211
  stopMarks();
9191
9212
  overlay?.dispose();
9192
9213
  workshopLayers?.dispose();
9193
- for (const layer2 of [puffs, sparks, glows, chunks, shocks, lights, marks, sprites]) layer2.dispose();
9214
+ for (const layer22 of [puffs, sparks, glows, chunks, shocks, lights, marks, sprites]) layer22.dispose();
9194
9215
  rockGeometry.dispose();
9195
9216
  plateGeometry.dispose();
9196
9217
  options.scene.remove(root);
@@ -9274,13 +9295,154 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9274
9295
  return r << 16 | g << 8 | bl;
9275
9296
  }
9276
9297
 
9298
+ // src/fragment-shapes.ts
9299
+ var FRAGMENT_SHAPE_VARIANTS = 6;
9300
+ var SLIVER_STATIONS = 6;
9301
+ var SLIVER_FRAY = 0.3;
9302
+ 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]];
9303
+ var sliverWidth = (t) => t < 0.25 ? 0.6 + 1.6 * t : t > 0.5 ? Math.max(0, (1 - t) / 0.5) ** 0.85 : 1;
9304
+ var pushTriangle = (out, a, b, c) => {
9305
+ out.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]);
9306
+ };
9307
+ var faceted = (THREE, positions) => {
9308
+ const geometry = new THREE.BufferGeometry();
9309
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
9310
+ geometry.computeVertexNormals();
9311
+ return geometry;
9312
+ };
9313
+ function buildSliverGeometry(THREE, seed) {
9314
+ const rnd2 = createRng(7e3 + seed);
9315
+ const rings = [];
9316
+ const bendY = (rnd2() - 0.5) * 0.3;
9317
+ const bendZ = (rnd2() - 0.5) * 0.3;
9318
+ for (let i = 0; i < SLIVER_STATIONS - 1; i++) {
9319
+ const t = i / (SLIVER_STATIONS - 1);
9320
+ const x = -0.5 + t + (i > 0 ? (rnd2() - 0.5) * 0.08 : 0);
9321
+ const width = sliverWidth(t);
9322
+ const lift = Math.sin(t * Math.PI);
9323
+ rings.push(SLIVER_SECTION.map(([y, z]) => {
9324
+ const fray = 1 - SLIVER_FRAY + SLIVER_FRAY * 1.4 * rnd2();
9325
+ 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))];
9326
+ }));
9327
+ }
9328
+ const tip = [0.5, (rnd2() - 0.5) * 0.5, (rnd2() - 0.5) * 0.3];
9329
+ const torn = [-0.5 + rnd2() * 0.12, (rnd2() - 0.5) * 0.2, (rnd2() - 0.5) * 0.2];
9330
+ const sides = SLIVER_SECTION.length;
9331
+ const positions = [];
9332
+ for (let k = 0; k < sides; k++) {
9333
+ const next = (k + 1) % sides;
9334
+ pushTriangle(positions, torn, rings[0][next], rings[0][k]);
9335
+ for (let i = 0; i < rings.length - 1; i++) {
9336
+ const a = rings[i][k], b = rings[i][next], c = rings[i + 1][next], d = rings[i + 1][k];
9337
+ pushTriangle(positions, a, b, c);
9338
+ pushTriangle(positions, a, c, d);
9339
+ }
9340
+ const last = rings[rings.length - 1];
9341
+ pushTriangle(positions, last[k], last[next], tip);
9342
+ }
9343
+ return faceted(THREE, positions);
9344
+ }
9345
+ function buildLumpGeometry(THREE, seed) {
9346
+ const rnd2 = createRng(8e3 + seed);
9347
+ const base = new THREE.IcosahedronGeometry(0.5, 0);
9348
+ const source = base.getAttribute("position");
9349
+ const pushOf = /* @__PURE__ */ new Map();
9350
+ const positions = [];
9351
+ for (let i = 0; i < source.count; i++) {
9352
+ const x = source.getX(i), y = source.getY(i), z = source.getZ(i);
9353
+ const key = `${x.toFixed(3)},${y.toFixed(3)},${z.toFixed(3)}`;
9354
+ let push = pushOf.get(key);
9355
+ if (push === void 0) {
9356
+ push = 0.6 + 0.55 * rnd2();
9357
+ pushOf.set(key, push);
9358
+ }
9359
+ positions.push(x * push, y * push, z * push);
9360
+ }
9361
+ base.dispose();
9362
+ const extent = [0, 0, 0];
9363
+ for (let i = 0; i < positions.length; i++) extent[i % 3] = Math.max(extent[i % 3], Math.abs(positions[i]));
9364
+ for (let i = 0; i < positions.length; i++) positions[i] *= 0.5 / Math.max(extent[i % 3], 1e-3);
9365
+ return faceted(THREE, positions);
9366
+ }
9367
+ var VENEER_EDGE = 11;
9368
+ function buildVeneerGeometry(THREE, seed) {
9369
+ const rnd2 = createRng(9e3 + seed);
9370
+ const shape = new THREE.Shape();
9371
+ const outline = [];
9372
+ const edge = (y, inward) => y - inward * (rnd2() * 0.16 + (rnd2() < 0.15 ? 0.12 : 0));
9373
+ for (let i = 0; i <= VENEER_EDGE; i++) outline.push([-0.5 + i / VENEER_EDGE, edge(0.5, 1)]);
9374
+ 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()]);
9375
+ for (let i = VENEER_EDGE; i >= 0; i--) outline.push([-0.5 + i / VENEER_EDGE, edge(-0.5, -1)]);
9376
+ outline.push([-0.5 + rnd2() * 0.12, -0.2 * rnd2()], [-0.5, 0.1 * rnd2()], [-0.5 + rnd2() * 0.12, 0.25 * rnd2()]);
9377
+ shape.moveTo(outline[0][0], outline[0][1]);
9378
+ for (let i = 1; i < outline.length; i++) shape.lineTo(outline[i][0], outline[i][1]);
9379
+ shape.closePath();
9380
+ const geometry = new THREE.ExtrudeGeometry(shape, { depth: 1, bevelEnabled: false });
9381
+ geometry.translate(0, 0, -0.5);
9382
+ return geometry;
9383
+ }
9384
+ var TORN_END = 0.24;
9385
+ var TORN_STRANDS = [4, 7];
9386
+ var pushQuad = (out, quad) => {
9387
+ pushTriangle(out, quad[0], quad[1], quad[2]);
9388
+ pushTriangle(out, quad[0], quad[2], quad[3]);
9389
+ };
9390
+ var unevenCuts = (count, rnd2) => {
9391
+ const shares = Array.from({ length: count }, () => 0.35 + 1.65 * rnd2());
9392
+ const total = shares.reduce((sum, share) => sum + share, 0);
9393
+ const cuts = [-0.5];
9394
+ for (const share of shares) cuts.push(cuts[cuts.length - 1] + share / total);
9395
+ cuts[cuts.length - 1] = 0.5;
9396
+ return cuts;
9397
+ };
9398
+ function buildBrokenBoardGeometry(THREE, seed) {
9399
+ const rnd2 = createRng(1e4 + seed);
9400
+ const end = 0.5 - TORN_END;
9401
+ const positions = [];
9402
+ const lo = -0.5, hi = 0.5;
9403
+ pushQuad(positions, [[lo, lo, hi], [end, lo, hi], [end, hi, hi], [lo, hi, hi]]);
9404
+ pushQuad(positions, [[lo, hi, lo], [end, hi, lo], [end, lo, lo], [lo, lo, lo]]);
9405
+ pushQuad(positions, [[lo, hi, hi], [end, hi, hi], [end, hi, lo], [lo, hi, lo]]);
9406
+ pushQuad(positions, [[lo, lo, lo], [end, lo, lo], [end, lo, hi], [lo, lo, hi]]);
9407
+ pushQuad(positions, [[lo, lo, lo], [lo, lo, hi], [lo, hi, hi], [lo, hi, lo]]);
9408
+ const count = TORN_STRANDS[0] + Math.floor(rnd2() * (TORN_STRANDS[1] - TORN_STRANDS[0] + 1));
9409
+ const cuts = unevenCuts(count, rnd2);
9410
+ for (let k = 0; k < count; k++) {
9411
+ const y0 = cuts[k], y1 = cuts[k + 1];
9412
+ const length = TORN_END * (0.15 + 0.85 * rnd2());
9413
+ const midX = end + length * (0.35 + 0.3 * rnd2());
9414
+ const narrow = 0.55 + 0.35 * rnd2();
9415
+ const shiftY = (rnd2() - 0.5) * (y1 - y0) * 0.6;
9416
+ const shiftZ = (rnd2() - 0.5) * 0.5;
9417
+ const cy = (y0 + y1) / 2 + shiftY, hy = (y1 - y0) / 2 * narrow, hz2 = 0.5 * (0.4 + 0.5 * rnd2());
9418
+ const base = [[end, y0, lo], [end, y1, lo], [end, y1, hi], [end, y0, hi]];
9419
+ const clampZ = (z) => Math.max(lo, Math.min(hi, z));
9420
+ 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)]];
9421
+ const tip = [end + length, y0 + (y1 - y0) * rnd2(), clampZ(shiftZ + (rnd2() - 0.5) * 0.4)];
9422
+ for (let i = 0; i < 4; i++) {
9423
+ const next = (i + 1) % 4;
9424
+ pushQuad(positions, [base[i], base[next], mid[next], mid[i]]);
9425
+ pushTriangle(positions, mid[i], mid[next], tip);
9426
+ }
9427
+ }
9428
+ return faceted(THREE, positions);
9429
+ }
9430
+
9277
9431
  // src/fragments.ts
9432
+ var BUILDERS = {
9433
+ sliver: buildSliverGeometry,
9434
+ lump: buildLumpGeometry,
9435
+ veneer: buildVeneerGeometry,
9436
+ board: buildBrokenBoardGeometry
9437
+ };
9278
9438
  var SHEET_MAX = 4e-3;
9279
9439
  var SHEET_AIR = 2.5;
9280
9440
  var SHEET_SWAY = 3;
9281
9441
  var BIG_LENGTH = 0.2;
9282
9442
  var BIG_WIDTH = 0.03;
9283
9443
  var FRAGMENT_SEED = 1374496523;
9444
+ var SUPPORT_CHECKS = 32;
9445
+ var SUPPORT_GAP = 0.02;
9284
9446
  var SHARD_SHAPES = 6;
9285
9447
  var isBig = (size) => {
9286
9448
  const [, middle, longest] = [...size].sort((a, b) => a - b);
@@ -9295,6 +9457,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9295
9457
  /** The mesh box of a fragment before its scale. */
9296
9458
  __publicField(this, "unitBox", { min: { x: -0.5, y: -0.5, z: -0.5 }, max: { x: 0.5, y: 0.5, z: 0.5 } });
9297
9459
  __publicField(this, "shardGeos", []);
9460
+ __publicField(this, "shapeGeos", /* @__PURE__ */ new Map());
9298
9461
  __publicField(this, "materials", /* @__PURE__ */ new Map());
9299
9462
  __publicField(this, "q");
9300
9463
  __publicField(this, "dq");
@@ -9304,6 +9467,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9304
9467
  __publicField(this, "rng", createRng(FRAGMENT_SEED));
9305
9468
  /** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
9306
9469
  __publicField(this, "tick", 0);
9470
+ __publicField(this, "supportTurn", 0);
9307
9471
  __publicField(this, "markGeometry", null);
9308
9472
  __publicField(this, "markMaterial", null);
9309
9473
  this.box = new THREE.BoxGeometry(1, 1, 1);
@@ -9352,21 +9516,54 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9352
9516
  }
9353
9517
  return g;
9354
9518
  }
9519
+ sharedGeometry(shape, seed) {
9520
+ const idx = seed % FRAGMENT_SHAPE_VARIANTS;
9521
+ let variants = this.shapeGeos.get(shape);
9522
+ if (!variants) {
9523
+ variants = [];
9524
+ this.shapeGeos.set(shape, variants);
9525
+ }
9526
+ let g = variants[idx];
9527
+ if (!g) {
9528
+ g = BUILDERS[shape](this.THREE, idx);
9529
+ variants[idx] = g;
9530
+ }
9531
+ return g;
9532
+ }
9533
+ /**
9534
+ * 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
9535
+ * board ends in torn strands at +x; the rest are boxes.
9536
+ */
9537
+ geometryOf(f) {
9538
+ const seed = hashString(f.id);
9539
+ if (f.shape === "shard") return this.shardGeometry(seed);
9540
+ if (f.shape === "splinter") return this.sharedGeometry("sliver", seed);
9541
+ const form = this.o.formOf?.(f.material);
9542
+ if (f.shape === "chunk" && form === "granules") return this.sharedGeometry("lump", seed);
9543
+ if (f.shape === "scrap" && form === "plies") return this.sharedGeometry("veneer", seed);
9544
+ if (f.shape === "slab" && (form === "slivers" || form === "plies")) return this.sharedGeometry("board", seed);
9545
+ return this.box;
9546
+ }
9355
9547
  markParts() {
9356
9548
  this.markGeometry ?? (this.markGeometry = new this.THREE.CircleGeometry(1, 10));
9357
9549
  this.markMaterial ?? (this.markMaterial = new this.THREE.MeshBasicMaterial({ color: 723465, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
9358
9550
  return { geometry: this.markGeometry, material: this.markMaterial };
9359
9551
  }
9360
9552
  /**
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
9553
+ * 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
9554
  * glass materials. A host draws them at load so the first blast does not upload these buffers in the middle of a frame.
9363
9555
  */
9364
9556
  warmMeshes() {
9365
9557
  const opaque = this.materialFor("warm", 8421504, false);
9366
9558
  const glass2 = this.materialFor("warm", 8421504, true);
9367
9559
  const geometries = [this.box, ...Array.from({ length: SHARD_SHAPES }, (_, index) => this.shardGeometry(index))];
9560
+ const wood = ["sliver", "lump", "veneer", "board"].flatMap((shape) => Array.from({ length: FRAGMENT_SHAPE_VARIANTS }, (_, index) => this.sharedGeometry(shape, index)));
9368
9561
  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)];
9562
+ return [
9563
+ ...geometries.flatMap((shared) => [new this.THREE.Mesh(shared, opaque), new this.THREE.Mesh(shared, glass2)]),
9564
+ ...wood.map((shared) => new this.THREE.Mesh(shared, opaque)),
9565
+ new this.THREE.Mesh(geometry, material)
9566
+ ];
9370
9567
  }
9371
9568
  /**
9372
9569
  * 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 +9588,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9391
9588
  * 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
9589
  * out. Returns null when the fragment was not made.
9393
9590
  */
9394
- spawn(f, material, home, maxAllowed) {
9591
+ /**
9592
+ * 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
9593
+ * stays: it has a body the player and the pieces run into, however thin it is, and it never fades.
9594
+ */
9595
+ spawn(f, material, home, maxAllowed, lasting = false) {
9395
9596
  if (this.list.length >= maxAllowed) {
9396
9597
  let alive = 0;
9397
9598
  let oldest;
@@ -9408,7 +9609,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9408
9609
  oldest.dying = true;
9409
9610
  }
9410
9611
  }
9411
- const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
9612
+ const geo = this.geometryOf(f);
9412
9613
  const mesh2 = new this.THREE.Mesh(geo, material);
9413
9614
  mesh2.position.set(f.center[0], f.center[1], f.center[2]);
9414
9615
  mesh2.quaternion.set(f.rotation[0], f.rotation[1], f.rotation[2], f.rotation[3]).normalize();
@@ -9436,9 +9637,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9436
9637
  air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
9437
9638
  body: null,
9438
9639
  born: this.tick,
9439
- life: Math.max(0, typeof this.o.life === "function" ? this.o.life(f, home) : this.o.life)
9640
+ life: lasting ? 0 : Math.max(0, typeof this.o.life === "function" ? this.o.life(f, home) : this.o.life)
9440
9641
  };
9441
- if (f.shape !== "shard" && isBig(f.size)) {
9642
+ if (lasting || f.shape !== "shard" && isBig(f.size)) {
9442
9643
  live.body = {
9443
9644
  mass: f.mass,
9444
9645
  push: (x, z) => {
@@ -9498,6 +9699,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9498
9699
  }
9499
9700
  update(dt) {
9500
9701
  this.tick++;
9702
+ this.wakeUnsupported();
9501
9703
  const g = this.o.gravity;
9502
9704
  for (let i = this.list.length - 1; i >= 0; i--) {
9503
9705
  const f = this.list[i];
@@ -9522,6 +9724,18 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9522
9724
  this.step(f, dt, g);
9523
9725
  }
9524
9726
  }
9727
+ /** A few fragments asleep each update (`SUPPORT_CHECKS`, in turn) look at the ground under them again: one left over air falls. */
9728
+ wakeUnsupported() {
9729
+ const n = this.list.length;
9730
+ for (let k = 0; k < Math.min(SUPPORT_CHECKS, n); k++) {
9731
+ this.supportTurn = (this.supportTurn + 1) % n;
9732
+ const f = this.list[this.supportTurn];
9733
+ if (!f.asleep || f.dying) continue;
9734
+ const e = this.m.makeRotationFromQuaternion(f.mesh.quaternion).elements;
9735
+ const ext = Math.abs(e[1]) * f.half.x + Math.abs(e[5]) * f.half.y + Math.abs(e[9]) * f.half.z;
9736
+ if (f.mesh.position.y - ext > this.o.groundAt(f.mesh.position.x, f.mesh.position.z) + SUPPORT_GAP) this.rouse(f);
9737
+ }
9738
+ }
9525
9739
  step(f, dt, g) {
9526
9740
  const mesh2 = f.mesh;
9527
9741
  f.vel.y -= g * dt;
@@ -9629,6 +9843,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9629
9843
  this.markMaterial?.dispose();
9630
9844
  for (const g of this.shardGeos) g?.dispose();
9631
9845
  this.shardGeos = [];
9846
+ for (const variants of this.shapeGeos.values()) for (const g of variants) g?.dispose();
9847
+ this.shapeGeos.clear();
9632
9848
  for (const m of this.materials.values()) m.dispose();
9633
9849
  this.materials.clear();
9634
9850
  }
@@ -9738,6 +9954,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9738
9954
  var SOLVER_PASSES = 10;
9739
9955
  var SIDE_BOUNCE = 0.1;
9740
9956
  var EDGE_POINTS = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
9957
+ var SUPPORT_CHECKS2 = 16;
9958
+ var SUPPORT_GAP2 = 0.02;
9741
9959
  var boxTopAt = (s, x, z) => {
9742
9960
  const e = s.e;
9743
9961
  const dx = x - s.c[0], dz = z - s.c[2];
@@ -9770,6 +9988,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9770
9988
  __publicField(this, "m");
9771
9989
  __publicField(this, "ax");
9772
9990
  __publicField(this, "solids", []);
9991
+ __publicField(this, "supportTurn", 0);
9773
9992
  this.p = new THREE.Vector3();
9774
9993
  this.q = new THREE.Quaternion();
9775
9994
  this.dq = new THREE.Quaternion();
@@ -9865,10 +10084,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9865
10084
  return this.mulInvInertiaWith(b, this.m.elements, x, y, z);
9866
10085
  }
9867
10086
  mulInvInertiaWith(b, e, x, y, z) {
9868
- const [hx, hy, hz] = b.halfExtents;
10087
+ const [hx, hy, hz2] = b.halfExtents;
9869
10088
  const k = 3 / b.mass;
9870
- const ix = k / (hy * hy + hz * hz);
9871
- const iy = k / (hx * hx + hz * hz);
10089
+ const ix = k / (hy * hy + hz2 * hz2);
10090
+ const iy = k / (hx * hx + hz2 * hz2);
9872
10091
  const iz = k / (hx * hx + hy * hy);
9873
10092
  const lx = (e[0] * x + e[1] * y + e[2] * z) * ix;
9874
10093
  const ly = (e[4] * x + e[5] * y + e[6] * z) * iy;
@@ -9877,11 +10096,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9877
10096
  }
9878
10097
  /** 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
10098
  samples(b, edges) {
9880
- const [hx, hy, hz] = b.halfExtents;
10099
+ const [hx, hy, hz2] = b.halfExtents;
9881
10100
  const out = [];
9882
- for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz : -hz]);
10101
+ for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz2 : -hz2]);
9883
10102
  if (!edges) return out;
9884
- const h = [hx, hy, hz];
10103
+ const h = [hx, hy, hz2];
9885
10104
  for (let axis2 = 0; axis2 < 3; axis2++) {
9886
10105
  const a1 = (axis2 + 1) % 3;
9887
10106
  const a2 = (axis2 + 2) % 3;
@@ -9929,6 +10148,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9929
10148
  const h = body.halfExtents;
9930
10149
  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
10150
  });
10151
+ this.wakeUnsupported();
9932
10152
  for (const b of this.list) {
9933
10153
  if (b.held) {
9934
10154
  b.velocity = [0, 0, 0];
@@ -9943,6 +10163,33 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9943
10163
  }
9944
10164
  this.separate(dt);
9945
10165
  }
10166
+ /** A few sleeping pieces each update (`SUPPORT_CHECKS`, in turn) look under them again: one whose table broke, or whose shelf fell over, falls. */
10167
+ wakeUnsupported() {
10168
+ const n = this.list.length;
10169
+ for (let k = 0; k < Math.min(SUPPORT_CHECKS2, n); k++) {
10170
+ this.supportTurn = (this.supportTurn + 1) % n;
10171
+ const b = this.list[this.supportTurn];
10172
+ if (!b.sleeping || b.held || this.supported(b, false) || this.supported(b, true)) continue;
10173
+ b.sleeping = false;
10174
+ b.still = 0;
10175
+ b.calmAt = null;
10176
+ }
10177
+ }
10178
+ /**
10179
+ * 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:
10180
+ * a book propped on a fallen case rests by a corner high on it while its lowest one hangs beside it.
10181
+ */
10182
+ supported(b, edges) {
10183
+ const c = this.centerOf(b, this.p);
10184
+ const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
10185
+ for (const [sx, sy, sz] of this.samples(b, edges)) {
10186
+ const y = c.y + e[1] * sx + e[5] * sy + e[9] * sz;
10187
+ const x = c.x + e[0] * sx + e[4] * sy + e[8] * sz;
10188
+ const z = c.z + e[2] * sx + e[6] * sy + e[10] * sz;
10189
+ if (this.supportAt(b, x, y, z, STEP) >= y - SUPPORT_GAP2) return true;
10190
+ }
10191
+ return false;
10192
+ }
9946
10193
  /** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
9947
10194
  supportAt(self, x, y, z, step) {
9948
10195
  let top = this.o.groundAt(x, z);
@@ -10202,8 +10449,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10202
10449
  unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
10203
10450
  if (b.tipping) {
10204
10451
  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;
10452
+ const [hx, hy, hz2] = b.halfExtents;
10453
+ const upHalf = Math.abs(e[1]) * hx + Math.abs(e[5]) * hy + Math.abs(e[9]) * hz2;
10207
10454
  if (upHalf < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
10208
10455
  else {
10209
10456
  w[0] += b.tipping.x * TIP_ACCELERATION * h;
@@ -10316,6 +10563,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10316
10563
  constructor(THREE, scene2, radius = 4e-3) {
10317
10564
  __publicField(this, "THREE", THREE);
10318
10565
  __publicField(this, "scene", scene2);
10566
+ __publicField(this, "radius", radius);
10319
10567
  __publicField(this, "cages", /* @__PURE__ */ new Map());
10320
10568
  __publicField(this, "geometry");
10321
10569
  __publicField(this, "material");
@@ -10344,28 +10592,62 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10344
10592
  meshes() {
10345
10593
  return [...this.cages.values()];
10346
10594
  }
10347
- /** Draw `bars` (world space) as the cage of `key`; an empty list removes it. */
10348
- set(key, bars) {
10595
+ /**
10596
+ * 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
10597
+ * by default), so it goes where the part goes: a beam that comes down takes its bare bars with it.
10598
+ */
10599
+ set(key, bars, holder) {
10349
10600
  this.remove(key);
10350
10601
  if (bars.length === 0) return;
10602
+ const parent = holder ?? this.scene;
10603
+ parent.updateWorldMatrix(true, false);
10604
+ const toParent = new this.THREE.Matrix4().copy(parent.matrixWorld).invert();
10351
10605
  const mesh2 = new this.THREE.InstancedMesh(this.geometry, this.material, bars.length);
10352
10606
  bars.forEach(([a, b], i) => {
10353
10607
  this.dir.set(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
10354
10608
  const length = this.dir.length();
10355
10609
  this.mid.set((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2);
10356
10610
  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));
10611
+ this.m.compose(this.mid, this.q, this.scale.set(1, Math.max(length, 1e-4), 1)).premultiply(toParent);
10358
10612
  mesh2.setMatrixAt(i, this.m);
10359
10613
  });
10360
10614
  mesh2.instanceMatrix.needsUpdate = true;
10361
10615
  mesh2.castShadow = true;
10362
- this.scene.add(mesh2);
10616
+ mesh2.name = "rebar-cage";
10617
+ parent.add(mesh2);
10363
10618
  this.cages.set(key, mesh2);
10364
10619
  }
10620
+ /** 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. */
10621
+ move(from, to, holder) {
10622
+ const mesh2 = this.cages.get(from);
10623
+ if (!mesh2) return;
10624
+ this.cages.delete(from);
10625
+ this.remove(to);
10626
+ holder.attach(mesh2);
10627
+ this.cages.set(to, mesh2);
10628
+ }
10629
+ /** 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. */
10630
+ take(key) {
10631
+ const mesh2 = this.cages.get(key);
10632
+ if (!mesh2) return [];
10633
+ mesh2.updateWorldMatrix(true, false);
10634
+ const bars = [];
10635
+ for (let i = 0; i < mesh2.count; i++) {
10636
+ mesh2.getMatrixAt(i, this.m);
10637
+ this.m.premultiply(mesh2.matrixWorld).decompose(this.mid, this.q, this.scale);
10638
+ 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 });
10639
+ }
10640
+ this.remove(key);
10641
+ return bars;
10642
+ }
10643
+ /** Radius of the bars drawn, metres. */
10644
+ get barRadius() {
10645
+ return this.radius;
10646
+ }
10365
10647
  remove(key) {
10366
10648
  const mesh2 = this.cages.get(key);
10367
10649
  if (!mesh2) return;
10368
- this.scene.remove(mesh2);
10650
+ mesh2.removeFromParent();
10369
10651
  mesh2.dispose();
10370
10652
  this.cages.delete(key);
10371
10653
  }
@@ -10445,26 +10727,26 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10445
10727
  return [s.radius * 2, s.radius * 2, s.radius * 2];
10446
10728
  };
10447
10729
  var aabbOf = (s, center, q) => {
10448
- let hx, hy, hz;
10730
+ let hx, hy, hz2;
10449
10731
  if (s.kind === "sphere") {
10450
- hx = hy = hz = s.radius;
10732
+ hx = hy = hz2 = s.radius;
10451
10733
  } else {
10452
10734
  const m = quat.toMatrix(q);
10453
10735
  if (s.kind === "box") {
10454
10736
  const a = s.size[0] / 2, b = s.size[1] / 2, c = s.size[2] / 2;
10455
10737
  hx = Math.abs(m[0]) * a + Math.abs(m[1]) * b + Math.abs(m[2]) * c;
10456
10738
  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;
10739
+ hz2 = Math.abs(m[6]) * a + Math.abs(m[7]) * b + Math.abs(m[8]) * c;
10458
10740
  } else {
10459
10741
  const ax = [m[1], m[4], m[7]];
10460
10742
  const hh = s.height / 2;
10461
10743
  const e = (i) => s.radius * Math.sqrt(Math.max(0, 1 - ax[i] * ax[i])) + hh * Math.abs(ax[i]);
10462
10744
  hx = e(0);
10463
10745
  hy = e(1);
10464
- hz = e(2);
10746
+ hz2 = e(2);
10465
10747
  }
10466
10748
  }
10467
- return { min: [center[0] - hx, center[1] - hy, center[2] - hz], max: [center[0] + hx, center[1] + hy, center[2] + hz] };
10749
+ return { min: [center[0] - hx, center[1] - hy, center[2] - hz2], max: [center[0] + hx, center[1] + hy, center[2] + hz2] };
10468
10750
  };
10469
10751
  var FLEXURE_DEFAULTS = {
10470
10752
  enabled: true,
@@ -10649,6 +10931,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10649
10931
  },
10650
10932
  {
10651
10933
  id: "wood_soft",
10934
+ splitForm: "slivers",
10652
10935
  density: 500,
10653
10936
  hpPerKg: 90,
10654
10937
  penetration: 9,
@@ -10694,6 +10977,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10694
10977
  },
10695
10978
  {
10696
10979
  id: "wood_hard",
10980
+ splitForm: "slivers",
10697
10981
  density: 750,
10698
10982
  hpPerKg: 140,
10699
10983
  penetration: 14,
@@ -10738,6 +11022,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10738
11022
  },
10739
11023
  {
10740
11024
  id: "plywood",
11025
+ splitForm: "plies",
10741
11026
  density: 600,
10742
11027
  hpPerKg: 70,
10743
11028
  penetration: 7,
@@ -10785,6 +11070,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10785
11070
  },
10786
11071
  {
10787
11072
  id: "mdf",
11073
+ splitForm: "granules",
10788
11074
  density: 750,
10789
11075
  hpPerKg: 45,
10790
11076
  penetration: 6,
@@ -11296,6 +11582,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11296
11582
  },
11297
11583
  {
11298
11584
  id: "green_wood",
11585
+ splitForm: "slivers",
11299
11586
  density: 650,
11300
11587
  hpPerKg: 5,
11301
11588
  penetration: 10,
@@ -11576,6 +11863,125 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11576
11863
  "brittle"
11577
11864
  ],
11578
11865
  compressive: 1e5
11866
+ },
11867
+ {
11868
+ id: "mud",
11869
+ density: 1700,
11870
+ hpPerKg: 1,
11871
+ penetration: 5,
11872
+ ductility: 0.8,
11873
+ fracture: "crumble",
11874
+ fragmentSize: [
11875
+ 0.02,
11876
+ 0.08
11877
+ ],
11878
+ hitDebris: "dust",
11879
+ breakDebris: "dust",
11880
+ thermal: {
11881
+ conductivity: 0.5,
11882
+ heatCapacity: 3.2
11883
+ },
11884
+ tags: [
11885
+ "soil",
11886
+ "wet"
11887
+ ],
11888
+ compressive: 5e3
11889
+ },
11890
+ {
11891
+ id: "clay",
11892
+ density: 1800,
11893
+ hpPerKg: 5,
11894
+ penetration: 7,
11895
+ ductility: 0.05,
11896
+ fracture: "crumble",
11897
+ fragmentSize: [
11898
+ 0.02,
11899
+ 0.1
11900
+ ],
11901
+ hitDebris: "chips",
11902
+ breakDebris: "dust",
11903
+ thermal: {
11904
+ conductivity: 0.15,
11905
+ heatCapacity: 2.2
11906
+ },
11907
+ tags: [
11908
+ "soil",
11909
+ "brittle"
11910
+ ],
11911
+ compressive: 6e4
11912
+ },
11913
+ {
11914
+ id: "snow",
11915
+ density: 300,
11916
+ hpPerKg: 1,
11917
+ penetration: 3,
11918
+ ductility: 0.6,
11919
+ fracture: "crumble",
11920
+ fragmentSize: [
11921
+ 0.01,
11922
+ 0.05
11923
+ ],
11924
+ hitDebris: "dust",
11925
+ breakDebris: "dust",
11926
+ thermal: {
11927
+ conductivity: 0.1,
11928
+ heatCapacity: 2.1
11929
+ },
11930
+ tags: [
11931
+ "soil",
11932
+ "frozen"
11933
+ ],
11934
+ compressive: 1e3
11935
+ },
11936
+ {
11937
+ id: "cobble",
11938
+ density: 2650,
11939
+ hpPerKg: 25,
11940
+ penetration: 25,
11941
+ penetrationClass: "concrete",
11942
+ penetrationScale: 0.9,
11943
+ ductility: 0.03,
11944
+ fracture: "crumble",
11945
+ fragmentSize: [
11946
+ 0.05,
11947
+ 0.15
11948
+ ],
11949
+ hitDebris: "chips",
11950
+ breakDebris: "chips",
11951
+ thermal: {
11952
+ conductivity: 0.2,
11953
+ heatCapacity: 2.5
11954
+ },
11955
+ tags: [
11956
+ "masonry",
11957
+ "brittle"
11958
+ ],
11959
+ compressive: 3e5
11960
+ },
11961
+ {
11962
+ id: "paver",
11963
+ density: 2300,
11964
+ hpPerKg: 14,
11965
+ penetration: 18,
11966
+ penetrationClass: "concrete",
11967
+ penetrationScale: 1,
11968
+ ductility: 0.03,
11969
+ fracture: "crumble",
11970
+ fragmentSize: [
11971
+ 0.04,
11972
+ 0.12
11973
+ ],
11974
+ hitDebris: "chips",
11975
+ breakDebris: "chips",
11976
+ thermal: {
11977
+ conductivity: 0.15,
11978
+ heatCapacity: 2.5
11979
+ },
11980
+ tags: [
11981
+ "masonry",
11982
+ "brittle"
11983
+ ],
11984
+ compressive: 35e4
11579
11985
  }
11580
11986
  ];
11581
11987
  var material_hints_default = {
@@ -11714,6 +12120,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11714
12120
  return out;
11715
12121
  };
11716
12122
  var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
12123
+ var BLAST_REFERENCE_RESISTANCE = 200 * 1900 * 0.13;
11717
12124
  var assemblies_default = [
11718
12125
  {
11719
12126
  id: "brick_wall_plastered",
@@ -12099,6 +12506,23 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12099
12506
  }
12100
12507
  };
12101
12508
  var tmpV = (THREE) => new THREE.Vector3();
12509
+ function plainPositions(mesh2, v0, v1) {
12510
+ const { collapsedRanges, plainPositions: plain } = mesh2.userData;
12511
+ if (!plain || !collapsedRanges?.some(([a, b]) => a < v1 && b > v0)) return null;
12512
+ return plain;
12513
+ }
12514
+ function showCollapsedRanges(mesh2) {
12515
+ const data = mesh2.userData;
12516
+ const plain = data.plainPositions;
12517
+ const position = mesh2.geometry?.getAttribute(POS_ATTR);
12518
+ if (!plain || !data.collapsedRanges?.length || !position) return;
12519
+ const array = position.array;
12520
+ for (const [v0, v1] of data.collapsedRanges) array.set(plain.subarray(v0 * 3, v1 * 3), v0 * 3);
12521
+ data.collapsedRanges = [];
12522
+ position.needsUpdate = true;
12523
+ mesh2.geometry.computeBoundingBox();
12524
+ mesh2.geometry.computeBoundingSphere();
12525
+ }
12102
12526
  var MIN_DENT_VERTICES = 10;
12103
12527
  var WIDEN_MARGIN = 1.1;
12104
12528
  var SKIN_FOLLOW = 0.85;
@@ -12864,7 +13288,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12864
13288
  const a = src.getAttribute(name);
12865
13289
  if (a.isInterleavedBufferAttribute) continue;
12866
13290
  const size = a.itemSize;
12867
- const from = (name === POS_ATTR ? host.work : null) ?? a.array;
13291
+ const from = (name === POS_ATTR ? plainPositions(host.mesh, v0, v1) ?? host.work : null) ?? a.array;
12868
13292
  const slice = from.slice(v0 * size, v1 * size);
12869
13293
  const Ctor = a.array.constructor;
12870
13294
  geo.setAttribute(name, new this.THREE.BufferAttribute(new Ctor(slice), size, a.normalized));
@@ -13022,11 +13446,29 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13022
13446
  // src/renderer.ts
13023
13447
  var rendererCount = 0;
13024
13448
  var COLLAPSE_CLOUD_KG = 1e3;
13449
+ var REBAR_COLOR = 7628380;
13450
+ var STEEL_DENSITY = 7850;
13025
13451
  var CHIP_DECAL_EDGE = 0.05;
13026
13452
  var WOOD_RIM = 1.5;
13027
13453
  var IMPACT_TAGS = ["metal", "glass", "wood"];
13028
13454
  var GLOW_FROM_C = 430;
13029
13455
  var QUEUE_CAP = 4e3;
13456
+ var STRUCTURAL_EVENTS = /* @__PURE__ */ new Set([
13457
+ "fracture",
13458
+ "detach",
13459
+ "collapse",
13460
+ "settle",
13461
+ "destroyed",
13462
+ "expose",
13463
+ "stage",
13464
+ "hole",
13465
+ "ignite",
13466
+ "primed",
13467
+ "extinguish",
13468
+ "burnout",
13469
+ "leak",
13470
+ "explode"
13471
+ ]);
13030
13472
  var MIN_FRAGMENTS_PER_SLICE = 8;
13031
13473
  var STATIC_REFRESH = 0.5;
13032
13474
  var STATIC_REFRESH_SLICE_MS = 1;
@@ -13135,6 +13577,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13135
13577
  __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
13136
13578
  __publicField(this, "staticAt", 0);
13137
13579
  __publicField(this, "queue", []);
13580
+ /** 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. */
13581
+ __publicField(this, "coverWhenWentOff", /* @__PURE__ */ new WeakMap());
13138
13582
  /** 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
13583
  __publicField(this, "slices", []);
13140
13584
  /** When the work of this frame must stop; `Infinity` outside a drain, where a call finishes what it started. */
@@ -13214,7 +13658,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13214
13658
  groundAt: this.groundAt,
13215
13659
  gravity: this.gravity,
13216
13660
  max: this.maxFragments,
13217
- life: o.fragmentLife ?? 12
13661
+ life: o.fragmentLife ?? 12,
13662
+ formOf: (id) => this.world.materials.get(id)?.splitForm
13218
13663
  });
13219
13664
  this.cages = new RebarCages(THREE, o.scene);
13220
13665
  this.pieceSys = new PieceSystem(THREE, {
@@ -13227,7 +13672,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13227
13672
  onSlam: (p, hit, speed) => this.slam(p, hit, speed)
13228
13673
  });
13229
13674
  this.unsubscribe = this.world.on("*", (ev) => {
13230
- if (this.queue.length < QUEUE_CAP) this.queue.push(ev);
13675
+ if (ev.type === "explode") this.coverWhenWentOff.set(ev, this.coverAround(ev));
13676
+ if (this.queue.length < QUEUE_CAP || STRUCTURAL_EVENTS.has(ev.type)) this.queue.push(ev);
13231
13677
  });
13232
13678
  }
13233
13679
  // ----------------------------------------------------------------------------------------------------------------------------------
@@ -13750,7 +14196,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13750
14196
  this.onChip(ev);
13751
14197
  break;
13752
14198
  case "expose":
13753
- this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
14199
+ if (ev.bars.length > 0) this.cages.set(`${ev.object}|${ev.part}`, ev.bars, this.bounds.get(ev.object)?.root);
14200
+ else this.dropCage(ev.object, ev.part);
13754
14201
  if (ev.bars.length > 0) {
13755
14202
  const [a, b] = ev.bars[0];
13756
14203
  this.playEvent({ kind: "expose" }, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2]);
@@ -14048,7 +14495,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14048
14495
  const rotation2 = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
14049
14496
  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
14497
  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 });
14498
+ 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
14499
  this.impactSound(pv?.material ?? "", ev.center, 0.7, ev.blow);
14053
14500
  }
14054
14501
  onFracture(ev) {
@@ -14067,6 +14514,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14067
14514
  this.visuals.hide(pv, "broken");
14068
14515
  }
14069
14516
  this.spawnFragments(ev, 0, { def, color, opacity, transparent });
14517
+ this.loosenIfFootless(ev.object);
14070
14518
  const up = [0, 1, 0];
14071
14519
  const sizeK = clamp6(ev.fragments.length / 10, 0.5, 2);
14072
14520
  switch (ev.style) {
@@ -14112,6 +14560,33 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14112
14560
  this.sound("detach", this.rootPoint(ev.object), 0.5);
14113
14561
  this.splitLoose(ev.object);
14114
14562
  }
14563
+ /**
14564
+ * 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
14565
+ * ground with a body (`FragmentSystem.spawn` with `lasting`), not as bars left standing in the air.
14566
+ */
14567
+ dropCage(objectId, partId) {
14568
+ const bars = this.cages.take(`${objectId}|${partId}`);
14569
+ if (bars.length === 0) return;
14570
+ const radius = this.cages.barRadius;
14571
+ const density = this.world.materials.get("steel")?.density ?? STEEL_DENSITY;
14572
+ const material = this.fragments.materialFor("steel", REBAR_COLOR, false);
14573
+ const rnd2 = createRng(hashString(`${objectId}|${partId}`));
14574
+ bars.forEach((bar, i) => {
14575
+ const fragment = {
14576
+ id: `${partId}#bar${i}`,
14577
+ part: partId,
14578
+ material: "steel",
14579
+ shape: "chunk",
14580
+ center: bar.center,
14581
+ size: [2 * radius, bar.length, 2 * radius],
14582
+ rotation: bar.rotation,
14583
+ velocity: [(rnd2() - 0.5) * 0.4, 0, (rnd2() - 0.5) * 0.4],
14584
+ angularVelocity: [(rnd2() - 0.5) * 1.5, (rnd2() - 0.5) * 0.5, (rnd2() - 0.5) * 1.5],
14585
+ mass: density * Math.PI * radius * radius * bar.length
14586
+ };
14587
+ this.fragments.spawn(fragment, material, objectId, this.maxFragments, true);
14588
+ });
14589
+ }
14115
14590
  /** Turn every loose group of the object (a detach, a collapse) into a piece of its own. */
14116
14591
  splitLoose(objectId) {
14117
14592
  if (!this.autoSplit) return;
@@ -14166,10 +14641,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14166
14641
  if (!pv) continue;
14167
14642
  from.parts.delete(p.id);
14168
14643
  nb.parts.set(p.id, pv);
14644
+ this.cages.move(`${from.id}|${p.id}`, `${newId}|${p.id}`, group);
14169
14645
  for (const mv of pv.meshes) {
14170
14646
  if (mv.host && !mv.loose) this.visuals.extract(mv, group);
14171
14647
  else {
14172
14648
  group.attach(mv.mesh);
14649
+ showCollapsedRanges(mv.mesh);
14173
14650
  mv.moved = true;
14174
14651
  }
14175
14652
  }
@@ -14196,6 +14673,45 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14196
14673
  if (kitOriginOf(from.root)?.kind === "furniture") this.tipIfTopHeavy(nb);
14197
14674
  this.o.onPiece?.(piece);
14198
14675
  }
14676
+ /**
14677
+ * 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
14678
+ * 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.
14679
+ */
14680
+ loosenIfFootless(objectId) {
14681
+ const b = this.bounds.get(objectId);
14682
+ if (!b || !this.autoSplit) return;
14683
+ const left = this.meshesOf(b).filter((mv) => this.visuals.shown(mv));
14684
+ if (left.length === 0) return;
14685
+ const body = b.piece ?? this.pieceSys.get(objectId);
14686
+ if (body) {
14687
+ const { half, center } = this.measure(b.root, left);
14688
+ body.halfExtents = half;
14689
+ this.pieceSys.setCenter(body, center);
14690
+ body.mass = Math.max(0.05, this.massOf(objectId));
14691
+ body.sleeping = false;
14692
+ return;
14693
+ }
14694
+ if (kitOriginOf(b.root)?.kind !== "furniture") return;
14695
+ const boxOf = (mv) => this.visuals.worldBox(mv, new this.THREE.Box3());
14696
+ const whole = [...b.parts.values()].flatMap((pv) => pv.meshes).map(boxOf).filter((one) => !one.isEmpty());
14697
+ const floor2 = Math.min(...whole.map((one) => one.min.y));
14698
+ const feetOf = (boxes) => boxes.filter((one) => one.min.y < floor2 + FEET_BAND).reduce((feet, one) => feet.union(one), new this.THREE.Box3());
14699
+ const was = feetOf(whole);
14700
+ const now = feetOf(left.map(boxOf).filter((one) => !one.isEmpty()));
14701
+ 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;
14702
+ if (same) return;
14703
+ if (!now.isEmpty()) {
14704
+ const box = left.map(boxOf).reduce((all, one) => all.union(one), new this.THREE.Box3());
14705
+ const centreX = (box.min.x + box.max.x) / 2;
14706
+ const centreZ = (box.min.z + box.max.z) / 2;
14707
+ const height = box.max.y - box.min.y;
14708
+ const narrow = height >= TIP_MIN_HEIGHT && Math.min(now.max.x - now.min.x, now.max.z - now.min.z) < TIP_NARROW * height;
14709
+ if (!narrow && centreX > now.min.x && centreX < now.max.x && centreZ > now.min.z && centreZ < now.max.z) return;
14710
+ }
14711
+ const piece = this.makePiece(objectId);
14712
+ this.tipIfTopHeavy(b);
14713
+ this.o.onLoosened?.(objectId, piece, "apart");
14714
+ }
14199
14715
  /**
14200
14716
  * 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
14717
  * high and its feet are thin, so it gets the start of the fall, across the line of its feet.
@@ -14227,7 +14743,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14227
14743
  const report = options.report;
14228
14744
  const confinement = report?.find((r) => r.blast !== void 0)?.blast?.confinement ?? 1;
14229
14745
  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;
14746
+ const wentOff = this.coverWhenWentOff.get(ev);
14747
+ 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
14748
  const shove = (pc) => {
14232
14749
  this.pieceSys.centerOf(pc, c);
14233
14750
  if (c.distanceTo(this.v2.set(p[0], p[1], p[2])) > ev.radius) return;
@@ -14247,6 +14764,29 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14247
14764
  ...report ? { confinement } : {}
14248
14765
  });
14249
14766
  }
14767
+ /** 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. */
14768
+ coverAround(ev) {
14769
+ const cover = /* @__PURE__ */ new Map();
14770
+ const reach = this.world.blastReach;
14771
+ if (!reach) return cover;
14772
+ const point = new this.THREE.Vector3(ev.point[0], ev.point[1], ev.point[2]);
14773
+ const centre = new this.THREE.Vector3();
14774
+ const read = (objectId) => {
14775
+ 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] }));
14776
+ };
14777
+ for (const pc of this.pieceSys.list) {
14778
+ this.pieceSys.centerOf(pc, centre);
14779
+ read(pc.objectId);
14780
+ }
14781
+ const shoveable = this.o.shoveable;
14782
+ if (!shoveable) return cover;
14783
+ for (const b of this.bounds.values()) {
14784
+ if (b.piece || cover.has(b.id) || !shoveable(b.id) || !this.world.has(b.id)) continue;
14785
+ centre.copy(this.measure(b.root, this.meshesOf(b)).center).applyQuaternion(b.root.quaternion).add(b.root.position);
14786
+ read(b.id);
14787
+ }
14788
+ return cover;
14789
+ }
14250
14790
  /** 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
14791
  loosen(point, radius, energy, reachOf) {
14252
14792
  const shoveable = this.o.shoveable;
@@ -14542,7 +15082,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14542
15082
  }
14543
15083
  };
14544
15084
 
14545
- // ../game-audio/dist/chunk-KBTUANNG.js
15085
+ // ../game-audio/dist/chunk-C2K7XN2K.js
14546
15086
  var DISTANCE_CURVES = {
14547
15087
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
14548
15088
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -14759,7 +15299,22 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14759
15299
  ["smg_45_auto", 2, "sfx_pistol"],
14760
15300
  ["weapon_draw", 2],
14761
15301
  ["suppressor_attach", 3],
14762
- ["suppressor_detach", 3]
15302
+ ["suppressor_detach", 3],
15303
+ ["minigun_loop", 3],
15304
+ ["smg_5_7x28", 3],
15305
+ ["mg_belt_adjust", 3],
15306
+ ["mg_belt_box_attach", 3],
15307
+ ["mg_cover_open", 3],
15308
+ ["mg_cover_close", 3],
15309
+ ["weapon_holster", 3],
15310
+ ["scope_zoom", 3]
15311
+ ]),
15312
+ ...entries("impactos", [
15313
+ ["knife_hit_wall", 3],
15314
+ ["helmet_hit", 3],
15315
+ ["vest_hit", 3],
15316
+ ["headshot", 3],
15317
+ ["blood_squirt", 3]
14763
15318
  ]),
14764
15319
  ...entries("explosoes-fogo", [
14765
15320
  ["explosion_grenade", 1, "boom"],
@@ -14777,7 +15332,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14777
15332
  ["grenade_bounce", 1],
14778
15333
  ["c4_plant", 2],
14779
15334
  ["c4_beep", 2],
14780
- ["c4_disarm", 2]
15335
+ ["c4_disarm", 2],
15336
+ ["c4_disarming_loop", 3],
15337
+ ["c4_remote_detonate", 3],
15338
+ ["smoke_grenade_hiss", 3],
15339
+ ["grenade_pin_pull", 3],
15340
+ ["electric_shock", 3],
15341
+ ["electric_spark", 3]
14781
15342
  ]),
14782
15343
  ...entries("personagem", [
14783
15344
  ["footstep_concrete_walk", 1, "sfx_step"],
@@ -14805,7 +15366,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14805
15366
  ["zombie_attack", 1],
14806
15367
  ["zombie_pain", 1],
14807
15368
  ["zombie_claw_hit", 1, "sfx_flesh"],
14808
- ["zombie_claw_miss", 2]
15369
+ ["zombie_claw_miss", 2],
15370
+ ["swing_whoosh", 3],
15371
+ ["heartbeat_critical", 3],
15372
+ ["fall_water", 3],
15373
+ ["fall_damage", 3],
15374
+ ["fall_scream", 3],
15375
+ ["footstep_glass_walk", 3]
14809
15376
  ]),
14810
15377
  ...entries("veiculos", [
14811
15378
  ["engine_loop_car", 1],
@@ -14815,7 +15382,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14815
15382
  ["car_door_open", 1, "latch"],
14816
15383
  ["car_door_close", 1, "shut_metal"],
14817
15384
  ["horn_car", 2, "horn"],
14818
- ["tire_skid_asphalt", 2]
15385
+ ["tire_skid_asphalt", 2],
15386
+ ["jet_flyby", 3],
15387
+ ["helicopter_turbine", 3],
15388
+ ["tire_burnout", 3],
15389
+ ["handbrake_turn", 3],
15390
+ ["siren_police", 3]
14819
15391
  ]),
14820
15392
  ...entries("itens-portas", [
14821
15393
  ["item_pickup_metal", 1],
@@ -14826,7 +15398,27 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14826
15398
  ["door_creak", 1, "creak_wood"],
14827
15399
  ["door_locked_rattle", 1, "rattle"],
14828
15400
  ["drawer_open", 2, "slide"],
14829
- ["fridge_close", 2, "shut_metal"]
15401
+ ["fridge_close", 2, "shut_metal"],
15402
+ ["inventory_open", 3],
15403
+ ["inventory_close", 3],
15404
+ ["inventory_select", 3],
15405
+ ["inventory_place", 3],
15406
+ ["pickup_chime", 3],
15407
+ ["item_pickup_coins", 3],
15408
+ ["bandage_use", 3],
15409
+ ["bandage_refuse", 3],
15410
+ ["flashlight_toggle", 3],
15411
+ ["gear_draw", 3],
15412
+ ["gear_holster", 3],
15413
+ ["button_press", 3],
15414
+ ["lock_desk_open", 3],
15415
+ ["elevator_move_loop", 3],
15416
+ ["elevator_stop", 3],
15417
+ ["door_sliding", 3],
15418
+ ["door_kick", 3],
15419
+ ["door_wood_break", 3],
15420
+ ["gate_iron_open", 3],
15421
+ ["gate_iron_close", 3]
14830
15422
  ]),
14831
15423
  ...entries("ambiente", [
14832
15424
  ["rain_loop", 2],
@@ -14834,7 +15426,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14834
15426
  ["thunder_near", 2],
14835
15427
  ["thunder_far", 2],
14836
15428
  ["water_splash", 2],
14837
- ["helicopter_loop", 3]
15429
+ ["helicopter_loop", 3],
15430
+ ["bird_flush", 3],
15431
+ ["dog_growl", 3],
15432
+ ["dog_whimper", 3],
15433
+ ["dog_howl", 3],
15434
+ ["zombie_dog_bark", 3],
15435
+ ["zombie_dog_howl_far", 3],
15436
+ ["insect_squash", 3],
15437
+ ["insect_crawl", 3],
15438
+ ["fan_loop", 3]
14838
15439
  ])
14839
15440
  ];
14840
15441
  var SFX_MATERIALS = [
@@ -14970,6 +15571,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14970
15571
  }
14971
15572
  return { id: options.id, samples };
14972
15573
  }
15574
+ var CATALOG = new Map([...SFX_CATALOG, ...SFX_IMPACTS].map((entry) => [entry.id, entry]));
14973
15575
  var PT = {
14974
15576
  group: "Som",
14975
15577
  name: "Painel de som (arquivos e C1\u2013C10)",
@@ -15044,9 +15646,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15044
15646
  strengths: { light: "leve", medium: "m\xE9dio", heavy: "forte" },
15045
15647
  states: { file: "A", synth: "S", missing: "F" },
15046
15648
  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" },
15047
- 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" },
15649
+ 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" },
15048
15650
  run: { idle: "parada", measuring: "medindo\u2026", done: "pronta", failed: "falhou", recording: "gravando\u2026", recorded: "grava\xE7\xE3o baixada" },
15049
- words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono" },
15651
+ 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" },
15050
15652
  sources: { sdk: "SDK", user: "seu som", synth: "sintetizado" },
15051
15653
  probe: "Fonte do som",
15052
15654
  wall: "Parede (oclus\xE3o)",
@@ -15117,9 +15719,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15117
15719
  environments: { indoor: "indoor", outdoor: "outdoor" },
15118
15720
  strengths: { light: "light", medium: "medium", heavy: "heavy" },
15119
15721
  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" },
15120
- 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" },
15722
+ 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" },
15121
15723
  run: { idle: "idle", measuring: "measuring\u2026", done: "done", failed: "failed", recording: "recording\u2026", recorded: "recording downloaded" },
15122
- words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner" },
15724
+ words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner", local_file: "local pack file", sdk_file: "SDK recording", synth: "SDK synthesis" },
15123
15725
  sources: { sdk: "SDK", user: "your sound", synth: "synthesized" },
15124
15726
  probe: "Sound source",
15125
15727
  wall: "Wall (occlusion)",
@@ -15202,6 +15804,66 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15202
15804
  sources: [`cena:${line}`]
15203
15805
  };
15204
15806
  }
15807
+ var hz = (midi) => 440 * 2 ** ((midi - 69) / 12);
15808
+ var layer2 = (note, beat, wave, level) => {
15809
+ const [at3, midi, beats] = note;
15810
+ const length = beats * beat;
15811
+ return {
15812
+ at: at3 * beat,
15813
+ duration: length,
15814
+ source: { kind: "osc", wave, frequency: { set: hz(midi) } },
15815
+ 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"]]
15816
+ };
15817
+ };
15818
+ function radioTrack(spec) {
15819
+ const beat = 60 / spec.bpm;
15820
+ const recipes = Array.from({ length: spec.bars }, (_, bar) => {
15821
+ 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]]);
15822
+ const layers = [
15823
+ ...inBar(spec.bass).map((n) => layer2(n, beat, "triangle", 0.16)),
15824
+ ...inBar(spec.chords).map((n) => layer2(n, beat, "sine", 0.05)),
15825
+ ...inBar(spec.melody).map((n) => layer2(n, beat, spec.wave, 0.07))
15826
+ ];
15827
+ return { id: `${spec.id}_${bar}`, bus: "world", duration: 4 * beat, layers, falloff: { model: "none" } };
15828
+ });
15829
+ return { song: { id: spec.id, barSeconds: 4 * beat, bars: recipes.map((r) => r.id) }, recipes };
15830
+ }
15831
+ var chord = (bar, root, minor = false) => [[bar * 4, root, 4], [bar * 4, root + (minor ? 3 : 4), 4], [bar * 4, root + 7, 4]];
15832
+ var bassLine = (roots, pattern) => roots.flatMap((root, bar) => pattern.map((b) => [bar * 4 + b, root - 12, 0.9]));
15833
+ var TRACKS = [
15834
+ // a bright pop loop: I–V–vi–IV in C
15835
+ radioTrack({
15836
+ id: "radio_track_pop",
15837
+ bpm: 112,
15838
+ bars: 4,
15839
+ wave: "square",
15840
+ bass: bassLine([48, 55, 57, 53], [0, 1.5, 2, 3]),
15841
+ chords: [...chord(0, 60), ...chord(1, 55), ...chord(2, 57, true), ...chord(3, 53)],
15842
+ 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]]
15843
+ }),
15844
+ // a syncopated loop in A minor, the bass on the off-beats
15845
+ radioTrack({
15846
+ id: "radio_track_groove",
15847
+ bpm: 100,
15848
+ bars: 4,
15849
+ wave: "triangle",
15850
+ bass: bassLine([57, 62, 64, 57], [0, 0.75, 1.5, 2.5, 3.25]),
15851
+ chords: [...chord(0, 57, true), ...chord(1, 62, true), ...chord(2, 64), ...chord(3, 57, true)],
15852
+ 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]]
15853
+ }),
15854
+ // a slow blues-like loop in F with a swung melody
15855
+ radioTrack({
15856
+ id: "radio_track_slow",
15857
+ bpm: 76,
15858
+ bars: 4,
15859
+ wave: "sine",
15860
+ bass: bassLine([53, 58, 53, 60], [0, 2]),
15861
+ chords: [...chord(0, 65), ...chord(1, 58), ...chord(2, 65), ...chord(3, 60)],
15862
+ 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]]
15863
+ })
15864
+ ];
15865
+ var RADIO_TRACKS = TRACKS.map((t) => t.song);
15866
+ var RADIO_BANK = { id: "vehicle_radio", sounds: TRACKS.flatMap((t) => t.recipes), loops: [] };
15205
15867
  var FOLEY_FALLOFF = {
15206
15868
  model: "inverse_square",
15207
15869
  k: 0.02,
@@ -15808,11 +16470,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15808
16470
  ...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 })] : []
15809
16471
  ];
15810
16472
  }
15811
- var quieter = (level, layers) => layers.map((layer2) => {
15812
- const gain = layer2.gain ?? 1;
15813
- if (typeof gain === "number") return { ...layer2, gain: gain * level };
15814
- if (Array.isArray(gain)) return { ...layer2, gain: [gain[0] * level, gain[1] * level] };
15815
- return layer2;
16473
+ var quieter = (level, layers) => layers.map((layer22) => {
16474
+ const gain = layer22.gain ?? 1;
16475
+ if (typeof gain === "number") return { ...layer22, gain: gain * level };
16476
+ if (Array.isArray(gain)) return { ...layer22, gain: [gain[0] * level, gain[1] * level] };
16477
+ return layer22;
15816
16478
  });
15817
16479
  var recipe = (id, layers, falloff, bus = "world") => ({ id, bus, pan: true, layers, falloff, sources: ["sdk"] });
15818
16480
  var takeId = (id, index) => `${id}_${String(index + 1).padStart(2, "0")}`;
@@ -15828,7 +16490,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15828
16490
  (take) => quieter(LANDING_LEVEL, [
15829
16491
  // both feet, a hair apart, then the knees taking the weight (a low body thump) and the clothes settling
15830
16492
  ...footLayers(surface, take, GAITS.run, LANDINGS[landing2].weight),
15831
- ...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 })),
16493
+ ...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 })),
15832
16494
  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 }),
15833
16495
  sceneNoise({ type: "bandpass", f: [1800, 2600], q: 0.8, d: 0.18, vol: LANDINGS[landing2].cloth * take.v, delay: 0.04 })
15834
16496
  ]),
@@ -15874,7 +16536,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15874
16536
  fall_damage: (take) => [
15875
16537
  ...BODY.body_fall(take),
15876
16538
  sceneNoise({ type: "highpass", f: 2800, d: 0.02, vol: 0.35 * take.v, delay: 0.01 }),
15877
- ...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 }))
16539
+ ...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 }))
15878
16540
  ],
15879
16541
  player_hurt: voice({ f0: 150, f1: 105, d: 0.26, fA: 620, fB: 1100, noise: 0.3, vol: 0.35 }),
15880
16542
  player_breath_run: (take) => [