@adatechnology/game-destruction-three 0.3.2 → 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 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 {
@@ -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?.();