@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.
- package/README.md +10 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.global.js +634 -152
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +366 -56
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.global.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
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
|
-
|
|
3570
|
+
variable("puffs"),
|
|
3521
3571
|
puff2(
|
|
3522
3572
|
ORIGIN2,
|
|
3523
|
-
[add(
|
|
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(
|
|
3526
|
-
add(
|
|
3527
|
-
|
|
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
|
|
3582
|
-
aliasOf(
|
|
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
|
|
10526
|
+
const fallback2 = options.fallback ?? "plastic";
|
|
10340
10527
|
const minSize = options.minSize ?? 0.015;
|
|
10341
|
-
root
|
|
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 : ""}`) ??
|
|
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] -
|
|
10765
|
-
const dy = work[i + 1] -
|
|
10766
|
-
const dz = work[i + 2] -
|
|
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 >=
|
|
10769
|
-
const f = 1 - d2 /
|
|
11009
|
+
if (d2 >= reach2) continue;
|
|
11010
|
+
const f = 1 - d2 / reach2;
|
|
10770
11011
|
const k = depth * f * f;
|
|
10771
|
-
work[i] -=
|
|
10772
|
-
work[i + 1] -=
|
|
10773
|
-
work[i + 2] -=
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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({
|
|
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
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
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
|
-
|
|
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
|
|
12111
|
-
|
|
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
|
-
|
|
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 <=
|
|
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
|
|
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 = [
|
|
12238
|
-
if (l.fluid === "gas") this.vfx.spawn("steam",
|
|
12239
|
-
else this.vfx.spawn("drips",
|
|
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();
|