@adatechnology/game-destruction-three 0.11.1 → 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.
@@ -7727,6 +7727,11 @@ void main(){
7727
7727
  var MAX_CONFORM_STEPS = 48;
7728
7728
  var MIN_CONFORM_STEPS = 16;
7729
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;
7730
7735
  function createMarkLayer(THREE, options) {
7731
7736
  const group = new THREE.Group();
7732
7737
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -7750,110 +7755,168 @@ void main(){
7750
7755
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
7751
7756
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
7752
7757
  owned.push(bloodMaterial, bloodGeometry);
7753
- const rockMaterials = /* @__PURE__ */ new Map();
7754
- const blood = [];
7755
- let meshCount = 0;
7756
- function rockMaterial(color) {
7757
- let material = rockMaterials.get(color);
7758
- if (!material) {
7759
- material = new THREE.MeshStandardMaterial({ color, roughness: 0.9, metalness: 0, flatShading: true });
7760
- rockMaterials.set(color, material);
7761
- 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);
7762
7764
  }
7763
- 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();
7764
7808
  }
7765
7809
  function conform(mark, material) {
7766
7810
  const radius = mark.radius;
7767
7811
  const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
7768
- const geometry = new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
7769
- 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;
7770
7816
  const cos2 = Math.cos(mark.rotation);
7771
7817
  const sin2 = Math.sin(mark.rotation);
7772
- for (let index = 0; index < position.count; index++) {
7773
- const localX = position.getX(index) * radius;
7774
- 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;
7775
7821
  const worldX = mark.x + localX * cos2 - localZ * sin2;
7776
7822
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
7777
- 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);
7778
7824
  }
7825
+ grid.needsUpdate = true;
7779
7826
  geometry.computeVertexNormals();
7827
+ geometry.attributes.normal.needsUpdate = true;
7828
+ geometry.boundingSphere = null;
7829
+ geometry.boundingBox = null;
7780
7830
  const mesh2 = new THREE.Mesh(geometry, material);
7781
7831
  mesh2.receiveShadow = true;
7782
- return mesh2;
7832
+ group.add(mesh2);
7833
+ groundMarks.push({ mesh: mesh2, steps });
7783
7834
  }
7784
7835
  function addRock(mark) {
7785
- const mesh2 = new THREE.Mesh(mark.plate ? options.plateGeometry : options.rockGeometry, rockMaterial(mark.color));
7786
- mesh2.position.set(mark.x, mark.y, mark.z);
7787
- mesh2.scale.set(mark.scale[0], mark.scale[1], mark.scale[2]);
7788
- mesh2.rotation.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]);
7789
- mesh2.castShadow = true;
7790
- mesh2.receiveShadow = true;
7791
- 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;
7792
7851
  }
7793
7852
  function addBlood(mark) {
7794
- const mesh2 = new THREE.Mesh(bloodGeometry, bloodMaterial);
7795
- mesh2.position.set(mark.x, mark.y, mark.z);
7796
- mesh2.scale.set(mark.scaleX, 1, mark.scaleZ);
7797
- mesh2.rotation.y = mark.rotation;
7798
- mesh2.receiveShadow = true;
7799
- mesh2.scale.multiplyScalar(0.05);
7800
- blood.push({ mesh: mesh2, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
7801
- 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);
7802
7858
  }
7803
7859
  function build(mark) {
7804
- if (mark.kind === "scorch") return conform(mark, scorchMaterials[mark.variant]);
7805
- if (mark.kind === "dirt") return conform(mark, dirtMaterial);
7806
- if (mark.kind === "rock") return addRock(mark);
7807
- return addBlood(mark);
7808
- }
7809
- function clearMeshes() {
7810
- for (const child of [...group.children]) {
7811
- group.remove(child);
7812
- const geometry = child.geometry;
7813
- if (geometry !== options.rockGeometry && geometry !== options.plateGeometry && geometry !== bloodGeometry) geometry.dispose();
7814
- }
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;
7815
7871
  blood.length = 0;
7816
- 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;
7817
7874
  }
7818
7875
  return {
7819
7876
  group,
7820
7877
  handle(event) {
7821
- if (event.type === "clear") {
7822
- clearMeshes();
7823
- return;
7824
- }
7825
- group.add(build(event.mark));
7826
- meshCount++;
7878
+ if (event.type === "clear") clearMarks();
7879
+ else build(event.mark);
7880
+ },
7881
+ prepare() {
7882
+ bloodMesh();
7883
+ rockMesh();
7884
+ plateMesh();
7827
7885
  },
7828
7886
  update(dt) {
7829
- for (const drawn of blood) {
7830
- 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;
7831
7890
  drawn.age += dt * BLOOD_GROW_RATE;
7832
- const ease3 = 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5);
7833
- 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)));
7834
7892
  }
7835
7893
  },
7836
- count: () => meshCount,
7894
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh2) => mesh2 && mesh2.count > 0).length,
7837
7895
  dispose() {
7838
- clearMeshes();
7896
+ clearMarks();
7897
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) mesh2?.dispose();
7839
7898
  for (const item of owned) item.dispose();
7840
7899
  }
7841
7900
  };
7842
7901
  }
7843
7902
  var SPHERE_DEFAULT_WIDTH = 8;
7844
7903
  var SPHERE_DEFAULT_HEIGHT = 6;
7904
+ var MAX_PARKED_MESHES_PER_KIND = 256;
7845
7905
  function geometryOf(THREE, particle) {
7846
7906
  switch (particle.shape) {
7847
7907
  case "cone":
7848
- 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);
7849
7909
  case "cone_open":
7850
- 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);
7851
7911
  case "sphere":
7852
- 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);
7853
7913
  case "icosahedron":
7854
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
7914
+ return new THREE.IcosahedronGeometry(1, particle.segments);
7855
7915
  }
7856
7916
  }
7917
+ function isCone(particle) {
7918
+ return particle.shape === "cone" || particle.shape === "cone_open";
7919
+ }
7857
7920
  function materialOf(THREE, particle) {
7858
7921
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
7859
7922
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -7864,51 +7927,82 @@ void main(){
7864
7927
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
7865
7928
  );
7866
7929
  }
7930
+ function geometryKeyOf(particle) {
7931
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
7932
+ }
7867
7933
  function createMeshLayer(THREE, pool) {
7868
7934
  const group = new THREE.Group();
7869
7935
  group.name = "workshop-meshes";
7870
7936
  const drawn = /* @__PURE__ */ new Map();
7937
+ const geometries = /* @__PURE__ */ new Map();
7938
+ const parked = /* @__PURE__ */ new Map();
7871
7939
  const frame = new THREE.Quaternion();
7872
7940
  const euler = new THREE.Euler();
7873
7941
  const turn = new THREE.Quaternion();
7874
7942
  const offset = new THREE.Vector3();
7875
- function release(mesh2) {
7876
- mesh2.removeFromParent();
7877
- mesh2.geometry.dispose();
7878
- 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();
7879
7971
  }
7880
7972
  return {
7881
7973
  group,
7882
7974
  sync() {
7883
- const live = /* @__PURE__ */ new Set();
7975
+ stamp++;
7884
7976
  for (const particle of pool.particles) {
7885
- live.add(particle.id);
7886
- let mesh2 = drawn.get(particle.id);
7887
- if (!mesh2) {
7888
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
7889
- mesh2.frustumCulled = false;
7890
- mesh2.renderOrder = particle.additive ? 5 : 3;
7891
- drawn.set(particle.id, mesh2);
7892
- group.add(mesh2);
7977
+ let entry = drawn.get(particle.id);
7978
+ if (!entry) {
7979
+ entry = acquire(particle);
7980
+ drawn.set(particle.id, entry);
7893
7981
  }
7982
+ entry.stamp = stamp;
7983
+ const mesh2 = entry.mesh;
7894
7984
  frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
7895
7985
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
7896
7986
  mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
7897
7987
  turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
7898
7988
  mesh2.quaternion.copy(frame).multiply(turn);
7899
- 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);
7900
7990
  mesh2.material.opacity = particle.opacity;
7901
7991
  }
7902
- for (const [id, mesh2] of drawn) {
7903
- if (live.has(id)) continue;
7904
- release(mesh2);
7992
+ for (const [id, entry] of drawn) {
7993
+ if (entry.stamp === stamp) continue;
7994
+ park(entry);
7905
7995
  drawn.delete(id);
7906
7996
  }
7907
7997
  return pool.count;
7908
7998
  },
7909
7999
  dispose() {
7910
- for (const mesh2 of drawn.values()) release(mesh2);
8000
+ for (const entry of drawn.values()) park(entry);
7911
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();
7912
8006
  group.removeFromParent();
7913
8007
  }
7914
8008
  };
@@ -8476,7 +8570,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8476
8570
  const shocks = createShockLayer(THREE, pools.shocks);
8477
8571
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
8478
8572
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
8479
- 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 });
8480
8574
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
8481
8575
  const stopMarks = world.onMarks((event) => marks.handle(event));
8482
8576
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -8502,6 +8596,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8502
8596
  const position = options.camera.position;
8503
8597
  world.setCamera([position.x, position.y, position.z]);
8504
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
+ }
8505
8609
  function syncAll() {
8506
8610
  lastDrawn.puffs = puffs.sync();
8507
8611
  lastDrawn.sparks = sparks.sync();
@@ -8510,15 +8614,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8510
8614
  lastDrawn.shocks = shocks.sync();
8511
8615
  lastDrawn.sprites = sprites.sync();
8512
8616
  lights.sync();
8513
- const engine = world.peekWorkshop();
8514
- if (engine && engine !== workshopEngine) {
8515
- workshopLayers?.dispose();
8516
- if (workshopLayers) root.remove(workshopLayers.group);
8517
- workshopEngine = engine;
8518
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
8519
- root.add(workshopLayers.group);
8520
- applyLayers();
8521
- }
8617
+ attachWorkshopLayers();
8522
8618
  const workshopCalls = workshopLayers?.sync() ?? 0;
8523
8619
  const glow = pools.lights.glow;
8524
8620
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -8571,6 +8667,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8571
8667
  },
8572
8668
  warmUpProbes() {
8573
8669
  releaseAnchors();
8670
+ marks.prepare();
8671
+ world.workshop();
8672
+ attachWorkshopLayers();
8574
8673
  const meshes = createMeshLayerProbes(THREE);
8575
8674
  for (const mesh2 of meshes) mesh2.frustumCulled = false;
8576
8675
  const group = new THREE.Group();
@@ -8691,6 +8790,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8691
8790
  var SHEET_SWAY = 3;
8692
8791
  var BIG_LENGTH = 0.2;
8693
8792
  var BIG_WIDTH = 0.03;
8793
+ var FRAGMENT_SEED = 1374496523;
8694
8794
  var isBig = (size) => {
8695
8795
  const [, middle, longest] = [...size].sort((a, b) => a - b);
8696
8796
  return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
@@ -8710,7 +8810,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8710
8810
  __publicField(this, "m");
8711
8811
  __publicField(this, "v");
8712
8812
  __publicField(this, "w");
8713
- __publicField(this, "rng", createRng(1374496523));
8813
+ __publicField(this, "rng", createRng(FRAGMENT_SEED));
8714
8814
  /** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
8715
8815
  __publicField(this, "tick", 0);
8716
8816
  __publicField(this, "markGeometry", null);
@@ -8811,6 +8911,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8811
8911
  mesh2.scale.copy(sc);
8812
8912
  mesh2.castShadow = true;
8813
8913
  mesh2.userData.destructionDebris = true;
8914
+ mesh2.name = "fragment";
8915
+ markKitOrigin(mesh2, { owner: "game-destruction-three", kind: "fragment", id: f.material });
8814
8916
  this.o.scene.add(mesh2);
8815
8917
  const live = {
8816
8918
  mesh: mesh2,
@@ -8827,7 +8929,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8827
8929
  dying: false,
8828
8930
  air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
8829
8931
  body: null,
8830
- 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)
8831
8934
  };
8832
8935
  if (f.shape !== "shard" && isBig(f.size)) {
8833
8936
  live.body = {
@@ -8836,8 +8939,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8836
8939
  live.vel.x = x;
8837
8940
  live.vel.z = z;
8838
8941
  live.vel.y = Math.max(live.vel.y, 0.3);
8839
- live.asleep = false;
8840
- live.rest = 0;
8942
+ this.rouse(live);
8841
8943
  }
8842
8944
  };
8843
8945
  }
@@ -8872,16 +8974,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8872
8974
  d.y += k * 0.4;
8873
8975
  f.vel.add(d);
8874
8976
  f.ang.add(this.w.set(this.rng() - 0.5, this.rng() - 0.5, this.rng() - 0.5).multiplyScalar(k));
8875
- f.asleep = false;
8876
- f.rest = 0;
8977
+ this.rouse(f);
8877
8978
  }
8878
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
+ }
8879
8986
  /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
8880
8987
  wakeRegion(x, z, radius) {
8881
8988
  for (const f of this.list) {
8882
8989
  if (!f.asleep || Math.hypot(f.mesh.position.x - x, f.mesh.position.z - z) > radius) continue;
8883
- f.asleep = false;
8884
- f.rest = 0;
8990
+ this.rouse(f);
8885
8991
  }
8886
8992
  }
8887
8993
  update(dt) {
@@ -8890,6 +8996,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8890
8996
  for (let i = this.list.length - 1; i >= 0; i--) {
8891
8997
  const f = this.list[i];
8892
8998
  if (f.dying) {
8999
+ f.mesh.matrixAutoUpdate = true;
8893
9000
  f.fade -= dt / 0.6;
8894
9001
  if (f.fade <= 0) {
8895
9002
  this.o.scene.remove(f.mesh);
@@ -8900,7 +9007,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8900
9007
  }
8901
9008
  if (f.asleep) {
8902
9009
  f.rest += dt;
8903
- 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
+ }
8904
9014
  continue;
8905
9015
  }
8906
9016
  this.step(f, dt, g);
@@ -8949,6 +9059,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8949
9059
  f.still += dt;
8950
9060
  if (f.still > 0.4) {
8951
9061
  f.asleep = true;
9062
+ f.mesh.updateMatrix();
9063
+ f.mesh.matrixAutoUpdate = false;
8952
9064
  f.vel.set(0, 0, 0);
8953
9065
  f.ang.set(0, 0, 0);
8954
9066
  }
@@ -8998,6 +9110,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8998
9110
  for (const f of this.list) this.o.scene.remove(f.mesh);
8999
9111
  this.list.length = 0;
9000
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
+ }
9001
9119
  dispose() {
9002
9120
  this.clear();
9003
9121
  this.box.dispose();
@@ -9096,6 +9214,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9096
9214
  var SUB_DT = 1 / 120;
9097
9215
  var MU = 0.7;
9098
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;
9099
9220
  var HARD_LANDING = 3;
9100
9221
  var SLOP = 2e-3;
9101
9222
  var NARROW = 0.3;
@@ -9104,6 +9225,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9104
9225
  var TIP_LYING = 0.25;
9105
9226
  var TIP_SECONDS = 3;
9106
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];
9107
9232
  var boxTopAt = (s, x, z) => {
9108
9233
  const e = s.e;
9109
9234
  const dx = x - s.c[0], dz = z - s.c[2];
@@ -9174,6 +9299,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9174
9299
  localCenter: [localCenter.x, localCenter.y, localCenter.z],
9175
9300
  centerV: localCenter.clone(),
9176
9301
  still: 0,
9302
+ calmAt: null,
9303
+ calm: 0,
9177
9304
  lastHit: -1,
9178
9305
  onGround: false
9179
9306
  };
@@ -9192,6 +9319,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9192
9319
  clear() {
9193
9320
  this.list.length = 0;
9194
9321
  }
9322
+ /** `clear` and the clock back to 0. */
9323
+ reset() {
9324
+ this.clear();
9325
+ this.time = 0;
9326
+ }
9195
9327
  /** World position of the centre of mass. */
9196
9328
  centerOf(b, out) {
9197
9329
  return out.copy(b.centerV).applyQuaternion(b.root.quaternion).add(b.root.position);
@@ -9216,6 +9348,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9216
9348
  }
9217
9349
  b.sleeping = false;
9218
9350
  b.still = 0;
9351
+ b.calmAt = null;
9219
9352
  }
9220
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). */
9221
9354
  mulInvInertia(b, x, y, z) {
@@ -9233,12 +9366,51 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9233
9366
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
9234
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];
9235
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
+ }
9236
9407
  /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
9237
9408
  wakeRegion(x, z, radius) {
9238
9409
  for (const b of this.list) {
9239
9410
  if (!b.sleeping || b.held || Math.hypot(b.root.position.x - x, b.root.position.z - z) > radius) continue;
9240
9411
  b.sleeping = false;
9241
9412
  b.still = 0;
9413
+ b.calmAt = null;
9242
9414
  }
9243
9415
  }
9244
9416
  update(dt) {
@@ -9254,6 +9426,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9254
9426
  b.angularVelocity = [0, 0, 0];
9255
9427
  b.sleeping = false;
9256
9428
  b.still = 0;
9429
+ b.calmAt = null;
9257
9430
  continue;
9258
9431
  }
9259
9432
  if (b.sleeping) continue;
@@ -9278,7 +9451,6 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9278
9451
  const q = this.q.copy(b.root.quaternion);
9279
9452
  const v = b.velocity;
9280
9453
  const w = b.angularVelocity;
9281
- const [hx, hy, hz] = b.halfExtents;
9282
9454
  const invM = 1 / b.mass;
9283
9455
  const steps = clamp6(Math.ceil(dt / SUB_DT), 1, 8);
9284
9456
  const h = dt / steps;
@@ -9287,7 +9459,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9287
9459
  let contactedLast = false;
9288
9460
  let touched = false;
9289
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]);
9290
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
+ }
9291
9486
  v[1] -= this.o.gravity * h;
9292
9487
  pos[0] += v[0] * h;
9293
9488
  pos[1] += v[1] * h;
@@ -9304,20 +9499,60 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9304
9499
  this.m.makeRotationFromQuaternion(q);
9305
9500
  const e = this.m.elements;
9306
9501
  const rs = [];
9502
+ const ids = [];
9307
9503
  let maxPen = 0;
9308
- for (let i = 0; i < 8; i++) {
9309
- const sx = i & 1 ? hx : -hx;
9310
- const sy = i & 2 ? hy : -hy;
9311
- 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];
9312
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];
9313
9512
  const cy = pos[1] + r[1];
9314
- 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);
9315
9542
  const pen = gy - cy;
9316
9543
  if (pen > -SLOP) {
9317
9544
  rs.push(r);
9545
+ ids.push(i);
9318
9546
  if (pen > maxPen) maxPen = pen;
9547
+ lowest = Math.min(lowest, gy);
9319
9548
  }
9320
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
+ }
9321
9556
  contactedLast = rs.length > 0;
9322
9557
  if (contactedLast) touched = true;
9323
9558
  if (!contactedLast) continue;
@@ -9337,45 +9572,88 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9337
9572
  const lz = pos[2] + sz / rs.length;
9338
9573
  landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
9339
9574
  }
9340
- for (let it = 0; it < 4; it++) {
9341
- for (const r of rs) {
9342
- let vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
9343
- let vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
9344
- let vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
9345
- const vn = vy2;
9346
- 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]);
9347
9602
  const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
9348
9603
  const denom = invM + (a[2] * r[0] - a[0] * r[2]);
9349
- const j = -(1 + (it === 0 ? restitution : 0)) * vn / denom;
9350
- v[1] += j * invM;
9351
- const dw = this.mulInvInertiaWith(b, e, -r[2] * j, 0, r[0] * j);
9352
- w[0] += dw[0];
9353
- w[1] += dw[1];
9354
- w[2] += dw[2];
9355
- vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
9356
- vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
9357
- 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]);
9358
9615
  const vt = Math.hypot(vx2, vz2);
9359
- 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) {
9360
9620
  const tx = vx2 / vt;
9361
9621
  const tz = vz2 / vt;
9362
- const rx = r[1] * tz;
9363
- const ry = r[2] * tx - r[0] * tz;
9364
- const rz = -r[1] * tx;
9365
- 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);
9366
9623
  const cx = at3[1] * r[2] - at3[2] * r[1];
9367
9624
  const cz = at3[0] * r[1] - at3[1] * r[0];
9368
- const denomT = invM + cx * tx + cz * tz;
9369
- const jt = Math.min(vt / denomT, MU * j);
9370
- v[0] -= tx * jt * invM;
9371
- v[2] -= tz * jt * invM;
9372
- const dwt = this.mulInvInertiaWith(b, e, rx * jt, ry * jt, rz * jt);
9373
- w[0] -= dwt[0];
9374
- w[1] -= dwt[1];
9375
- w[2] -= dwt[2];
9625
+ const jt = vt / (invM + cx * tx + cz * tz);
9626
+ fx -= tx * jt;
9627
+ fz -= tz * jt;
9376
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;
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];
9377
9645
  }
9378
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
+ }
9379
9657
  let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
9380
9658
  for (const r of rs) {
9381
9659
  x0 = Math.min(x0, r[0]);
@@ -9390,7 +9668,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9390
9668
  unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
9391
9669
  if (b.tipping) {
9392
9670
  b.tipping.seconds -= h;
9393
- 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;
9394
9674
  else {
9395
9675
  w[0] += b.tipping.x * TIP_ACCELERATION * h;
9396
9676
  w[2] += b.tipping.z * TIP_ACCELERATION * h;
@@ -9422,16 +9702,26 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9422
9702
  this.o.onHardLanding(b, landPoint, landing);
9423
9703
  }
9424
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;
9425
- if (still) {
9426
- b.still += dt;
9427
- if (b.still >= SLEEP_AFTER) {
9428
- b.sleeping = true;
9429
- v[0] = v[1] = v[2] = 0;
9430
- w[0] = w[1] = w[2] = 0;
9431
- }
9432
- } else {
9433
- b.still = 0;
9434
- }
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;
9435
9725
  }
9436
9726
  /** Half the height of the box of a piece as it is turned now. */
9437
9727
  halfHeight(b) {
@@ -9449,8 +9739,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9449
9739
  const a = this.list[i];
9450
9740
  for (let j = i + 1; j < n; j++) {
9451
9741
  const b = this.list[j];
9452
- if (a.held && b.held) continue;
9453
- 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;
9454
9745
  const ra = Math.hypot(a.halfExtents[0], a.halfExtents[1], a.halfExtents[2]);
9455
9746
  const rb = Math.hypot(b.halfExtents[0], b.halfExtents[1], b.halfExtents[2]);
9456
9747
  this.centerOf(a, ca);
@@ -9468,19 +9759,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9468
9759
  d = 1;
9469
9760
  }
9470
9761
  const push = Math.min(maxMove, (rh - d) * 0.5);
9471
- const wa = a.held ? 0 : b.held ? 1 : b.mass / (a.mass + b.mass);
9472
- 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);
9473
9764
  const ux = dx / (d || 1);
9474
9765
  const uz = dz / (d || 1);
9475
- if (!a.held) {
9766
+ if (!fixedA) {
9476
9767
  a.root.position.x -= ux * push * 2 * wa;
9477
9768
  a.root.position.z -= uz * push * 2 * wa;
9478
- a.sleeping = false;
9479
9769
  }
9480
- if (!b.held) {
9770
+ if (!fixedB) {
9481
9771
  b.root.position.x += ux * push * 2 * wb;
9482
9772
  b.root.position.z += uz * push * 2 * wb;
9483
- b.sleeping = false;
9484
9773
  }
9485
9774
  }
9486
9775
  }
@@ -10889,6 +11178,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10889
11178
  }
10890
11179
  return out;
10891
11180
  };
11181
+ var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
10892
11182
 
10893
11183
  // src/spec.ts
10894
11184
  function containsOf(value) {
@@ -10947,6 +11237,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10947
11237
  const fill = options.fillOf?.(o, material);
10948
11238
  const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
10949
11239
  const contains = containsOf(ud.destructionContains);
11240
+ const powder = typeof ud.destructionPowder?.color === "number" ? { color: ud.destructionPowder.color } : void 0;
10950
11241
  const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
10951
11242
  const existing = explicit ? byKey.get(explicit) : void 0;
10952
11243
  if (existing) {
@@ -10960,9 +11251,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10960
11251
  existing.skin = existing.skin && skin;
10961
11252
  if (fill !== void 0) existing.fill = fill;
10962
11253
  if (contains) existing.contains = contains;
11254
+ if (powder) existing.powder = powder;
10963
11255
  if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
10964
11256
  } else {
10965
- 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 };
10966
11258
  entries.push(e);
10967
11259
  if (explicit) byKey.set(explicit, e);
10968
11260
  }
@@ -11037,6 +11329,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11037
11329
  }
11038
11330
  if (e.tags) part.tags = [...e.tags];
11039
11331
  if (e.contains) part.contains = { ...e.contains };
11332
+ if (e.powder) part.powder = { ...e.powder };
11040
11333
  const ao = options.anchorBelow ?? -1;
11041
11334
  if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
11042
11335
  parts.push(part);
@@ -11214,6 +11507,22 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11214
11507
  var SKIN_SLACK = 5e-4;
11215
11508
  var SKIN_LAYER_SLACK = 4;
11216
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;
11217
11526
  var Visuals = class {
11218
11527
  constructor(THREE, lowQuality, decalCap) {
11219
11528
  __publicField(this, "THREE", THREE);
@@ -11223,6 +11532,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11223
11532
  __publicField(this, "a");
11224
11533
  __publicField(this, "b");
11225
11534
  __publicField(this, "m4");
11535
+ __publicField(this, "ray");
11226
11536
  __publicField(this, "box");
11227
11537
  __publicField(this, "t");
11228
11538
  __publicField(this, "lbox");
@@ -11233,6 +11543,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11233
11543
  this.a = tmpV(THREE);
11234
11544
  this.b = tmpV(THREE);
11235
11545
  this.m4 = new THREE.Matrix4();
11546
+ this.ray = new THREE.Raycaster();
11236
11547
  this.box = new THREE.Box3();
11237
11548
  this.t = tmpV(THREE);
11238
11549
  this.lbox = new THREE.Box3();
@@ -11386,7 +11697,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11386
11697
  host.ownGeo = true;
11387
11698
  const g = host.mesh.geometry.clone();
11388
11699
  host.mesh.geometry = g;
11389
- host.work = new Float32Array(positionAttr(g).array);
11700
+ host.work = new Float32Array(basePositions(host) ?? positionAttr(g).array);
11390
11701
  return true;
11391
11702
  }
11392
11703
  // ------------------------------------------------------------------------------------------------------------------------------------
@@ -11556,12 +11867,38 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11556
11867
  // ------------------------------------------------------------------------------------------------------------------------------------
11557
11868
  // Decals
11558
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
+ }
11559
11896
  /** Decal pose in mesh space: position and a rotation taking +Z to the surface normal. */
11560
11897
  decalPose(mesh2, worldPoint, worldNormal, lift) {
11561
11898
  mesh2.updateWorldMatrix(true, false);
11562
11899
  const s = mesh2.matrixWorld.getMaxScaleOnAxis() || 1;
11563
11900
  const ln = worldNormal.clone().transformDirection(this.m4.copy(mesh2.matrixWorld).invert()).normalize();
11564
- 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);
11565
11902
  const quat2 = new this.THREE.Quaternion().setFromUnitVectors(new this.THREE.Vector3(0, 0, 1), ln);
11566
11903
  return { s, pos, quat: quat2 };
11567
11904
  }
@@ -11800,6 +12137,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11800
12137
  if (!pos || !work) return;
11801
12138
  const arr = pos.array;
11802
12139
  arr.set(work);
12140
+ collapseForeign(host.mesh, arr);
11803
12141
  for (const s of host.sharers) {
11804
12142
  if (s.merged && s.merged.host === host) {
11805
12143
  if (s.collapsed) this.collapseRange(arr, s.merged.range);
@@ -12004,9 +12342,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12004
12342
  mv.touched = false;
12005
12343
  if (!host.ownGeo) return;
12006
12344
  const work = host.work;
12007
- const op = positionAttr(host.origGeometry);
12345
+ const op = basePositions(host);
12008
12346
  const [v0, v1] = merged.range;
12009
- 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);
12010
12348
  const attr = host.mesh.geometry.getAttribute("color");
12011
12349
  if (attr) {
12012
12350
  const orig = host.origGeometry.getAttribute("color");
@@ -12020,6 +12358,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12020
12358
  if (host.sharers.every((s) => !s.touched)) {
12021
12359
  host.mesh.geometry.dispose();
12022
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
+ }
12023
12368
  host.work = null;
12024
12369
  host.ownGeo = false;
12025
12370
  if (host.ownMat) {
@@ -12079,9 +12424,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12079
12424
  var rendererCount = 0;
12080
12425
  var COLLAPSE_CLOUD_KG = 1e3;
12081
12426
  var CHIP_DECAL_EDGE = 0.05;
12427
+ var OPAQUE_CRACK = 0.1;
12428
+ var WOOD_RIM = 1.5;
12082
12429
  var IMPACT_TAGS = ["metal", "glass", "wood"];
12083
12430
  var GLOW_FROM_C = 430;
12084
12431
  var QUEUE_CAP = 4e3;
12432
+ var MIN_FRAGMENTS_PER_SLICE = 8;
12085
12433
  var STATIC_REFRESH = 0.5;
12086
12434
  var STATIC_SPAN = 3;
12087
12435
  var FEET_BAND = 0.03;
@@ -12157,6 +12505,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12157
12505
  const r = v;
12158
12506
  return !!r.mesh && Array.isArray(r.range);
12159
12507
  }
12508
+ var RNG_SEED = 12648430;
12160
12509
  function createDestructionRenderer(THREE, options) {
12161
12510
  return new Renderer(THREE, options);
12162
12511
  }
@@ -12172,6 +12521,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12172
12521
  __publicField(this, "gravity");
12173
12522
  __publicField(this, "autoSplit");
12174
12523
  __publicField(this, "maxFragments");
12524
+ __publicField(this, "eventBudgetMs");
12175
12525
  __publicField(this, "bounds", /* @__PURE__ */ new Map());
12176
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. */
12177
12527
  __publicField(this, "staticBoxes", /* @__PURE__ */ new Map());
@@ -12182,6 +12532,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12182
12532
  __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
12183
12533
  __publicField(this, "staticAt", 0);
12184
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);
12185
12539
  __publicField(this, "unsubscribe");
12186
12540
  __publicField(this, "visuals");
12187
12541
  __publicField(this, "fragments");
@@ -12195,7 +12549,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12195
12549
  __publicField(this, "leaks", []);
12196
12550
  __publicField(this, "homeTags", []);
12197
12551
  __publicField(this, "throttles", /* @__PURE__ */ new Map());
12198
- __publicField(this, "rng", createRng(12648430));
12552
+ __publicField(this, "rng", createRng(RNG_SEED));
12199
12553
  __publicField(this, "vfx");
12200
12554
  /** Names this renderer to the world's drips, so a shared world tells each renderer where its own landed. */
12201
12555
  __publicField(this, "owner", `destruction-${++rendererCount}`);
@@ -12219,6 +12573,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12219
12573
  this.gravity = o.gravity ?? 9.81;
12220
12574
  this.autoSplit = o.autoSplit !== false;
12221
12575
  this.maxFragments = o.maxFragments ?? 160;
12576
+ this.eventBudgetMs = o.eventBudgetMs ?? Infinity;
12222
12577
  this.box = new THREE.Box3();
12223
12578
  this.tmpQ = new THREE.Quaternion();
12224
12579
  this.v1 = new THREE.Vector3();
@@ -12304,6 +12659,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12304
12659
  if (leftovers.length) this.matchByBox(objectId, root, leftovers, add2);
12305
12660
  }
12306
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;
12307
12663
  const hosts = /* @__PURE__ */ new Map();
12308
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));
12309
12665
  for (const [pid, meshes] of byPart) {
@@ -12463,6 +12819,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12463
12819
  dt = clamp6(dt, 0, 0.1);
12464
12820
  this.time += dt;
12465
12821
  this.drainQueue();
12822
+ for (const b of this.bounds.values()) b.onFrame?.(dt);
12466
12823
  for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
12467
12824
  this.updateActive(dt);
12468
12825
  this.updateLeaks(dt);
@@ -12474,11 +12831,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12474
12831
  }
12475
12832
  drainQueue() {
12476
12833
  const batch = this.queue.splice(0, this.queue.length);
12477
- 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++) {
12478
12856
  try {
12479
- this.apply(ev);
12857
+ this.slices.shift()();
12480
12858
  } catch (err) {
12481
- 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);
12482
12860
  }
12483
12861
  }
12484
12862
  }
@@ -12876,12 +13254,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12876
13254
  const seed = hashNumbers(ev.time, point[0], point[1], point[2]);
12877
13255
  this.visuals.addCrack(mv, wp, normal, 7, Math.max(0.05, ev.hole.diameter * 5), 15267071, seed);
12878
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);
12879
13259
  this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
12880
13260
  }
12881
13261
  };
12882
13262
  draw(ev.worldPoint, n);
12883
13263
  if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
12884
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
+ }
12885
13270
  this.sound(impactKey(def), ev.worldPoint, 0.6);
12886
13271
  }
12887
13272
  onCrack(ev) {
@@ -12892,8 +13277,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12892
13277
  const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
12893
13278
  const mv = this.nearestMesh(pv, wp);
12894
13279
  if (mv) {
12895
- const color = def?.visual?.transparent ? 15267071 : 2762532;
12896
- 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]));
12897
13285
  }
12898
13286
  this.sound(impactKey(def), ev.worldPoint, 0.5);
12899
13287
  }
@@ -12931,6 +13319,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12931
13319
  const core = def?.visual?.innerColor;
12932
13320
  if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
12933
13321
  else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
13322
+ if (mv.mesh.userData.drawnSpalls?.includes(ev.part)) return;
12934
13323
  this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
12935
13324
  }
12936
13325
  /**
@@ -12968,6 +13357,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12968
13357
  const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
12969
13358
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone, false), ev.object, maxAlive);
12970
13359
  }
13360
+ if (ev.torn) {
13361
+ this.sound(impactKey(def), ev.center, 0.5);
13362
+ return;
13363
+ }
12971
13364
  const T = this.THREE;
12972
13365
  const center = new T.Vector3(ev.center[0], ev.center[1], ev.center[2]);
12973
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);
@@ -12992,15 +13385,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12992
13385
  if (m && m.transparent) opacity = clamp6(m.opacity, 0.2, 0.8);
12993
13386
  this.visuals.hide(pv, "broken");
12994
13387
  }
12995
- const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
12996
- const step = this.low ? 2 : 1;
12997
- for (let i = 0; i < ev.fragments.length; i += step) {
12998
- const fr = ev.fragments[i];
12999
- const fdef = this.world.materials.get(fr.material);
13000
- const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def || !fdef ? color : fallbackColor(fr.material, fdef);
13001
- const mat = this.fragments.materialFor(fr.material, tone, transparent || fdef?.visual?.transparent === true, opacity);
13002
- this.fragments.spawn(fr, mat, ev.object, maxAlive);
13003
- }
13388
+ this.spawnFragments(ev, 0, { def, color, opacity, transparent });
13004
13389
  const up = [0, 1, 0];
13005
13390
  const sizeK = clamp6(ev.fragments.length / 10, 0.5, 2);
13006
13391
  switch (ev.style) {
@@ -13026,6 +13411,22 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13026
13411
  }
13027
13412
  this.sound(fractureKey(def, ev.style), ev.point, clamp6(0.4 + ev.fragments.length * 0.03, 0.4, 1));
13028
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
+ }
13029
13430
  onDetach(ev) {
13030
13431
  this.sound("detach", this.rootPoint(ev.object), 0.5);
13031
13432
  this.splitLoose(ev.object);
@@ -13039,6 +13440,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13039
13440
  if (!st || st.groups.length < 2) return;
13040
13441
  const loose = st.groups.length - 1;
13041
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
+ }
13042
13450
  let newId;
13043
13451
  try {
13044
13452
  newId = this.world.splitGroup(objectId, 1);
@@ -13055,8 +13463,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13055
13463
  old.mass = Math.max(0.05, this.massOf(objectId));
13056
13464
  old.sleeping = false;
13057
13465
  } else if (kitOriginOf(b.root)?.kind === "furniture") {
13058
- this.makePiece(objectId);
13466
+ const piece = this.makePiece(objectId);
13059
13467
  this.tipIfTopHeavy(b);
13468
+ this.o.onLoosened?.(objectId, piece, "apart");
13060
13469
  }
13061
13470
  }
13062
13471
  /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
@@ -13165,7 +13574,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13165
13574
  const piece = this.makePiece(b.id, { mass });
13166
13575
  const body = this.pieceSys.get(b.id);
13167
13576
  if (!body) continue;
13168
- this.o.onLoosened?.(b.id, piece);
13577
+ this.o.onLoosened?.(b.id, piece, "blast");
13169
13578
  const spread = half[0];
13170
13579
  const momentum = hit.speed * mass;
13171
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]);
@@ -13360,6 +13769,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13360
13769
  }
13361
13770
  clear() {
13362
13771
  this.queue.length = 0;
13772
+ this.slices.length = 0;
13363
13773
  this.active.clear();
13364
13774
  for (const key of [...this.fires.keys()]) this.stopFire(key);
13365
13775
  this.leaks.length = 0;
@@ -13379,6 +13789,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13379
13789
  this.vfx.sprites().clear();
13380
13790
  this.ownVfx?.world.clear();
13381
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
+ }
13382
13801
  wakeRegion(x, z, radius) {
13383
13802
  this.fragments.wakeRegion(x, z, radius);
13384
13803
  this.pieceSys.wakeRegion(x, z, radius);
@@ -13683,7 +14102,18 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13683
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] }))]),
13684
14103
  drag("drag_soft", [sceneNoise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
13685
14104
  ];
13686
- 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] };
13687
14117
 
13688
14118
  // src/audio/sound.ts
13689
14119
  var DESTRUCTION_SOUND_KEYS = {
@@ -13698,6 +14128,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13698
14128
  drag_glass: { sound: "drag_glass" },
13699
14129
  drag_masonry: { sound: "drag_masonry" },
13700
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" },
13701
14157
  shatter_glass: { sound: "glass_shatter" },
13702
14158
  crumble: { sound: "crumble" },
13703
14159
  splinter: { sound: "crack" },