@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.
- package/README.md +10 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.global.js +558 -144
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +282 -49
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
|
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
|
|
5
5
|
// src/renderer.ts
|
|
6
|
+
import { blastShove } from "@adatechnology/game-contracts";
|
|
6
7
|
import { createVfx, effectForEvent, EVENT_EFFECTS_MAP } from "@adatechnology/game-vfx";
|
|
7
8
|
import { createVfxRenderer } from "@adatechnology/game-vfx-three";
|
|
8
9
|
|
|
@@ -135,6 +136,7 @@ var FragmentSystem = class {
|
|
|
135
136
|
const sc = new this.THREE.Vector3(Math.max(5e-3, sz[0]), Math.max(5e-3, sz[1]), Math.max(3e-3, sz[2]));
|
|
136
137
|
mesh.scale.copy(sc);
|
|
137
138
|
mesh.castShadow = true;
|
|
139
|
+
mesh.userData.destructionDebris = true;
|
|
138
140
|
this.o.scene.add(mesh);
|
|
139
141
|
const live = {
|
|
140
142
|
mesh,
|
|
@@ -342,6 +344,7 @@ var GroundSpots = class {
|
|
|
342
344
|
mesh.position.set(x, y, z);
|
|
343
345
|
mesh.scale.setScalar(1e-3);
|
|
344
346
|
mesh.renderOrder = 1;
|
|
347
|
+
mesh.userData.destructionDebris = true;
|
|
345
348
|
this.scene.add(mesh);
|
|
346
349
|
this.spots.push({ mesh, kind, home, x, z, radius: 1e-3, target: Math.min(MAX_RADIUS, radius) });
|
|
347
350
|
}
|
|
@@ -770,7 +773,12 @@ var RebarCages = class {
|
|
|
770
773
|
};
|
|
771
774
|
|
|
772
775
|
// src/spec.ts
|
|
773
|
-
import { guessMaterial, inferJoints } from "@adatechnology/game-destruction";
|
|
776
|
+
import { guessMaterial, inferJoints, MATERIALS, partMass } from "@adatechnology/game-destruction";
|
|
777
|
+
function containsOf(value) {
|
|
778
|
+
if (!value || typeof value !== "object") return void 0;
|
|
779
|
+
const v = value;
|
|
780
|
+
return typeof v.fluid === "string" && typeof v.liters === "number" && v.liters > 0 ? { fluid: v.fluid, liters: v.liters } : void 0;
|
|
781
|
+
}
|
|
774
782
|
function partHintOf(value) {
|
|
775
783
|
if (typeof value === "string") return value ? { id: value } : null;
|
|
776
784
|
if (!value || typeof value !== "object") return null;
|
|
@@ -791,11 +799,17 @@ function isMesh(o) {
|
|
|
791
799
|
function sizeOf(box) {
|
|
792
800
|
return [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
|
|
793
801
|
}
|
|
802
|
+
function specFrameInverse(THREE, root) {
|
|
803
|
+
root.updateMatrixWorld(true);
|
|
804
|
+
const position = new THREE.Vector3();
|
|
805
|
+
const quaternion = new THREE.Quaternion();
|
|
806
|
+
root.matrixWorld.decompose(position, quaternion, new THREE.Vector3());
|
|
807
|
+
return new THREE.Matrix4().compose(position, quaternion, new THREE.Vector3(1, 1, 1)).invert();
|
|
808
|
+
}
|
|
794
809
|
function specFromObject3D(THREE, root, options = {}) {
|
|
795
810
|
const fallback = options.fallback ?? "plastic";
|
|
796
811
|
const minSize = options.minSize ?? 0.015;
|
|
797
|
-
root
|
|
798
|
-
const inv = root.matrixWorld.clone().invert();
|
|
812
|
+
const inv = specFrameInverse(THREE, root);
|
|
799
813
|
const entries = [];
|
|
800
814
|
const byKey = /* @__PURE__ */ new Map();
|
|
801
815
|
const visit = (o) => {
|
|
@@ -812,8 +826,10 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
812
826
|
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;
|
|
813
827
|
const explicit = hint?.id ?? null;
|
|
814
828
|
const hollow = ud.hollow === true || /hollow/i.test(o.name);
|
|
829
|
+
const skin = ud.destructionSkin === true;
|
|
815
830
|
const fill = options.fillOf?.(o, material);
|
|
816
831
|
const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
|
|
832
|
+
const contains = containsOf(ud.destructionContains);
|
|
817
833
|
const existing = explicit ? byKey.get(explicit) : void 0;
|
|
818
834
|
if (existing) {
|
|
819
835
|
const before = sizeOf(existing.box);
|
|
@@ -823,9 +839,11 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
823
839
|
existing.meshes.push(o);
|
|
824
840
|
if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
|
|
825
841
|
existing.hollow = existing.hollow || hollow;
|
|
842
|
+
existing.skin = existing.skin && skin;
|
|
826
843
|
if (fill !== void 0) existing.fill = fill;
|
|
844
|
+
if (contains) existing.contains = contains;
|
|
827
845
|
} else {
|
|
828
|
-
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 };
|
|
846
|
+
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 };
|
|
829
847
|
entries.push(e);
|
|
830
848
|
if (explicit) byKey.set(explicit, e);
|
|
831
849
|
}
|
|
@@ -844,7 +862,7 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
844
862
|
const largest = (e) => Math.max(...sizeOf(e.box));
|
|
845
863
|
const kept = [];
|
|
846
864
|
const tiny = [];
|
|
847
|
-
for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
|
|
865
|
+
for (const e of entries) (e.skin || !e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
|
|
848
866
|
for (const t of tiny) {
|
|
849
867
|
let best = null;
|
|
850
868
|
let bestV = 0;
|
|
@@ -898,11 +916,21 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
898
916
|
part.thickness = Math.max(minSide, 5e-4);
|
|
899
917
|
}
|
|
900
918
|
if (e.tags) part.tags = [...e.tags];
|
|
919
|
+
if (e.contains) part.contains = { ...e.contains };
|
|
901
920
|
const ao = options.anchorBelow ?? -1;
|
|
902
921
|
if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
|
|
903
922
|
parts.push(part);
|
|
904
923
|
meshesOf[id] = e.meshes;
|
|
905
924
|
}
|
|
925
|
+
const knownMass = root.userData?.destructionMass;
|
|
926
|
+
if (typeof knownMass === "number" && knownMass > 0) {
|
|
927
|
+
const shares = parts.map((p) => {
|
|
928
|
+
const material = MATERIALS.find((m) => m.id === p.material);
|
|
929
|
+
return material ? partMass(p, material) : 0;
|
|
930
|
+
});
|
|
931
|
+
const total = shares.reduce((a, b) => a + b, 0);
|
|
932
|
+
if (total > 0) parts.forEach((p, i) => p.mass = knownMass * shares[i] / total);
|
|
933
|
+
}
|
|
906
934
|
const spec = { parts };
|
|
907
935
|
if (options.name !== void 0) spec.name = options.name;
|
|
908
936
|
else if (root.name) spec.name = root.name;
|
|
@@ -954,6 +982,46 @@ function heatColor(temperatureC) {
|
|
|
954
982
|
return last[1];
|
|
955
983
|
}
|
|
956
984
|
|
|
985
|
+
// src/skin-dent.ts
|
|
986
|
+
function shellDisplacementAt(THREE, worldPoint, shells, out) {
|
|
987
|
+
const triangle = new THREE.Triangle();
|
|
988
|
+
const corners = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()];
|
|
989
|
+
const local = new THREE.Vector3();
|
|
990
|
+
const closest = new THREE.Vector3();
|
|
991
|
+
const weights = new THREE.Vector3();
|
|
992
|
+
const moved = new THREE.Vector3();
|
|
993
|
+
let nearest = Infinity;
|
|
994
|
+
out.set(0, 0, 0);
|
|
995
|
+
let nearestScale = 1;
|
|
996
|
+
for (const shell of shells) {
|
|
997
|
+
shell.mesh.updateWorldMatrix(true, false);
|
|
998
|
+
shell.mesh.worldToLocal(local.copy(worldPoint));
|
|
999
|
+
const index = shell.mesh.geometry.index;
|
|
1000
|
+
const triangles = index ? index.count / 3 : shell.before.length / 9;
|
|
1001
|
+
for (let t = 0; t < triangles; t++) {
|
|
1002
|
+
const ids = [0, 1, 2].map((corner) => index ? index.getX(t * 3 + corner) : t * 3 + corner);
|
|
1003
|
+
ids.forEach((id, corner) => corners[corner].set(shell.before[id * 3], shell.before[id * 3 + 1], shell.before[id * 3 + 2]));
|
|
1004
|
+
triangle.set(corners[0], corners[1], corners[2]);
|
|
1005
|
+
triangle.closestPointToPoint(local, closest);
|
|
1006
|
+
const distance = closest.distanceToSquared(local);
|
|
1007
|
+
if (distance >= nearest) continue;
|
|
1008
|
+
nearest = distance;
|
|
1009
|
+
nearestScale = shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
1010
|
+
triangle.getBarycoord(closest, weights);
|
|
1011
|
+
moved.set(0, 0, 0);
|
|
1012
|
+
ids.forEach((id, corner) => {
|
|
1013
|
+
const weight = corner === 0 ? weights.x : corner === 1 ? weights.y : weights.z;
|
|
1014
|
+
moved.x += weight * (shell.after[id * 3] - shell.before[id * 3]);
|
|
1015
|
+
moved.y += weight * (shell.after[id * 3 + 1] - shell.before[id * 3 + 1]);
|
|
1016
|
+
moved.z += weight * (shell.after[id * 3 + 2] - shell.before[id * 3 + 2]);
|
|
1017
|
+
});
|
|
1018
|
+
out.copy(moved).transformDirection(shell.mesh.matrixWorld);
|
|
1019
|
+
out.multiplyScalar(moved.length() * (shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return Math.sqrt(nearest) * nearestScale;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
957
1025
|
// src/visual.ts
|
|
958
1026
|
var POS_ATTR = "position";
|
|
959
1027
|
function positionAttr(geo) {
|
|
@@ -1018,6 +1086,12 @@ var DecalKit = class {
|
|
|
1018
1086
|
}
|
|
1019
1087
|
};
|
|
1020
1088
|
var tmpV = (THREE) => new THREE.Vector3();
|
|
1089
|
+
var MIN_DENT_VERTICES = 10;
|
|
1090
|
+
var WIDEN_MARGIN = 1.1;
|
|
1091
|
+
var SKIN_FOLLOW = 0.85;
|
|
1092
|
+
var SKIN_SLACK = 5e-4;
|
|
1093
|
+
var SKIN_LAYER_SLACK = 4;
|
|
1094
|
+
var NEAREST_MARGIN = 1.5;
|
|
1021
1095
|
var Visuals = class {
|
|
1022
1096
|
constructor(THREE, lowQuality, decalCap) {
|
|
1023
1097
|
__publicField(this, "THREE", THREE);
|
|
@@ -1186,14 +1260,14 @@ var Visuals = class {
|
|
|
1186
1260
|
* Push the vertices of the mesh within `dent.radius` of `worldPoint` inwards along `worldNormal` by `depth * (1 - (d/r)^2)^2`.
|
|
1187
1261
|
* Returns false when nothing was changed (too far, low quality on a heavy mesh, or no editable positions).
|
|
1188
1262
|
*/
|
|
1189
|
-
applyDent(mv, worldPoint, worldNormal, dent) {
|
|
1263
|
+
applyDent(mv, worldPoint, worldNormal, dent, reachWorld = 0) {
|
|
1190
1264
|
const mesh = mv.mesh;
|
|
1191
1265
|
if (this.lowQuality && this.vertexCount(mv) > 2e4) return false;
|
|
1192
1266
|
mesh.updateWorldMatrix(true, false);
|
|
1193
1267
|
const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
1194
1268
|
const lp = mesh.worldToLocal(this.a.copy(worldPoint));
|
|
1195
1269
|
const ln = this.b.copy(worldNormal).transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
|
|
1196
|
-
const r = dent.radius / s;
|
|
1270
|
+
const r = Math.max(dent.radius, reachWorld) / s;
|
|
1197
1271
|
const depth = dent.depth / s;
|
|
1198
1272
|
const geo = mesh.geometry;
|
|
1199
1273
|
let bb;
|
|
@@ -1206,34 +1280,110 @@ var Visuals = class {
|
|
|
1206
1280
|
if (!this.ownGeometry(mv)) return false;
|
|
1207
1281
|
const work = mv.host ? mv.host.work : mv.work;
|
|
1208
1282
|
const pos = positionAttr(mesh.geometry);
|
|
1209
|
-
const r2 = r * r;
|
|
1210
|
-
let touched = false;
|
|
1211
|
-
const px = lp.x;
|
|
1212
|
-
const py = lp.y;
|
|
1213
|
-
const pz = lp.z;
|
|
1214
|
-
const nx = ln.x;
|
|
1215
|
-
const ny = ln.y;
|
|
1216
|
-
const nz = ln.z;
|
|
1217
1283
|
const from = mv.range ? mv.range[0] * 3 : 0;
|
|
1218
1284
|
const to = mv.range ? mv.range[1] * 3 : work.length;
|
|
1285
|
+
const reach = r;
|
|
1286
|
+
const reach2 = reach * reach;
|
|
1219
1287
|
for (let i = from; i < to; i += 3) {
|
|
1220
|
-
const dx = work[i] -
|
|
1221
|
-
const dy = work[i + 1] -
|
|
1222
|
-
const dz = work[i + 2] -
|
|
1288
|
+
const dx = work[i] - lp.x;
|
|
1289
|
+
const dy = work[i + 1] - lp.y;
|
|
1290
|
+
const dz = work[i + 2] - lp.z;
|
|
1223
1291
|
const d2 = dx * dx + dy * dy + dz * dz;
|
|
1224
|
-
if (d2 >=
|
|
1225
|
-
const f = 1 - d2 /
|
|
1292
|
+
if (d2 >= reach2) continue;
|
|
1293
|
+
const f = 1 - d2 / reach2;
|
|
1226
1294
|
const k = depth * f * f;
|
|
1227
|
-
work[i] -=
|
|
1228
|
-
work[i + 1] -=
|
|
1229
|
-
work[i + 2] -=
|
|
1230
|
-
touched = true;
|
|
1295
|
+
work[i] -= ln.x * k;
|
|
1296
|
+
work[i + 1] -= ln.y * k;
|
|
1297
|
+
work[i + 2] -= ln.z * k;
|
|
1231
1298
|
}
|
|
1232
|
-
if (!touched) return false;
|
|
1233
1299
|
mv.touched = true;
|
|
1234
1300
|
this.writePositions(mv, pos);
|
|
1235
1301
|
return true;
|
|
1236
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* 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
|
|
1305
|
+
* (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.
|
|
1306
|
+
*/
|
|
1307
|
+
dentReach(mv, worldPoint, dent) {
|
|
1308
|
+
const mesh = mv.mesh;
|
|
1309
|
+
mesh.updateWorldMatrix(true, false);
|
|
1310
|
+
const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
1311
|
+
const lp = mesh.worldToLocal(this.a.copy(worldPoint));
|
|
1312
|
+
const source = (mv.host ? mv.host.work : mv.work) ?? positionAttr(mesh.geometry)?.array;
|
|
1313
|
+
if (!source) return dent.radius;
|
|
1314
|
+
let bounds;
|
|
1315
|
+
if (mv.host) bounds = this.localBox(mv, this.lbox);
|
|
1316
|
+
else {
|
|
1317
|
+
if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
|
|
1318
|
+
bounds = mesh.geometry.boundingBox;
|
|
1319
|
+
}
|
|
1320
|
+
if (bounds && !bounds.isEmpty() && bounds.distanceToPoint(lp) > dent.radius / s) return dent.radius;
|
|
1321
|
+
const from = mv.range ? mv.range[0] * 3 : 0;
|
|
1322
|
+
const to = mv.range ? mv.range[1] * 3 : source.length;
|
|
1323
|
+
const r2 = (dent.radius / s) ** 2;
|
|
1324
|
+
const distances = [];
|
|
1325
|
+
let inside = 0;
|
|
1326
|
+
for (let i = from; i < to; i += 3) {
|
|
1327
|
+
const dx = source[i] - lp.x;
|
|
1328
|
+
const dy = source[i + 1] - lp.y;
|
|
1329
|
+
const dz = source[i + 2] - lp.z;
|
|
1330
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
1331
|
+
if (d2 < r2) inside++;
|
|
1332
|
+
distances.push(d2);
|
|
1333
|
+
}
|
|
1334
|
+
if (inside >= MIN_DENT_VERTICES || distances.length < MIN_DENT_VERTICES) return dent.radius;
|
|
1335
|
+
distances.sort((x, y) => x - y);
|
|
1336
|
+
const nearest = Math.sqrt(distances[0]);
|
|
1337
|
+
return Math.max(dent.radius, Math.sqrt(distances[MIN_DENT_VERTICES - 1]) * s * WIDEN_MARGIN, nearest * s * NEAREST_MARGIN);
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* 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
|
|
1341
|
+
* and moves with the shell's surface instead (see `shellDisplacementAt`), so the shell never shows through it whatever the two meshes' vertex grids are.
|
|
1342
|
+
*/
|
|
1343
|
+
applyPartDent(meshes, worldPoint, worldNormal, dent, reachWorld) {
|
|
1344
|
+
const isSkin = (mv) => mv.mesh.userData.destructionSkin === true && !mv.host;
|
|
1345
|
+
const shells = meshes.filter((mv) => !isSkin(mv));
|
|
1346
|
+
const snapshots = [];
|
|
1347
|
+
for (const mv of shells) {
|
|
1348
|
+
const heavy = this.lowQuality && this.vertexCount(mv) > 2e4;
|
|
1349
|
+
const plain = !mv.host && !heavy && this.ownGeometry(mv) && mv.work;
|
|
1350
|
+
const before = plain ? new Float32Array(mv.work) : null;
|
|
1351
|
+
this.applyDent(mv, worldPoint, worldNormal, dent, reachWorld);
|
|
1352
|
+
if (before) snapshots.push({ mesh: mv.mesh, before, after: mv.work });
|
|
1353
|
+
}
|
|
1354
|
+
const displacement = this.t;
|
|
1355
|
+
const vertex = this.a;
|
|
1356
|
+
for (const mv of meshes) {
|
|
1357
|
+
if (!isSkin(mv)) continue;
|
|
1358
|
+
if (snapshots.length === 0) {
|
|
1359
|
+
this.applyDent(mv, worldPoint, worldNormal, dent, reachWorld);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (!this.ownGeometry(mv)) continue;
|
|
1363
|
+
const work = mv.work;
|
|
1364
|
+
const mesh = mv.mesh;
|
|
1365
|
+
mesh.updateWorldMatrix(true, false);
|
|
1366
|
+
const toLocal = this.m4.copy(mesh.matrixWorld).invert();
|
|
1367
|
+
const scale = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
1368
|
+
let touched = false;
|
|
1369
|
+
for (let i = 0; i < work.length; i += 3) {
|
|
1370
|
+
vertex.set(work[i], work[i + 1], work[i + 2]).applyMatrix4(mesh.matrixWorld);
|
|
1371
|
+
if (vertex.distanceTo(worldPoint) > reachWorld) continue;
|
|
1372
|
+
const height = shellDisplacementAt(this.THREE, vertex, snapshots, displacement);
|
|
1373
|
+
const length = displacement.length();
|
|
1374
|
+
if (length === 0) continue;
|
|
1375
|
+
const falloff = 1 - (vertex.distanceTo(worldPoint) / reachWorld) ** 2;
|
|
1376
|
+
const smooth = dent.depth * falloff * falloff;
|
|
1377
|
+
const pull = Math.max(0, Math.min(length, smooth) * SKIN_FOLLOW - SKIN_SLACK - height * SKIN_LAYER_SLACK);
|
|
1378
|
+
displacement.transformDirection(toLocal).multiplyScalar(pull / scale);
|
|
1379
|
+
work[i] += displacement.x;
|
|
1380
|
+
work[i + 1] += displacement.y;
|
|
1381
|
+
work[i + 2] += displacement.z;
|
|
1382
|
+
touched = true;
|
|
1383
|
+
}
|
|
1384
|
+
if (touched) this.writePositions(mv, positionAttr(mesh.geometry));
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1237
1387
|
/** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
|
|
1238
1388
|
writePositions(mv, pos) {
|
|
1239
1389
|
if (mv.host) {
|
|
@@ -1280,6 +1430,7 @@ var Visuals = class {
|
|
|
1280
1430
|
return { s, pos, quat };
|
|
1281
1431
|
}
|
|
1282
1432
|
addDecal(mv, decal) {
|
|
1433
|
+
decal.obj.userData.destructionDebris = true;
|
|
1283
1434
|
mv.mesh.add(decal.obj);
|
|
1284
1435
|
mv.decals.push(decal);
|
|
1285
1436
|
this.decalCount++;
|
|
@@ -1864,6 +2015,8 @@ var Renderer = class {
|
|
|
1864
2015
|
__publicField(this, "tmpQ");
|
|
1865
2016
|
__publicField(this, "v1");
|
|
1866
2017
|
__publicField(this, "v2");
|
|
2018
|
+
// 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.
|
|
2019
|
+
__publicField(this, "warmProbes");
|
|
1867
2020
|
this.world = o.world;
|
|
1868
2021
|
this.scene = o.scene;
|
|
1869
2022
|
this.quality = o.quality ?? "medium";
|
|
@@ -1961,6 +2114,24 @@ var Renderer = class {
|
|
|
1961
2114
|
if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes, hosts));
|
|
1962
2115
|
}
|
|
1963
2116
|
this.bounds.set(objectId, bound);
|
|
2117
|
+
for (const pv of bound.parts.values()) this.tagPart(pv, objectId);
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Label the meshes of a part so a host can go from a raycast hit to the world: `userData.destructionPart` (the part id) and
|
|
2121
|
+
* `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
|
|
2122
|
+
* 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`.
|
|
2123
|
+
*/
|
|
2124
|
+
tagPart(pv, objectId) {
|
|
2125
|
+
for (const mv of pv.meshes) {
|
|
2126
|
+
if (mv.merged && !mv.loose) continue;
|
|
2127
|
+
mv.mesh.traverse((c) => {
|
|
2128
|
+
if (!c.isMesh) return;
|
|
2129
|
+
const ud = c.userData;
|
|
2130
|
+
if (ud.destructionDebris) return;
|
|
2131
|
+
if (partHintOf(ud.destructionPart)?.id !== pv.id) ud.destructionPart = pv.id;
|
|
2132
|
+
ud.destructionObject = objectId;
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
1964
2135
|
}
|
|
1965
2136
|
makePartVis(home, id, material, meshes, hosts) {
|
|
1966
2137
|
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" };
|
|
@@ -1974,13 +2145,13 @@ var Renderer = class {
|
|
|
1974
2145
|
specParts = void 0;
|
|
1975
2146
|
}
|
|
1976
2147
|
if (!specParts) return;
|
|
1977
|
-
root
|
|
2148
|
+
const frame = specFrameInverse(this.THREE, root);
|
|
1978
2149
|
const e = new this.THREE.Euler();
|
|
1979
2150
|
const q = new this.THREE.Quaternion();
|
|
1980
2151
|
for (const m of meshes) {
|
|
1981
2152
|
this.box.setFromObject(m);
|
|
1982
2153
|
if (this.box.isEmpty()) continue;
|
|
1983
|
-
const c =
|
|
2154
|
+
const c = this.box.getCenter(this.v1).applyMatrix4(frame);
|
|
1984
2155
|
let best = null;
|
|
1985
2156
|
let bestVol = Infinity;
|
|
1986
2157
|
for (const p of specParts) {
|
|
@@ -2017,10 +2188,9 @@ var Renderer = class {
|
|
|
2017
2188
|
// ----------------------------------------------------------------------------------------------------------------------------------
|
|
2018
2189
|
// Pieces
|
|
2019
2190
|
// ----------------------------------------------------------------------------------------------------------------------------------
|
|
2020
|
-
/** Half extents and centre of the box around some meshes, in the
|
|
2191
|
+
/** Half extents and centre of the box around some meshes, in the metric frame of `root` (its pose removed, its scale kept). */
|
|
2021
2192
|
measure(root, meshes) {
|
|
2022
|
-
root
|
|
2023
|
-
const inv = root.matrixWorld.clone().invert();
|
|
2193
|
+
const inv = specFrameInverse(this.THREE, root);
|
|
2024
2194
|
const total = new this.THREE.Box3();
|
|
2025
2195
|
const m4 = new this.THREE.Matrix4();
|
|
2026
2196
|
const lb = new this.THREE.Box3();
|
|
@@ -2095,6 +2265,7 @@ var Renderer = class {
|
|
|
2095
2265
|
dt = clamp(dt, 0, 0.1);
|
|
2096
2266
|
this.time += dt;
|
|
2097
2267
|
this.drainQueue();
|
|
2268
|
+
for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
|
|
2098
2269
|
this.updateActive(dt);
|
|
2099
2270
|
this.updateLeaks(dt);
|
|
2100
2271
|
this.pieceSys.update(dt);
|
|
@@ -2113,6 +2284,27 @@ var Renderer = class {
|
|
|
2113
2284
|
}
|
|
2114
2285
|
}
|
|
2115
2286
|
}
|
|
2287
|
+
/**
|
|
2288
|
+
* 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
|
|
2289
|
+
* root keeps the pose the host gave and the world keeps getting it from `syncTransform`.
|
|
2290
|
+
*/
|
|
2291
|
+
onSettle(objectId) {
|
|
2292
|
+
const b = this.bounds.get(objectId);
|
|
2293
|
+
const soil = this.world.state(objectId)?.soil;
|
|
2294
|
+
if (!b || !soil) return;
|
|
2295
|
+
let node = b.soilNode;
|
|
2296
|
+
if (!node) {
|
|
2297
|
+
node = new this.THREE.Group();
|
|
2298
|
+
node.name = "destruction-soil";
|
|
2299
|
+
for (const c of [...b.root.children]) node.add(c);
|
|
2300
|
+
b.root.add(node);
|
|
2301
|
+
b.soilNode = node;
|
|
2302
|
+
}
|
|
2303
|
+
const { position: p, rotation: q } = soil.offset;
|
|
2304
|
+
node.position.set(p[0], p[1], p[2]);
|
|
2305
|
+
node.quaternion.set(q[0], q[1], q[2], q[3]);
|
|
2306
|
+
node.updateMatrixWorld(true);
|
|
2307
|
+
}
|
|
2116
2308
|
syncTransforms() {
|
|
2117
2309
|
for (const b of this.bounds.values()) this.syncOne(b);
|
|
2118
2310
|
}
|
|
@@ -2167,6 +2359,9 @@ var Renderer = class {
|
|
|
2167
2359
|
case "detach":
|
|
2168
2360
|
this.onDetach(ev);
|
|
2169
2361
|
break;
|
|
2362
|
+
case "settle":
|
|
2363
|
+
this.onSettle(ev.object);
|
|
2364
|
+
break;
|
|
2170
2365
|
case "collapse":
|
|
2171
2366
|
this.splitLoose(ev.object);
|
|
2172
2367
|
this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
|
|
@@ -2258,7 +2453,7 @@ var Renderer = class {
|
|
|
2258
2453
|
same.direction = ev.direction;
|
|
2259
2454
|
} else {
|
|
2260
2455
|
if (this.leaks.length >= 24) this.leaks.shift();
|
|
2261
|
-
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 });
|
|
2456
|
+
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 });
|
|
2262
2457
|
}
|
|
2263
2458
|
void key;
|
|
2264
2459
|
break;
|
|
@@ -2335,10 +2530,9 @@ var Renderer = class {
|
|
|
2335
2530
|
const def = this.world.materials.get(pv.material);
|
|
2336
2531
|
const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
|
|
2337
2532
|
const wn = this.worldDir(b, ev.dent.normal);
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
}
|
|
2533
|
+
const shown = pv.meshes.filter((mv) => this.visuals.shown(mv));
|
|
2534
|
+
const reach = shown.reduce((widest, mv) => Math.max(widest, this.visuals.dentReach(mv, wp, ev.dent)), ev.dent.radius);
|
|
2535
|
+
this.visuals.applyPartDent(shown, wp, wn, ev.dent, reach);
|
|
2342
2536
|
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);
|
|
2343
2537
|
this.sound(impactKey(def), ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1));
|
|
2344
2538
|
}
|
|
@@ -2534,6 +2728,7 @@ var Renderer = class {
|
|
|
2534
2728
|
}
|
|
2535
2729
|
}
|
|
2536
2730
|
this.bounds.set(newId, nb);
|
|
2731
|
+
for (const pv of nb.parts.values()) this.tagPart(pv, newId);
|
|
2537
2732
|
const meshes = this.meshesOf(nb);
|
|
2538
2733
|
if (meshes.length === 0) return;
|
|
2539
2734
|
const { half, center } = this.measure(group, meshes);
|
|
@@ -2561,24 +2756,40 @@ var Renderer = class {
|
|
|
2561
2756
|
const report = options.report;
|
|
2562
2757
|
const confinement = report?.find((r) => r.blast !== void 0)?.blast?.confinement ?? 1;
|
|
2563
2758
|
const energy = ev.energy * confinement;
|
|
2564
|
-
|
|
2759
|
+
const reachOf = (objectId) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? 1;
|
|
2760
|
+
const shove = (pc) => {
|
|
2565
2761
|
this.pieceSys.centerOf(pc, c);
|
|
2566
|
-
const
|
|
2567
|
-
|
|
2568
|
-
const dz = c.z - p[2];
|
|
2569
|
-
const d = Math.hypot(dx, dy, dz);
|
|
2570
|
-
if (d > ev.radius) continue;
|
|
2571
|
-
const fall = (1 - d / ev.radius) ** 2;
|
|
2572
|
-
const reach = report?.find((r) => r.object === pc.objectId)?.blast?.shelter ?? 1;
|
|
2573
|
-
const speed = Math.min(30, Math.sqrt(2 * energy * reach * 0.02 / pc.mass)) * fall;
|
|
2574
|
-
const inv = d > 1e-4 ? 1 / d : 0;
|
|
2575
|
-
const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
|
|
2762
|
+
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);
|
|
2763
|
+
if (!hit) return;
|
|
2576
2764
|
const spread = pc.halfExtents[0];
|
|
2577
|
-
|
|
2578
|
-
|
|
2765
|
+
const momentum = hit.speed * pc.mass;
|
|
2766
|
+
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]);
|
|
2767
|
+
};
|
|
2768
|
+
for (const pc of [...this.pieceSys.list]) shove(pc);
|
|
2769
|
+
if (this.o.shoveable) this.loosen(p, ev.radius, energy, reachOf);
|
|
2579
2770
|
this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(energy) * 0.08));
|
|
2580
2771
|
this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1));
|
|
2581
2772
|
}
|
|
2773
|
+
/** 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. */
|
|
2774
|
+
loosen(point, radius, energy, reachOf) {
|
|
2775
|
+
const shoveable = this.o.shoveable;
|
|
2776
|
+
if (!shoveable) return;
|
|
2777
|
+
for (const b of [...this.bounds.values()]) {
|
|
2778
|
+
if (b.piece || this.pieceSys.get(b.id) || !shoveable(b.id) || !this.world.has(b.id)) continue;
|
|
2779
|
+
const { half, center } = this.measure(b.root, this.meshesOf(b));
|
|
2780
|
+
const world = center.clone().applyQuaternion(b.root.quaternion).add(b.root.position);
|
|
2781
|
+
const mass = this.massOf(b.id);
|
|
2782
|
+
const hit = blastShove({ point, energy, radius, reach: reachOf(b.id) }, { position: [world.x, world.y, world.z], mass }, this.o.blastShove);
|
|
2783
|
+
if (!hit) continue;
|
|
2784
|
+
const piece = this.makePiece(b.id, { mass });
|
|
2785
|
+
const body = this.pieceSys.get(b.id);
|
|
2786
|
+
if (!body) continue;
|
|
2787
|
+
this.o.onLoosened?.(b.id, piece);
|
|
2788
|
+
const spread = half[0];
|
|
2789
|
+
const momentum = hit.speed * mass;
|
|
2790
|
+
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]);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2582
2793
|
playExplosion(ev, options) {
|
|
2583
2794
|
const named = options.effect ?? ev.effect;
|
|
2584
2795
|
const direction = options.direction;
|
|
@@ -2679,7 +2890,7 @@ var Renderer = class {
|
|
|
2679
2890
|
for (let i = this.leaks.length - 1; i >= 0; i--) {
|
|
2680
2891
|
const l = this.leaks[i];
|
|
2681
2892
|
const ps = this.world.part(l.object, l.part);
|
|
2682
|
-
if (!ps || ps.liters === void 0 || ps.liters <= 0) {
|
|
2893
|
+
if (!ps || ps.liters === void 0 || ps.liters <= 0 || l.opened && !ps.holes.some((h) => h.through)) {
|
|
2683
2894
|
this.leaks.splice(i, 1);
|
|
2684
2895
|
continue;
|
|
2685
2896
|
}
|
|
@@ -2730,6 +2941,7 @@ var Renderer = class {
|
|
|
2730
2941
|
for (const [pid, pv] of [...b.parts]) {
|
|
2731
2942
|
if (pv.home !== home) continue;
|
|
2732
2943
|
this.visuals.restorePart(pv);
|
|
2944
|
+
this.tagPart(pv, home);
|
|
2733
2945
|
if (b !== homeBound && homeBound) {
|
|
2734
2946
|
b.parts.delete(pid);
|
|
2735
2947
|
homeBound.parts.set(pid, pv);
|
|
@@ -2771,10 +2983,31 @@ var Renderer = class {
|
|
|
2771
2983
|
const { puffs, sparks, glows, chunks } = this.vfx.stats();
|
|
2772
2984
|
return puffs + sparks + glows + chunks;
|
|
2773
2985
|
}
|
|
2986
|
+
warmUpProbes() {
|
|
2987
|
+
this.releaseWarmProbes();
|
|
2988
|
+
const { kit } = this.visuals;
|
|
2989
|
+
const T = this.THREE;
|
|
2990
|
+
const geometry = new T.BufferGeometry();
|
|
2991
|
+
geometry.setAttribute("position", new T.BufferAttribute(new Float32Array(6), 3));
|
|
2992
|
+
const materials = [kit.discMaterial().clone(), kit.patchMaterial(8421504).clone(), kit.lineMaterial(0).clone()];
|
|
2993
|
+
const group = new T.Group();
|
|
2994
|
+
group.add(new T.Mesh(kit.discGeometry(), materials[0]), new T.Mesh(kit.discGeometry(), materials[1]), new T.LineSegments(geometry, materials[2]));
|
|
2995
|
+
for (const probe of group.children) probe.frustumCulled = false;
|
|
2996
|
+
this.warmProbes = { group, geometry, materials };
|
|
2997
|
+
return { group, dispose: () => {
|
|
2998
|
+
} };
|
|
2999
|
+
}
|
|
3000
|
+
releaseWarmProbes() {
|
|
3001
|
+
if (!this.warmProbes) return;
|
|
3002
|
+
this.warmProbes.geometry.dispose();
|
|
3003
|
+
for (const material of this.warmProbes.materials) material.dispose();
|
|
3004
|
+
this.warmProbes = void 0;
|
|
3005
|
+
}
|
|
2774
3006
|
rebarCount() {
|
|
2775
3007
|
return this.cages.count;
|
|
2776
3008
|
}
|
|
2777
3009
|
dispose() {
|
|
3010
|
+
this.releaseWarmProbes();
|
|
2778
3011
|
if (this.disposed) return;
|
|
2779
3012
|
this.unsubscribe();
|
|
2780
3013
|
this.clear();
|