@adatechnology/game-destruction-three 0.7.3 → 0.8.1

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.
@@ -35,6 +35,24 @@ var GameDestructionThree = (() => {
35
35
  specFromObject3D: () => specFromObject3D
36
36
  });
37
37
 
38
+ // ../game-contracts/dist/index.js
39
+ var DEFAULT_BLAST_SHOVE = { coupling: 0.02, maxSpeed: 20, lift: 0.3, minSpeed: 0.3, falloffPower: 2 };
40
+ function blastShove(blast, target, options = {}) {
41
+ const o = { ...DEFAULT_BLAST_SHOVE, ...options };
42
+ const dx = target.position[0] - blast.point[0];
43
+ const dy = target.position[1] - blast.point[1];
44
+ const dz = target.position[2] - blast.point[2];
45
+ const distance = Math.hypot(dx, dy, dz);
46
+ if (!(blast.radius > 0) || distance > blast.radius) return void 0;
47
+ const falloff = (1 - distance / blast.radius) ** o.falloffPower;
48
+ const mass = Math.max(target.mass, 0.05);
49
+ const speed = Math.min(o.maxSpeed, Math.sqrt(2 * blast.energy * (blast.reach ?? 1) * o.coupling / mass)) * falloff;
50
+ if (!(speed >= o.minSpeed)) return void 0;
51
+ const direction = distance > 1e-4 ? [dx / distance, dy / distance + o.lift, dz / distance] : [0, 1, 0];
52
+ const length = Math.hypot(direction[0], direction[1], direction[2]);
53
+ return { speed, direction: [direction[0] / length, direction[1] / length, direction[2] / length], distance, falloff };
54
+ }
55
+
38
56
  // ../game-vfx/dist/index.js
39
57
  var __defProp2 = Object.defineProperty;
40
58
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -3085,6 +3103,17 @@ var GameDestructionThree = (() => {
3085
3103
  ])
3086
3104
  ]
3087
3105
  };
3106
+ var FUEL_LEAK_ONLY = {
3107
+ id: "fuel_leak_only",
3108
+ pageId: "vazamento",
3109
+ team: "vehicles",
3110
+ budget: { particles: 200, lights: 0 },
3111
+ loop: 11,
3112
+ view: FUEL_LEAK.view,
3113
+ setup: FUEL_LEAK.setup,
3114
+ timeline: [decal(POOL[0], 0, POOL[2], 2.2, 2040368, { grow: 3, life: 60, ar: 0.6, fluid: "oil" })],
3115
+ tick: [emit("drip", "normal", 12, { p: TANK, box: [0.05, 0, 0.05], sp: 0, life: 1, size: 0.03, c0: 5921402, a: 0.9, g: 9.8, fi: 0, gnd: 1 })]
3116
+ };
3088
3117
  var CAR_EXPLOSION = {
3089
3118
  id: "car_explosion",
3090
3119
  pageId: "explosao",
@@ -3438,6 +3467,8 @@ var GameDestructionThree = (() => {
3438
3467
  ];
3439
3468
  var PI = Math.PI;
3440
3469
  var SMOKE = { color: 9079430, width: 10, height: 8, gravity: -0.08, drag: 2.2, growth: 2.1 };
3470
+ var DEFAULT_SMOKE = { puffs: 2, radius: 0.01, opacity: 0.14, life: 0.7, drift: 0.2, rise: 0.06, growth: 2.1, flash: 1 };
3471
+ var DEFAULT_SPREAD = { drift: 0.1, rise: 0.04, side: 0.06 };
3441
3472
  var FLASH_SECONDS = 0.055;
3442
3473
  var FLASH_RAMP = [0.6, 0.9];
3443
3474
  var FLASH_OPACITY = 0.95;
@@ -3445,8 +3476,10 @@ var GameDestructionThree = (() => {
3445
3476
  var FLAME_ADDITIVE = 3;
3446
3477
  var ORIGIN2 = [0, 0, 0];
3447
3478
  var SIZE = variable("size");
3479
+ var FLASH_SCALE = variable("flash");
3480
+ var fallback = (name, defaultValue) => ite(ne(variable(name), void 0), variable(name), defaultValue);
3448
3481
  var wide = (width) => mul(sub(rand(), 0.5), width);
3449
- var puff2 = (point, velocity, world, radius, life, opacity) => mesh({
3482
+ var puff2 = (point, velocity, world, radius, life, opacity, growth = SMOKE.growth) => mesh({
3450
3483
  shape: "sphere",
3451
3484
  p: point,
3452
3485
  v: velocity,
@@ -3461,7 +3494,7 @@ var GameDestructionThree = (() => {
3461
3494
  lit: true,
3462
3495
  g: SMOKE.gravity,
3463
3496
  drag: SMOKE.drag,
3464
- grow: SMOKE.growth
3497
+ grow: growth
3465
3498
  });
3466
3499
  var flameBit = (point, velocity, world, radius, life) => [
3467
3500
  set("bitPoint", point),
@@ -3494,37 +3527,55 @@ var GameDestructionThree = (() => {
3494
3527
  id: "viewmodel_muzzle_flash",
3495
3528
  pageId: "event:viewmodel_muzzle_flash",
3496
3529
  team: "weapons",
3497
- budget: { particles: 12, lights: 1 },
3530
+ budget: { particles: 14, lights: 1 },
3498
3531
  loop: 0.1,
3532
+ setup: [
3533
+ set("size", fallback("size", 1)),
3534
+ set("puffs", fallback("puffs", DEFAULT_SMOKE.puffs)),
3535
+ set("radius", fallback("radius", DEFAULT_SMOKE.radius)),
3536
+ set("opacity", fallback("opacity", DEFAULT_SMOKE.opacity)),
3537
+ set("life", fallback("life", DEFAULT_SMOKE.life)),
3538
+ set("drift", fallback("drift", DEFAULT_SMOKE.drift)),
3539
+ set("rise", fallback("rise", DEFAULT_SMOKE.rise)),
3540
+ set("growth", fallback("growth", DEFAULT_SMOKE.growth)),
3541
+ set("flash", fallback("flash", DEFAULT_SMOKE.flash)),
3542
+ set("driftSpread", fallback("driftSpread", DEFAULT_SPREAD.drift)),
3543
+ set("riseSpread", fallback("riseSpread", DEFAULT_SPREAD.rise)),
3544
+ set("sideSpread", fallback("sideSpread", DEFAULT_SPREAD.side)),
3545
+ set("lifeSpread", ite(ne(variable("lifeSpread"), void 0), variable("lifeSpread"), mul(variable("life"), 0.3)))
3546
+ ],
3499
3547
  timeline: [
3500
3548
  set("angles", []),
3501
3549
  repeat(4, append("angles", add(div(mul(INDEX, PI), 2), mul(rand(), 0.6)))),
3502
3550
  set("roll", mul(rand(), PI)),
3503
- flash(ORIGIN2, 16756832, mul(4, SIZE), FLASH_SECONDS, mul(2, SIZE), true, true),
3504
- flashPiece("cone_open", mul(0.013, SIZE), mul(0.085, SIZE), 10, [mul(0.042, SIZE), 0, 0], [0, 0, -PI / 2], 16757575),
3505
- repeat(
3506
- 4,
3507
- set("angle", pick(variable("angles"), INDEX)),
3508
- flashPiece(
3509
- "cone",
3510
- mul(5e-3, SIZE),
3511
- mul(0.04, SIZE),
3512
- 6,
3513
- [mul(0.012, SIZE), mul(cos(variable("angle")), 0.016, SIZE), mul(sin(variable("angle")), 0.016, SIZE)],
3514
- [variable("angle"), 0, -PI / 2],
3515
- 16757575
3516
- )
3517
- ),
3518
- flashPiece("sphere", mul(0.016, SIZE), void 0, 12, [mul(8e-3, SIZE), 0, 0], [0, 0, 0], 16773568),
3551
+ when(gt(FLASH_SCALE, 0), [
3552
+ flash(ORIGIN2, 16756832, mul(4, SIZE, FLASH_SCALE), FLASH_SECONDS, mul(2, SIZE, FLASH_SCALE), true, true),
3553
+ flashPiece("cone_open", mul(0.013, SIZE, FLASH_SCALE), mul(0.085, SIZE, FLASH_SCALE), 10, [mul(0.042, SIZE, FLASH_SCALE), 0, 0], [0, 0, -PI / 2], 16757575),
3554
+ repeat(
3555
+ 4,
3556
+ set("angle", pick(variable("angles"), INDEX)),
3557
+ flashPiece(
3558
+ "cone",
3559
+ mul(5e-3, SIZE, FLASH_SCALE),
3560
+ mul(0.04, SIZE, FLASH_SCALE),
3561
+ 6,
3562
+ [mul(0.012, SIZE, FLASH_SCALE), mul(cos(variable("angle")), 0.016, SIZE, FLASH_SCALE), mul(sin(variable("angle")), 0.016, SIZE, FLASH_SCALE)],
3563
+ [variable("angle"), 0, -PI / 2],
3564
+ 16757575
3565
+ )
3566
+ ),
3567
+ flashPiece("sphere", mul(0.016, SIZE, FLASH_SCALE), void 0, 12, [mul(8e-3, SIZE, FLASH_SCALE), 0, 0], [0, 0, 0], 16773568)
3568
+ ]),
3519
3569
  repeat(
3520
- 6,
3570
+ variable("puffs"),
3521
3571
  puff2(
3522
3572
  ORIGIN2,
3523
- [add(0.15, mul(rand(), 0.35)), add(0.03, mul(rand(), 0.12)), mul(sub(rand(), 0.5), 0.14)],
3573
+ [add(variable("drift"), mul(rand(), variable("driftSpread"))), add(variable("rise"), mul(rand(), variable("riseSpread"))), mul(sub(rand(), 0.5), variable("sideSpread"))],
3524
3574
  [0, 0, 0],
3525
- mul(0.013, SIZE),
3526
- add(1.3, mul(rand(), 0.6)),
3527
- 0.45
3575
+ mul(variable("radius"), SIZE),
3576
+ add(variable("life"), mul(rand(), variable("lifeSpread"))),
3577
+ variable("opacity"),
3578
+ variable("growth")
3528
3579
  )
3529
3580
  )
3530
3581
  ]
@@ -3578,8 +3629,10 @@ var GameDestructionThree = (() => {
3578
3629
  aliasOf(ENGINE_SMOKE, "vehicle_fire", { params: { id: "condition", options: ["100", "60", "30", "10"], default: 3 }, loop: 12 }),
3579
3630
  /** The exhaust of a car under way: `engine_smoke` in the best condition, for half a second so a moving car asks again. */
3580
3631
  aliasOf(ENGINE_SMOKE, "vehicle_exhaust", { params: { id: "condition", options: ["100", "60", "30", "10"], default: 0 }, loop: 0.5 }),
3581
- /** A hole in the tank: the page's own petrol leak (drips, pool, sparks, fire running to the tank). */
3582
- aliasOf(FUEL_LEAK, "vehicle_leak", {})
3632
+ /** A hole in the tank: the page's petrol leak without the fire and the blast (the pool and the drips). A normal bullet does only this. */
3633
+ aliasOf(FUEL_LEAK_ONLY, "vehicle_leak", {}),
3634
+ /** The leak caught fire (an incendiary or tracer round, a spark, a fire near): the page's whole sequence, the fire running along the pool to the tank and the blast. */
3635
+ aliasOf(FUEL_LEAK, "vehicle_leak_ignited", {})
3583
3636
  ];
3584
3637
  var ALL_EFFECTS = [BLOOD_HIT, HEADSHOT, DISMEMBER, BLOATER_CLOUD, SCREAMER_SHRIEK, CORPSE_DECAY, REANIMATION, EYES_IN_FOG, MUZZLE_FLASH, IMPACT_BY_MATERIAL, PENETRATION, CASINGS_AND_MAGAZINE, MOLOTOV, GRENADE, EXPLOSIVE_BARREL, SHOPFRONT_GLASS, DOOR_STAGES, BUILDING_COLLAPSE, BLOCK_WALL, METAL_DOOR_EFFECT, IRON_RAILING, WOODEN_FENCE, HIT_POINT_BREAKAGE, PRESSURE_CYLINDERS, BUILDING_FIRE, RAIN_AND_PUDDLES, FOG_AND_ASH, SCRAPING_SPARKS, ENGINE_SMOKE, FUEL_LEAK, CAR_EXPLOSION, WINDSHIELD, HEADLIGHTS_IN_RAIN, DAMAGE_AND_BITE, INFECTION_PHASES, PANIC, BLOATER_BLINDNESS, CAMPFIRE, COUGH_AND_BREATH];
3585
3638
  var WORKSHOP_EFFECTS = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]));
@@ -7752,6 +7805,79 @@ void main(){
7752
7805
  }
7753
7806
  };
7754
7807
  }
7808
+ var SPHERE_DEFAULT_WIDTH = 8;
7809
+ var SPHERE_DEFAULT_HEIGHT = 6;
7810
+ function geometryOf(THREE, particle) {
7811
+ switch (particle.shape) {
7812
+ case "cone":
7813
+ return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
7814
+ case "cone_open":
7815
+ return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
7816
+ case "sphere":
7817
+ return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
7818
+ case "icosahedron":
7819
+ return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
7820
+ }
7821
+ }
7822
+ function materialOf(THREE, particle) {
7823
+ if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
7824
+ return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
7825
+ }
7826
+ function createMeshLayerProbes(THREE) {
7827
+ const geometry = new THREE.BoxGeometry(1e-3, 1e-3, 1e-3);
7828
+ return [{ lit: true, additive: false }, { lit: false, additive: false }, { lit: false, additive: true }].map(
7829
+ (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
7830
+ );
7831
+ }
7832
+ function createMeshLayer(THREE, pool) {
7833
+ const group = new THREE.Group();
7834
+ group.name = "workshop-meshes";
7835
+ const drawn = /* @__PURE__ */ new Map();
7836
+ const frame = new THREE.Quaternion();
7837
+ const euler = new THREE.Euler();
7838
+ const turn = new THREE.Quaternion();
7839
+ const offset = new THREE.Vector3();
7840
+ function release(mesh2) {
7841
+ mesh2.removeFromParent();
7842
+ mesh2.geometry.dispose();
7843
+ mesh2.material.dispose();
7844
+ }
7845
+ return {
7846
+ group,
7847
+ sync() {
7848
+ const live = /* @__PURE__ */ new Set();
7849
+ for (const particle of pool.particles) {
7850
+ live.add(particle.id);
7851
+ let mesh2 = drawn.get(particle.id);
7852
+ if (!mesh2) {
7853
+ mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
7854
+ mesh2.frustumCulled = false;
7855
+ mesh2.renderOrder = particle.additive ? 5 : 3;
7856
+ drawn.set(particle.id, mesh2);
7857
+ group.add(mesh2);
7858
+ }
7859
+ frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
7860
+ offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
7861
+ mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
7862
+ turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
7863
+ mesh2.quaternion.copy(frame).multiply(turn);
7864
+ mesh2.scale.setScalar(particle.scale);
7865
+ mesh2.material.opacity = particle.opacity;
7866
+ }
7867
+ for (const [id, mesh2] of drawn) {
7868
+ if (live.has(id)) continue;
7869
+ release(mesh2);
7870
+ drawn.delete(id);
7871
+ }
7872
+ return pool.count;
7873
+ },
7874
+ dispose() {
7875
+ for (const mesh2 of drawn.values()) release(mesh2);
7876
+ drawn.clear();
7877
+ group.removeFromParent();
7878
+ }
7879
+ };
7880
+ }
7755
7881
  var clamp5 = (value, min2, max2) => value < min2 ? min2 : value > max2 ? max2 : value;
7756
7882
  function layer(container, name, blend) {
7757
7883
  const element = container.ownerDocument.createElement("div");
@@ -7921,73 +8047,6 @@ void main() {
7921
8047
  }
7922
8048
  };
7923
8049
  }
7924
- var SPHERE_DEFAULT_WIDTH = 8;
7925
- var SPHERE_DEFAULT_HEIGHT = 6;
7926
- function geometryOf(THREE, particle) {
7927
- switch (particle.shape) {
7928
- case "cone":
7929
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
7930
- case "cone_open":
7931
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
7932
- case "sphere":
7933
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
7934
- case "icosahedron":
7935
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
7936
- }
7937
- }
7938
- function materialOf(THREE, particle) {
7939
- if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
7940
- return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
7941
- }
7942
- function createMeshLayer(THREE, pool) {
7943
- const group = new THREE.Group();
7944
- group.name = "workshop-meshes";
7945
- const drawn = /* @__PURE__ */ new Map();
7946
- const frame = new THREE.Quaternion();
7947
- const euler = new THREE.Euler();
7948
- const turn = new THREE.Quaternion();
7949
- const offset = new THREE.Vector3();
7950
- function release(mesh2) {
7951
- mesh2.removeFromParent();
7952
- mesh2.geometry.dispose();
7953
- mesh2.material.dispose();
7954
- }
7955
- return {
7956
- group,
7957
- sync() {
7958
- const live = /* @__PURE__ */ new Set();
7959
- for (const particle of pool.particles) {
7960
- live.add(particle.id);
7961
- let mesh2 = drawn.get(particle.id);
7962
- if (!mesh2) {
7963
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
7964
- mesh2.frustumCulled = false;
7965
- mesh2.renderOrder = particle.additive ? 5 : 3;
7966
- drawn.set(particle.id, mesh2);
7967
- group.add(mesh2);
7968
- }
7969
- frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
7970
- offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
7971
- mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
7972
- turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
7973
- mesh2.quaternion.copy(frame).multiply(turn);
7974
- mesh2.scale.setScalar(particle.scale);
7975
- mesh2.material.opacity = particle.opacity;
7976
- }
7977
- for (const [id, mesh2] of drawn) {
7978
- if (live.has(id)) continue;
7979
- release(mesh2);
7980
- drawn.delete(id);
7981
- }
7982
- return pool.count;
7983
- },
7984
- dispose() {
7985
- for (const mesh2 of drawn.values()) release(mesh2);
7986
- drawn.clear();
7987
- group.removeFromParent();
7988
- }
7989
- };
7990
- }
7991
8050
  var WORKSHOP_SPRITE_VERTEX = `attribute float size;attribute float alpha;attribute vec3 pcol;varying vec3 vC;varying float vA;uniform float uScale;
7992
8051
  void main(){vC=pcol;vA=alpha;vec4 mv=modelViewMatrix*vec4(position,1.);gl_PointSize=clamp(size*uScale/-mv.z,1.,512.);gl_Position=projectionMatrix*mv;}`;
7993
8052
  var WORKSHOP_SPRITE_FRAGMENT = `uniform float uSoft;varying vec3 vC;varying float vA;
@@ -8352,6 +8411,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8352
8411
  const requested = options.vfx;
8353
8412
  const world = isWorld(requested) ? requested : createVfx({ profile: options.quality ?? "medium", reducedMotion, ...requested });
8354
8413
  const pools = world.pools();
8414
+ let anchors;
8415
+ const releaseAnchors = () => {
8416
+ if (!anchors) return;
8417
+ anchors.meshes[0]?.geometry.dispose();
8418
+ for (const mesh2 of anchors.meshes) mesh2.material.dispose();
8419
+ anchors = void 0;
8420
+ };
8355
8421
  const root = new THREE.Group();
8356
8422
  options.scene.add(root);
8357
8423
  const direction = new THREE.Vector3(...options.lightDirection ?? LAB_LIGHT_DIRECTION).normalize();
@@ -8468,6 +8534,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8468
8534
  syncAll();
8469
8535
  overlay?.update(screenNow());
8470
8536
  },
8537
+ warmUpProbes() {
8538
+ releaseAnchors();
8539
+ const meshes = createMeshLayerProbes(THREE);
8540
+ for (const mesh2 of meshes) mesh2.frustumCulled = false;
8541
+ const group = new THREE.Group();
8542
+ group.add(...meshes);
8543
+ anchors = { meshes, group };
8544
+ return { group, dispose: () => {
8545
+ } };
8546
+ },
8471
8547
  applyCamera() {
8472
8548
  const shake = screenNow().shake;
8473
8549
  if (reducedMotion || !(shake > SHAKE_FLOOR)) return () => {
@@ -8486,6 +8562,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8486
8562
  },
8487
8563
  screen: screenNow,
8488
8564
  dispose() {
8565
+ releaseAnchors();
8489
8566
  stopMarks();
8490
8567
  overlay?.dispose();
8491
8568
  workshopLayers?.dispose();
@@ -8627,6 +8704,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8627
8704
  const sc = new this.THREE.Vector3(Math.max(5e-3, sz[0]), Math.max(5e-3, sz[1]), Math.max(3e-3, sz[2]));
8628
8705
  mesh2.scale.copy(sc);
8629
8706
  mesh2.castShadow = true;
8707
+ mesh2.userData.destructionDebris = true;
8630
8708
  this.o.scene.add(mesh2);
8631
8709
  const live = {
8632
8710
  mesh: mesh2,
@@ -8834,6 +8912,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8834
8912
  mesh2.position.set(x, y, z);
8835
8913
  mesh2.scale.setScalar(1e-3);
8836
8914
  mesh2.renderOrder = 1;
8915
+ mesh2.userData.destructionDebris = true;
8837
8916
  this.scene.add(mesh2);
8838
8917
  this.spots.push({ mesh: mesh2, kind, home, x, z, radius: 1e-3, target: Math.min(MAX_RADIUS, radius) });
8839
8918
  }
@@ -9344,12 +9423,68 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9344
9423
  }
9345
9424
  return { min: [center[0] - hx, center[1] - hy, center[2] - hz], max: [center[0] + hx, center[1] + hy, center[2] + hz] };
9346
9425
  };
9426
+ var FLEXURE_DEFAULTS = {
9427
+ enabled: true,
9428
+ roles: ["beam", "slab", "roof", "stair"],
9429
+ scope: "rebar",
9430
+ crackFactor: 1.5,
9431
+ rebarYieldMPa: 500,
9432
+ cover: 0.03,
9433
+ neutralAxisLimit: 0.45,
9434
+ failRotationDeg: 10,
9435
+ loadFactor: 0.64,
9436
+ massFactor: 0.5,
9437
+ tntJoules: 4184e3,
9438
+ impulse: 200,
9439
+ reflection: 2,
9440
+ reflectionNear: 4,
9441
+ nearScaled: 0.5
9442
+ };
9443
+ var FLEXURAL_ROLES = FLEXURE_DEFAULTS.roles;
9444
+ var SOIL_DEFAULTS = {
9445
+ safety: 3,
9446
+ pileSafety: 2,
9447
+ rigidity: 0.82,
9448
+ influence: 2,
9449
+ imperfection: 1 / 200,
9450
+ cells: 4,
9451
+ sinkRate: 0.05,
9452
+ tiltRate: 1.5,
9453
+ pileElasticGPa: 25,
9454
+ footingWidth: 0.6
9455
+ };
9456
+ var WIND_DEFAULTS = {
9457
+ speed: 40,
9458
+ direction: [1, 0, 0],
9459
+ terrain: "III",
9460
+ class: "auto",
9461
+ topography: 1,
9462
+ statistical: 1,
9463
+ drag: 1.3,
9464
+ damping: 0.016,
9465
+ airDensity: 1.226,
9466
+ minHeight: 5,
9467
+ bandHeight: 3,
9468
+ stiffnessFactor: 0.8,
9469
+ beamFactor: 0.4,
9470
+ flange: 0.1,
9471
+ driftLimit: 1 / 500,
9472
+ loadFactor: 1.4,
9473
+ chord: false,
9474
+ meanRatio: 0.69,
9475
+ gustFactor: { A: 1, B: 0.98, C: 0.95 },
9476
+ gust: { components: 400, maxHz: 2, scale: 1200, seed: 1 },
9477
+ peakDecay: 60,
9478
+ tuning: "den-hartog"
9479
+ };
9347
9480
  var materials_default = [
9348
9481
  {
9349
9482
  id: "steel",
9350
9483
  density: 7850,
9351
9484
  hpPerKg: 400,
9352
9485
  penetration: 110,
9486
+ penetrationClass: "steel",
9487
+ penetrationScale: 1.3,
9353
9488
  ductility: 0.9,
9354
9489
  fracture: "none",
9355
9490
  damageScale: {
@@ -9379,6 +9514,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9379
9514
  density: 7200,
9380
9515
  hpPerKg: 150,
9381
9516
  penetration: 160,
9517
+ penetrationClass: "steel",
9518
+ penetrationScale: 1,
9382
9519
  ductility: 0.2,
9383
9520
  fracture: "snap",
9384
9521
  fragmentSize: [
@@ -9412,6 +9549,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9412
9549
  density: 2700,
9413
9550
  hpPerKg: 250,
9414
9551
  penetration: 60,
9552
+ penetrationClass: "steel",
9553
+ penetrationScale: 3,
9415
9554
  ductility: 0.85,
9416
9555
  fracture: "none",
9417
9556
  damageScale: {
@@ -9440,6 +9579,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9440
9579
  density: 8960,
9441
9580
  hpPerKg: 300,
9442
9581
  penetration: 70,
9582
+ penetrationClass: "steel",
9583
+ penetrationScale: 1.5,
9443
9584
  ductility: 0.95,
9444
9585
  fracture: "none",
9445
9586
  damageScale: {
@@ -9468,6 +9609,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9468
9609
  density: 500,
9469
9610
  hpPerKg: 90,
9470
9611
  penetration: 9,
9612
+ penetrationClass: "wood",
9613
+ penetrationScale: 1.5,
9471
9614
  ductility: 0.35,
9472
9615
  fracture: "splinter",
9473
9616
  fragmentSize: [
@@ -9504,6 +9647,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9504
9647
  density: 750,
9505
9648
  hpPerKg: 140,
9506
9649
  penetration: 14,
9650
+ penetrationClass: "wood",
9651
+ penetrationScale: 1,
9507
9652
  ductility: 0.3,
9508
9653
  fracture: "splinter",
9509
9654
  fragmentSize: [
@@ -9539,6 +9684,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9539
9684
  density: 600,
9540
9685
  hpPerKg: 70,
9541
9686
  penetration: 7,
9687
+ penetrationClass: "wood",
9688
+ penetrationScale: 2,
9542
9689
  ductility: 0.3,
9543
9690
  fracture: "snap",
9544
9691
  fragmentSize: [
@@ -9574,6 +9721,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9574
9721
  density: 750,
9575
9722
  hpPerKg: 45,
9576
9723
  penetration: 6,
9724
+ penetrationClass: "wood",
9725
+ penetrationScale: 2.5,
9577
9726
  ductility: 0.15,
9578
9727
  fracture: "snap",
9579
9728
  fragmentSize: [
@@ -9702,6 +9851,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9702
9851
  density: 2300,
9703
9852
  hpPerKg: 14,
9704
9853
  penetration: 18,
9854
+ penetrationClass: "concrete",
9855
+ penetrationScale: 1,
9705
9856
  ductility: 0.03,
9706
9857
  fracture: "crumble",
9707
9858
  fragmentSize: [
@@ -9721,13 +9872,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9721
9872
  "masonry",
9722
9873
  "brittle"
9723
9874
  ],
9724
- compressive: 444e3
9875
+ compressive: 444e3,
9876
+ strength: {
9877
+ compressiveMPa: 25,
9878
+ tensileMPa: 2.56,
9879
+ elasticGPa: 28
9880
+ }
9725
9881
  },
9726
9882
  {
9727
9883
  id: "brick",
9728
9884
  density: 1900,
9729
9885
  hpPerKg: 12,
9730
9886
  penetration: 14,
9887
+ penetrationClass: "brick",
9888
+ penetrationScale: 1,
9731
9889
  ductility: 0.05,
9732
9890
  fracture: "crumble",
9733
9891
  fragmentSize: [
@@ -9744,13 +9902,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9744
9902
  "masonry",
9745
9903
  "brittle"
9746
9904
  ],
9747
- compressive: 4e4
9905
+ compressive: 4e4,
9906
+ strength: {
9907
+ compressiveMPa: 2.35,
9908
+ tensileMPa: 0.1,
9909
+ elasticGPa: 2.35
9910
+ }
9748
9911
  },
9749
9912
  {
9750
9913
  id: "plaster",
9751
9914
  density: 850,
9752
9915
  hpPerKg: 18,
9753
9916
  penetration: 3,
9917
+ penetrationClass: "brick",
9918
+ penetrationScale: 3,
9754
9919
  ductility: 0.05,
9755
9920
  fracture: "crumble",
9756
9921
  fragmentSize: [
@@ -9774,6 +9939,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9774
9939
  density: 2700,
9775
9940
  hpPerKg: 25,
9776
9941
  penetration: 25,
9942
+ penetrationClass: "concrete",
9943
+ penetrationScale: 0.9,
9777
9944
  ductility: 0.03,
9778
9945
  fracture: "crumble",
9779
9946
  fragmentSize: [
@@ -10037,6 +10204,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10037
10204
  density: 650,
10038
10205
  hpPerKg: 5,
10039
10206
  penetration: 10,
10207
+ penetrationClass: "wood",
10208
+ penetrationScale: 1.2,
10040
10209
  ductility: 0.5,
10041
10210
  fracture: "splinter",
10042
10211
  fragmentSize: [
@@ -10288,6 +10457,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10288
10457
  for (const [key, id] of HINTS) if (n.includes(key)) return id;
10289
10458
  return void 0;
10290
10459
  };
10460
+ var partVolume = (shape) => {
10461
+ if (shape.kind === "box") return shape.size[0] * shape.size[1] * shape.size[2];
10462
+ if (shape.kind === "cylinder") return Math.PI * shape.radius * shape.radius * shape.height;
10463
+ return 4 / 3 * Math.PI * shape.radius ** 3;
10464
+ };
10465
+ var partMass = (part, material) => part.mass ?? material.density * partVolume(part.shape) * (part.fill ?? 1);
10291
10466
  var partThickness = (part) => {
10292
10467
  const dims = shapeDims(part.shape);
10293
10468
  const min2 = Math.min(dims[0], dims[1], dims[2]);
@@ -10315,6 +10490,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10315
10490
  };
10316
10491
 
10317
10492
  // src/spec.ts
10493
+ function containsOf(value) {
10494
+ if (!value || typeof value !== "object") return void 0;
10495
+ const v = value;
10496
+ return typeof v.fluid === "string" && typeof v.liters === "number" && v.liters > 0 ? { fluid: v.fluid, liters: v.liters } : void 0;
10497
+ }
10318
10498
  function partHintOf(value) {
10319
10499
  if (typeof value === "string") return value ? { id: value } : null;
10320
10500
  if (!value || typeof value !== "object") return null;
@@ -10335,11 +10515,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10335
10515
  function sizeOf(box) {
10336
10516
  return [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
10337
10517
  }
10518
+ function specFrameInverse(THREE, root) {
10519
+ root.updateMatrixWorld(true);
10520
+ const position = new THREE.Vector3();
10521
+ const quaternion = new THREE.Quaternion();
10522
+ root.matrixWorld.decompose(position, quaternion, new THREE.Vector3());
10523
+ return new THREE.Matrix4().compose(position, quaternion, new THREE.Vector3(1, 1, 1)).invert();
10524
+ }
10338
10525
  function specFromObject3D(THREE, root, options = {}) {
10339
- const fallback = options.fallback ?? "plastic";
10526
+ const fallback2 = options.fallback ?? "plastic";
10340
10527
  const minSize = options.minSize ?? 0.015;
10341
- root.updateMatrixWorld(true);
10342
- const inv = root.matrixWorld.clone().invert();
10528
+ const inv = specFrameInverse(THREE, root);
10343
10529
  const entries = [];
10344
10530
  const byKey = /* @__PURE__ */ new Map();
10345
10531
  const visit = (o) => {
@@ -10353,11 +10539,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10353
10539
  const matName = mats[0]?.name ?? "";
10354
10540
  const ud = o.userData ?? {};
10355
10541
  const hint = partHintOf(ud.destructionPart);
10356
- const material = options.materialOf?.(o) ?? (typeof ud.destructionMaterial === "string" ? ud.destructionMaterial : void 0) ?? hint?.material ?? guessMaterial(`${o.name} ${matName} ${typeof ud.material === "string" ? ud.material : ""}`) ?? fallback;
10542
+ const material = options.materialOf?.(o) ?? (typeof ud.destructionMaterial === "string" ? ud.destructionMaterial : void 0) ?? hint?.material ?? guessMaterial(`${o.name} ${matName} ${typeof ud.material === "string" ? ud.material : ""}`) ?? fallback2;
10357
10543
  const explicit = hint?.id ?? null;
10358
10544
  const hollow = ud.hollow === true || /hollow/i.test(o.name);
10545
+ const skin = ud.destructionSkin === true;
10359
10546
  const fill = options.fillOf?.(o, material);
10360
10547
  const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
10548
+ const contains = containsOf(ud.destructionContains);
10361
10549
  const existing = explicit ? byKey.get(explicit) : void 0;
10362
10550
  if (existing) {
10363
10551
  const before = sizeOf(existing.box);
@@ -10367,9 +10555,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10367
10555
  existing.meshes.push(o);
10368
10556
  if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
10369
10557
  existing.hollow = existing.hollow || hollow;
10558
+ existing.skin = existing.skin && skin;
10370
10559
  if (fill !== void 0) existing.fill = fill;
10560
+ if (contains) existing.contains = contains;
10371
10561
  } else {
10372
- const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, tags, fixedId: !!explicit, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
10562
+ const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
10373
10563
  entries.push(e);
10374
10564
  if (explicit) byKey.set(explicit, e);
10375
10565
  }
@@ -10388,7 +10578,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10388
10578
  const largest = (e) => Math.max(...sizeOf(e.box));
10389
10579
  const kept = [];
10390
10580
  const tiny = [];
10391
- for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
10581
+ for (const e of entries) (e.skin || !e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
10392
10582
  for (const t of tiny) {
10393
10583
  let best = null;
10394
10584
  let bestV = 0;
@@ -10442,11 +10632,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10442
10632
  part.thickness = Math.max(minSide, 5e-4);
10443
10633
  }
10444
10634
  if (e.tags) part.tags = [...e.tags];
10635
+ if (e.contains) part.contains = { ...e.contains };
10445
10636
  const ao = options.anchorBelow ?? -1;
10446
10637
  if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
10447
10638
  parts.push(part);
10448
10639
  meshesOf[id] = e.meshes;
10449
10640
  }
10641
+ const knownMass = root.userData?.destructionMass;
10642
+ if (typeof knownMass === "number" && knownMass > 0) {
10643
+ const shares = parts.map((p) => {
10644
+ const material = MATERIALS2.find((m) => m.id === p.material);
10645
+ return material ? partMass(p, material) : 0;
10646
+ });
10647
+ const total = shares.reduce((a, b) => a + b, 0);
10648
+ if (total > 0) parts.forEach((p, i) => p.mass = knownMass * shares[i] / total);
10649
+ }
10450
10650
  const spec = { parts };
10451
10651
  if (options.name !== void 0) spec.name = options.name;
10452
10652
  else if (root.name) spec.name = root.name;
@@ -10498,6 +10698,46 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10498
10698
  return last[1];
10499
10699
  }
10500
10700
 
10701
+ // src/skin-dent.ts
10702
+ function shellDisplacementAt(THREE, worldPoint, shells, out) {
10703
+ const triangle = new THREE.Triangle();
10704
+ const corners = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()];
10705
+ const local2 = new THREE.Vector3();
10706
+ const closest = new THREE.Vector3();
10707
+ const weights = new THREE.Vector3();
10708
+ const moved = new THREE.Vector3();
10709
+ let nearest = Infinity;
10710
+ out.set(0, 0, 0);
10711
+ let nearestScale = 1;
10712
+ for (const shell of shells) {
10713
+ shell.mesh.updateWorldMatrix(true, false);
10714
+ shell.mesh.worldToLocal(local2.copy(worldPoint));
10715
+ const index = shell.mesh.geometry.index;
10716
+ const triangles = index ? index.count / 3 : shell.before.length / 9;
10717
+ for (let t = 0; t < triangles; t++) {
10718
+ const ids = [0, 1, 2].map((corner) => index ? index.getX(t * 3 + corner) : t * 3 + corner);
10719
+ ids.forEach((id, corner) => corners[corner].set(shell.before[id * 3], shell.before[id * 3 + 1], shell.before[id * 3 + 2]));
10720
+ triangle.set(corners[0], corners[1], corners[2]);
10721
+ triangle.closestPointToPoint(local2, closest);
10722
+ const distance = closest.distanceToSquared(local2);
10723
+ if (distance >= nearest) continue;
10724
+ nearest = distance;
10725
+ nearestScale = shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1;
10726
+ triangle.getBarycoord(closest, weights);
10727
+ moved.set(0, 0, 0);
10728
+ ids.forEach((id, corner) => {
10729
+ const weight = corner === 0 ? weights.x : corner === 1 ? weights.y : weights.z;
10730
+ moved.x += weight * (shell.after[id * 3] - shell.before[id * 3]);
10731
+ moved.y += weight * (shell.after[id * 3 + 1] - shell.before[id * 3 + 1]);
10732
+ moved.z += weight * (shell.after[id * 3 + 2] - shell.before[id * 3 + 2]);
10733
+ });
10734
+ out.copy(moved).transformDirection(shell.mesh.matrixWorld);
10735
+ out.multiplyScalar(moved.length() * (shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1));
10736
+ }
10737
+ }
10738
+ return Math.sqrt(nearest) * nearestScale;
10739
+ }
10740
+
10501
10741
  // src/visual.ts
10502
10742
  var POS_ATTR = "position";
10503
10743
  function positionAttr(geo) {
@@ -10530,7 +10770,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10530
10770
  depthWrite: false,
10531
10771
  polygonOffset: true,
10532
10772
  polygonOffsetFactor: -4,
10533
- polygonOffsetUnits: -4
10773
+ polygonOffsetUnits: -4,
10774
+ side: this.THREE.DoubleSide
10534
10775
  }));
10535
10776
  }
10536
10777
  /** Opaque patch in the colour of what shows under a spalled coat. */
@@ -10562,6 +10803,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10562
10803
  }
10563
10804
  };
10564
10805
  var tmpV = (THREE) => new THREE.Vector3();
10806
+ var MIN_DENT_VERTICES = 10;
10807
+ var WIDEN_MARGIN = 1.1;
10808
+ var SKIN_FOLLOW = 0.85;
10809
+ var SKIN_SLACK = 5e-4;
10810
+ var SKIN_LAYER_SLACK = 4;
10811
+ var NEAREST_MARGIN = 1.5;
10565
10812
  var Visuals = class {
10566
10813
  constructor(THREE, lowQuality, decalCap) {
10567
10814
  __publicField(this, "THREE", THREE);
@@ -10730,14 +10977,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10730
10977
  * Push the vertices of the mesh within `dent.radius` of `worldPoint` inwards along `worldNormal` by `depth * (1 - (d/r)^2)^2`.
10731
10978
  * Returns false when nothing was changed (too far, low quality on a heavy mesh, or no editable positions).
10732
10979
  */
10733
- applyDent(mv, worldPoint, worldNormal, dent) {
10980
+ applyDent(mv, worldPoint, worldNormal, dent, reachWorld = 0) {
10734
10981
  const mesh2 = mv.mesh;
10735
10982
  if (this.lowQuality && this.vertexCount(mv) > 2e4) return false;
10736
10983
  mesh2.updateWorldMatrix(true, false);
10737
10984
  const s = mesh2.matrixWorld.getMaxScaleOnAxis() || 1;
10738
10985
  const lp = mesh2.worldToLocal(this.a.copy(worldPoint));
10739
10986
  const ln = this.b.copy(worldNormal).transformDirection(this.m4.copy(mesh2.matrixWorld).invert()).normalize();
10740
- const r = dent.radius / s;
10987
+ const r = Math.max(dent.radius, reachWorld) / s;
10741
10988
  const depth = dent.depth / s;
10742
10989
  const geo = mesh2.geometry;
10743
10990
  let bb;
@@ -10750,34 +10997,110 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10750
10997
  if (!this.ownGeometry(mv)) return false;
10751
10998
  const work = mv.host ? mv.host.work : mv.work;
10752
10999
  const pos = positionAttr(mesh2.geometry);
10753
- const r2 = r * r;
10754
- let touched = false;
10755
- const px = lp.x;
10756
- const py = lp.y;
10757
- const pz = lp.z;
10758
- const nx = ln.x;
10759
- const ny = ln.y;
10760
- const nz = ln.z;
10761
11000
  const from = mv.range ? mv.range[0] * 3 : 0;
10762
11001
  const to = mv.range ? mv.range[1] * 3 : work.length;
11002
+ const reach = r;
11003
+ const reach2 = reach * reach;
10763
11004
  for (let i = from; i < to; i += 3) {
10764
- const dx = work[i] - px;
10765
- const dy = work[i + 1] - py;
10766
- const dz = work[i + 2] - pz;
11005
+ const dx = work[i] - lp.x;
11006
+ const dy = work[i + 1] - lp.y;
11007
+ const dz = work[i + 2] - lp.z;
10767
11008
  const d2 = dx * dx + dy * dy + dz * dz;
10768
- if (d2 >= r2) continue;
10769
- const f = 1 - d2 / r2;
11009
+ if (d2 >= reach2) continue;
11010
+ const f = 1 - d2 / reach2;
10770
11011
  const k = depth * f * f;
10771
- work[i] -= nx * k;
10772
- work[i + 1] -= ny * k;
10773
- work[i + 2] -= nz * k;
10774
- touched = true;
11012
+ work[i] -= ln.x * k;
11013
+ work[i + 1] -= ln.y * k;
11014
+ work[i + 2] -= ln.z * k;
10775
11015
  }
10776
- if (!touched) return false;
10777
11016
  mv.touched = true;
10778
11017
  this.writePositions(mv, pos);
10779
11018
  return true;
10780
11019
  }
11020
+ /**
11021
+ * World radius a dent needs on this mesh to reach `MIN_DENT_VERTICES` vertices (the dent's own radius when the mesh is fine enough). A part with several meshes
11022
+ * (a shell and its label) must dent them all with the largest of these, so the skin and the shell follow the same smooth surface and never cross.
11023
+ */
11024
+ dentReach(mv, worldPoint, dent) {
11025
+ const mesh2 = mv.mesh;
11026
+ mesh2.updateWorldMatrix(true, false);
11027
+ const s = mesh2.matrixWorld.getMaxScaleOnAxis() || 1;
11028
+ const lp = mesh2.worldToLocal(this.a.copy(worldPoint));
11029
+ const source = (mv.host ? mv.host.work : mv.work) ?? positionAttr(mesh2.geometry)?.array;
11030
+ if (!source) return dent.radius;
11031
+ let bounds;
11032
+ if (mv.host) bounds = this.localBox(mv, this.lbox);
11033
+ else {
11034
+ if (!mesh2.geometry.boundingBox) mesh2.geometry.computeBoundingBox();
11035
+ bounds = mesh2.geometry.boundingBox;
11036
+ }
11037
+ if (bounds && !bounds.isEmpty() && bounds.distanceToPoint(lp) > dent.radius / s) return dent.radius;
11038
+ const from = mv.range ? mv.range[0] * 3 : 0;
11039
+ const to = mv.range ? mv.range[1] * 3 : source.length;
11040
+ const r2 = (dent.radius / s) ** 2;
11041
+ const distances = [];
11042
+ let inside = 0;
11043
+ for (let i = from; i < to; i += 3) {
11044
+ const dx = source[i] - lp.x;
11045
+ const dy = source[i + 1] - lp.y;
11046
+ const dz = source[i + 2] - lp.z;
11047
+ const d2 = dx * dx + dy * dy + dz * dz;
11048
+ if (d2 < r2) inside++;
11049
+ distances.push(d2);
11050
+ }
11051
+ if (inside >= MIN_DENT_VERTICES || distances.length < MIN_DENT_VERTICES) return dent.radius;
11052
+ distances.sort((x, y) => x - y);
11053
+ const nearest = Math.sqrt(distances[0]);
11054
+ return Math.max(dent.radius, Math.sqrt(distances[MIN_DENT_VERTICES - 1]) * s * WIDEN_MARGIN, nearest * s * NEAREST_MARGIN);
11055
+ }
11056
+ /**
11057
+ * Dent all the meshes of a part. The shells take the dent as `applyDent` gives it; a skin (label, coat: `userData.destructionSkin`) sits a hair above its shell
11058
+ * and moves with the shell's surface instead (see `shellDisplacementAt`), so the shell never shows through it whatever the two meshes' vertex grids are.
11059
+ */
11060
+ applyPartDent(meshes, worldPoint, worldNormal, dent, reachWorld) {
11061
+ const isSkin = (mv) => mv.mesh.userData.destructionSkin === true && !mv.host;
11062
+ const shells = meshes.filter((mv) => !isSkin(mv));
11063
+ const snapshots = [];
11064
+ for (const mv of shells) {
11065
+ const heavy = this.lowQuality && this.vertexCount(mv) > 2e4;
11066
+ const plain = !mv.host && !heavy && this.ownGeometry(mv) && mv.work;
11067
+ const before = plain ? new Float32Array(mv.work) : null;
11068
+ this.applyDent(mv, worldPoint, worldNormal, dent, reachWorld);
11069
+ if (before) snapshots.push({ mesh: mv.mesh, before, after: mv.work });
11070
+ }
11071
+ const displacement = this.t;
11072
+ const vertex = this.a;
11073
+ for (const mv of meshes) {
11074
+ if (!isSkin(mv)) continue;
11075
+ if (snapshots.length === 0) {
11076
+ this.applyDent(mv, worldPoint, worldNormal, dent, reachWorld);
11077
+ continue;
11078
+ }
11079
+ if (!this.ownGeometry(mv)) continue;
11080
+ const work = mv.work;
11081
+ const mesh2 = mv.mesh;
11082
+ mesh2.updateWorldMatrix(true, false);
11083
+ const toLocal = this.m4.copy(mesh2.matrixWorld).invert();
11084
+ const scale = mesh2.matrixWorld.getMaxScaleOnAxis() || 1;
11085
+ let touched = false;
11086
+ for (let i = 0; i < work.length; i += 3) {
11087
+ vertex.set(work[i], work[i + 1], work[i + 2]).applyMatrix4(mesh2.matrixWorld);
11088
+ if (vertex.distanceTo(worldPoint) > reachWorld) continue;
11089
+ const height = shellDisplacementAt(this.THREE, vertex, snapshots, displacement);
11090
+ const length = displacement.length();
11091
+ if (length === 0) continue;
11092
+ const falloff = 1 - (vertex.distanceTo(worldPoint) / reachWorld) ** 2;
11093
+ const smooth = dent.depth * falloff * falloff;
11094
+ const pull = Math.max(0, Math.min(length, smooth) * SKIN_FOLLOW - SKIN_SLACK - height * SKIN_LAYER_SLACK);
11095
+ displacement.transformDirection(toLocal).multiplyScalar(pull / scale);
11096
+ work[i] += displacement.x;
11097
+ work[i + 1] += displacement.y;
11098
+ work[i + 2] += displacement.z;
11099
+ touched = true;
11100
+ }
11101
+ if (touched) this.writePositions(mv, positionAttr(mesh2.geometry));
11102
+ }
11103
+ }
10781
11104
  /** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
10782
11105
  writePositions(mv, pos) {
10783
11106
  if (mv.host) {
@@ -10824,6 +11147,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10824
11147
  return { s, pos, quat: quat2 };
10825
11148
  }
10826
11149
  addDecal(mv, decal2) {
11150
+ decal2.obj.userData.destructionDebris = true;
10827
11151
  mv.mesh.add(decal2.obj);
10828
11152
  mv.decals.push(decal2);
10829
11153
  this.decalCount++;
@@ -10840,6 +11164,27 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10840
11164
  }
10841
11165
  addHole(mv, worldPoint, worldNormal, diameter) {
10842
11166
  const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
11167
+ const work = mv.host ? mv.host.work : mv.work;
11168
+ if (work && mv.touched) {
11169
+ const ln = worldNormal.clone().transformDirection(this.m4.copy(mv.mesh.matrixWorld).invert()).normalize();
11170
+ let minDistSq = Infinity;
11171
+ let nearestDisp = 0;
11172
+ const from = mv.range ? mv.range[0] * 3 : 0;
11173
+ const to = mv.range ? mv.range[1] * 3 : work.length;
11174
+ for (let i = from; i < to; i += 3) {
11175
+ const dx = work[i] - pos.x;
11176
+ const dy = work[i + 1] - pos.y;
11177
+ const dz = work[i + 2] - pos.z;
11178
+ const d2 = dx * dx + dy * dy + dz * dz;
11179
+ if (d2 < minDistSq) {
11180
+ minDistSq = d2;
11181
+ nearestDisp = dx * ln.x + dy * ln.y + dz * ln.z;
11182
+ }
11183
+ }
11184
+ if (minDistSq < (0.06 / s) ** 2 && nearestDisp < 0) {
11185
+ pos.addScaledVector(ln, nearestDisp);
11186
+ }
11187
+ }
10843
11188
  const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.discMaterial());
10844
11189
  disc.position.copy(pos);
10845
11190
  disc.quaternion.copy(quat2);
@@ -11311,7 +11656,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11311
11656
  gas: 15921906,
11312
11657
  sap: 12098362,
11313
11658
  acid: 10211900,
11314
- blood: 9046538
11659
+ blood: 9046538,
11660
+ soda: 3808786,
11661
+ coffee: 3875860,
11662
+ beer: 14259516,
11663
+ milk: 16052714,
11664
+ wine: 7016479,
11665
+ juice: 15238923
11315
11666
  };
11316
11667
  var SMOKE_TONES = { white: 15132390, grey: 9079434, black: 2763306 };
11317
11668
  var DEFAULT_SPEED = {
@@ -11408,6 +11759,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11408
11759
  __publicField(this, "tmpQ");
11409
11760
  __publicField(this, "v1");
11410
11761
  __publicField(this, "v2");
11762
+ // The last warm-up keeps its materials until the next one: a material that does not use lights keeps the program of the lights it was first drawn with, so each warm-up needs materials of its own.
11763
+ __publicField(this, "warmProbes");
11411
11764
  this.world = o.world;
11412
11765
  this.scene = o.scene;
11413
11766
  this.quality = o.quality ?? "medium";
@@ -11505,6 +11858,24 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11505
11858
  if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes, hosts));
11506
11859
  }
11507
11860
  this.bounds.set(objectId, bound);
11861
+ for (const pv of bound.parts.values()) this.tagPart(pv, objectId);
11862
+ }
11863
+ /**
11864
+ * Label the meshes of a part so a host can go from a raycast hit to the world: `userData.destructionPart` (the part id) and
11865
+ * `userData.destructionObject` (the id of the world object that part belongs to now; a loose piece has an id of its own). Parts drawn by a range
11866
+ * of a merged mesh share that mesh with others and are left alone until they get a mesh of their own. Decals are marked `destructionDebris`.
11867
+ */
11868
+ tagPart(pv, objectId) {
11869
+ for (const mv of pv.meshes) {
11870
+ if (mv.merged && !mv.loose) continue;
11871
+ mv.mesh.traverse((c) => {
11872
+ if (!c.isMesh) return;
11873
+ const ud = c.userData;
11874
+ if (ud.destructionDebris) return;
11875
+ if (partHintOf(ud.destructionPart)?.id !== pv.id) ud.destructionPart = pv.id;
11876
+ ud.destructionObject = objectId;
11877
+ });
11878
+ }
11508
11879
  }
11509
11880
  makePartVis(home, id, material, meshes, hosts) {
11510
11881
  return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m.mesh, home, m.range, hosts)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
@@ -11518,13 +11889,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11518
11889
  specParts = void 0;
11519
11890
  }
11520
11891
  if (!specParts) return;
11521
- root.updateMatrixWorld(true);
11892
+ const frame = specFrameInverse(this.THREE, root);
11522
11893
  const e = new this.THREE.Euler();
11523
11894
  const q = new this.THREE.Quaternion();
11524
11895
  for (const m of meshes) {
11525
11896
  this.box.setFromObject(m);
11526
11897
  if (this.box.isEmpty()) continue;
11527
- const c = root.worldToLocal(this.box.getCenter(this.v1));
11898
+ const c = this.box.getCenter(this.v1).applyMatrix4(frame);
11528
11899
  let best = null;
11529
11900
  let bestVol = Infinity;
11530
11901
  for (const p of specParts) {
@@ -11561,10 +11932,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11561
11932
  // ----------------------------------------------------------------------------------------------------------------------------------
11562
11933
  // Pieces
11563
11934
  // ----------------------------------------------------------------------------------------------------------------------------------
11564
- /** Half extents and centre of the box around some meshes, in the local space of `root`. */
11935
+ /** Half extents and centre of the box around some meshes, in the metric frame of `root` (its pose removed, its scale kept). */
11565
11936
  measure(root, meshes) {
11566
- root.updateMatrixWorld(true);
11567
- const inv = root.matrixWorld.clone().invert();
11937
+ const inv = specFrameInverse(this.THREE, root);
11568
11938
  const total = new this.THREE.Box3();
11569
11939
  const m4 = new this.THREE.Matrix4();
11570
11940
  const lb = new this.THREE.Box3();
@@ -11639,6 +12009,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11639
12009
  dt = clamp6(dt, 0, 0.1);
11640
12010
  this.time += dt;
11641
12011
  this.drainQueue();
12012
+ for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
11642
12013
  this.updateActive(dt);
11643
12014
  this.updateLeaks(dt);
11644
12015
  this.pieceSys.update(dt);
@@ -11657,6 +12028,27 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11657
12028
  }
11658
12029
  }
11659
12030
  }
12031
+ /**
12032
+ * The ground under an object settled or tilted: its parts are drawn under a node with the soil's offset (`ObjectState.soil.offset`), so the host's
12033
+ * root keeps the pose the host gave and the world keeps getting it from `syncTransform`.
12034
+ */
12035
+ onSettle(objectId) {
12036
+ const b = this.bounds.get(objectId);
12037
+ const soil = this.world.state(objectId)?.soil;
12038
+ if (!b || !soil) return;
12039
+ let node = b.soilNode;
12040
+ if (!node) {
12041
+ node = new this.THREE.Group();
12042
+ node.name = "destruction-soil";
12043
+ for (const c of [...b.root.children]) node.add(c);
12044
+ b.root.add(node);
12045
+ b.soilNode = node;
12046
+ }
12047
+ const { position: p, rotation: q } = soil.offset;
12048
+ node.position.set(p[0], p[1], p[2]);
12049
+ node.quaternion.set(q[0], q[1], q[2], q[3]);
12050
+ node.updateMatrixWorld(true);
12051
+ }
11660
12052
  syncTransforms() {
11661
12053
  for (const b of this.bounds.values()) this.syncOne(b);
11662
12054
  }
@@ -11711,6 +12103,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11711
12103
  case "detach":
11712
12104
  this.onDetach(ev);
11713
12105
  break;
12106
+ case "settle":
12107
+ this.onSettle(ev.object);
12108
+ break;
11714
12109
  case "collapse":
11715
12110
  this.splitLoose(ev.object);
11716
12111
  this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
@@ -11796,13 +12191,39 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11796
12191
  }
11797
12192
  case "leak": {
11798
12193
  const key = `${ev.object}|${ev.part}`;
12194
+ const b = this.bounds.get(ev.object);
12195
+ let localPoint;
12196
+ let localDirection;
12197
+ if (b) {
12198
+ b.root.updateWorldMatrix(true, false);
12199
+ const inv = new this.THREE.Matrix4().copy(b.root.matrixWorld).invert();
12200
+ const lp = this.v1.set(ev.point[0], ev.point[1], ev.point[2]).applyMatrix4(inv);
12201
+ const ld = this.v2.set(ev.direction[0], ev.direction[1], ev.direction[2]).transformDirection(inv).normalize();
12202
+ localPoint = [lp.x, lp.y, lp.z];
12203
+ localDirection = [ld.x, ld.y, ld.z];
12204
+ }
11799
12205
  const same = this.leaks.find((l) => l.object === ev.object && l.part === ev.part && Math.hypot(l.point[0] - ev.point[0], l.point[1] - ev.point[1], l.point[2] - ev.point[2]) < 0.03);
11800
12206
  if (same) {
11801
12207
  same.rate = ev.rate;
11802
12208
  same.direction = ev.direction;
12209
+ if (localPoint) same.localPoint = localPoint;
12210
+ if (localDirection) same.localDirection = localDirection;
11803
12211
  } else {
11804
12212
  if (this.leaks.length >= 24) this.leaks.shift();
11805
- this.leaks.push({ object: ev.object, part: ev.part, fluid: ev.fluid, point: ev.point, direction: ev.direction, rate: ev.rate, pressurized: ev.pressurized, acc: 0 });
12213
+ this.leaks.push({
12214
+ object: ev.object,
12215
+ part: ev.part,
12216
+ fluid: ev.fluid,
12217
+ point: ev.point,
12218
+ direction: ev.direction,
12219
+ rate: ev.rate,
12220
+ pressurized: ev.pressurized,
12221
+ acc: 0,
12222
+ opened: this.world.part(ev.object, ev.part)?.holes.some((h) => h.through) ?? false,
12223
+ localPoint,
12224
+ localDirection,
12225
+ life: 0
12226
+ });
11806
12227
  }
11807
12228
  void key;
11808
12229
  break;
@@ -11879,10 +12300,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11879
12300
  const def = this.world.materials.get(pv.material);
11880
12301
  const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
11881
12302
  const wn = this.worldDir(b, ev.dent.normal);
11882
- for (const mv of pv.meshes) {
11883
- if (!this.visuals.shown(mv)) continue;
11884
- this.visuals.applyDent(mv, wp, wn, ev.dent);
11885
- }
12303
+ const shown = pv.meshes.filter((mv) => this.visuals.shown(mv));
12304
+ const reach = shown.reduce((widest, mv) => Math.max(widest, this.visuals.dentReach(mv, wp, ev.dent)), ev.dent.radius);
12305
+ this.visuals.applyPartDent(shown, wp, wn, ev.dent, reach);
11886
12306
  if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [wn.x, wn.y, wn.z], 4 + Math.min(8, ev.dent.depth * 150), 3.2);
11887
12307
  this.sound(impactKey(def), ev.worldPoint, clamp6(0.3 + ev.dent.depth * 12, 0.3, 1));
11888
12308
  }
@@ -12078,6 +12498,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12078
12498
  }
12079
12499
  }
12080
12500
  this.bounds.set(newId, nb);
12501
+ for (const pv of nb.parts.values()) this.tagPart(pv, newId);
12081
12502
  const meshes = this.meshesOf(nb);
12082
12503
  if (meshes.length === 0) return;
12083
12504
  const { half, center } = this.measure(group, meshes);
@@ -12105,24 +12526,40 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12105
12526
  const report = options.report;
12106
12527
  const confinement = report?.find((r) => r.blast !== void 0)?.blast?.confinement ?? 1;
12107
12528
  const energy = ev.energy * confinement;
12108
- for (const pc of this.pieceSys.list) {
12529
+ const reachOf = (objectId) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? 1;
12530
+ const shove = (pc) => {
12109
12531
  this.pieceSys.centerOf(pc, c);
12110
- const dx = c.x - p[0];
12111
- const dy = c.y - p[1];
12112
- const dz = c.z - p[2];
12113
- const d = Math.hypot(dx, dy, dz);
12114
- if (d > ev.radius) continue;
12115
- const fall = (1 - d / ev.radius) ** 2;
12116
- const reach = report?.find((r) => r.object === pc.objectId)?.blast?.shelter ?? 1;
12117
- const speed = Math.min(30, Math.sqrt(2 * energy * reach * 0.02 / pc.mass)) * fall;
12118
- const inv = d > 1e-4 ? 1 / d : 0;
12119
- const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
12532
+ const hit = blastShove({ point: p, energy, radius: ev.radius, reach: reachOf(pc.objectId) }, { position: [c.x, c.y, c.z], mass: pc.mass }, this.o.blastShove);
12533
+ if (!hit) return;
12120
12534
  const spread = pc.halfExtents[0];
12121
- this.pieceSys.impulse(pc, [dir[0] * speed * pc.mass, dir[1] * speed * pc.mass, dir[2] * speed * pc.mass], [c.x + (this.rng() - 0.5) * spread, c.y + spread * 0.5, c.z + (this.rng() - 0.5) * spread]);
12122
- }
12535
+ const momentum = hit.speed * pc.mass;
12536
+ this.pieceSys.impulse(pc, [hit.direction[0] * momentum, hit.direction[1] * momentum, hit.direction[2] * momentum], [c.x + (this.rng() - 0.5) * spread, c.y + spread * 0.5, c.z + (this.rng() - 0.5) * spread]);
12537
+ };
12538
+ for (const pc of [...this.pieceSys.list]) shove(pc);
12539
+ if (this.o.shoveable) this.loosen(p, ev.radius, energy, reachOf);
12123
12540
  this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(energy) * 0.08));
12124
12541
  this.sound("explode", p, clamp6(0.5 + ev.radius / 10, 0.5, 1));
12125
12542
  }
12543
+ /** The bound objects without a piece that the host lets a blast move (`shoveable`): the ones it reaches with enough speed become pieces and get the shove. */
12544
+ loosen(point, radius, energy, reachOf) {
12545
+ const shoveable = this.o.shoveable;
12546
+ if (!shoveable) return;
12547
+ for (const b of [...this.bounds.values()]) {
12548
+ if (b.piece || this.pieceSys.get(b.id) || !shoveable(b.id) || !this.world.has(b.id)) continue;
12549
+ const { half, center } = this.measure(b.root, this.meshesOf(b));
12550
+ const world = center.clone().applyQuaternion(b.root.quaternion).add(b.root.position);
12551
+ const mass = this.massOf(b.id);
12552
+ const hit = blastShove({ point, energy, radius, reach: reachOf(b.id) }, { position: [world.x, world.y, world.z], mass }, this.o.blastShove);
12553
+ if (!hit) continue;
12554
+ const piece = this.makePiece(b.id, { mass });
12555
+ const body = this.pieceSys.get(b.id);
12556
+ if (!body) continue;
12557
+ this.o.onLoosened?.(b.id, piece);
12558
+ const spread = half[0];
12559
+ const momentum = hit.speed * mass;
12560
+ 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]);
12561
+ }
12562
+ }
12126
12563
  playExplosion(ev, options) {
12127
12564
  const named = options.effect ?? ev.effect;
12128
12565
  const direction = options.direction;
@@ -12222,21 +12659,44 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12222
12659
  updateLeaks(dt) {
12223
12660
  for (let i = this.leaks.length - 1; i >= 0; i--) {
12224
12661
  const l = this.leaks[i];
12662
+ const b = this.bounds.get(l.object);
12225
12663
  const ps = this.world.part(l.object, l.part);
12226
- if (!ps || ps.liters === void 0 || ps.liters <= 0) {
12664
+ if (!b || !ps || ps.liters === void 0 || ps.liters <= 1e-4 || l.opened && !ps.holes.some((h) => h.through)) {
12227
12665
  this.leaks.splice(i, 1);
12228
12666
  continue;
12229
12667
  }
12668
+ l.life = (l.life ?? 0) + dt;
12669
+ if (l.pressurized && l.life > 0.8) l.pressurized = false;
12670
+ if (l.lastLiters !== void 0 && Math.abs(ps.liters - l.lastLiters) < 1e-5) {
12671
+ l.idleSeconds = (l.idleSeconds ?? 0) + dt;
12672
+ if (l.idleSeconds > 0.4) {
12673
+ this.leaks.splice(i, 1);
12674
+ continue;
12675
+ }
12676
+ } else {
12677
+ l.idleSeconds = 0;
12678
+ l.lastLiters = ps.liters;
12679
+ }
12680
+ let pt = l.point;
12681
+ let dir = l.direction;
12682
+ if (l.localPoint && l.localDirection) {
12683
+ b.root.updateWorldMatrix(true, false);
12684
+ const wp = this.v1.set(l.localPoint[0], l.localPoint[1], l.localPoint[2]).applyMatrix4(b.root.matrixWorld);
12685
+ const wd = this.v2.set(l.localDirection[0], l.localDirection[1], l.localDirection[2]).transformDirection(b.root.matrixWorld).normalize();
12686
+ pt = [wp.x, wp.y, wp.z];
12687
+ dir = [wd.x, wd.y, wd.z];
12688
+ l.point = pt;
12689
+ l.direction = dir;
12690
+ }
12230
12691
  l.acc += clamp6(20 + l.rate * 400, 15, 120) * dt;
12231
12692
  const color = FLUID_COLORS[l.fluid] ?? 11184810;
12232
- const d = l.direction;
12233
- const dl = Math.hypot(d[0], d[1], d[2]) || 1;
12693
+ const dl = Math.hypot(dir[0], dir[1], dir[2]) || 1;
12234
12694
  const sp = l.pressurized ? 2.5 + this.rng() * 1.5 : 0.3 + this.rng() * 0.3;
12235
12695
  while (l.acc >= 1) {
12236
12696
  l.acc -= 1;
12237
- const vel = [d[0] / dl * sp + (this.rng() - 0.5) * 0.25, d[1] / dl * sp + (this.rng() - 0.5) * 0.25, d[2] / dl * sp + (this.rng() - 0.5) * 0.25];
12238
- if (l.fluid === "gas") this.vfx.spawn("steam", l.point, vel, { color, size: 0.5, alpha: 0.5 });
12239
- else this.vfx.spawn("drips", l.point, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0, owner: this.owner });
12697
+ const vel = [dir[0] / dl * sp + (this.rng() - 0.5) * 0.25, dir[1] / dl * sp + (this.rng() - 0.5) * 0.25, dir[2] / dl * sp + (this.rng() - 0.5) * 0.25];
12698
+ if (l.fluid === "gas") this.vfx.spawn("steam", pt, vel, { color, size: 0.5, alpha: 0.5 });
12699
+ else this.vfx.spawn("drips", pt, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0, owner: this.owner });
12240
12700
  }
12241
12701
  }
12242
12702
  }
@@ -12274,6 +12734,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12274
12734
  for (const [pid, pv] of [...b.parts]) {
12275
12735
  if (pv.home !== home) continue;
12276
12736
  this.visuals.restorePart(pv);
12737
+ this.tagPart(pv, home);
12277
12738
  if (b !== homeBound && homeBound) {
12278
12739
  b.parts.delete(pid);
12279
12740
  homeBound.parts.set(pid, pv);
@@ -12315,10 +12776,31 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12315
12776
  const { puffs, sparks, glows, chunks } = this.vfx.stats();
12316
12777
  return puffs + sparks + glows + chunks;
12317
12778
  }
12779
+ warmUpProbes() {
12780
+ this.releaseWarmProbes();
12781
+ const { kit } = this.visuals;
12782
+ const T = this.THREE;
12783
+ const geometry = new T.BufferGeometry();
12784
+ geometry.setAttribute("position", new T.BufferAttribute(new Float32Array(6), 3));
12785
+ const materials = [kit.discMaterial().clone(), kit.patchMaterial(8421504).clone(), kit.lineMaterial(0).clone()];
12786
+ const group = new T.Group();
12787
+ group.add(new T.Mesh(kit.discGeometry(), materials[0]), new T.Mesh(kit.discGeometry(), materials[1]), new T.LineSegments(geometry, materials[2]));
12788
+ for (const probe of group.children) probe.frustumCulled = false;
12789
+ this.warmProbes = { group, geometry, materials };
12790
+ return { group, dispose: () => {
12791
+ } };
12792
+ }
12793
+ releaseWarmProbes() {
12794
+ if (!this.warmProbes) return;
12795
+ this.warmProbes.geometry.dispose();
12796
+ for (const material of this.warmProbes.materials) material.dispose();
12797
+ this.warmProbes = void 0;
12798
+ }
12318
12799
  rebarCount() {
12319
12800
  return this.cages.count;
12320
12801
  }
12321
12802
  dispose() {
12803
+ this.releaseWarmProbes();
12322
12804
  if (this.disposed) return;
12323
12805
  this.unsubscribe();
12324
12806
  this.clear();