@adatechnology/game-destruction-three 0.11.0 → 0.12.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.
@@ -6390,6 +6390,24 @@ var GameDestructionThree = (() => {
6390
6390
  for (const decal2 of this.decals) decal2.on = false;
6391
6391
  this.count = 0;
6392
6392
  }
6393
+ /**
6394
+ * Takes away the marks inside a box: world `center`, `half` extents along the axes of the world `rotation` (a quaternion, x y z w; none means the
6395
+ * world axes). The surface they were on is gone (a brick came out of a wall with its bullet holes). Returns how many went.
6396
+ */
6397
+ clearInside(center, half, rotation = [0, 0, 0, 1]) {
6398
+ const qx = -rotation[0], qy = -rotation[1], qz = -rotation[2], qw = rotation[3];
6399
+ let gone = 0;
6400
+ for (const decal2 of this.decals) {
6401
+ if (!decal2.on) continue;
6402
+ const vx2 = decal2.x - center[0], vy2 = decal2.y - center[1], vz2 = decal2.z - center[2];
6403
+ const tx = 2 * (qy * vz2 - qz * vy2), ty = 2 * (qz * vx2 - qx * vz2), tz = 2 * (qx * vy2 - qy * vx2);
6404
+ const lx = vx2 + qw * tx + (qy * tz - qz * ty), ly = vy2 + qw * ty + (qz * tx - qx * tz), lz = vz2 + qw * tz + (qx * ty - qy * tx);
6405
+ if (Math.abs(lx) > half[0] || Math.abs(ly) > half[1] || Math.abs(lz) > half[2]) continue;
6406
+ decal2.on = false;
6407
+ gone++;
6408
+ }
6409
+ return gone;
6410
+ }
6393
6411
  update(dt) {
6394
6412
  this.count = 0;
6395
6413
  for (const decal2 of this.decals) {
@@ -7709,6 +7727,11 @@ void main(){
7709
7727
  var MAX_CONFORM_STEPS = 48;
7710
7728
  var MIN_CONFORM_STEPS = 16;
7711
7729
  var BLOOD_GROW_RATE = 1.2;
7730
+ var BLOOD_START_GROWTH = 0.05;
7731
+ var DEFAULT_MAX_GROUND_MARKS = 64;
7732
+ var DEFAULT_MAX_ROCKS = 512;
7733
+ var DEFAULT_MAX_BLOOD = 256;
7734
+ var MAX_SPARE_GROUND_GEOMETRIES = 16;
7712
7735
  function createMarkLayer(THREE, options) {
7713
7736
  const group = new THREE.Group();
7714
7737
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -7732,110 +7755,168 @@ void main(){
7732
7755
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
7733
7756
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
7734
7757
  owned.push(bloodMaterial, bloodGeometry);
7735
- const rockMaterials = /* @__PURE__ */ new Map();
7736
- const blood = [];
7737
- let meshCount = 0;
7738
- function rockMaterial(color) {
7739
- let material = rockMaterials.get(color);
7740
- if (!material) {
7741
- material = new THREE.MeshStandardMaterial({ color, roughness: 0.9, metalness: 0, flatShading: true });
7742
- rockMaterials.set(color, material);
7743
- owned.push(material);
7758
+ const maxGroundMarks = options.maxGroundMarks ?? DEFAULT_MAX_GROUND_MARKS;
7759
+ const instanced = {};
7760
+ function rockMaterial() {
7761
+ if (!instanced.rockMaterial) {
7762
+ instanced.rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.9, metalness: 0, flatShading: true });
7763
+ owned.push(instanced.rockMaterial);
7744
7764
  }
7745
- return material;
7765
+ return instanced.rockMaterial;
7766
+ }
7767
+ function bloodMesh() {
7768
+ return instanced.blood ?? (instanced.blood = attach(createInstanced(bloodGeometry, bloodMaterial, options.maxBlood ?? DEFAULT_MAX_BLOOD)));
7769
+ }
7770
+ function rockMesh() {
7771
+ return instanced.rock ?? (instanced.rock = attach(createInstanced(options.rockGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
7772
+ }
7773
+ function plateMesh() {
7774
+ return instanced.plate ?? (instanced.plate = attach(createInstanced(options.plateGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
7775
+ }
7776
+ function attach(mesh2, castShadow = false) {
7777
+ mesh2.castShadow = castShadow;
7778
+ group.add(mesh2);
7779
+ return mesh2;
7780
+ }
7781
+ const groundMarks = [];
7782
+ const spareGeometries = [];
7783
+ const blood = [];
7784
+ const placed = { blood: 0, rock: 0, plate: 0 };
7785
+ const matrix = new THREE.Matrix4();
7786
+ const position = new THREE.Vector3();
7787
+ const quaternion = new THREE.Quaternion();
7788
+ const euler = new THREE.Euler();
7789
+ const scale = new THREE.Vector3();
7790
+ const color = new THREE.Color();
7791
+ const up = new THREE.Vector3(0, 1, 0);
7792
+ function createInstanced(geometry, material, capacity) {
7793
+ const mesh2 = new THREE.InstancedMesh(geometry, material, capacity);
7794
+ mesh2.count = 0;
7795
+ mesh2.frustumCulled = false;
7796
+ mesh2.receiveShadow = true;
7797
+ return mesh2;
7798
+ }
7799
+ function takeSpare(vertexCount) {
7800
+ const found = spareGeometries.findIndex((geometry) => geometry.getAttribute("position").count === vertexCount);
7801
+ return found < 0 ? void 0 : spareGeometries.splice(found, 1)[0];
7802
+ }
7803
+ function evictOldestGroundMark() {
7804
+ const oldest = groundMarks.shift();
7805
+ group.remove(oldest.mesh);
7806
+ spareGeometries.push(oldest.mesh.geometry);
7807
+ if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
7746
7808
  }
7747
7809
  function conform(mark, material) {
7748
7810
  const radius = mark.radius;
7749
7811
  const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
7750
- const geometry = new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
7751
- const position = geometry.attributes.position;
7812
+ if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
7813
+ const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
7814
+ const grid = geometry.attributes.position;
7815
+ const cell = 2 / steps;
7752
7816
  const cos2 = Math.cos(mark.rotation);
7753
7817
  const sin2 = Math.sin(mark.rotation);
7754
- for (let index = 0; index < position.count; index++) {
7755
- const localX = position.getX(index) * radius;
7756
- const localZ = position.getZ(index) * radius;
7818
+ for (let index = 0; index < grid.count; index++) {
7819
+ const localX = Math.fround(index % (steps + 1) * cell - 1) * radius;
7820
+ const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
7757
7821
  const worldX = mark.x + localX * cos2 - localZ * sin2;
7758
7822
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
7759
- position.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
7823
+ grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
7760
7824
  }
7825
+ grid.needsUpdate = true;
7761
7826
  geometry.computeVertexNormals();
7827
+ geometry.attributes.normal.needsUpdate = true;
7828
+ geometry.boundingSphere = null;
7829
+ geometry.boundingBox = null;
7762
7830
  const mesh2 = new THREE.Mesh(geometry, material);
7763
7831
  mesh2.receiveShadow = true;
7764
- return mesh2;
7832
+ group.add(mesh2);
7833
+ groundMarks.push({ mesh: mesh2, steps });
7765
7834
  }
7766
7835
  function addRock(mark) {
7767
- const mesh2 = new THREE.Mesh(mark.plate ? options.plateGeometry : options.rockGeometry, rockMaterial(mark.color));
7768
- mesh2.position.set(mark.x, mark.y, mark.z);
7769
- mesh2.scale.set(mark.scale[0], mark.scale[1], mark.scale[2]);
7770
- mesh2.rotation.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]);
7771
- mesh2.castShadow = true;
7772
- mesh2.receiveShadow = true;
7773
- return mesh2;
7836
+ const mesh2 = mark.plate ? plateMesh() : rockMesh();
7837
+ const slot = mark.plate ? "plate" : "rock";
7838
+ const index = placed[slot]++ % mesh2.instanceMatrix.count;
7839
+ quaternion.setFromEuler(euler.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]));
7840
+ mesh2.setMatrixAt(index, matrix.compose(position.set(mark.x, mark.y, mark.z), quaternion, scale.set(mark.scale[0], mark.scale[1], mark.scale[2])));
7841
+ mesh2.setColorAt(index, color.set(mark.color));
7842
+ mesh2.count = Math.min(placed[slot], mesh2.instanceMatrix.count);
7843
+ mesh2.instanceMatrix.needsUpdate = true;
7844
+ mesh2.instanceColor.needsUpdate = true;
7845
+ }
7846
+ function writeBlood(index, drawn, growth) {
7847
+ quaternion.setFromAxisAngle(up, drawn.rotation);
7848
+ const mesh2 = bloodMesh();
7849
+ mesh2.setMatrixAt(index, matrix.compose(position.set(drawn.position[0], drawn.position[1], drawn.position[2]), quaternion, scale.set(drawn.scaleX * growth, 1, drawn.scaleZ * growth)));
7850
+ mesh2.instanceMatrix.needsUpdate = true;
7774
7851
  }
7775
7852
  function addBlood(mark) {
7776
- const mesh2 = new THREE.Mesh(bloodGeometry, bloodMaterial);
7777
- mesh2.position.set(mark.x, mark.y, mark.z);
7778
- mesh2.scale.set(mark.scaleX, 1, mark.scaleZ);
7779
- mesh2.rotation.y = mark.rotation;
7780
- mesh2.receiveShadow = true;
7781
- mesh2.scale.multiplyScalar(0.05);
7782
- blood.push({ mesh: mesh2, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
7783
- return mesh2;
7853
+ const index = placed.blood++ % bloodMesh().instanceMatrix.count;
7854
+ const drawn = { position: [mark.x, mark.y, mark.z], rotation: mark.rotation, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 };
7855
+ blood[index] = drawn;
7856
+ writeBlood(index, drawn, BLOOD_START_GROWTH);
7857
+ bloodMesh().count = Math.min(placed.blood, bloodMesh().instanceMatrix.count);
7784
7858
  }
7785
7859
  function build(mark) {
7786
- if (mark.kind === "scorch") return conform(mark, scorchMaterials[mark.variant]);
7787
- if (mark.kind === "dirt") return conform(mark, dirtMaterial);
7788
- if (mark.kind === "rock") return addRock(mark);
7789
- return addBlood(mark);
7790
- }
7791
- function clearMeshes() {
7792
- for (const child of [...group.children]) {
7793
- group.remove(child);
7794
- const geometry = child.geometry;
7795
- if (geometry !== options.rockGeometry && geometry !== options.plateGeometry && geometry !== bloodGeometry) geometry.dispose();
7796
- }
7860
+ if (mark.kind === "scorch") conform(mark, scorchMaterials[mark.variant]);
7861
+ else if (mark.kind === "dirt") conform(mark, dirtMaterial);
7862
+ else if (mark.kind === "rock") addRock(mark);
7863
+ else addBlood(mark);
7864
+ }
7865
+ function clearMarks() {
7866
+ for (const drawn of groundMarks) group.remove(drawn.mesh);
7867
+ for (const drawn of groundMarks) drawn.mesh.geometry.dispose();
7868
+ groundMarks.length = 0;
7869
+ for (const geometry of spareGeometries) geometry.dispose();
7870
+ spareGeometries.length = 0;
7797
7871
  blood.length = 0;
7798
- meshCount = 0;
7872
+ placed.blood = placed.rock = placed.plate = 0;
7873
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) if (mesh2) mesh2.count = 0;
7799
7874
  }
7800
7875
  return {
7801
7876
  group,
7802
7877
  handle(event) {
7803
- if (event.type === "clear") {
7804
- clearMeshes();
7805
- return;
7806
- }
7807
- group.add(build(event.mark));
7808
- meshCount++;
7878
+ if (event.type === "clear") clearMarks();
7879
+ else build(event.mark);
7880
+ },
7881
+ prepare() {
7882
+ bloodMesh();
7883
+ rockMesh();
7884
+ plateMesh();
7809
7885
  },
7810
7886
  update(dt) {
7811
- for (const drawn of blood) {
7812
- if (drawn.age > 1) continue;
7887
+ for (let index = 0; index < blood.length; index++) {
7888
+ const drawn = blood[index];
7889
+ if (!drawn || drawn.age > 1) continue;
7813
7890
  drawn.age += dt * BLOOD_GROW_RATE;
7814
- const ease3 = 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5);
7815
- drawn.mesh.scale.set(drawn.scaleX * Math.max(0.05, ease3), 1, drawn.scaleZ * Math.max(0.05, ease3));
7891
+ writeBlood(index, drawn, Math.max(BLOOD_START_GROWTH, 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5)));
7816
7892
  }
7817
7893
  },
7818
- count: () => meshCount,
7894
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh2) => mesh2 && mesh2.count > 0).length,
7819
7895
  dispose() {
7820
- clearMeshes();
7896
+ clearMarks();
7897
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) mesh2?.dispose();
7821
7898
  for (const item of owned) item.dispose();
7822
7899
  }
7823
7900
  };
7824
7901
  }
7825
7902
  var SPHERE_DEFAULT_WIDTH = 8;
7826
7903
  var SPHERE_DEFAULT_HEIGHT = 6;
7904
+ var MAX_PARKED_MESHES_PER_KIND = 256;
7827
7905
  function geometryOf(THREE, particle) {
7828
7906
  switch (particle.shape) {
7829
7907
  case "cone":
7830
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
7908
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, false);
7831
7909
  case "cone_open":
7832
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
7910
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, true);
7833
7911
  case "sphere":
7834
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
7912
+ return new THREE.SphereGeometry(1, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
7835
7913
  case "icosahedron":
7836
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
7914
+ return new THREE.IcosahedronGeometry(1, particle.segments);
7837
7915
  }
7838
7916
  }
7917
+ function isCone(particle) {
7918
+ return particle.shape === "cone" || particle.shape === "cone_open";
7919
+ }
7839
7920
  function materialOf(THREE, particle) {
7840
7921
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
7841
7922
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -7846,51 +7927,82 @@ void main(){
7846
7927
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
7847
7928
  );
7848
7929
  }
7930
+ function geometryKeyOf(particle) {
7931
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
7932
+ }
7849
7933
  function createMeshLayer(THREE, pool) {
7850
7934
  const group = new THREE.Group();
7851
7935
  group.name = "workshop-meshes";
7852
7936
  const drawn = /* @__PURE__ */ new Map();
7937
+ const geometries = /* @__PURE__ */ new Map();
7938
+ const parked = /* @__PURE__ */ new Map();
7853
7939
  const frame = new THREE.Quaternion();
7854
7940
  const euler = new THREE.Euler();
7855
7941
  const turn = new THREE.Quaternion();
7856
7942
  const offset = new THREE.Vector3();
7857
- function release(mesh2) {
7858
- mesh2.removeFromParent();
7859
- mesh2.geometry.dispose();
7860
- mesh2.material.dispose();
7943
+ let stamp = 0;
7944
+ function geometryFor(particle) {
7945
+ const key = geometryKeyOf(particle);
7946
+ let geometry = geometries.get(key);
7947
+ if (!geometry) {
7948
+ geometry = geometryOf(THREE, particle);
7949
+ geometries.set(key, geometry);
7950
+ }
7951
+ return geometry;
7952
+ }
7953
+ function acquire(particle) {
7954
+ const parkingKey = `${geometryKeyOf(particle)}|${particle.lit}|${particle.additive}`;
7955
+ const mesh2 = parked.get(parkingKey)?.pop() ?? new THREE.Mesh(geometryFor(particle), materialOf(THREE, particle));
7956
+ mesh2.material.color.set(particle.color);
7957
+ mesh2.frustumCulled = false;
7958
+ mesh2.renderOrder = particle.additive ? 5 : 3;
7959
+ group.add(mesh2);
7960
+ return { mesh: mesh2, parkingKey, stamp };
7961
+ }
7962
+ function park(entry) {
7963
+ entry.mesh.removeFromParent();
7964
+ let free = parked.get(entry.parkingKey);
7965
+ if (!free) {
7966
+ free = [];
7967
+ parked.set(entry.parkingKey, free);
7968
+ }
7969
+ if (free.length < MAX_PARKED_MESHES_PER_KIND) free.push(entry.mesh);
7970
+ else entry.mesh.material.dispose();
7861
7971
  }
7862
7972
  return {
7863
7973
  group,
7864
7974
  sync() {
7865
- const live = /* @__PURE__ */ new Set();
7975
+ stamp++;
7866
7976
  for (const particle of pool.particles) {
7867
- live.add(particle.id);
7868
- let mesh2 = drawn.get(particle.id);
7869
- if (!mesh2) {
7870
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
7871
- mesh2.frustumCulled = false;
7872
- mesh2.renderOrder = particle.additive ? 5 : 3;
7873
- drawn.set(particle.id, mesh2);
7874
- group.add(mesh2);
7977
+ let entry = drawn.get(particle.id);
7978
+ if (!entry) {
7979
+ entry = acquire(particle);
7980
+ drawn.set(particle.id, entry);
7875
7981
  }
7982
+ entry.stamp = stamp;
7983
+ const mesh2 = entry.mesh;
7876
7984
  frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
7877
7985
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
7878
7986
  mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
7879
7987
  turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
7880
7988
  mesh2.quaternion.copy(frame).multiply(turn);
7881
- mesh2.scale.setScalar(particle.scale);
7989
+ mesh2.scale.set(particle.radius * particle.scale, (isCone(particle) ? particle.height : particle.radius) * particle.scale, particle.radius * particle.scale);
7882
7990
  mesh2.material.opacity = particle.opacity;
7883
7991
  }
7884
- for (const [id, mesh2] of drawn) {
7885
- if (live.has(id)) continue;
7886
- release(mesh2);
7992
+ for (const [id, entry] of drawn) {
7993
+ if (entry.stamp === stamp) continue;
7994
+ park(entry);
7887
7995
  drawn.delete(id);
7888
7996
  }
7889
7997
  return pool.count;
7890
7998
  },
7891
7999
  dispose() {
7892
- for (const mesh2 of drawn.values()) release(mesh2);
8000
+ for (const entry of drawn.values()) park(entry);
7893
8001
  drawn.clear();
8002
+ for (const free of parked.values()) for (const mesh2 of free) mesh2.material.dispose();
8003
+ parked.clear();
8004
+ for (const geometry of geometries.values()) geometry.dispose();
8005
+ geometries.clear();
7894
8006
  group.removeFromParent();
7895
8007
  }
7896
8008
  };
@@ -8458,7 +8570,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8458
8570
  const shocks = createShockLayer(THREE, pools.shocks);
8459
8571
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
8460
8572
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
8461
- const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry });
8573
+ const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
8462
8574
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
8463
8575
  const stopMarks = world.onMarks((event) => marks.handle(event));
8464
8576
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -8484,6 +8596,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8484
8596
  const position = options.camera.position;
8485
8597
  world.setCamera([position.x, position.y, position.z]);
8486
8598
  }
8599
+ function attachWorkshopLayers() {
8600
+ const engine = world.peekWorkshop();
8601
+ if (!engine || engine === workshopEngine) return;
8602
+ workshopLayers?.dispose();
8603
+ if (workshopLayers) root.remove(workshopLayers.group);
8604
+ workshopEngine = engine;
8605
+ workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
8606
+ root.add(workshopLayers.group);
8607
+ applyLayers();
8608
+ }
8487
8609
  function syncAll() {
8488
8610
  lastDrawn.puffs = puffs.sync();
8489
8611
  lastDrawn.sparks = sparks.sync();
@@ -8492,15 +8614,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8492
8614
  lastDrawn.shocks = shocks.sync();
8493
8615
  lastDrawn.sprites = sprites.sync();
8494
8616
  lights.sync();
8495
- const engine = world.peekWorkshop();
8496
- if (engine && engine !== workshopEngine) {
8497
- workshopLayers?.dispose();
8498
- if (workshopLayers) root.remove(workshopLayers.group);
8499
- workshopEngine = engine;
8500
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
8501
- root.add(workshopLayers.group);
8502
- applyLayers();
8503
- }
8617
+ attachWorkshopLayers();
8504
8618
  const workshopCalls = workshopLayers?.sync() ?? 0;
8505
8619
  const glow = pools.lights.glow;
8506
8620
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -8553,6 +8667,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8553
8667
  },
8554
8668
  warmUpProbes() {
8555
8669
  releaseAnchors();
8670
+ marks.prepare();
8671
+ world.workshop();
8672
+ attachWorkshopLayers();
8556
8673
  const meshes = createMeshLayerProbes(THREE);
8557
8674
  for (const mesh2 of meshes) mesh2.frustumCulled = false;
8558
8675
  const group = new THREE.Group();
@@ -8673,6 +8790,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8673
8790
  var SHEET_SWAY = 3;
8674
8791
  var BIG_LENGTH = 0.2;
8675
8792
  var BIG_WIDTH = 0.03;
8793
+ var FRAGMENT_SEED = 1374496523;
8676
8794
  var isBig = (size) => {
8677
8795
  const [, middle, longest] = [...size].sort((a, b) => a - b);
8678
8796
  return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
@@ -8692,7 +8810,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8692
8810
  __publicField(this, "m");
8693
8811
  __publicField(this, "v");
8694
8812
  __publicField(this, "w");
8695
- __publicField(this, "rng", createRng(1374496523));
8813
+ __publicField(this, "rng", createRng(FRAGMENT_SEED));
8696
8814
  /** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
8697
8815
  __publicField(this, "tick", 0);
8698
8816
  __publicField(this, "markGeometry", null);
@@ -8793,6 +8911,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8793
8911
  mesh2.scale.copy(sc);
8794
8912
  mesh2.castShadow = true;
8795
8913
  mesh2.userData.destructionDebris = true;
8914
+ mesh2.name = "fragment";
8915
+ markKitOrigin(mesh2, { owner: "game-destruction-three", kind: "fragment", id: f.material });
8796
8916
  this.o.scene.add(mesh2);
8797
8917
  const live = {
8798
8918
  mesh: mesh2,
@@ -8809,7 +8929,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8809
8929
  dying: false,
8810
8930
  air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
8811
8931
  body: null,
8812
- born: this.tick
8932
+ born: this.tick,
8933
+ life: Math.max(0, typeof this.o.life === "function" ? this.o.life(f, home) : this.o.life)
8813
8934
  };
8814
8935
  if (f.shape !== "shard" && isBig(f.size)) {
8815
8936
  live.body = {
@@ -8818,8 +8939,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8818
8939
  live.vel.x = x;
8819
8940
  live.vel.z = z;
8820
8941
  live.vel.y = Math.max(live.vel.y, 0.3);
8821
- live.asleep = false;
8822
- live.rest = 0;
8942
+ this.rouse(live);
8823
8943
  }
8824
8944
  };
8825
8945
  }
@@ -8854,16 +8974,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8854
8974
  d.y += k * 0.4;
8855
8975
  f.vel.add(d);
8856
8976
  f.ang.add(this.w.set(this.rng() - 0.5, this.rng() - 0.5, this.rng() - 0.5).multiplyScalar(k));
8857
- f.asleep = false;
8858
- f.rest = 0;
8977
+ this.rouse(f);
8859
8978
  }
8860
8979
  }
8980
+ /** A fragment that came to rest stops having its matrix recomputed on every frame; whatever moves it again calls this first. */
8981
+ rouse(f) {
8982
+ f.asleep = false;
8983
+ f.rest = 0;
8984
+ f.mesh.matrixAutoUpdate = true;
8985
+ }
8861
8986
  /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
8862
8987
  wakeRegion(x, z, radius) {
8863
8988
  for (const f of this.list) {
8864
8989
  if (!f.asleep || Math.hypot(f.mesh.position.x - x, f.mesh.position.z - z) > radius) continue;
8865
- f.asleep = false;
8866
- f.rest = 0;
8990
+ this.rouse(f);
8867
8991
  }
8868
8992
  }
8869
8993
  update(dt) {
@@ -8872,6 +8996,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8872
8996
  for (let i = this.list.length - 1; i >= 0; i--) {
8873
8997
  const f = this.list[i];
8874
8998
  if (f.dying) {
8999
+ f.mesh.matrixAutoUpdate = true;
8875
9000
  f.fade -= dt / 0.6;
8876
9001
  if (f.fade <= 0) {
8877
9002
  this.o.scene.remove(f.mesh);
@@ -8882,7 +9007,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8882
9007
  }
8883
9008
  if (f.asleep) {
8884
9009
  f.rest += dt;
8885
- if (this.o.life > 0 && f.rest > this.o.life && !f.body) f.dying = true;
9010
+ if (f.life > 0 && f.rest > f.life && !f.body) {
9011
+ f.dying = true;
9012
+ f.mesh.matrixAutoUpdate = true;
9013
+ }
8886
9014
  continue;
8887
9015
  }
8888
9016
  this.step(f, dt, g);
@@ -8931,6 +9059,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8931
9059
  f.still += dt;
8932
9060
  if (f.still > 0.4) {
8933
9061
  f.asleep = true;
9062
+ f.mesh.updateMatrix();
9063
+ f.mesh.matrixAutoUpdate = false;
8934
9064
  f.vel.set(0, 0, 0);
8935
9065
  f.ang.set(0, 0, 0);
8936
9066
  }
@@ -8980,6 +9110,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8980
9110
  for (const f of this.list) this.o.scene.remove(f.mesh);
8981
9111
  this.list.length = 0;
8982
9112
  }
9113
+ /** `clear`, the random stream back to its seed and the tick to 0: what comes next flies as on a new system. */
9114
+ reset() {
9115
+ this.clear();
9116
+ this.rng = createRng(FRAGMENT_SEED);
9117
+ this.tick = 0;
9118
+ }
8983
9119
  dispose() {
8984
9120
  this.clear();
8985
9121
  this.box.dispose();
@@ -9078,6 +9214,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9078
9214
  var SUB_DT = 1 / 120;
9079
9215
  var MU = 0.7;
9080
9216
  var SLEEP_AFTER = 0.5;
9217
+ var CALM_DRIFT = 0.01;
9218
+ var CALM_TURN = 0.05;
9219
+ var CALM_SLEEP_AFTER = 1.5;
9081
9220
  var HARD_LANDING = 3;
9082
9221
  var SLOP = 2e-3;
9083
9222
  var NARROW = 0.3;
@@ -9086,6 +9225,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9086
9225
  var TIP_LYING = 0.25;
9087
9226
  var TIP_SECONDS = 3;
9088
9227
  var STEP = 0.03;
9228
+ var WARM_SLOTS = 8 + 12 * 7;
9229
+ var SOLVER_PASSES = 10;
9230
+ var SIDE_BOUNCE = 0.1;
9231
+ var EDGE_POINTS = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
9089
9232
  var boxTopAt = (s, x, z) => {
9090
9233
  const e = s.e;
9091
9234
  const dx = x - s.c[0], dz = z - s.c[2];
@@ -9156,6 +9299,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9156
9299
  localCenter: [localCenter.x, localCenter.y, localCenter.z],
9157
9300
  centerV: localCenter.clone(),
9158
9301
  still: 0,
9302
+ calmAt: null,
9303
+ calm: 0,
9159
9304
  lastHit: -1,
9160
9305
  onGround: false
9161
9306
  };
@@ -9174,6 +9319,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9174
9319
  clear() {
9175
9320
  this.list.length = 0;
9176
9321
  }
9322
+ /** `clear` and the clock back to 0. */
9323
+ reset() {
9324
+ this.clear();
9325
+ this.time = 0;
9326
+ }
9177
9327
  /** World position of the centre of mass. */
9178
9328
  centerOf(b, out) {
9179
9329
  return out.copy(b.centerV).applyQuaternion(b.root.quaternion).add(b.root.position);
@@ -9198,6 +9348,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9198
9348
  }
9199
9349
  b.sleeping = false;
9200
9350
  b.still = 0;
9351
+ b.calmAt = null;
9201
9352
  }
9202
9353
  /** I^-1 * (x, y, z) in world space for a box with the body's half extents and mass (needs `this.m` to hold the body's rotation). */
9203
9354
  mulInvInertia(b, x, y, z) {
@@ -9215,12 +9366,51 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9215
9366
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
9216
9367
  return [e[0] * lx + e[4] * ly + e[8] * lz, e[1] * lx + e[5] * ly + e[9] * lz, e[2] * lx + e[6] * ly + e[10] * lz];
9217
9368
  }
9369
+ /** 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. */
9370
+ samples(b, edges) {
9371
+ const [hx, hy, hz] = b.halfExtents;
9372
+ const out = [];
9373
+ for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz : -hz]);
9374
+ if (!edges) return out;
9375
+ const h = [hx, hy, hz];
9376
+ for (let axis2 = 0; axis2 < 3; axis2++) {
9377
+ const a1 = (axis2 + 1) % 3;
9378
+ const a2 = (axis2 + 2) % 3;
9379
+ for (let k = 0; k < 4; k++) {
9380
+ for (const f of EDGE_POINTS) {
9381
+ const p = [0, 0, 0];
9382
+ p[axis2] = f * h[axis2];
9383
+ p[a1] = k & 1 ? h[a1] : -h[a1];
9384
+ p[a2] = k & 2 ? h[a2] : -h[a2];
9385
+ out.push(p);
9386
+ }
9387
+ }
9388
+ }
9389
+ return out;
9390
+ }
9391
+ /**
9392
+ * An impulse along the unit normal `n` at `r` (from the centre of mass, world) that stops the point closing in along `n` (with `bounce`); `e` holds the body's rotation.
9393
+ */
9394
+ contactImpulse(b, e, r, n, bounce2) {
9395
+ const v = b.velocity;
9396
+ const w = b.angularVelocity;
9397
+ const vn = (v[0] + w[1] * r[2] - w[2] * r[1]) * n[0] + (v[1] + w[2] * r[0] - w[0] * r[2]) * n[1] + (v[2] + w[0] * r[1] - w[1] * r[0]) * n[2];
9398
+ if (vn >= 0) return;
9399
+ const a = this.mulInvInertiaWith(b, e, r[1] * n[2] - r[2] * n[1], r[2] * n[0] - r[0] * n[2], r[0] * n[1] - r[1] * n[0]);
9400
+ const k = (a[1] * r[2] - a[2] * r[1]) * n[0] + (a[2] * r[0] - a[0] * r[2]) * n[1] + (a[0] * r[1] - a[1] * r[0]) * n[2];
9401
+ const j = -(1 + bounce2) * vn / (1 / b.mass + k);
9402
+ for (let i = 0; i < 3; i++) {
9403
+ v[i] += j * n[i] / b.mass;
9404
+ w[i] += a[i] * j;
9405
+ }
9406
+ }
9218
9407
  /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
9219
9408
  wakeRegion(x, z, radius) {
9220
9409
  for (const b of this.list) {
9221
9410
  if (!b.sleeping || b.held || Math.hypot(b.root.position.x - x, b.root.position.z - z) > radius) continue;
9222
9411
  b.sleeping = false;
9223
9412
  b.still = 0;
9413
+ b.calmAt = null;
9224
9414
  }
9225
9415
  }
9226
9416
  update(dt) {
@@ -9236,6 +9426,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9236
9426
  b.angularVelocity = [0, 0, 0];
9237
9427
  b.sleeping = false;
9238
9428
  b.still = 0;
9429
+ b.calmAt = null;
9239
9430
  continue;
9240
9431
  }
9241
9432
  if (b.sleeping) continue;
@@ -9260,7 +9451,6 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9260
9451
  const q = this.q.copy(b.root.quaternion);
9261
9452
  const v = b.velocity;
9262
9453
  const w = b.angularVelocity;
9263
- const [hx, hy, hz] = b.halfExtents;
9264
9454
  const invM = 1 / b.mass;
9265
9455
  const steps = clamp6(Math.ceil(dt / SUB_DT), 1, 8);
9266
9456
  const h = dt / steps;
@@ -9269,7 +9459,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9269
9459
  let contactedLast = false;
9270
9460
  let touched = false;
9271
9461
  let unstable = false;
9462
+ const corners = this.samples(b, false);
9463
+ this.m.makeRotationFromQuaternion(q);
9464
+ let gLow = Infinity;
9465
+ let gHigh = -Infinity;
9466
+ for (const [sx, sy, sz] of corners) {
9467
+ const e0 = this.m.elements;
9468
+ const y = pos[1] + e0[1] * sx + e0[5] * sy + e0[9] * sz;
9469
+ const x = pos[0] + e0[0] * sx + e0[4] * sy + e0[8] * sz;
9470
+ const z = pos[2] + e0[2] * sx + e0[6] * sy + e0[10] * sz;
9471
+ const g = Math.max(this.o.groundAt(x, z), this.o.staticTopAt?.(x, z, y, STEP) ?? -Infinity);
9472
+ gLow = Math.min(gLow, g);
9473
+ gHigh = Math.max(gHigh, g);
9474
+ }
9475
+ const local2 = gHigh - gLow > STEP ? this.samples(b, true) : corners;
9476
+ const prev = local2.map(() => [0, 0, 0]);
9272
9477
  for (let s = 0; s < steps; s++) {
9478
+ this.m.makeRotationFromQuaternion(q);
9479
+ const e0 = this.m.elements;
9480
+ for (let i = 0; i < local2.length; i++) {
9481
+ const [sx, sy, sz] = local2[i];
9482
+ prev[i][0] = pos[0] + e0[0] * sx + e0[4] * sy + e0[8] * sz;
9483
+ prev[i][1] = pos[1] + e0[1] * sx + e0[5] * sy + e0[9] * sz;
9484
+ prev[i][2] = pos[2] + e0[2] * sx + e0[6] * sy + e0[10] * sz;
9485
+ }
9273
9486
  v[1] -= this.o.gravity * h;
9274
9487
  pos[0] += v[0] * h;
9275
9488
  pos[1] += v[1] * h;
@@ -9286,20 +9499,60 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9286
9499
  this.m.makeRotationFromQuaternion(q);
9287
9500
  const e = this.m.elements;
9288
9501
  const rs = [];
9502
+ const ids = [];
9289
9503
  let maxPen = 0;
9290
- for (let i = 0; i < 8; i++) {
9291
- const sx = i & 1 ? hx : -hx;
9292
- const sy = i & 2 ? hy : -hy;
9293
- const sz = i & 4 ? hz : -hz;
9504
+ const rise = STEP + Math.max(0, -v[1]) * h * 2;
9505
+ let side = null;
9506
+ let lowest = Infinity;
9507
+ for (let i = 0; i < local2.length; i++) {
9508
+ const [sx, sy, sz] = local2[i];
9294
9509
  const r = [e[0] * sx + e[4] * sy + e[8] * sz, e[1] * sx + e[5] * sy + e[9] * sz, e[2] * sx + e[6] * sy + e[10] * sz];
9510
+ const px = pos[0] + r[0];
9511
+ const pz = pos[2] + r[2];
9295
9512
  const cy = pos[1] + r[1];
9296
- const gy = this.supportAt(b, pos[0] + r[0], cy, pos[2] + r[2], STEP + Math.max(0, -v[1]) * h * 2);
9513
+ const was = prev[i];
9514
+ const raw = this.o.groundAt(px, pz);
9515
+ if (raw > cy - SLOP && was[1] < raw - STEP) {
9516
+ if (this.o.groundAt(was[0], was[2]) >= raw - STEP) {
9517
+ const floor2 = b.floor;
9518
+ if (floor2 !== void 0 && floor2 - cy > -SLOP && floor2 < raw - STEP) {
9519
+ rs.push(r);
9520
+ ids.push(i);
9521
+ maxPen = Math.max(maxPen, floor2 - cy);
9522
+ }
9523
+ continue;
9524
+ }
9525
+ const dx = px - was[0];
9526
+ const dz = pz - was[2];
9527
+ const len = Math.hypot(dx, dz);
9528
+ if (len > 1e-6) {
9529
+ let lo = 0;
9530
+ let hi = 1;
9531
+ for (let it = 0; it < 6; it++) {
9532
+ const mid = (lo + hi) / 2;
9533
+ if (this.o.groundAt(was[0] + dx * mid, was[2] + dz * mid) >= raw - STEP) hi = mid;
9534
+ else lo = mid;
9535
+ }
9536
+ const back = (1 - lo) * len + 1e-3;
9537
+ if (!side || back > side.back) side = { back, dir: [dx / len, dz / len], r };
9538
+ }
9539
+ continue;
9540
+ }
9541
+ const gy = i < 8 ? this.supportAt(b, px, cy, pz, rise) : Math.max(raw, this.o.staticTopAt?.(px, pz, cy, rise) ?? -Infinity);
9297
9542
  const pen = gy - cy;
9298
9543
  if (pen > -SLOP) {
9299
9544
  rs.push(r);
9545
+ ids.push(i);
9300
9546
  if (pen > maxPen) maxPen = pen;
9547
+ lowest = Math.min(lowest, gy);
9301
9548
  }
9302
9549
  }
9550
+ if (lowest < Infinity) b.floor = lowest;
9551
+ if (side) {
9552
+ pos[0] -= side.dir[0] * side.back;
9553
+ pos[2] -= side.dir[1] * side.back;
9554
+ this.contactImpulse(b, e, side.r, [-side.dir[0], 0, -side.dir[1]], SIDE_BOUNCE);
9555
+ }
9303
9556
  contactedLast = rs.length > 0;
9304
9557
  if (contactedLast) touched = true;
9305
9558
  if (!contactedLast) continue;
@@ -9319,45 +9572,88 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9319
9572
  const lz = pos[2] + sz / rs.length;
9320
9573
  landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
9321
9574
  }
9322
- for (let it = 0; it < 4; it++) {
9323
- for (const r of rs) {
9324
- let vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
9325
- let vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
9326
- let vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
9327
- const vn = vy2;
9328
- if (vn >= 0) continue;
9575
+ const goal = rs.map((r) => {
9576
+ const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
9577
+ return vy2 < 0 ? -restitution * vy2 : 0;
9578
+ });
9579
+ const vx0 = v[0];
9580
+ const vz0 = v[2];
9581
+ if (!b.warm || b.warm.length !== WARM_SLOTS * 3) b.warm = new Float64Array(WARM_SLOTS * 3);
9582
+ const warm = b.warm;
9583
+ const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 3] : 0);
9584
+ const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 3 + 1], warm[id * 3 + 2]] : [0, 0]);
9585
+ for (let k = 0; k < rs.length; k++) {
9586
+ const r = rs[k];
9587
+ const jn = accN[k];
9588
+ const [fx, fz] = accT[k];
9589
+ if (jn === 0 && fx === 0 && fz === 0) continue;
9590
+ v[0] += fx * invM;
9591
+ v[1] += jn * invM;
9592
+ v[2] += fz * invM;
9593
+ const dw = this.mulInvInertiaWith(b, e, r[1] * fz - r[2] * jn, r[2] * fx - r[0] * fz, r[0] * jn - r[1] * fx);
9594
+ w[0] += dw[0];
9595
+ w[1] += dw[1];
9596
+ w[2] += dw[2];
9597
+ }
9598
+ for (let it = 0; it < SOLVER_PASSES; it++) {
9599
+ for (let k = 0; k < rs.length; k++) {
9600
+ const r = rs[k];
9601
+ const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
9329
9602
  const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
9330
9603
  const denom = invM + (a[2] * r[0] - a[0] * r[2]);
9331
- const j = -(1 + (it === 0 ? restitution : 0)) * vn / denom;
9332
- v[1] += j * invM;
9333
- const dw = this.mulInvInertiaWith(b, e, -r[2] * j, 0, r[0] * j);
9334
- w[0] += dw[0];
9335
- w[1] += dw[1];
9336
- w[2] += dw[2];
9337
- vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
9338
- vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
9339
- vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
9604
+ const total = Math.max(0, accN[k] + (goal[k] - vy2) / denom);
9605
+ const j = total - accN[k];
9606
+ accN[k] = total;
9607
+ if (j !== 0) {
9608
+ v[1] += j * invM;
9609
+ w[0] += a[0] * j;
9610
+ w[1] += a[1] * j;
9611
+ w[2] += a[2] * j;
9612
+ }
9613
+ const vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
9614
+ const vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
9340
9615
  const vt = Math.hypot(vx2, vz2);
9341
- if (vt > 1e-5) {
9616
+ if (vt < 1e-6 && accT[k][0] === 0 && accT[k][1] === 0) continue;
9617
+ let fx = accT[k][0];
9618
+ let fz = accT[k][1];
9619
+ if (vt > 1e-6) {
9342
9620
  const tx = vx2 / vt;
9343
9621
  const tz = vz2 / vt;
9344
- const rx = r[1] * tz;
9345
- const ry = r[2] * tx - r[0] * tz;
9346
- const rz = -r[1] * tx;
9347
- const at3 = this.mulInvInertiaWith(b, e, rx, ry, rz);
9622
+ const at3 = this.mulInvInertiaWith(b, e, r[1] * tz, r[2] * tx - r[0] * tz, -r[1] * tx);
9348
9623
  const cx = at3[1] * r[2] - at3[2] * r[1];
9349
9624
  const cz = at3[0] * r[1] - at3[1] * r[0];
9350
- const denomT = invM + cx * tx + cz * tz;
9351
- const jt = Math.min(vt / denomT, MU * j);
9352
- v[0] -= tx * jt * invM;
9353
- v[2] -= tz * jt * invM;
9354
- const dwt = this.mulInvInertiaWith(b, e, rx * jt, ry * jt, rz * jt);
9355
- w[0] -= dwt[0];
9356
- w[1] -= dwt[1];
9357
- w[2] -= dwt[2];
9625
+ const jt = vt / (invM + cx * tx + cz * tz);
9626
+ fx -= tx * jt;
9627
+ fz -= tz * jt;
9628
+ }
9629
+ const most = MU * accN[k];
9630
+ const f = Math.hypot(fx, fz);
9631
+ if (f > most) {
9632
+ fx *= most / f;
9633
+ fz *= most / f;
9358
9634
  }
9635
+ const dx = fx - accT[k][0];
9636
+ const dz = fz - accT[k][1];
9637
+ accT[k] = [fx, fz];
9638
+ if (dx === 0 && dz === 0) continue;
9639
+ v[0] += dx * invM;
9640
+ v[2] += dz * invM;
9641
+ const dw = this.mulInvInertiaWith(b, e, r[1] * dz, r[2] * dx - r[0] * dz, -r[1] * dx);
9642
+ w[0] += dw[0];
9643
+ w[1] += dw[1];
9644
+ w[2] += dw[2];
9359
9645
  }
9360
9646
  }
9647
+ pos[0] += (v[0] - vx0) * h;
9648
+ pos[2] += (v[2] - vz0) * h;
9649
+ warm.fill(0);
9650
+ for (let k = 0; k < rs.length; k++) {
9651
+ const id = ids[k];
9652
+ if (id >= WARM_SLOTS) continue;
9653
+ warm[id * 3] = accN[k];
9654
+ warm[id * 3 + 1] = accT[k][0];
9655
+ warm[id * 3 + 2] = accT[k][1];
9656
+ }
9361
9657
  let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
9362
9658
  for (const r of rs) {
9363
9659
  x0 = Math.min(x0, r[0]);
@@ -9372,7 +9668,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9372
9668
  unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
9373
9669
  if (b.tipping) {
9374
9670
  b.tipping.seconds -= h;
9375
- if (height < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
9671
+ const [hx, hy, hz] = b.halfExtents;
9672
+ const upHalf = Math.abs(e[1]) * hx + Math.abs(e[5]) * hy + Math.abs(e[9]) * hz;
9673
+ if (upHalf < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
9376
9674
  else {
9377
9675
  w[0] += b.tipping.x * TIP_ACCELERATION * h;
9378
9676
  w[2] += b.tipping.z * TIP_ACCELERATION * h;
@@ -9404,16 +9702,26 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9404
9702
  this.o.onHardLanding(b, landPoint, landing);
9405
9703
  }
9406
9704
  const still = touched && !unstable && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
9407
- if (still) {
9408
- b.still += dt;
9409
- if (b.still >= SLEEP_AFTER) {
9410
- b.sleeping = true;
9411
- v[0] = v[1] = v[2] = 0;
9412
- w[0] = w[1] = w[2] = 0;
9413
- }
9414
- } else {
9415
- b.still = 0;
9416
- }
9705
+ if (still) b.still += dt;
9706
+ else b.still = 0;
9707
+ if (b.still >= SLEEP_AFTER || this.staysInPlace(b, touched && !b.tipping, pos, q, dt)) {
9708
+ b.sleeping = true;
9709
+ b.calmAt = null;
9710
+ v[0] = v[1] = v[2] = 0;
9711
+ w[0] = w[1] = w[2] = 0;
9712
+ }
9713
+ }
9714
+ /** Whether the piece has kept to the same spot on the ground for `CALM_SLEEP_AFTER` (see `CALM_DRIFT`). */
9715
+ staysInPlace(b, touched, center, q, dt) {
9716
+ const at3 = b.calmAt;
9717
+ const turned = at3 ? 2 * Math.acos(Math.min(1, Math.abs(at3.quaternion[0] * q.x + at3.quaternion[1] * q.y + at3.quaternion[2] * q.z + at3.quaternion[3] * q.w))) : Infinity;
9718
+ if (!touched || !at3 || Math.hypot(center[0] - at3.center[0], center[1] - at3.center[1], center[2] - at3.center[2]) > CALM_DRIFT || turned > CALM_TURN) {
9719
+ b.calmAt = touched ? { center: [center[0], center[1], center[2]], quaternion: [q.x, q.y, q.z, q.w] } : null;
9720
+ b.calm = 0;
9721
+ return false;
9722
+ }
9723
+ b.calm += dt;
9724
+ return b.calm >= CALM_SLEEP_AFTER;
9417
9725
  }
9418
9726
  /** Half the height of the box of a piece as it is turned now. */
9419
9727
  halfHeight(b) {
@@ -9431,8 +9739,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9431
9739
  const a = this.list[i];
9432
9740
  for (let j = i + 1; j < n; j++) {
9433
9741
  const b = this.list[j];
9434
- if (a.held && b.held) continue;
9435
- if (a.sleeping && b.sleeping) continue;
9742
+ const fixedA = a.held || a.sleeping;
9743
+ const fixedB = b.held || b.sleeping;
9744
+ if (fixedA && fixedB) continue;
9436
9745
  const ra = Math.hypot(a.halfExtents[0], a.halfExtents[1], a.halfExtents[2]);
9437
9746
  const rb = Math.hypot(b.halfExtents[0], b.halfExtents[1], b.halfExtents[2]);
9438
9747
  this.centerOf(a, ca);
@@ -9450,19 +9759,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9450
9759
  d = 1;
9451
9760
  }
9452
9761
  const push = Math.min(maxMove, (rh - d) * 0.5);
9453
- const wa = a.held ? 0 : b.held ? 1 : b.mass / (a.mass + b.mass);
9454
- const wb = b.held ? 0 : a.held ? 1 : a.mass / (a.mass + b.mass);
9762
+ const wa = fixedA ? 0 : fixedB ? 1 : b.mass / (a.mass + b.mass);
9763
+ const wb = fixedB ? 0 : fixedA ? 1 : a.mass / (a.mass + b.mass);
9455
9764
  const ux = dx / (d || 1);
9456
9765
  const uz = dz / (d || 1);
9457
- if (!a.held) {
9766
+ if (!fixedA) {
9458
9767
  a.root.position.x -= ux * push * 2 * wa;
9459
9768
  a.root.position.z -= uz * push * 2 * wa;
9460
- a.sleeping = false;
9461
9769
  }
9462
- if (!b.held) {
9770
+ if (!fixedB) {
9463
9771
  b.root.position.x += ux * push * 2 * wb;
9464
9772
  b.root.position.z += uz * push * 2 * wb;
9465
- b.sleeping = false;
9466
9773
  }
9467
9774
  }
9468
9775
  }
@@ -10111,6 +10418,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10111
10418
  },
10112
10419
  hitDebris: "chips",
10113
10420
  breakDebris: "dust",
10421
+ visual: { innerColor: 10197652 },
10114
10422
  thermal: {
10115
10423
  conductivity: 0.15,
10116
10424
  heatCapacity: 2.5
@@ -10141,6 +10449,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10141
10449
  ],
10142
10450
  hitDebris: "chips",
10143
10451
  breakDebris: "dust",
10452
+ visual: { innerColor: 12085567 },
10144
10453
  thermal: {
10145
10454
  conductivity: 0.15,
10146
10455
  heatCapacity: 2.5
@@ -10171,6 +10480,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10171
10480
  ],
10172
10481
  hitDebris: "dust",
10173
10482
  breakDebris: "dust",
10483
+ visual: { innerColor: 14999507 },
10174
10484
  thermal: {
10175
10485
  conductivity: 0.1,
10176
10486
  heatCapacity: 1.5
@@ -10196,6 +10506,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10196
10506
  ],
10197
10507
  hitDebris: "chips",
10198
10508
  breakDebris: "dust",
10509
+ visual: { innerColor: 9407364 },
10199
10510
  thermal: {
10200
10511
  conductivity: 0.2,
10201
10512
  heatCapacity: 2.5
@@ -10867,6 +11178,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10867
11178
  }
10868
11179
  return out;
10869
11180
  };
11181
+ var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
10870
11182
 
10871
11183
  // src/spec.ts
10872
11184
  function containsOf(value) {
@@ -10925,6 +11237,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10925
11237
  const fill = options.fillOf?.(o, material);
10926
11238
  const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
10927
11239
  const contains = containsOf(ud.destructionContains);
11240
+ const powder = typeof ud.destructionPowder?.color === "number" ? { color: ud.destructionPowder.color } : void 0;
10928
11241
  const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
10929
11242
  const existing = explicit ? byKey.get(explicit) : void 0;
10930
11243
  if (existing) {
@@ -10938,9 +11251,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10938
11251
  existing.skin = existing.skin && skin;
10939
11252
  if (fill !== void 0) existing.fill = fill;
10940
11253
  if (contains) existing.contains = contains;
11254
+ if (powder) existing.powder = powder;
10941
11255
  if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
10942
11256
  } else {
10943
- const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
11257
+ const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...powder ? { powder } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
10944
11258
  entries.push(e);
10945
11259
  if (explicit) byKey.set(explicit, e);
10946
11260
  }
@@ -11015,6 +11329,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11015
11329
  }
11016
11330
  if (e.tags) part.tags = [...e.tags];
11017
11331
  if (e.contains) part.contains = { ...e.contains };
11332
+ if (e.powder) part.powder = { ...e.powder };
11018
11333
  const ao = options.anchorBelow ?? -1;
11019
11334
  if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
11020
11335
  parts.push(part);
@@ -11192,6 +11507,22 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11192
11507
  var SKIN_SLACK = 5e-4;
11193
11508
  var SKIN_LAYER_SLACK = 4;
11194
11509
  var NEAREST_MARGIN = 1.5;
11510
+ var basePositions = (host) => {
11511
+ const p0 = host.mesh.userData._p0;
11512
+ if (p0) return p0;
11513
+ const op = positionAttr(host.origGeometry);
11514
+ return op ? op.array : null;
11515
+ };
11516
+ var collapseForeign = (mesh2, arr) => {
11517
+ for (const [v0, v1] of mesh2.userData.collapsedRanges ?? []) {
11518
+ for (let k = v0 + 1; k < v1; k++) {
11519
+ arr[k * 3] = arr[v0 * 3];
11520
+ arr[k * 3 + 1] = arr[v0 * 3 + 1];
11521
+ arr[k * 3 + 2] = arr[v0 * 3 + 2];
11522
+ }
11523
+ }
11524
+ };
11525
+ var DECAL_SNAP = 0.03;
11195
11526
  var Visuals = class {
11196
11527
  constructor(THREE, lowQuality, decalCap) {
11197
11528
  __publicField(this, "THREE", THREE);
@@ -11201,6 +11532,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11201
11532
  __publicField(this, "a");
11202
11533
  __publicField(this, "b");
11203
11534
  __publicField(this, "m4");
11535
+ __publicField(this, "ray");
11204
11536
  __publicField(this, "box");
11205
11537
  __publicField(this, "t");
11206
11538
  __publicField(this, "lbox");
@@ -11211,6 +11543,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11211
11543
  this.a = tmpV(THREE);
11212
11544
  this.b = tmpV(THREE);
11213
11545
  this.m4 = new THREE.Matrix4();
11546
+ this.ray = new THREE.Raycaster();
11214
11547
  this.box = new THREE.Box3();
11215
11548
  this.t = tmpV(THREE);
11216
11549
  this.lbox = new THREE.Box3();
@@ -11364,7 +11697,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11364
11697
  host.ownGeo = true;
11365
11698
  const g = host.mesh.geometry.clone();
11366
11699
  host.mesh.geometry = g;
11367
- host.work = new Float32Array(positionAttr(g).array);
11700
+ host.work = new Float32Array(basePositions(host) ?? positionAttr(g).array);
11368
11701
  return true;
11369
11702
  }
11370
11703
  // ------------------------------------------------------------------------------------------------------------------------------------
@@ -11534,12 +11867,38 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11534
11867
  // ------------------------------------------------------------------------------------------------------------------------------------
11535
11868
  // Decals
11536
11869
  // ------------------------------------------------------------------------------------------------------------------------------------
11870
+ /**
11871
+ * The point on the drawn face of `mesh` under a world point, within `DECAL_SNAP` metres along the normal: the world's box of a part and the face
11872
+ * the renderer drew (a coat of plaster, a merged house mesh) can be a few millimetres apart, and a mark behind the drawn face does not show.
11873
+ */
11874
+ onFace(mesh2, worldPoint, worldNormal) {
11875
+ if (!mesh2.isMesh || !mesh2.geometry?.attributes?.position) return worldPoint;
11876
+ const n = worldNormal.clone().normalize();
11877
+ this.ray.set(worldPoint.clone().addScaledVector(n, DECAL_SNAP), n.clone().negate());
11878
+ this.ray.near = 0;
11879
+ this.ray.far = 2 * DECAL_SNAP;
11880
+ const hits = [];
11881
+ mesh2.raycast(this.ray, hits);
11882
+ for (const c of mesh2.parent?.children ?? []) {
11883
+ if (c === mesh2 || c.name !== "breach-shell") continue;
11884
+ c.raycast(this.ray, hits);
11885
+ const behind = c.userData.behind?.();
11886
+ if (behind) behind.raycast(this.ray, hits);
11887
+ }
11888
+ let best = null;
11889
+ for (const h of hits) {
11890
+ if (h.object.userData.cutAt?.(h.point)) continue;
11891
+ if (h.face && h.face.normal.clone().transformDirection(h.object.matrixWorld).dot(n) <= 0.3) continue;
11892
+ if (!best || Math.abs(h.distance - DECAL_SNAP) < Math.abs(best.distance - DECAL_SNAP)) best = h;
11893
+ }
11894
+ return best ? best.point : worldPoint;
11895
+ }
11537
11896
  /** Decal pose in mesh space: position and a rotation taking +Z to the surface normal. */
11538
11897
  decalPose(mesh2, worldPoint, worldNormal, lift) {
11539
11898
  mesh2.updateWorldMatrix(true, false);
11540
11899
  const s = mesh2.matrixWorld.getMaxScaleOnAxis() || 1;
11541
11900
  const ln = worldNormal.clone().transformDirection(this.m4.copy(mesh2.matrixWorld).invert()).normalize();
11542
- const pos = mesh2.worldToLocal(worldPoint.clone()).addScaledVector(ln, lift / s);
11901
+ const pos = mesh2.worldToLocal(this.onFace(mesh2, worldPoint, worldNormal).clone()).addScaledVector(ln, lift / s);
11543
11902
  const quat2 = new this.THREE.Quaternion().setFromUnitVectors(new this.THREE.Vector3(0, 0, 1), ln);
11544
11903
  return { s, pos, quat: quat2 };
11545
11904
  }
@@ -11559,6 +11918,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11559
11918
  d.geometry?.dispose();
11560
11919
  this.decalCount = Math.max(0, this.decalCount - 1);
11561
11920
  }
11921
+ /**
11922
+ * Takes away the decals of a mesh that sit inside a box (world `center`, `half` extents along the axes of the world `rotation`): the surface they
11923
+ * marked is gone (a unit came out of the wall with its bullet holes and spalls).
11924
+ */
11925
+ clearDecals(mv, center, half, rotation) {
11926
+ const inv = rotation.clone().invert();
11927
+ const p = new this.THREE.Vector3();
11928
+ for (let i = mv.decals.length - 1; i >= 0; i--) {
11929
+ const d = mv.decals[i];
11930
+ d.obj.getWorldPosition(p).sub(center).applyQuaternion(inv);
11931
+ if (Math.abs(p.x) > half.x || Math.abs(p.y) > half.y || Math.abs(p.z) > half.z) continue;
11932
+ this.disposeDecal(d);
11933
+ mv.decals.splice(i, 1);
11934
+ }
11935
+ }
11562
11936
  addHole(mv, worldPoint, worldNormal, diameter) {
11563
11937
  const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
11564
11938
  const work = mv.host ? mv.host.work : mv.work;
@@ -11763,6 +12137,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11763
12137
  if (!pos || !work) return;
11764
12138
  const arr = pos.array;
11765
12139
  arr.set(work);
12140
+ collapseForeign(host.mesh, arr);
11766
12141
  for (const s of host.sharers) {
11767
12142
  if (s.merged && s.merged.host === host) {
11768
12143
  if (s.collapsed) this.collapseRange(arr, s.merged.range);
@@ -11967,9 +12342,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11967
12342
  mv.touched = false;
11968
12343
  if (!host.ownGeo) return;
11969
12344
  const work = host.work;
11970
- const op = positionAttr(host.origGeometry);
12345
+ const op = basePositions(host);
11971
12346
  const [v0, v1] = merged.range;
11972
- if (op) work.set(op.array.subarray(v0 * 3, v1 * 3), v0 * 3);
12347
+ if (op) work.set(op.subarray(v0 * 3, v1 * 3), v0 * 3);
11973
12348
  const attr = host.mesh.geometry.getAttribute("color");
11974
12349
  if (attr) {
11975
12350
  const orig = host.origGeometry.getAttribute("color");
@@ -11983,6 +12358,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11983
12358
  if (host.sharers.every((s) => !s.touched)) {
11984
12359
  host.mesh.geometry.dispose();
11985
12360
  host.mesh.geometry = host.origGeometry;
12361
+ const base = basePositions(host), back = positionAttr(host.origGeometry);
12362
+ if (base && back) {
12363
+ const arr = back.array;
12364
+ arr.set(base);
12365
+ collapseForeign(host.mesh, arr);
12366
+ back.needsUpdate = true;
12367
+ }
11986
12368
  host.work = null;
11987
12369
  host.ownGeo = false;
11988
12370
  if (host.ownMat) {
@@ -12041,9 +12423,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12041
12423
  // src/renderer.ts
12042
12424
  var rendererCount = 0;
12043
12425
  var COLLAPSE_CLOUD_KG = 1e3;
12426
+ var CHIP_DECAL_EDGE = 0.05;
12427
+ var OPAQUE_CRACK = 0.1;
12428
+ var WOOD_RIM = 1.5;
12044
12429
  var IMPACT_TAGS = ["metal", "glass", "wood"];
12045
12430
  var GLOW_FROM_C = 430;
12046
12431
  var QUEUE_CAP = 4e3;
12432
+ var MIN_FRAGMENTS_PER_SLICE = 8;
12047
12433
  var STATIC_REFRESH = 0.5;
12048
12434
  var STATIC_SPAN = 3;
12049
12435
  var FEET_BAND = 0.03;
@@ -12119,6 +12505,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12119
12505
  const r = v;
12120
12506
  return !!r.mesh && Array.isArray(r.range);
12121
12507
  }
12508
+ var RNG_SEED = 12648430;
12122
12509
  function createDestructionRenderer(THREE, options) {
12123
12510
  return new Renderer(THREE, options);
12124
12511
  }
@@ -12134,6 +12521,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12134
12521
  __publicField(this, "gravity");
12135
12522
  __publicField(this, "autoSplit");
12136
12523
  __publicField(this, "maxFragments");
12524
+ __publicField(this, "eventBudgetMs");
12137
12525
  __publicField(this, "bounds", /* @__PURE__ */ new Map());
12138
12526
  /** World boxes (min xyz, max xyz) of the drawn meshes of the bound objects that are not pieces, for the pieces to land on. */
12139
12527
  __publicField(this, "staticBoxes", /* @__PURE__ */ new Map());
@@ -12144,6 +12532,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12144
12532
  __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
12145
12533
  __publicField(this, "staticAt", 0);
12146
12534
  __publicField(this, "queue", []);
12535
+ /** Presentation work an event left for later frames (fragments still to spawn, loose groups still to split), run in order within the frame budget. */
12536
+ __publicField(this, "slices", []);
12537
+ /** When the work of this frame must stop; `Infinity` outside `drainQueue`, where a call finishes what it started. */
12538
+ __publicField(this, "sliceDeadline", Infinity);
12147
12539
  __publicField(this, "unsubscribe");
12148
12540
  __publicField(this, "visuals");
12149
12541
  __publicField(this, "fragments");
@@ -12157,7 +12549,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12157
12549
  __publicField(this, "leaks", []);
12158
12550
  __publicField(this, "homeTags", []);
12159
12551
  __publicField(this, "throttles", /* @__PURE__ */ new Map());
12160
- __publicField(this, "rng", createRng(12648430));
12552
+ __publicField(this, "rng", createRng(RNG_SEED));
12161
12553
  __publicField(this, "vfx");
12162
12554
  /** Names this renderer to the world's drips, so a shared world tells each renderer where its own landed. */
12163
12555
  __publicField(this, "owner", `destruction-${++rendererCount}`);
@@ -12181,6 +12573,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12181
12573
  this.gravity = o.gravity ?? 9.81;
12182
12574
  this.autoSplit = o.autoSplit !== false;
12183
12575
  this.maxFragments = o.maxFragments ?? 160;
12576
+ this.eventBudgetMs = o.eventBudgetMs ?? Infinity;
12184
12577
  this.box = new THREE.Box3();
12185
12578
  this.tmpQ = new THREE.Quaternion();
12186
12579
  this.v1 = new THREE.Vector3();
@@ -12266,6 +12659,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12266
12659
  if (leftovers.length) this.matchByBox(objectId, root, leftovers, add2);
12267
12660
  }
12268
12661
  const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
12662
+ if (options.onFrame) bound.onFrame = options.onFrame;
12269
12663
  const hosts = /* @__PURE__ */ new Map();
12270
12664
  for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? [], hosts));
12271
12665
  for (const [pid, meshes] of byPart) {
@@ -12425,6 +12819,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12425
12819
  dt = clamp6(dt, 0, 0.1);
12426
12820
  this.time += dt;
12427
12821
  this.drainQueue();
12822
+ for (const b of this.bounds.values()) b.onFrame?.(dt);
12428
12823
  for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
12429
12824
  this.updateActive(dt);
12430
12825
  this.updateLeaks(dt);
@@ -12436,11 +12831,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12436
12831
  }
12437
12832
  drainQueue() {
12438
12833
  const batch = this.queue.splice(0, this.queue.length);
12439
- for (const ev of batch) {
12834
+ const deadline = performance.now() + this.eventBudgetMs;
12835
+ this.sliceDeadline = deadline;
12836
+ try {
12837
+ this.runSlices(deadline);
12838
+ for (let i = 0; i < batch.length; i++) {
12839
+ if (i > 0 && performance.now() >= deadline) {
12840
+ this.queue.unshift(...batch.slice(i));
12841
+ return;
12842
+ }
12843
+ try {
12844
+ this.apply(batch[i]);
12845
+ } catch (err) {
12846
+ if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", batch[i].type, err);
12847
+ }
12848
+ }
12849
+ } finally {
12850
+ this.sliceDeadline = Infinity;
12851
+ }
12852
+ }
12853
+ /** Runs the work left by earlier frames until the deadline; at least one piece per frame, so it always finishes. */
12854
+ runSlices(deadline) {
12855
+ for (let ran = 0; this.slices.length > 0 && (ran === 0 || performance.now() < deadline); ran++) {
12440
12856
  try {
12441
- this.apply(ev);
12857
+ this.slices.shift()();
12442
12858
  } catch (err) {
12443
- if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", ev.type, err);
12859
+ if (typeof console !== "undefined") console.error("game-destruction-three: failed to run deferred work", err);
12444
12860
  }
12445
12861
  }
12446
12862
  }
@@ -12604,7 +13020,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12604
13020
  break;
12605
13021
  }
12606
13022
  case "debris":
12607
- this.vfx.burst(ev.kind, ev.point, ev.normal, ev.count, ev.speed > 0 ? ev.speed : DEFAULT_SPEED[ev.kind] ?? 1, this.debrisOptions(ev.kind, ev.object, ev.part));
13023
+ this.vfx.burst(ev.kind, ev.point, ev.normal, ev.count, ev.speed > 0 ? ev.speed : DEFAULT_SPEED[ev.kind] ?? 1, this.debrisOptions(ev.kind, ev.object, ev.part, ev.material));
12608
13024
  break;
12609
13025
  case "fracture":
12610
13026
  this.onFracture(ev);
@@ -12777,10 +13193,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12777
13193
  }
12778
13194
  return def ? fallbackColor(def.id, def) : null;
12779
13195
  }
12780
- debrisOptions(kind, objectId, partId) {
13196
+ debrisOptions(kind, objectId, partId, material) {
12781
13197
  const pv = this.partVis(objectId, partId);
12782
- const def = pv ? this.world.materials.get(pv.material) : void 0;
13198
+ const def = this.world.materials.get(material ?? pv?.material ?? "");
12783
13199
  if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
13200
+ if ((kind === "dust" || kind === "chips") && def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
12784
13201
  if (kind === "fibers" || kind === "leaves") {
12785
13202
  if (def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
12786
13203
  const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
@@ -12837,12 +13254,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12837
13254
  const seed = hashNumbers(ev.time, point[0], point[1], point[2]);
12838
13255
  this.visuals.addCrack(mv, wp, normal, 7, Math.max(0.05, ev.hole.diameter * 5), 15267071, seed);
12839
13256
  } else {
13257
+ const fresh = def?.penetrationClass === "wood" ? def.visual?.innerColor : void 0;
13258
+ if (fresh !== void 0) this.visuals.addSpall(mv, wp, normal, Math.max(6e-3, ev.hole.diameter * WOOD_RIM), fresh);
12840
13259
  this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
12841
13260
  }
12842
13261
  };
12843
13262
  draw(ev.worldPoint, n);
12844
13263
  if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
12845
13264
  if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
13265
+ if (ev.powder) {
13266
+ const puff3 = { color: ev.powder.color, size: 0.6, life: 1.6, alpha: 1.3 };
13267
+ this.vfx.burst("dust", ev.worldPoint, [n.x, n.y, n.z], this.low ? 5 : 10, 0.7, puff3);
13268
+ if (ev.hole.through && ev.exitPoint) this.vfx.burst("dust", ev.exitPoint, [-n.x, -n.y, -n.z], this.low ? 10 : 20, 1.3, puff3);
13269
+ }
12846
13270
  this.sound(impactKey(def), ev.worldPoint, 0.6);
12847
13271
  }
12848
13272
  onCrack(ev) {
@@ -12853,8 +13277,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12853
13277
  const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
12854
13278
  const mv = this.nearestMesh(pv, wp);
12855
13279
  if (mv) {
12856
- const color = def?.visual?.transparent ? 15267071 : 2762532;
12857
- this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, ev.crack.length, color, hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]));
13280
+ const glass = !!def?.visual?.transparent;
13281
+ const own = this.partColor(pv, def) ?? fallbackColor(pv.material, def);
13282
+ const color = glass ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
13283
+ const length = glass ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
13284
+ this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, length, color, hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]));
12858
13285
  }
12859
13286
  this.sound(impactKey(def), ev.worldPoint, 0.5);
12860
13287
  }
@@ -12888,8 +13315,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12888
13315
  const mv = this.nearestMesh(pv, wp);
12889
13316
  if (!mv) return;
12890
13317
  const def = this.world.materials.get(pv.material);
12891
- this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
12892
- this.visuals.addSpall(mv, wp, this.faceNormal(mv, wp), Math.sqrt(ev.area / Math.PI), fallbackColor(pv.material, def));
13318
+ const normal = this.faceNormal(mv, wp);
13319
+ const core = def?.visual?.innerColor;
13320
+ if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
13321
+ else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
13322
+ if (mv.mesh.userData.drawnSpalls?.includes(ev.part)) return;
13323
+ this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
12893
13324
  }
12894
13325
  /**
12895
13326
  * Stage 2 of a staged part: the unit-sized fragments come out and the part stays drawn (hiding it is what `fracture` does). The opening itself
@@ -12912,8 +13343,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12912
13343
  this.sound(impactKey(def), ev.opening.center, 0.8);
12913
13344
  }
12914
13345
  /**
12915
- * A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust. The gap itself is the drawing's (the house renderer redraws
12916
- * the wall as its courses without that unit); a part drawn as one box keeps its box.
13346
+ * A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust, and the marks drawn on it (bullet holes, spalls, the impact
13347
+ * marks of the effects) go with it. The gap itself is the drawing's (the house renderer cuts the wall where the unit was); a part drawn as one box
13348
+ * keeps its box.
12917
13349
  */
12918
13350
  onChip(ev) {
12919
13351
  const pv = this.partVis(ev.object, ev.part);
@@ -12925,7 +13357,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12925
13357
  const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
12926
13358
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone, false), ev.object, maxAlive);
12927
13359
  }
12928
- if (!this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.center)) this.vfx.burst("dust", ev.center, [0, 1, 0], 6, 0.8, { color });
13360
+ if (ev.torn) {
13361
+ this.sound(impactKey(def), ev.center, 0.5);
13362
+ return;
13363
+ }
13364
+ const T = this.THREE;
13365
+ const center = new T.Vector3(ev.center[0], ev.center[1], ev.center[2]);
13366
+ const half = new T.Vector3(ev.size[0] / 2 + CHIP_DECAL_EDGE, ev.size[1] / 2 + CHIP_DECAL_EDGE, ev.size[2] / 2 + CHIP_DECAL_EDGE);
13367
+ const rotation = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
13368
+ for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation);
13369
+ this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
13370
+ this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
12929
13371
  this.sound(impactKey(def), ev.center, 0.7);
12930
13372
  }
12931
13373
  onFracture(ev) {
@@ -12943,15 +13385,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12943
13385
  if (m && m.transparent) opacity = clamp6(m.opacity, 0.2, 0.8);
12944
13386
  this.visuals.hide(pv, "broken");
12945
13387
  }
12946
- const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
12947
- const step = this.low ? 2 : 1;
12948
- for (let i = 0; i < ev.fragments.length; i += step) {
12949
- const fr = ev.fragments[i];
12950
- const fdef = this.world.materials.get(fr.material);
12951
- const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def || !fdef ? color : fallbackColor(fr.material, fdef);
12952
- const mat = this.fragments.materialFor(fr.material, tone, transparent || fdef?.visual?.transparent === true, opacity);
12953
- this.fragments.spawn(fr, mat, ev.object, maxAlive);
12954
- }
13388
+ this.spawnFragments(ev, 0, { def, color, opacity, transparent });
12955
13389
  const up = [0, 1, 0];
12956
13390
  const sizeK = clamp6(ev.fragments.length / 10, 0.5, 2);
12957
13391
  switch (ev.style) {
@@ -12977,6 +13411,22 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12977
13411
  }
12978
13412
  this.sound(fractureKey(def, ev.style), ev.point, clamp6(0.4 + ev.fragments.length * 0.03, 0.4, 1));
12979
13413
  }
13414
+ /** Spawns the fragments of a fracture from index `from`; what does not fit in this frame's budget is spawned by the next frames. */
13415
+ spawnFragments(ev, from, look) {
13416
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
13417
+ const step = this.low ? 2 : 1;
13418
+ for (let i = from; i < ev.fragments.length; i += step) {
13419
+ if (i - from >= MIN_FRAGMENTS_PER_SLICE * step && performance.now() >= this.sliceDeadline) {
13420
+ this.slices.push(() => this.spawnFragments(ev, i, look));
13421
+ return;
13422
+ }
13423
+ const fr = ev.fragments[i];
13424
+ const fdef = this.world.materials.get(fr.material);
13425
+ const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === look.def || !fdef ? look.color : fallbackColor(fr.material, fdef);
13426
+ const mat = this.fragments.materialFor(fr.material, tone, look.transparent || fdef?.visual?.transparent === true, look.opacity);
13427
+ this.fragments.spawn(fr, mat, ev.object, maxAlive);
13428
+ }
13429
+ }
12980
13430
  onDetach(ev) {
12981
13431
  this.sound("detach", this.rootPoint(ev.object), 0.5);
12982
13432
  this.splitLoose(ev.object);
@@ -12990,6 +13440,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12990
13440
  if (!st || st.groups.length < 2) return;
12991
13441
  const loose = st.groups.length - 1;
12992
13442
  for (let i = 0; i < loose; i++) {
13443
+ if (i > 0 && performance.now() >= this.sliceDeadline) {
13444
+ this.slices.push(() => {
13445
+ this.staticDirty.add(objectId);
13446
+ this.splitLoose(objectId);
13447
+ });
13448
+ return;
13449
+ }
12993
13450
  let newId;
12994
13451
  try {
12995
13452
  newId = this.world.splitGroup(objectId, 1);
@@ -13006,8 +13463,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13006
13463
  old.mass = Math.max(0.05, this.massOf(objectId));
13007
13464
  old.sleeping = false;
13008
13465
  } else if (kitOriginOf(b.root)?.kind === "furniture") {
13009
- this.makePiece(objectId);
13466
+ const piece = this.makePiece(objectId);
13010
13467
  this.tipIfTopHeavy(b);
13468
+ this.o.onLoosened?.(objectId, piece, "apart");
13011
13469
  }
13012
13470
  }
13013
13471
  /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
@@ -13116,7 +13574,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13116
13574
  const piece = this.makePiece(b.id, { mass });
13117
13575
  const body = this.pieceSys.get(b.id);
13118
13576
  if (!body) continue;
13119
- this.o.onLoosened?.(b.id, piece);
13577
+ this.o.onLoosened?.(b.id, piece, "blast");
13120
13578
  const spread = half[0];
13121
13579
  const momentum = hit.speed * mass;
13122
13580
  this.pieceSys.impulse(body, [hit.direction[0] * momentum, hit.direction[1] * momentum, hit.direction[2] * momentum], [world.x + (this.rng() - 0.5) * spread, world.y + spread * 0.5, world.z + (this.rng() - 0.5) * spread]);
@@ -13311,6 +13769,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13311
13769
  }
13312
13770
  clear() {
13313
13771
  this.queue.length = 0;
13772
+ this.slices.length = 0;
13314
13773
  this.active.clear();
13315
13774
  for (const key of [...this.fires.keys()]) this.stopFire(key);
13316
13775
  this.leaks.length = 0;
@@ -13330,6 +13789,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13330
13789
  this.vfx.sprites().clear();
13331
13790
  this.ownVfx?.world.clear();
13332
13791
  }
13792
+ reset() {
13793
+ this.clear();
13794
+ this.rng = createRng(RNG_SEED);
13795
+ this.time = 0;
13796
+ this.staticAt = 0;
13797
+ this.throttles.clear();
13798
+ this.fragments.reset();
13799
+ this.pieceSys.reset();
13800
+ }
13333
13801
  wakeRegion(x, z, radius) {
13334
13802
  this.fragments.wakeRegion(x, z, radius);
13335
13803
  this.pieceSys.wakeRegion(x, z, radius);
@@ -13634,7 +14102,18 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13634
14102
  drag("drag_masonry", [sceneNoise({ type: "bandpass", f: 260, q: 0.9, d: 0.3, vol: 0.62 }), ...sceneMany(3, () => sceneNoise({ type: "bandpass", f: [600, 1200], q: 2, d: 0.04, vol: 0.14, delay: [0, 0.2] }))]),
13635
14103
  drag("drag_soft", [sceneNoise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
13636
14104
  ];
13637
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
14105
+ var leaf = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a door, lid or drawer worked by hand"] });
14106
+ var LEAF_SOUNDS = [
14107
+ leaf("latch", [sceneNoise({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.35 }), sceneTone({ f: 1900, f2: 1500, d: 0.025, vol: 0.06, delay: 0.035 })]),
14108
+ leaf("creak_wood", [sceneTone({ f: [190, 240], f2: 310, d: 0.5, vol: 0.05 }), sceneNoise({ type: "bandpass", f: 520, q: 6, a: 0.05, d: 0.45, vol: 0.12 })]),
14109
+ leaf("creak_metal", [sceneTone({ f: [620, 760], f2: 900, d: 0.35, vol: 0.035 }), sceneNoise({ type: "bandpass", f: 1400, q: 8, a: 0.04, d: 0.3, vol: 0.08 })]),
14110
+ leaf("shut_wood", [sceneTone({ f: 120, f2: 60, d: 0.14, vol: 0.5 }), sceneNoise({ type: "bandpass", f: 600, q: 1.2, d: 0.1, vol: 0.55, wet: 0.25 }), sceneNoise({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.25, delay: 0.02 })]),
14111
+ leaf("shut_metal", [sceneTone({ f: 160, f2: 85, d: 0.18, vol: 0.55 }), sceneNoise({ type: "bandpass", f: 900, q: 1.6, d: 0.12, vol: 0.45, wet: 0.3 }), sceneTone({ f: 1250, f2: 1150, d: 0.12, vol: 0.06 })]),
14112
+ leaf("shut_soft", [sceneTone({ f: 95, f2: 55, d: 0.12, vol: 0.35 }), sceneNoise({ type: "lowpass", f: 500, d: 0.1, vol: 0.3 })]),
14113
+ leaf("rattle", sceneMany(5, () => sceneNoise({ type: "bandpass", f: [1600, 2600], q: 3, d: 0.035, vol: 0.32, delay: [0, 0.45] }))),
14114
+ leaf("slide", [sceneNoise({ type: "bandpass", f: 700, q: 2, a: 0.04, d: 0.35, vol: 0.22 }), sceneNoise({ type: "bandpass", f: 2200, q: 5, d: 0.3, vol: 0.05 })])
14115
+ ];
14116
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
13638
14117
 
13639
14118
  // src/audio/sound.ts
13640
14119
  var DESTRUCTION_SOUND_KEYS = {
@@ -13649,6 +14128,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13649
14128
  drag_glass: { sound: "drag_glass" },
13650
14129
  drag_masonry: { sound: "drag_masonry" },
13651
14130
  drag_soft: { sound: "drag_soft" },
14131
+ // a leaf worked by hand (doors, lids, drawers): the latch, the hinge turning, the leaf meeting the frame, a locked one shaken
14132
+ leaf_latch: { sound: "latch" },
14133
+ leaf_creak_wood: { sound: "creak_wood" },
14134
+ leaf_creak_metal: { sound: "creak_metal" },
14135
+ leaf_shut_wood: { sound: "shut_wood" },
14136
+ leaf_shut_metal: { sound: "shut_metal" },
14137
+ leaf_shut_glass: { sound: "shut_metal", volumeScale: 0.6 },
14138
+ leaf_shut_masonry: { sound: "shut_wood" },
14139
+ leaf_shut_soft: { sound: "shut_soft" },
14140
+ leaf_rattle: { sound: "rattle" },
14141
+ leaf_slide: { sound: "slide" },
14142
+ // the keys of the item clips (furniture and vehicle doors, hoods, trunks, drawers)
14143
+ door_latch: { sound: "latch" },
14144
+ hood_latch: { sound: "latch" },
14145
+ trunk_latch: { sound: "latch" },
14146
+ door_open: { sound: "latch" },
14147
+ door_shut: { sound: "shut_metal" },
14148
+ hood_shut: { sound: "shut_metal" },
14149
+ trunk_shut: { sound: "shut_metal" },
14150
+ door_close: { sound: "shut_wood", volumeScale: 0.7 },
14151
+ drawer_open: { sound: "slide" },
14152
+ drawer_close: { sound: "slide" },
14153
+ lid_open: { sound: "latch" },
14154
+ lid_close: { sound: "shut_soft" },
14155
+ flap_open: { sound: "latch" },
14156
+ flap_close: { sound: "shut_soft" },
13652
14157
  shatter_glass: { sound: "glass_shatter" },
13653
14158
  crumble: { sound: "crumble" },
13654
14159
  splinter: { sound: "crack" },