@adatechnology/game-destruction-three 0.3.3 → 0.4.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 +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.global.js +173 -0
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +173 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -102,6 +102,9 @@ loose meshes. An invalid range (outside the geometry or empty) throws from `bind
|
|
|
102
102
|
| `ricochet` | sparks along the reflected direction | `ricochet` |
|
|
103
103
|
| `debris` | a burst of the requested kind (`sparks`, `dust`, `shards`, `splinters`, `chips`, `fibers`, `leaves`, `drips`, `ash`, `smoke`, `steam`) | |
|
|
104
104
|
| `fracture` | part hidden; fragments (boxes, thin irregular prisms for shards) with gravity, ground bounce, settling, sleep and shrink-away; debris burst by style | `shatter_glass`, `crumble`, `splinter`, `impact_*` |
|
|
105
|
+
| `spall` | opaque patch (disc decal) in the colour of the part's core material where a coat came off, under the holes; sits on the face of the mesh the point is nearest to (the thin axis of its world box) | |
|
|
106
|
+
| `stage` (2, with `fragments`) | the unit-sized fragments of the breach fly out with the same physics as `fracture` fragments, the holes of `Fragment.marks` ride on them as dark discs, a dust burst; the part stays drawn (cutting the opening in its faces is left to the house renderer) | `crumble`, `impact_*` |
|
|
107
|
+
| `expose` | the bars as instanced cylinders (radius 4 mm), one mesh per part, replaced by each new `expose`; empty `bars` removes the cage; `renderer.rebarCount()` | |
|
|
105
108
|
| `detach` | with `autoSplit`, each loose group becomes its own `Group` (world transforms kept) and a `Piece` (`onPiece`) | `detach` |
|
|
106
109
|
| `disassemble_progress` | | `unscrew` (throttled) |
|
|
107
110
|
| `ignite` / `extinguish` / `burnout` | fire and smoke from the surface while `part.burning`; steam when put out; ash at burnout; char lerps the colour to the material `charColor`; status `ashes` hides the part and leaves an ash pile | `ignite`, `burn_loop`, `extinguish` |
|
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,8 @@ interface DestructionRenderer {
|
|
|
114
114
|
pieces: number;
|
|
115
115
|
decals: number;
|
|
116
116
|
};
|
|
117
|
+
/** Bars of exposed rebar drawn now (`expose` events; one instanced mesh per part). */
|
|
118
|
+
rebarCount(): number;
|
|
117
119
|
dispose(): void;
|
|
118
120
|
}
|
|
119
121
|
interface SpecFromObjectOptions {
|
package/dist/index.global.js
CHANGED
|
@@ -83,6 +83,8 @@ var GameDestructionThree = (() => {
|
|
|
83
83
|
__publicField(this, "v");
|
|
84
84
|
__publicField(this, "w");
|
|
85
85
|
__publicField(this, "rng", createRng(1374496523));
|
|
86
|
+
__publicField(this, "markGeometry", null);
|
|
87
|
+
__publicField(this, "markMaterial", null);
|
|
86
88
|
this.box = new THREE.BoxGeometry(1, 1, 1);
|
|
87
89
|
this.q = new THREE.Quaternion();
|
|
88
90
|
this.dq = new THREE.Quaternion();
|
|
@@ -129,6 +131,25 @@ var GameDestructionThree = (() => {
|
|
|
129
131
|
}
|
|
130
132
|
return g;
|
|
131
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Bullet holes that stay on a fragment (`Fragment.marks`): dark discs that are children of its mesh, so they fall, spin and settle with it. The mesh is
|
|
136
|
+
* a unit box scaled to the fragment, so each disc is scaled back; exact for a mark on a face, which is where a bullet hole is.
|
|
137
|
+
*/
|
|
138
|
+
addMarks(mesh, f) {
|
|
139
|
+
if (!f.marks?.length) return;
|
|
140
|
+
this.markGeometry ?? (this.markGeometry = new this.THREE.CircleGeometry(1, 10));
|
|
141
|
+
this.markMaterial ?? (this.markMaterial = new this.THREE.MeshBasicMaterial({ color: 723465, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
|
|
142
|
+
const sc = mesh.scale;
|
|
143
|
+
for (const mark of f.marks) {
|
|
144
|
+
const disc = new this.THREE.Mesh(this.markGeometry, this.markMaterial);
|
|
145
|
+
const radius = Math.max(4e-3, mark.diameter) * 0.6;
|
|
146
|
+
const n = this.v.set(mark.normal[0], mark.normal[1], mark.normal[2]).normalize();
|
|
147
|
+
disc.position.set(mark.point[0] / sc.x + n.x * 2e-3 / sc.x, mark.point[1] / sc.y + n.y * 2e-3 / sc.y, mark.point[2] / sc.z + n.z * 2e-3 / sc.z);
|
|
148
|
+
disc.quaternion.setFromUnitVectors(this.w.set(0, 0, 1), n);
|
|
149
|
+
disc.scale.set(radius / sc.x, radius / sc.y, radius / sc.z);
|
|
150
|
+
mesh.add(disc);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
132
153
|
/** Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget. */
|
|
133
154
|
spawn(f, material, home, maxAllowed) {
|
|
134
155
|
const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
|
|
@@ -162,6 +183,8 @@ var GameDestructionThree = (() => {
|
|
|
162
183
|
alive--;
|
|
163
184
|
}
|
|
164
185
|
}
|
|
186
|
+
this.addMarks(mesh, f);
|
|
187
|
+
return mesh;
|
|
165
188
|
}
|
|
166
189
|
/** Add velocity to every fragment within `radius` of a point (blasts). */
|
|
167
190
|
blast(point, radius, speed) {
|
|
@@ -285,6 +308,8 @@ var GameDestructionThree = (() => {
|
|
|
285
308
|
dispose() {
|
|
286
309
|
this.clear();
|
|
287
310
|
this.box.dispose();
|
|
311
|
+
this.markGeometry?.dispose();
|
|
312
|
+
this.markMaterial?.dispose();
|
|
288
313
|
for (const g of this.shardGeos) g?.dispose();
|
|
289
314
|
this.shardGeos = [];
|
|
290
315
|
for (const m of this.materials.values()) m.dispose();
|
|
@@ -1036,6 +1061,78 @@ void main() {
|
|
|
1036
1061
|
}
|
|
1037
1062
|
};
|
|
1038
1063
|
|
|
1064
|
+
// src/rebar.ts
|
|
1065
|
+
var RebarCages = class {
|
|
1066
|
+
constructor(THREE, scene, radius = 4e-3) {
|
|
1067
|
+
__publicField(this, "THREE", THREE);
|
|
1068
|
+
__publicField(this, "scene", scene);
|
|
1069
|
+
__publicField(this, "cages", /* @__PURE__ */ new Map());
|
|
1070
|
+
__publicField(this, "geometry");
|
|
1071
|
+
__publicField(this, "material");
|
|
1072
|
+
__publicField(this, "up");
|
|
1073
|
+
__publicField(this, "dir");
|
|
1074
|
+
__publicField(this, "mid");
|
|
1075
|
+
__publicField(this, "q");
|
|
1076
|
+
__publicField(this, "scale");
|
|
1077
|
+
__publicField(this, "m");
|
|
1078
|
+
this.geometry = new THREE.CylinderGeometry(radius, radius, 1, 6);
|
|
1079
|
+
this.material = new THREE.MeshStandardMaterial({ color: 7628380, roughness: 0.55, metalness: 0.6 });
|
|
1080
|
+
this.up = new THREE.Vector3(0, 1, 0);
|
|
1081
|
+
this.dir = new THREE.Vector3();
|
|
1082
|
+
this.mid = new THREE.Vector3();
|
|
1083
|
+
this.q = new THREE.Quaternion();
|
|
1084
|
+
this.scale = new THREE.Vector3();
|
|
1085
|
+
this.m = new THREE.Matrix4();
|
|
1086
|
+
}
|
|
1087
|
+
/** Bars drawn now, over every part. */
|
|
1088
|
+
get count() {
|
|
1089
|
+
let n = 0;
|
|
1090
|
+
for (const mesh of this.cages.values()) n += mesh.count;
|
|
1091
|
+
return n;
|
|
1092
|
+
}
|
|
1093
|
+
/** Meshes of the live cages (tests and picking). */
|
|
1094
|
+
meshes() {
|
|
1095
|
+
return [...this.cages.values()];
|
|
1096
|
+
}
|
|
1097
|
+
/** Draw `bars` (world space) as the cage of `key`; an empty list removes it. */
|
|
1098
|
+
set(key, bars) {
|
|
1099
|
+
this.remove(key);
|
|
1100
|
+
if (bars.length === 0) return;
|
|
1101
|
+
const mesh = new this.THREE.InstancedMesh(this.geometry, this.material, bars.length);
|
|
1102
|
+
bars.forEach(([a, b], i) => {
|
|
1103
|
+
this.dir.set(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
|
|
1104
|
+
const length = this.dir.length();
|
|
1105
|
+
this.mid.set((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2);
|
|
1106
|
+
this.q.setFromUnitVectors(this.up, length > 1e-9 ? this.dir.divideScalar(length) : this.up);
|
|
1107
|
+
this.m.compose(this.mid, this.q, this.scale.set(1, Math.max(length, 1e-4), 1));
|
|
1108
|
+
mesh.setMatrixAt(i, this.m);
|
|
1109
|
+
});
|
|
1110
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
1111
|
+
mesh.castShadow = true;
|
|
1112
|
+
this.scene.add(mesh);
|
|
1113
|
+
this.cages.set(key, mesh);
|
|
1114
|
+
}
|
|
1115
|
+
remove(key) {
|
|
1116
|
+
const mesh = this.cages.get(key);
|
|
1117
|
+
if (!mesh) return;
|
|
1118
|
+
this.scene.remove(mesh);
|
|
1119
|
+
mesh.dispose();
|
|
1120
|
+
this.cages.delete(key);
|
|
1121
|
+
}
|
|
1122
|
+
/** Remove the cages of every part of `object` (keys are `object|part`). */
|
|
1123
|
+
removeObject(object) {
|
|
1124
|
+
for (const key of [...this.cages.keys()]) if (key.startsWith(`${object}|`)) this.remove(key);
|
|
1125
|
+
}
|
|
1126
|
+
clear() {
|
|
1127
|
+
for (const key of [...this.cages.keys()]) this.remove(key);
|
|
1128
|
+
}
|
|
1129
|
+
dispose() {
|
|
1130
|
+
this.clear();
|
|
1131
|
+
this.geometry.dispose();
|
|
1132
|
+
this.material.dispose();
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1039
1136
|
// ../game-destruction/dist/index.js
|
|
1040
1137
|
var EPS = 1e-9;
|
|
1041
1138
|
var IDENTITY = [0, 0, 0, 1];
|
|
@@ -2288,6 +2385,7 @@ void main() {
|
|
|
2288
2385
|
__publicField(this, "disc", null);
|
|
2289
2386
|
__publicField(this, "discMat", null);
|
|
2290
2387
|
__publicField(this, "lineMats", /* @__PURE__ */ new Map());
|
|
2388
|
+
__publicField(this, "patchMats", /* @__PURE__ */ new Map());
|
|
2291
2389
|
}
|
|
2292
2390
|
discGeometry() {
|
|
2293
2391
|
return this.disc ?? (this.disc = new this.THREE.CircleGeometry(1, 14));
|
|
@@ -2303,6 +2401,15 @@ void main() {
|
|
|
2303
2401
|
polygonOffsetUnits: -4
|
|
2304
2402
|
}));
|
|
2305
2403
|
}
|
|
2404
|
+
/** Opaque patch in the colour of what shows under a spalled coat. */
|
|
2405
|
+
patchMaterial(color) {
|
|
2406
|
+
let m = this.patchMats.get(color);
|
|
2407
|
+
if (!m) {
|
|
2408
|
+
m = new this.THREE.MeshBasicMaterial({ color, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
|
|
2409
|
+
this.patchMats.set(color, m);
|
|
2410
|
+
}
|
|
2411
|
+
return m;
|
|
2412
|
+
}
|
|
2306
2413
|
lineMaterial(color) {
|
|
2307
2414
|
let m = this.lineMats.get(color);
|
|
2308
2415
|
if (!m) {
|
|
@@ -2315,6 +2422,8 @@ void main() {
|
|
|
2315
2422
|
this.disc?.dispose();
|
|
2316
2423
|
this.discMat?.dispose();
|
|
2317
2424
|
for (const m of this.lineMats.values()) m.dispose();
|
|
2425
|
+
for (const m of this.patchMats.values()) m.dispose();
|
|
2426
|
+
this.patchMats.clear();
|
|
2318
2427
|
this.disc = null;
|
|
2319
2428
|
this.discMat = null;
|
|
2320
2429
|
this.lineMats.clear();
|
|
@@ -2606,6 +2715,16 @@ void main() {
|
|
|
2606
2715
|
disc.renderOrder = 2;
|
|
2607
2716
|
this.addDecal(mv, { obj: disc });
|
|
2608
2717
|
}
|
|
2718
|
+
/** A patch where a surface coat came off, in the colour of the core under it; drawn under the bullet holes. */
|
|
2719
|
+
addSpall(mv, worldPoint, worldNormal, radius, color) {
|
|
2720
|
+
const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 15e-4);
|
|
2721
|
+
const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.patchMaterial(color));
|
|
2722
|
+
disc.position.copy(pos);
|
|
2723
|
+
disc.quaternion.copy(quat2);
|
|
2724
|
+
disc.scale.setScalar(Math.max(0.01, radius) / s);
|
|
2725
|
+
disc.renderOrder = 1;
|
|
2726
|
+
this.addDecal(mv, { obj: disc });
|
|
2727
|
+
}
|
|
2609
2728
|
/** A star of jagged lines: `rays` rays up to `length` metres, seeded so the same event always draws the same star. */
|
|
2610
2729
|
addCrack(mv, worldPoint, worldNormal, rays, length, color, seed) {
|
|
2611
2730
|
const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 2e-3);
|
|
@@ -3131,6 +3250,7 @@ void main() {
|
|
|
3131
3250
|
__publicField(this, "fragments");
|
|
3132
3251
|
__publicField(this, "ground");
|
|
3133
3252
|
__publicField(this, "pieceSys");
|
|
3253
|
+
__publicField(this, "cages");
|
|
3134
3254
|
__publicField(this, "active", /* @__PURE__ */ new Map());
|
|
3135
3255
|
__publicField(this, "fires", /* @__PURE__ */ new Map());
|
|
3136
3256
|
__publicField(this, "leaks", []);
|
|
@@ -3182,6 +3302,7 @@ void main() {
|
|
|
3182
3302
|
max: this.maxFragments,
|
|
3183
3303
|
life: o.fragmentLife ?? 12
|
|
3184
3304
|
});
|
|
3305
|
+
this.cages = new RebarCages(THREE, o.scene);
|
|
3185
3306
|
this.pieceSys = new PieceSystem(THREE, {
|
|
3186
3307
|
groundAt: this.groundAt,
|
|
3187
3308
|
gravity: this.gravity,
|
|
@@ -3483,6 +3604,15 @@ void main() {
|
|
|
3483
3604
|
this.sound("ignite", ev.point, 0.7);
|
|
3484
3605
|
}
|
|
3485
3606
|
break;
|
|
3607
|
+
case "spall":
|
|
3608
|
+
this.onSpall(ev);
|
|
3609
|
+
break;
|
|
3610
|
+
case "stage":
|
|
3611
|
+
this.onStage(ev);
|
|
3612
|
+
break;
|
|
3613
|
+
case "expose":
|
|
3614
|
+
this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
|
|
3615
|
+
break;
|
|
3486
3616
|
case "extinguish": {
|
|
3487
3617
|
const f = this.fires.get(`${ev.object}|${ev.part}`);
|
|
3488
3618
|
if (f) {
|
|
@@ -3648,6 +3778,43 @@ void main() {
|
|
|
3648
3778
|
}
|
|
3649
3779
|
this.sound(impactKey(def), ev.worldPoint, 0.5);
|
|
3650
3780
|
}
|
|
3781
|
+
/** Outward normal of the face of `mv` nearest to a world point: the thin axis of its world box (right for a wall; a sloped face is left to the vertex-range binding). */
|
|
3782
|
+
faceNormal(mv, point) {
|
|
3783
|
+
const box = this.visuals.worldBox(mv, this.box);
|
|
3784
|
+
const size = box.getSize(this.v1);
|
|
3785
|
+
const axis = size.x <= size.y && size.x <= size.z ? "x" : size.y <= size.z ? "y" : "z";
|
|
3786
|
+
const side = point[axis] >= (box.min[axis] + box.max[axis]) / 2 ? 1 : -1;
|
|
3787
|
+
return new this.THREE.Vector3(axis === "x" ? side : 0, axis === "y" ? side : 0, axis === "z" ? side : 0);
|
|
3788
|
+
}
|
|
3789
|
+
/** A coat came off: a patch in the colour of the core under it, below the bullet holes and cracks. */
|
|
3790
|
+
onSpall(ev) {
|
|
3791
|
+
const pv = this.partVis(ev.object, ev.part);
|
|
3792
|
+
if (!pv) return;
|
|
3793
|
+
const wp = new this.THREE.Vector3(ev.point[0], ev.point[1], ev.point[2]);
|
|
3794
|
+
const mv = this.nearestMesh(pv, wp);
|
|
3795
|
+
if (!mv) return;
|
|
3796
|
+
const def = this.world.materials.get(pv.material);
|
|
3797
|
+
this.visuals.addSpall(mv, wp, this.faceNormal(mv, wp), Math.sqrt(ev.area / Math.PI), fallbackColor(pv.material, def));
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* Stage 2 of a staged part: the unit-sized fragments come out and the part stays drawn (hiding it is what `fracture` does). The opening itself
|
|
3801
|
+
* (cutting the wall's faces) is not drawn yet: it needs the per-vertex binding of the house renderer.
|
|
3802
|
+
*/
|
|
3803
|
+
onStage(ev) {
|
|
3804
|
+
if (!ev.fragments || !ev.opening) return;
|
|
3805
|
+
const pv = this.partVis(ev.object, ev.part);
|
|
3806
|
+
const def = pv ? this.world.materials.get(pv.material) : void 0;
|
|
3807
|
+
const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
|
|
3808
|
+
const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
|
|
3809
|
+
const step = this.low ? 2 : 1;
|
|
3810
|
+
for (let i = 0; i < ev.fragments.length; i += step) {
|
|
3811
|
+
const fr = ev.fragments[i];
|
|
3812
|
+
const fdef = this.world.materials.get(fr.material);
|
|
3813
|
+
this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), false), ev.object, maxAlive);
|
|
3814
|
+
}
|
|
3815
|
+
this.particles.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
|
|
3816
|
+
this.sound(impactKey(def), ev.opening.center, 0.8);
|
|
3817
|
+
}
|
|
3651
3818
|
onFracture(ev) {
|
|
3652
3819
|
const b = this.bounds.get(ev.object);
|
|
3653
3820
|
const pv = b?.parts.get(ev.part);
|
|
@@ -3958,6 +4125,7 @@ void main() {
|
|
|
3958
4125
|
for (const home of homes) {
|
|
3959
4126
|
this.fragments.removeHome(home);
|
|
3960
4127
|
this.ground.removeHome(home);
|
|
4128
|
+
this.cages.removeObject(home);
|
|
3961
4129
|
}
|
|
3962
4130
|
}
|
|
3963
4131
|
restoreHome(home) {
|
|
@@ -3994,6 +4162,7 @@ void main() {
|
|
|
3994
4162
|
}
|
|
3995
4163
|
this.fragments.clear();
|
|
3996
4164
|
this.ground.clear();
|
|
4165
|
+
this.cages.clear();
|
|
3997
4166
|
this.particles.clear();
|
|
3998
4167
|
if (this.light) {
|
|
3999
4168
|
this.light.intensity = 0;
|
|
@@ -4004,6 +4173,9 @@ void main() {
|
|
|
4004
4173
|
stats() {
|
|
4005
4174
|
return { fragments: this.fragments.count, particles: this.particles.live, pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
|
|
4006
4175
|
}
|
|
4176
|
+
rebarCount() {
|
|
4177
|
+
return this.cages.count;
|
|
4178
|
+
}
|
|
4007
4179
|
dispose() {
|
|
4008
4180
|
if (this.disposed) return;
|
|
4009
4181
|
this.unsubscribe();
|
|
@@ -4011,6 +4183,7 @@ void main() {
|
|
|
4011
4183
|
for (const id of [...this.bounds.keys()]) this.unbind(id);
|
|
4012
4184
|
this.fragments.dispose();
|
|
4013
4185
|
this.ground.dispose();
|
|
4186
|
+
this.cages.dispose();
|
|
4014
4187
|
this.particles.dispose();
|
|
4015
4188
|
this.visuals.dispose();
|
|
4016
4189
|
this.light?.dispose?.();
|