@adatechnology/game-destruction-three 0.7.2 → 0.8.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.
@@ -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
  ]
@@ -3572,9 +3623,20 @@ var GameDestructionThree = (() => {
3572
3623
  ]
3573
3624
  };
3574
3625
  var VIEWMODEL_EFFECTS = [MUZZLE_FLASH2, SMOKE_PUFF, BACKBLAST, FLAME_JET];
3626
+ var aliasOf = (base, id, changes) => ({ ...base, id, pageId: `vehicle:${id}`, anchor: CAR_ORIGIN2, ...changes });
3627
+ var VEHICLE_EFFECTS = [
3628
+ /** The engine on fire: `engine_smoke` at its worst condition (black smoke, flames in the hood, a light). */
3629
+ aliasOf(ENGINE_SMOKE, "vehicle_fire", { params: { id: "condition", options: ["100", "60", "30", "10"], default: 3 }, loop: 12 }),
3630
+ /** The exhaust of a car under way: `engine_smoke` in the best condition, for half a second so a moving car asks again. */
3631
+ aliasOf(ENGINE_SMOKE, "vehicle_exhaust", { params: { id: "condition", options: ["100", "60", "30", "10"], default: 0 }, loop: 0.5 }),
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", {})
3636
+ ];
3575
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];
3576
3638
  var WORKSHOP_EFFECTS = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]));
3577
- var GAME_EFFECTS = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS].map((effect) => [effect.id, effect])) };
3639
+ var GAME_EFFECTS = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS, ...VEHICLE_EFFECTS].map((effect) => [effect.id, effect])) };
3578
3640
  var LEHMER_MULTIPLIER = 16807;
3579
3641
  var LEHMER_MODULUS = 2147483647;
3580
3642
  var HASH_MIX = 2654435761;
@@ -7743,6 +7805,79 @@ void main(){
7743
7805
  }
7744
7806
  };
7745
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
+ }
7746
7881
  var clamp5 = (value, min2, max2) => value < min2 ? min2 : value > max2 ? max2 : value;
7747
7882
  function layer(container, name, blend) {
7748
7883
  const element = container.ownerDocument.createElement("div");
@@ -7912,73 +8047,6 @@ void main() {
7912
8047
  }
7913
8048
  };
7914
8049
  }
7915
- var SPHERE_DEFAULT_WIDTH = 8;
7916
- var SPHERE_DEFAULT_HEIGHT = 6;
7917
- function geometryOf(THREE, particle) {
7918
- switch (particle.shape) {
7919
- case "cone":
7920
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
7921
- case "cone_open":
7922
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
7923
- case "sphere":
7924
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
7925
- case "icosahedron":
7926
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
7927
- }
7928
- }
7929
- function materialOf(THREE, particle) {
7930
- if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
7931
- return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
7932
- }
7933
- function createMeshLayer(THREE, pool) {
7934
- const group = new THREE.Group();
7935
- group.name = "workshop-meshes";
7936
- const drawn = /* @__PURE__ */ new Map();
7937
- const frame = new THREE.Quaternion();
7938
- const euler = new THREE.Euler();
7939
- const turn = new THREE.Quaternion();
7940
- const offset = new THREE.Vector3();
7941
- function release(mesh2) {
7942
- mesh2.removeFromParent();
7943
- mesh2.geometry.dispose();
7944
- mesh2.material.dispose();
7945
- }
7946
- return {
7947
- group,
7948
- sync() {
7949
- const live = /* @__PURE__ */ new Set();
7950
- for (const particle of pool.particles) {
7951
- live.add(particle.id);
7952
- let mesh2 = drawn.get(particle.id);
7953
- if (!mesh2) {
7954
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
7955
- mesh2.frustumCulled = false;
7956
- mesh2.renderOrder = particle.additive ? 5 : 3;
7957
- drawn.set(particle.id, mesh2);
7958
- group.add(mesh2);
7959
- }
7960
- frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
7961
- offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
7962
- mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
7963
- turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
7964
- mesh2.quaternion.copy(frame).multiply(turn);
7965
- mesh2.scale.setScalar(particle.scale);
7966
- mesh2.material.opacity = particle.opacity;
7967
- }
7968
- for (const [id, mesh2] of drawn) {
7969
- if (live.has(id)) continue;
7970
- release(mesh2);
7971
- drawn.delete(id);
7972
- }
7973
- return pool.count;
7974
- },
7975
- dispose() {
7976
- for (const mesh2 of drawn.values()) release(mesh2);
7977
- drawn.clear();
7978
- group.removeFromParent();
7979
- }
7980
- };
7981
- }
7982
8050
  var WORKSHOP_SPRITE_VERTEX = `attribute float size;attribute float alpha;attribute vec3 pcol;varying vec3 vC;varying float vA;uniform float uScale;
7983
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;}`;
7984
8052
  var WORKSHOP_SPRITE_FRAGMENT = `uniform float uSoft;varying vec3 vC;varying float vA;
@@ -8343,6 +8411,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8343
8411
  const requested = options.vfx;
8344
8412
  const world = isWorld(requested) ? requested : createVfx({ profile: options.quality ?? "medium", reducedMotion, ...requested });
8345
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
+ };
8346
8421
  const root = new THREE.Group();
8347
8422
  options.scene.add(root);
8348
8423
  const direction = new THREE.Vector3(...options.lightDirection ?? LAB_LIGHT_DIRECTION).normalize();
@@ -8459,6 +8534,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8459
8534
  syncAll();
8460
8535
  overlay?.update(screenNow());
8461
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
+ },
8462
8547
  applyCamera() {
8463
8548
  const shake = screenNow().shake;
8464
8549
  if (reducedMotion || !(shake > SHAKE_FLOOR)) return () => {
@@ -8477,6 +8562,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8477
8562
  },
8478
8563
  screen: screenNow,
8479
8564
  dispose() {
8565
+ releaseAnchors();
8480
8566
  stopMarks();
8481
8567
  overlay?.dispose();
8482
8568
  workshopLayers?.dispose();
@@ -8618,6 +8704,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8618
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]));
8619
8705
  mesh2.scale.copy(sc);
8620
8706
  mesh2.castShadow = true;
8707
+ mesh2.userData.destructionDebris = true;
8621
8708
  this.o.scene.add(mesh2);
8622
8709
  const live = {
8623
8710
  mesh: mesh2,
@@ -8825,6 +8912,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8825
8912
  mesh2.position.set(x, y, z);
8826
8913
  mesh2.scale.setScalar(1e-3);
8827
8914
  mesh2.renderOrder = 1;
8915
+ mesh2.userData.destructionDebris = true;
8828
8916
  this.scene.add(mesh2);
8829
8917
  this.spots.push({ mesh: mesh2, kind, home, x, z, radius: 1e-3, target: Math.min(MAX_RADIUS, radius) });
8830
8918
  }
@@ -9335,12 +9423,68 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9335
9423
  }
9336
9424
  return { min: [center[0] - hx, center[1] - hy, center[2] - hz], max: [center[0] + hx, center[1] + hy, center[2] + hz] };
9337
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
+ };
9338
9480
  var materials_default = [
9339
9481
  {
9340
9482
  id: "steel",
9341
9483
  density: 7850,
9342
9484
  hpPerKg: 400,
9343
9485
  penetration: 110,
9486
+ penetrationClass: "steel",
9487
+ penetrationScale: 1.3,
9344
9488
  ductility: 0.9,
9345
9489
  fracture: "none",
9346
9490
  damageScale: {
@@ -9370,6 +9514,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9370
9514
  density: 7200,
9371
9515
  hpPerKg: 150,
9372
9516
  penetration: 160,
9517
+ penetrationClass: "steel",
9518
+ penetrationScale: 1,
9373
9519
  ductility: 0.2,
9374
9520
  fracture: "snap",
9375
9521
  fragmentSize: [
@@ -9403,6 +9549,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9403
9549
  density: 2700,
9404
9550
  hpPerKg: 250,
9405
9551
  penetration: 60,
9552
+ penetrationClass: "steel",
9553
+ penetrationScale: 3,
9406
9554
  ductility: 0.85,
9407
9555
  fracture: "none",
9408
9556
  damageScale: {
@@ -9431,6 +9579,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9431
9579
  density: 8960,
9432
9580
  hpPerKg: 300,
9433
9581
  penetration: 70,
9582
+ penetrationClass: "steel",
9583
+ penetrationScale: 1.5,
9434
9584
  ductility: 0.95,
9435
9585
  fracture: "none",
9436
9586
  damageScale: {
@@ -9459,6 +9609,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9459
9609
  density: 500,
9460
9610
  hpPerKg: 90,
9461
9611
  penetration: 9,
9612
+ penetrationClass: "wood",
9613
+ penetrationScale: 1.5,
9462
9614
  ductility: 0.35,
9463
9615
  fracture: "splinter",
9464
9616
  fragmentSize: [
@@ -9495,6 +9647,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9495
9647
  density: 750,
9496
9648
  hpPerKg: 140,
9497
9649
  penetration: 14,
9650
+ penetrationClass: "wood",
9651
+ penetrationScale: 1,
9498
9652
  ductility: 0.3,
9499
9653
  fracture: "splinter",
9500
9654
  fragmentSize: [
@@ -9530,6 +9684,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9530
9684
  density: 600,
9531
9685
  hpPerKg: 70,
9532
9686
  penetration: 7,
9687
+ penetrationClass: "wood",
9688
+ penetrationScale: 2,
9533
9689
  ductility: 0.3,
9534
9690
  fracture: "snap",
9535
9691
  fragmentSize: [
@@ -9565,6 +9721,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9565
9721
  density: 750,
9566
9722
  hpPerKg: 45,
9567
9723
  penetration: 6,
9724
+ penetrationClass: "wood",
9725
+ penetrationScale: 2.5,
9568
9726
  ductility: 0.15,
9569
9727
  fracture: "snap",
9570
9728
  fragmentSize: [
@@ -9693,6 +9851,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9693
9851
  density: 2300,
9694
9852
  hpPerKg: 14,
9695
9853
  penetration: 18,
9854
+ penetrationClass: "concrete",
9855
+ penetrationScale: 1,
9696
9856
  ductility: 0.03,
9697
9857
  fracture: "crumble",
9698
9858
  fragmentSize: [
@@ -9712,13 +9872,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9712
9872
  "masonry",
9713
9873
  "brittle"
9714
9874
  ],
9715
- compressive: 444e3
9875
+ compressive: 444e3,
9876
+ strength: {
9877
+ compressiveMPa: 25,
9878
+ tensileMPa: 2.56,
9879
+ elasticGPa: 28
9880
+ }
9716
9881
  },
9717
9882
  {
9718
9883
  id: "brick",
9719
9884
  density: 1900,
9720
9885
  hpPerKg: 12,
9721
9886
  penetration: 14,
9887
+ penetrationClass: "brick",
9888
+ penetrationScale: 1,
9722
9889
  ductility: 0.05,
9723
9890
  fracture: "crumble",
9724
9891
  fragmentSize: [
@@ -9735,13 +9902,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9735
9902
  "masonry",
9736
9903
  "brittle"
9737
9904
  ],
9738
- compressive: 4e4
9905
+ compressive: 4e4,
9906
+ strength: {
9907
+ compressiveMPa: 2.35,
9908
+ tensileMPa: 0.1,
9909
+ elasticGPa: 2.35
9910
+ }
9739
9911
  },
9740
9912
  {
9741
9913
  id: "plaster",
9742
9914
  density: 850,
9743
9915
  hpPerKg: 18,
9744
9916
  penetration: 3,
9917
+ penetrationClass: "brick",
9918
+ penetrationScale: 3,
9745
9919
  ductility: 0.05,
9746
9920
  fracture: "crumble",
9747
9921
  fragmentSize: [
@@ -9765,6 +9939,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9765
9939
  density: 2700,
9766
9940
  hpPerKg: 25,
9767
9941
  penetration: 25,
9942
+ penetrationClass: "concrete",
9943
+ penetrationScale: 0.9,
9768
9944
  ductility: 0.03,
9769
9945
  fracture: "crumble",
9770
9946
  fragmentSize: [
@@ -10028,6 +10204,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10028
10204
  density: 650,
10029
10205
  hpPerKg: 5,
10030
10206
  penetration: 10,
10207
+ penetrationClass: "wood",
10208
+ penetrationScale: 1.2,
10031
10209
  ductility: 0.5,
10032
10210
  fracture: "splinter",
10033
10211
  fragmentSize: [
@@ -10279,6 +10457,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10279
10457
  for (const [key, id] of HINTS) if (n.includes(key)) return id;
10280
10458
  return void 0;
10281
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);
10282
10466
  var partThickness = (part) => {
10283
10467
  const dims = shapeDims(part.shape);
10284
10468
  const min2 = Math.min(dims[0], dims[1], dims[2]);
@@ -10306,6 +10490,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10306
10490
  };
10307
10491
 
10308
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
+ }
10309
10498
  function partHintOf(value) {
10310
10499
  if (typeof value === "string") return value ? { id: value } : null;
10311
10500
  if (!value || typeof value !== "object") return null;
@@ -10326,11 +10515,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10326
10515
  function sizeOf(box) {
10327
10516
  return [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
10328
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
+ }
10329
10525
  function specFromObject3D(THREE, root, options = {}) {
10330
- const fallback = options.fallback ?? "plastic";
10526
+ const fallback2 = options.fallback ?? "plastic";
10331
10527
  const minSize = options.minSize ?? 0.015;
10332
- root.updateMatrixWorld(true);
10333
- const inv = root.matrixWorld.clone().invert();
10528
+ const inv = specFrameInverse(THREE, root);
10334
10529
  const entries = [];
10335
10530
  const byKey = /* @__PURE__ */ new Map();
10336
10531
  const visit = (o) => {
@@ -10344,11 +10539,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10344
10539
  const matName = mats[0]?.name ?? "";
10345
10540
  const ud = o.userData ?? {};
10346
10541
  const hint = partHintOf(ud.destructionPart);
10347
- 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;
10348
10543
  const explicit = hint?.id ?? null;
10349
10544
  const hollow = ud.hollow === true || /hollow/i.test(o.name);
10545
+ const skin = ud.destructionSkin === true;
10350
10546
  const fill = options.fillOf?.(o, material);
10351
10547
  const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
10548
+ const contains = containsOf(ud.destructionContains);
10352
10549
  const existing = explicit ? byKey.get(explicit) : void 0;
10353
10550
  if (existing) {
10354
10551
  const before = sizeOf(existing.box);
@@ -10358,9 +10555,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10358
10555
  existing.meshes.push(o);
10359
10556
  if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
10360
10557
  existing.hollow = existing.hollow || hollow;
10558
+ existing.skin = existing.skin && skin;
10361
10559
  if (fill !== void 0) existing.fill = fill;
10560
+ if (contains) existing.contains = contains;
10362
10561
  } else {
10363
- 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 };
10364
10563
  entries.push(e);
10365
10564
  if (explicit) byKey.set(explicit, e);
10366
10565
  }
@@ -10379,7 +10578,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10379
10578
  const largest = (e) => Math.max(...sizeOf(e.box));
10380
10579
  const kept = [];
10381
10580
  const tiny = [];
10382
- 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);
10383
10582
  for (const t of tiny) {
10384
10583
  let best = null;
10385
10584
  let bestV = 0;
@@ -10433,11 +10632,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10433
10632
  part.thickness = Math.max(minSide, 5e-4);
10434
10633
  }
10435
10634
  if (e.tags) part.tags = [...e.tags];
10635
+ if (e.contains) part.contains = { ...e.contains };
10436
10636
  const ao = options.anchorBelow ?? -1;
10437
10637
  if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
10438
10638
  parts.push(part);
10439
10639
  meshesOf[id] = e.meshes;
10440
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
+ }
10441
10650
  const spec = { parts };
10442
10651
  if (options.name !== void 0) spec.name = options.name;
10443
10652
  else if (root.name) spec.name = root.name;
@@ -10489,6 +10698,46 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10489
10698
  return last[1];
10490
10699
  }
10491
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
+
10492
10741
  // src/visual.ts
10493
10742
  var POS_ATTR = "position";
10494
10743
  function positionAttr(geo) {
@@ -10553,6 +10802,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10553
10802
  }
10554
10803
  };
10555
10804
  var tmpV = (THREE) => new THREE.Vector3();
10805
+ var MIN_DENT_VERTICES = 10;
10806
+ var WIDEN_MARGIN = 1.1;
10807
+ var SKIN_FOLLOW = 0.85;
10808
+ var SKIN_SLACK = 5e-4;
10809
+ var SKIN_LAYER_SLACK = 4;
10810
+ var NEAREST_MARGIN = 1.5;
10556
10811
  var Visuals = class {
10557
10812
  constructor(THREE, lowQuality, decalCap) {
10558
10813
  __publicField(this, "THREE", THREE);
@@ -10721,14 +10976,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10721
10976
  * Push the vertices of the mesh within `dent.radius` of `worldPoint` inwards along `worldNormal` by `depth * (1 - (d/r)^2)^2`.
10722
10977
  * Returns false when nothing was changed (too far, low quality on a heavy mesh, or no editable positions).
10723
10978
  */
10724
- applyDent(mv, worldPoint, worldNormal, dent) {
10979
+ applyDent(mv, worldPoint, worldNormal, dent, reachWorld = 0) {
10725
10980
  const mesh2 = mv.mesh;
10726
10981
  if (this.lowQuality && this.vertexCount(mv) > 2e4) return false;
10727
10982
  mesh2.updateWorldMatrix(true, false);
10728
10983
  const s = mesh2.matrixWorld.getMaxScaleOnAxis() || 1;
10729
10984
  const lp = mesh2.worldToLocal(this.a.copy(worldPoint));
10730
10985
  const ln = this.b.copy(worldNormal).transformDirection(this.m4.copy(mesh2.matrixWorld).invert()).normalize();
10731
- const r = dent.radius / s;
10986
+ const r = Math.max(dent.radius, reachWorld) / s;
10732
10987
  const depth = dent.depth / s;
10733
10988
  const geo = mesh2.geometry;
10734
10989
  let bb;
@@ -10741,34 +10996,110 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10741
10996
  if (!this.ownGeometry(mv)) return false;
10742
10997
  const work = mv.host ? mv.host.work : mv.work;
10743
10998
  const pos = positionAttr(mesh2.geometry);
10744
- const r2 = r * r;
10745
- let touched = false;
10746
- const px = lp.x;
10747
- const py = lp.y;
10748
- const pz = lp.z;
10749
- const nx = ln.x;
10750
- const ny = ln.y;
10751
- const nz = ln.z;
10752
10999
  const from = mv.range ? mv.range[0] * 3 : 0;
10753
11000
  const to = mv.range ? mv.range[1] * 3 : work.length;
11001
+ const reach = r;
11002
+ const reach2 = reach * reach;
10754
11003
  for (let i = from; i < to; i += 3) {
10755
- const dx = work[i] - px;
10756
- const dy = work[i + 1] - py;
10757
- const dz = work[i + 2] - pz;
11004
+ const dx = work[i] - lp.x;
11005
+ const dy = work[i + 1] - lp.y;
11006
+ const dz = work[i + 2] - lp.z;
10758
11007
  const d2 = dx * dx + dy * dy + dz * dz;
10759
- if (d2 >= r2) continue;
10760
- const f = 1 - d2 / r2;
11008
+ if (d2 >= reach2) continue;
11009
+ const f = 1 - d2 / reach2;
10761
11010
  const k = depth * f * f;
10762
- work[i] -= nx * k;
10763
- work[i + 1] -= ny * k;
10764
- work[i + 2] -= nz * k;
10765
- touched = true;
11011
+ work[i] -= ln.x * k;
11012
+ work[i + 1] -= ln.y * k;
11013
+ work[i + 2] -= ln.z * k;
10766
11014
  }
10767
- if (!touched) return false;
10768
11015
  mv.touched = true;
10769
11016
  this.writePositions(mv, pos);
10770
11017
  return true;
10771
11018
  }
11019
+ /**
11020
+ * 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
11021
+ * (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.
11022
+ */
11023
+ dentReach(mv, worldPoint, dent) {
11024
+ const mesh2 = mv.mesh;
11025
+ mesh2.updateWorldMatrix(true, false);
11026
+ const s = mesh2.matrixWorld.getMaxScaleOnAxis() || 1;
11027
+ const lp = mesh2.worldToLocal(this.a.copy(worldPoint));
11028
+ const source = (mv.host ? mv.host.work : mv.work) ?? positionAttr(mesh2.geometry)?.array;
11029
+ if (!source) return dent.radius;
11030
+ let bounds;
11031
+ if (mv.host) bounds = this.localBox(mv, this.lbox);
11032
+ else {
11033
+ if (!mesh2.geometry.boundingBox) mesh2.geometry.computeBoundingBox();
11034
+ bounds = mesh2.geometry.boundingBox;
11035
+ }
11036
+ if (bounds && !bounds.isEmpty() && bounds.distanceToPoint(lp) > dent.radius / s) return dent.radius;
11037
+ const from = mv.range ? mv.range[0] * 3 : 0;
11038
+ const to = mv.range ? mv.range[1] * 3 : source.length;
11039
+ const r2 = (dent.radius / s) ** 2;
11040
+ const distances = [];
11041
+ let inside = 0;
11042
+ for (let i = from; i < to; i += 3) {
11043
+ const dx = source[i] - lp.x;
11044
+ const dy = source[i + 1] - lp.y;
11045
+ const dz = source[i + 2] - lp.z;
11046
+ const d2 = dx * dx + dy * dy + dz * dz;
11047
+ if (d2 < r2) inside++;
11048
+ distances.push(d2);
11049
+ }
11050
+ if (inside >= MIN_DENT_VERTICES || distances.length < MIN_DENT_VERTICES) return dent.radius;
11051
+ distances.sort((x, y) => x - y);
11052
+ const nearest = Math.sqrt(distances[0]);
11053
+ return Math.max(dent.radius, Math.sqrt(distances[MIN_DENT_VERTICES - 1]) * s * WIDEN_MARGIN, nearest * s * NEAREST_MARGIN);
11054
+ }
11055
+ /**
11056
+ * 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
11057
+ * and moves with the shell's surface instead (see `shellDisplacementAt`), so the shell never shows through it whatever the two meshes' vertex grids are.
11058
+ */
11059
+ applyPartDent(meshes, worldPoint, worldNormal, dent, reachWorld) {
11060
+ const isSkin = (mv) => mv.mesh.userData.destructionSkin === true && !mv.host;
11061
+ const shells = meshes.filter((mv) => !isSkin(mv));
11062
+ const snapshots = [];
11063
+ for (const mv of shells) {
11064
+ const heavy = this.lowQuality && this.vertexCount(mv) > 2e4;
11065
+ const plain = !mv.host && !heavy && this.ownGeometry(mv) && mv.work;
11066
+ const before = plain ? new Float32Array(mv.work) : null;
11067
+ this.applyDent(mv, worldPoint, worldNormal, dent, reachWorld);
11068
+ if (before) snapshots.push({ mesh: mv.mesh, before, after: mv.work });
11069
+ }
11070
+ const displacement = this.t;
11071
+ const vertex = this.a;
11072
+ for (const mv of meshes) {
11073
+ if (!isSkin(mv)) continue;
11074
+ if (snapshots.length === 0) {
11075
+ this.applyDent(mv, worldPoint, worldNormal, dent, reachWorld);
11076
+ continue;
11077
+ }
11078
+ if (!this.ownGeometry(mv)) continue;
11079
+ const work = mv.work;
11080
+ const mesh2 = mv.mesh;
11081
+ mesh2.updateWorldMatrix(true, false);
11082
+ const toLocal = this.m4.copy(mesh2.matrixWorld).invert();
11083
+ const scale = mesh2.matrixWorld.getMaxScaleOnAxis() || 1;
11084
+ let touched = false;
11085
+ for (let i = 0; i < work.length; i += 3) {
11086
+ vertex.set(work[i], work[i + 1], work[i + 2]).applyMatrix4(mesh2.matrixWorld);
11087
+ if (vertex.distanceTo(worldPoint) > reachWorld) continue;
11088
+ const height = shellDisplacementAt(this.THREE, vertex, snapshots, displacement);
11089
+ const length = displacement.length();
11090
+ if (length === 0) continue;
11091
+ const falloff = 1 - (vertex.distanceTo(worldPoint) / reachWorld) ** 2;
11092
+ const smooth = dent.depth * falloff * falloff;
11093
+ const pull = Math.max(0, Math.min(length, smooth) * SKIN_FOLLOW - SKIN_SLACK - height * SKIN_LAYER_SLACK);
11094
+ displacement.transformDirection(toLocal).multiplyScalar(pull / scale);
11095
+ work[i] += displacement.x;
11096
+ work[i + 1] += displacement.y;
11097
+ work[i + 2] += displacement.z;
11098
+ touched = true;
11099
+ }
11100
+ if (touched) this.writePositions(mv, positionAttr(mesh2.geometry));
11101
+ }
11102
+ }
10772
11103
  /** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
10773
11104
  writePositions(mv, pos) {
10774
11105
  if (mv.host) {
@@ -10815,6 +11146,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10815
11146
  return { s, pos, quat: quat2 };
10816
11147
  }
10817
11148
  addDecal(mv, decal2) {
11149
+ decal2.obj.userData.destructionDebris = true;
10818
11150
  mv.mesh.add(decal2.obj);
10819
11151
  mv.decals.push(decal2);
10820
11152
  this.decalCount++;
@@ -11399,6 +11731,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11399
11731
  __publicField(this, "tmpQ");
11400
11732
  __publicField(this, "v1");
11401
11733
  __publicField(this, "v2");
11734
+ // 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.
11735
+ __publicField(this, "warmProbes");
11402
11736
  this.world = o.world;
11403
11737
  this.scene = o.scene;
11404
11738
  this.quality = o.quality ?? "medium";
@@ -11496,6 +11830,24 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11496
11830
  if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes, hosts));
11497
11831
  }
11498
11832
  this.bounds.set(objectId, bound);
11833
+ for (const pv of bound.parts.values()) this.tagPart(pv, objectId);
11834
+ }
11835
+ /**
11836
+ * Label the meshes of a part so a host can go from a raycast hit to the world: `userData.destructionPart` (the part id) and
11837
+ * `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
11838
+ * 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`.
11839
+ */
11840
+ tagPart(pv, objectId) {
11841
+ for (const mv of pv.meshes) {
11842
+ if (mv.merged && !mv.loose) continue;
11843
+ mv.mesh.traverse((c) => {
11844
+ if (!c.isMesh) return;
11845
+ const ud = c.userData;
11846
+ if (ud.destructionDebris) return;
11847
+ if (partHintOf(ud.destructionPart)?.id !== pv.id) ud.destructionPart = pv.id;
11848
+ ud.destructionObject = objectId;
11849
+ });
11850
+ }
11499
11851
  }
11500
11852
  makePartVis(home, id, material, meshes, hosts) {
11501
11853
  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" };
@@ -11509,13 +11861,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11509
11861
  specParts = void 0;
11510
11862
  }
11511
11863
  if (!specParts) return;
11512
- root.updateMatrixWorld(true);
11864
+ const frame = specFrameInverse(this.THREE, root);
11513
11865
  const e = new this.THREE.Euler();
11514
11866
  const q = new this.THREE.Quaternion();
11515
11867
  for (const m of meshes) {
11516
11868
  this.box.setFromObject(m);
11517
11869
  if (this.box.isEmpty()) continue;
11518
- const c = root.worldToLocal(this.box.getCenter(this.v1));
11870
+ const c = this.box.getCenter(this.v1).applyMatrix4(frame);
11519
11871
  let best = null;
11520
11872
  let bestVol = Infinity;
11521
11873
  for (const p of specParts) {
@@ -11552,10 +11904,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11552
11904
  // ----------------------------------------------------------------------------------------------------------------------------------
11553
11905
  // Pieces
11554
11906
  // ----------------------------------------------------------------------------------------------------------------------------------
11555
- /** Half extents and centre of the box around some meshes, in the local space of `root`. */
11907
+ /** Half extents and centre of the box around some meshes, in the metric frame of `root` (its pose removed, its scale kept). */
11556
11908
  measure(root, meshes) {
11557
- root.updateMatrixWorld(true);
11558
- const inv = root.matrixWorld.clone().invert();
11909
+ const inv = specFrameInverse(this.THREE, root);
11559
11910
  const total = new this.THREE.Box3();
11560
11911
  const m4 = new this.THREE.Matrix4();
11561
11912
  const lb = new this.THREE.Box3();
@@ -11630,6 +11981,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11630
11981
  dt = clamp6(dt, 0, 0.1);
11631
11982
  this.time += dt;
11632
11983
  this.drainQueue();
11984
+ for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
11633
11985
  this.updateActive(dt);
11634
11986
  this.updateLeaks(dt);
11635
11987
  this.pieceSys.update(dt);
@@ -11648,6 +12000,27 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11648
12000
  }
11649
12001
  }
11650
12002
  }
12003
+ /**
12004
+ * 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
12005
+ * root keeps the pose the host gave and the world keeps getting it from `syncTransform`.
12006
+ */
12007
+ onSettle(objectId) {
12008
+ const b = this.bounds.get(objectId);
12009
+ const soil = this.world.state(objectId)?.soil;
12010
+ if (!b || !soil) return;
12011
+ let node = b.soilNode;
12012
+ if (!node) {
12013
+ node = new this.THREE.Group();
12014
+ node.name = "destruction-soil";
12015
+ for (const c of [...b.root.children]) node.add(c);
12016
+ b.root.add(node);
12017
+ b.soilNode = node;
12018
+ }
12019
+ const { position: p, rotation: q } = soil.offset;
12020
+ node.position.set(p[0], p[1], p[2]);
12021
+ node.quaternion.set(q[0], q[1], q[2], q[3]);
12022
+ node.updateMatrixWorld(true);
12023
+ }
11651
12024
  syncTransforms() {
11652
12025
  for (const b of this.bounds.values()) this.syncOne(b);
11653
12026
  }
@@ -11702,6 +12075,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11702
12075
  case "detach":
11703
12076
  this.onDetach(ev);
11704
12077
  break;
12078
+ case "settle":
12079
+ this.onSettle(ev.object);
12080
+ break;
11705
12081
  case "collapse":
11706
12082
  this.splitLoose(ev.object);
11707
12083
  this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
@@ -11793,7 +12169,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11793
12169
  same.direction = ev.direction;
11794
12170
  } else {
11795
12171
  if (this.leaks.length >= 24) this.leaks.shift();
11796
- 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 });
12172
+ 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, opened: this.world.part(ev.object, ev.part)?.holes.some((h) => h.through) ?? false });
11797
12173
  }
11798
12174
  void key;
11799
12175
  break;
@@ -11870,10 +12246,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11870
12246
  const def = this.world.materials.get(pv.material);
11871
12247
  const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
11872
12248
  const wn = this.worldDir(b, ev.dent.normal);
11873
- for (const mv of pv.meshes) {
11874
- if (!this.visuals.shown(mv)) continue;
11875
- this.visuals.applyDent(mv, wp, wn, ev.dent);
11876
- }
12249
+ const shown = pv.meshes.filter((mv) => this.visuals.shown(mv));
12250
+ const reach = shown.reduce((widest, mv) => Math.max(widest, this.visuals.dentReach(mv, wp, ev.dent)), ev.dent.radius);
12251
+ this.visuals.applyPartDent(shown, wp, wn, ev.dent, reach);
11877
12252
  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);
11878
12253
  this.sound(impactKey(def), ev.worldPoint, clamp6(0.3 + ev.dent.depth * 12, 0.3, 1));
11879
12254
  }
@@ -12069,6 +12444,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12069
12444
  }
12070
12445
  }
12071
12446
  this.bounds.set(newId, nb);
12447
+ for (const pv of nb.parts.values()) this.tagPart(pv, newId);
12072
12448
  const meshes = this.meshesOf(nb);
12073
12449
  if (meshes.length === 0) return;
12074
12450
  const { half, center } = this.measure(group, meshes);
@@ -12096,24 +12472,40 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12096
12472
  const report = options.report;
12097
12473
  const confinement = report?.find((r) => r.blast !== void 0)?.blast?.confinement ?? 1;
12098
12474
  const energy = ev.energy * confinement;
12099
- for (const pc of this.pieceSys.list) {
12475
+ const reachOf = (objectId) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? 1;
12476
+ const shove = (pc) => {
12100
12477
  this.pieceSys.centerOf(pc, c);
12101
- const dx = c.x - p[0];
12102
- const dy = c.y - p[1];
12103
- const dz = c.z - p[2];
12104
- const d = Math.hypot(dx, dy, dz);
12105
- if (d > ev.radius) continue;
12106
- const fall = (1 - d / ev.radius) ** 2;
12107
- const reach = report?.find((r) => r.object === pc.objectId)?.blast?.shelter ?? 1;
12108
- const speed = Math.min(30, Math.sqrt(2 * energy * reach * 0.02 / pc.mass)) * fall;
12109
- const inv = d > 1e-4 ? 1 / d : 0;
12110
- const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
12478
+ 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);
12479
+ if (!hit) return;
12111
12480
  const spread = pc.halfExtents[0];
12112
- 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]);
12113
- }
12481
+ const momentum = hit.speed * pc.mass;
12482
+ 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]);
12483
+ };
12484
+ for (const pc of [...this.pieceSys.list]) shove(pc);
12485
+ if (this.o.shoveable) this.loosen(p, ev.radius, energy, reachOf);
12114
12486
  this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(energy) * 0.08));
12115
12487
  this.sound("explode", p, clamp6(0.5 + ev.radius / 10, 0.5, 1));
12116
12488
  }
12489
+ /** 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. */
12490
+ loosen(point, radius, energy, reachOf) {
12491
+ const shoveable = this.o.shoveable;
12492
+ if (!shoveable) return;
12493
+ for (const b of [...this.bounds.values()]) {
12494
+ if (b.piece || this.pieceSys.get(b.id) || !shoveable(b.id) || !this.world.has(b.id)) continue;
12495
+ const { half, center } = this.measure(b.root, this.meshesOf(b));
12496
+ const world = center.clone().applyQuaternion(b.root.quaternion).add(b.root.position);
12497
+ const mass = this.massOf(b.id);
12498
+ const hit = blastShove({ point, energy, radius, reach: reachOf(b.id) }, { position: [world.x, world.y, world.z], mass }, this.o.blastShove);
12499
+ if (!hit) continue;
12500
+ const piece = this.makePiece(b.id, { mass });
12501
+ const body = this.pieceSys.get(b.id);
12502
+ if (!body) continue;
12503
+ this.o.onLoosened?.(b.id, piece);
12504
+ const spread = half[0];
12505
+ const momentum = hit.speed * mass;
12506
+ 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]);
12507
+ }
12508
+ }
12117
12509
  playExplosion(ev, options) {
12118
12510
  const named = options.effect ?? ev.effect;
12119
12511
  const direction = options.direction;
@@ -12214,7 +12606,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12214
12606
  for (let i = this.leaks.length - 1; i >= 0; i--) {
12215
12607
  const l = this.leaks[i];
12216
12608
  const ps = this.world.part(l.object, l.part);
12217
- if (!ps || ps.liters === void 0 || ps.liters <= 0) {
12609
+ if (!ps || ps.liters === void 0 || ps.liters <= 0 || l.opened && !ps.holes.some((h) => h.through)) {
12218
12610
  this.leaks.splice(i, 1);
12219
12611
  continue;
12220
12612
  }
@@ -12265,6 +12657,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12265
12657
  for (const [pid, pv] of [...b.parts]) {
12266
12658
  if (pv.home !== home) continue;
12267
12659
  this.visuals.restorePart(pv);
12660
+ this.tagPart(pv, home);
12268
12661
  if (b !== homeBound && homeBound) {
12269
12662
  b.parts.delete(pid);
12270
12663
  homeBound.parts.set(pid, pv);
@@ -12306,10 +12699,31 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12306
12699
  const { puffs, sparks, glows, chunks } = this.vfx.stats();
12307
12700
  return puffs + sparks + glows + chunks;
12308
12701
  }
12702
+ warmUpProbes() {
12703
+ this.releaseWarmProbes();
12704
+ const { kit } = this.visuals;
12705
+ const T = this.THREE;
12706
+ const geometry = new T.BufferGeometry();
12707
+ geometry.setAttribute("position", new T.BufferAttribute(new Float32Array(6), 3));
12708
+ const materials = [kit.discMaterial().clone(), kit.patchMaterial(8421504).clone(), kit.lineMaterial(0).clone()];
12709
+ const group = new T.Group();
12710
+ group.add(new T.Mesh(kit.discGeometry(), materials[0]), new T.Mesh(kit.discGeometry(), materials[1]), new T.LineSegments(geometry, materials[2]));
12711
+ for (const probe of group.children) probe.frustumCulled = false;
12712
+ this.warmProbes = { group, geometry, materials };
12713
+ return { group, dispose: () => {
12714
+ } };
12715
+ }
12716
+ releaseWarmProbes() {
12717
+ if (!this.warmProbes) return;
12718
+ this.warmProbes.geometry.dispose();
12719
+ for (const material of this.warmProbes.materials) material.dispose();
12720
+ this.warmProbes = void 0;
12721
+ }
12309
12722
  rebarCount() {
12310
12723
  return this.cages.count;
12311
12724
  }
12312
12725
  dispose() {
12726
+ this.releaseWarmProbes();
12313
12727
  if (this.disposed) return;
12314
12728
  this.unsubscribe();
12315
12729
  this.clear();