@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/dist/index.js CHANGED
@@ -57,6 +57,8 @@ var FragmentSystem = class {
57
57
  __publicField(this, "v");
58
58
  __publicField(this, "w");
59
59
  __publicField(this, "rng", createRng(1374496523));
60
+ __publicField(this, "markGeometry", null);
61
+ __publicField(this, "markMaterial", null);
60
62
  this.box = new THREE.BoxGeometry(1, 1, 1);
61
63
  this.q = new THREE.Quaternion();
62
64
  this.dq = new THREE.Quaternion();
@@ -103,6 +105,25 @@ var FragmentSystem = class {
103
105
  }
104
106
  return g;
105
107
  }
108
+ /**
109
+ * 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
110
+ * 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.
111
+ */
112
+ addMarks(mesh, f) {
113
+ if (!f.marks?.length) return;
114
+ this.markGeometry ?? (this.markGeometry = new this.THREE.CircleGeometry(1, 10));
115
+ this.markMaterial ?? (this.markMaterial = new this.THREE.MeshBasicMaterial({ color: 723465, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
116
+ const sc = mesh.scale;
117
+ for (const mark of f.marks) {
118
+ const disc = new this.THREE.Mesh(this.markGeometry, this.markMaterial);
119
+ const radius = Math.max(4e-3, mark.diameter) * 0.6;
120
+ const n = this.v.set(mark.normal[0], mark.normal[1], mark.normal[2]).normalize();
121
+ 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);
122
+ disc.quaternion.setFromUnitVectors(this.w.set(0, 0, 1), n);
123
+ disc.scale.set(radius / sc.x, radius / sc.y, radius / sc.z);
124
+ mesh.add(disc);
125
+ }
126
+ }
106
127
  /** Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget. */
107
128
  spawn(f, material, home, maxAllowed) {
108
129
  const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
@@ -136,6 +157,8 @@ var FragmentSystem = class {
136
157
  alive--;
137
158
  }
138
159
  }
160
+ this.addMarks(mesh, f);
161
+ return mesh;
139
162
  }
140
163
  /** Add velocity to every fragment within `radius` of a point (blasts). */
141
164
  blast(point, radius, speed) {
@@ -259,6 +282,8 @@ var FragmentSystem = class {
259
282
  dispose() {
260
283
  this.clear();
261
284
  this.box.dispose();
285
+ this.markGeometry?.dispose();
286
+ this.markMaterial?.dispose();
262
287
  for (const g of this.shardGeos) g?.dispose();
263
288
  this.shardGeos = [];
264
289
  for (const m of this.materials.values()) m.dispose();
@@ -1010,6 +1035,78 @@ var PieceSystem = class {
1010
1035
  }
1011
1036
  };
1012
1037
 
1038
+ // src/rebar.ts
1039
+ var RebarCages = class {
1040
+ constructor(THREE, scene, radius = 4e-3) {
1041
+ __publicField(this, "THREE", THREE);
1042
+ __publicField(this, "scene", scene);
1043
+ __publicField(this, "cages", /* @__PURE__ */ new Map());
1044
+ __publicField(this, "geometry");
1045
+ __publicField(this, "material");
1046
+ __publicField(this, "up");
1047
+ __publicField(this, "dir");
1048
+ __publicField(this, "mid");
1049
+ __publicField(this, "q");
1050
+ __publicField(this, "scale");
1051
+ __publicField(this, "m");
1052
+ this.geometry = new THREE.CylinderGeometry(radius, radius, 1, 6);
1053
+ this.material = new THREE.MeshStandardMaterial({ color: 7628380, roughness: 0.55, metalness: 0.6 });
1054
+ this.up = new THREE.Vector3(0, 1, 0);
1055
+ this.dir = new THREE.Vector3();
1056
+ this.mid = new THREE.Vector3();
1057
+ this.q = new THREE.Quaternion();
1058
+ this.scale = new THREE.Vector3();
1059
+ this.m = new THREE.Matrix4();
1060
+ }
1061
+ /** Bars drawn now, over every part. */
1062
+ get count() {
1063
+ let n = 0;
1064
+ for (const mesh of this.cages.values()) n += mesh.count;
1065
+ return n;
1066
+ }
1067
+ /** Meshes of the live cages (tests and picking). */
1068
+ meshes() {
1069
+ return [...this.cages.values()];
1070
+ }
1071
+ /** Draw `bars` (world space) as the cage of `key`; an empty list removes it. */
1072
+ set(key, bars) {
1073
+ this.remove(key);
1074
+ if (bars.length === 0) return;
1075
+ const mesh = new this.THREE.InstancedMesh(this.geometry, this.material, bars.length);
1076
+ bars.forEach(([a, b], i) => {
1077
+ this.dir.set(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
1078
+ const length = this.dir.length();
1079
+ this.mid.set((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2);
1080
+ this.q.setFromUnitVectors(this.up, length > 1e-9 ? this.dir.divideScalar(length) : this.up);
1081
+ this.m.compose(this.mid, this.q, this.scale.set(1, Math.max(length, 1e-4), 1));
1082
+ mesh.setMatrixAt(i, this.m);
1083
+ });
1084
+ mesh.instanceMatrix.needsUpdate = true;
1085
+ mesh.castShadow = true;
1086
+ this.scene.add(mesh);
1087
+ this.cages.set(key, mesh);
1088
+ }
1089
+ remove(key) {
1090
+ const mesh = this.cages.get(key);
1091
+ if (!mesh) return;
1092
+ this.scene.remove(mesh);
1093
+ mesh.dispose();
1094
+ this.cages.delete(key);
1095
+ }
1096
+ /** Remove the cages of every part of `object` (keys are `object|part`). */
1097
+ removeObject(object) {
1098
+ for (const key of [...this.cages.keys()]) if (key.startsWith(`${object}|`)) this.remove(key);
1099
+ }
1100
+ clear() {
1101
+ for (const key of [...this.cages.keys()]) this.remove(key);
1102
+ }
1103
+ dispose() {
1104
+ this.clear();
1105
+ this.geometry.dispose();
1106
+ this.material.dispose();
1107
+ }
1108
+ };
1109
+
1013
1110
  // src/spec.ts
1014
1111
  import { guessMaterial, inferJoints } from "@adatechnology/game-destruction";
1015
1112
  function partHintOf(value) {
@@ -1214,6 +1311,7 @@ var DecalKit = class {
1214
1311
  __publicField(this, "disc", null);
1215
1312
  __publicField(this, "discMat", null);
1216
1313
  __publicField(this, "lineMats", /* @__PURE__ */ new Map());
1314
+ __publicField(this, "patchMats", /* @__PURE__ */ new Map());
1217
1315
  }
1218
1316
  discGeometry() {
1219
1317
  return this.disc ?? (this.disc = new this.THREE.CircleGeometry(1, 14));
@@ -1229,6 +1327,15 @@ var DecalKit = class {
1229
1327
  polygonOffsetUnits: -4
1230
1328
  }));
1231
1329
  }
1330
+ /** Opaque patch in the colour of what shows under a spalled coat. */
1331
+ patchMaterial(color) {
1332
+ let m = this.patchMats.get(color);
1333
+ if (!m) {
1334
+ m = new this.THREE.MeshBasicMaterial({ color, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
1335
+ this.patchMats.set(color, m);
1336
+ }
1337
+ return m;
1338
+ }
1232
1339
  lineMaterial(color) {
1233
1340
  let m = this.lineMats.get(color);
1234
1341
  if (!m) {
@@ -1241,6 +1348,8 @@ var DecalKit = class {
1241
1348
  this.disc?.dispose();
1242
1349
  this.discMat?.dispose();
1243
1350
  for (const m of this.lineMats.values()) m.dispose();
1351
+ for (const m of this.patchMats.values()) m.dispose();
1352
+ this.patchMats.clear();
1244
1353
  this.disc = null;
1245
1354
  this.discMat = null;
1246
1355
  this.lineMats.clear();
@@ -1532,6 +1641,16 @@ var Visuals = class {
1532
1641
  disc.renderOrder = 2;
1533
1642
  this.addDecal(mv, { obj: disc });
1534
1643
  }
1644
+ /** A patch where a surface coat came off, in the colour of the core under it; drawn under the bullet holes. */
1645
+ addSpall(mv, worldPoint, worldNormal, radius, color) {
1646
+ const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 15e-4);
1647
+ const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.patchMaterial(color));
1648
+ disc.position.copy(pos);
1649
+ disc.quaternion.copy(quat);
1650
+ disc.scale.setScalar(Math.max(0.01, radius) / s);
1651
+ disc.renderOrder = 1;
1652
+ this.addDecal(mv, { obj: disc });
1653
+ }
1535
1654
  /** A star of jagged lines: `rays` rays up to `length` metres, seeded so the same event always draws the same star. */
1536
1655
  addCrack(mv, worldPoint, worldNormal, rays, length, color, seed) {
1537
1656
  const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 2e-3);
@@ -2057,6 +2176,7 @@ var Renderer = class {
2057
2176
  __publicField(this, "fragments");
2058
2177
  __publicField(this, "ground");
2059
2178
  __publicField(this, "pieceSys");
2179
+ __publicField(this, "cages");
2060
2180
  __publicField(this, "active", /* @__PURE__ */ new Map());
2061
2181
  __publicField(this, "fires", /* @__PURE__ */ new Map());
2062
2182
  __publicField(this, "leaks", []);
@@ -2108,6 +2228,7 @@ var Renderer = class {
2108
2228
  max: this.maxFragments,
2109
2229
  life: o.fragmentLife ?? 12
2110
2230
  });
2231
+ this.cages = new RebarCages(THREE, o.scene);
2111
2232
  this.pieceSys = new PieceSystem(THREE, {
2112
2233
  groundAt: this.groundAt,
2113
2234
  gravity: this.gravity,
@@ -2409,6 +2530,15 @@ var Renderer = class {
2409
2530
  this.sound("ignite", ev.point, 0.7);
2410
2531
  }
2411
2532
  break;
2533
+ case "spall":
2534
+ this.onSpall(ev);
2535
+ break;
2536
+ case "stage":
2537
+ this.onStage(ev);
2538
+ break;
2539
+ case "expose":
2540
+ this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
2541
+ break;
2412
2542
  case "extinguish": {
2413
2543
  const f = this.fires.get(`${ev.object}|${ev.part}`);
2414
2544
  if (f) {
@@ -2574,6 +2704,43 @@ var Renderer = class {
2574
2704
  }
2575
2705
  this.sound(impactKey(def), ev.worldPoint, 0.5);
2576
2706
  }
2707
+ /** 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). */
2708
+ faceNormal(mv, point) {
2709
+ const box = this.visuals.worldBox(mv, this.box);
2710
+ const size = box.getSize(this.v1);
2711
+ const axis = size.x <= size.y && size.x <= size.z ? "x" : size.y <= size.z ? "y" : "z";
2712
+ const side = point[axis] >= (box.min[axis] + box.max[axis]) / 2 ? 1 : -1;
2713
+ return new this.THREE.Vector3(axis === "x" ? side : 0, axis === "y" ? side : 0, axis === "z" ? side : 0);
2714
+ }
2715
+ /** A coat came off: a patch in the colour of the core under it, below the bullet holes and cracks. */
2716
+ onSpall(ev) {
2717
+ const pv = this.partVis(ev.object, ev.part);
2718
+ if (!pv) return;
2719
+ const wp = new this.THREE.Vector3(ev.point[0], ev.point[1], ev.point[2]);
2720
+ const mv = this.nearestMesh(pv, wp);
2721
+ if (!mv) return;
2722
+ const def = this.world.materials.get(pv.material);
2723
+ this.visuals.addSpall(mv, wp, this.faceNormal(mv, wp), Math.sqrt(ev.area / Math.PI), fallbackColor(pv.material, def));
2724
+ }
2725
+ /**
2726
+ * 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
2727
+ * (cutting the wall's faces) is not drawn yet: it needs the per-vertex binding of the house renderer.
2728
+ */
2729
+ onStage(ev) {
2730
+ if (!ev.fragments || !ev.opening) return;
2731
+ const pv = this.partVis(ev.object, ev.part);
2732
+ const def = pv ? this.world.materials.get(pv.material) : void 0;
2733
+ const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
2734
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
2735
+ const step = this.low ? 2 : 1;
2736
+ for (let i = 0; i < ev.fragments.length; i += step) {
2737
+ const fr = ev.fragments[i];
2738
+ const fdef = this.world.materials.get(fr.material);
2739
+ this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), false), ev.object, maxAlive);
2740
+ }
2741
+ this.particles.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
2742
+ this.sound(impactKey(def), ev.opening.center, 0.8);
2743
+ }
2577
2744
  onFracture(ev) {
2578
2745
  const b = this.bounds.get(ev.object);
2579
2746
  const pv = b?.parts.get(ev.part);
@@ -2884,6 +3051,7 @@ var Renderer = class {
2884
3051
  for (const home of homes) {
2885
3052
  this.fragments.removeHome(home);
2886
3053
  this.ground.removeHome(home);
3054
+ this.cages.removeObject(home);
2887
3055
  }
2888
3056
  }
2889
3057
  restoreHome(home) {
@@ -2920,6 +3088,7 @@ var Renderer = class {
2920
3088
  }
2921
3089
  this.fragments.clear();
2922
3090
  this.ground.clear();
3091
+ this.cages.clear();
2923
3092
  this.particles.clear();
2924
3093
  if (this.light) {
2925
3094
  this.light.intensity = 0;
@@ -2930,6 +3099,9 @@ var Renderer = class {
2930
3099
  stats() {
2931
3100
  return { fragments: this.fragments.count, particles: this.particles.live, pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
2932
3101
  }
3102
+ rebarCount() {
3103
+ return this.cages.count;
3104
+ }
2933
3105
  dispose() {
2934
3106
  if (this.disposed) return;
2935
3107
  this.unsubscribe();
@@ -2937,6 +3109,7 @@ var Renderer = class {
2937
3109
  for (const id of [...this.bounds.keys()]) this.unbind(id);
2938
3110
  this.fragments.dispose();
2939
3111
  this.ground.dispose();
3112
+ this.cages.dispose();
2940
3113
  this.particles.dispose();
2941
3114
  this.visuals.dispose();
2942
3115
  this.light?.dispose?.();