@adatechnology/game-destruction-three 0.12.1 → 0.13.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
@@ -3,7 +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
+ import { DEFERRED_QUEUE_ID, blastShove, warnOnce } from "@adatechnology/game-contracts";
7
7
  import { createVfx, effectForEvent, EVENT_EFFECTS_MAP } from "@adatechnology/game-vfx";
8
8
  import { createVfxRenderer } from "@adatechnology/game-vfx-three";
9
9
 
@@ -92,6 +92,7 @@ var SHEET_SWAY = 3;
92
92
  var BIG_LENGTH = 0.2;
93
93
  var BIG_WIDTH = 0.03;
94
94
  var FRAGMENT_SEED = 1374496523;
95
+ var SHARD_SHAPES = 6;
95
96
  var isBig = (size) => {
96
97
  const [, middle, longest] = [...size].sort((a, b) => a - b);
97
98
  return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
@@ -141,7 +142,7 @@ var FragmentSystem = class {
141
142
  return m;
142
143
  }
143
144
  shardGeometry(seed) {
144
- const idx = seed % 6;
145
+ const idx = seed % SHARD_SHAPES;
145
146
  let g = this.shardGeos[idx];
146
147
  if (!g) {
147
148
  const r = createRng(1e3 + idx);
@@ -162,17 +163,32 @@ var FragmentSystem = class {
162
163
  }
163
164
  return g;
164
165
  }
166
+ markParts() {
167
+ this.markGeometry ?? (this.markGeometry = new this.THREE.CircleGeometry(1, 10));
168
+ this.markMaterial ?? (this.markMaterial = new this.THREE.MeshBasicMaterial({ color: 723465, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
169
+ return { geometry: this.markGeometry, material: this.markMaterial };
170
+ }
171
+ /**
172
+ * Meshes, never put in a scene by this class, that draw every shared piece of a fragment once: the box, the six shards, the bullet-hole disc, and the opaque and
173
+ * glass materials. A host draws them at load so the first blast does not upload these buffers in the middle of a frame.
174
+ */
175
+ warmMeshes() {
176
+ const opaque = this.materialFor("warm", 8421504, false);
177
+ const glass = this.materialFor("warm", 8421504, true);
178
+ const geometries = [this.box, ...Array.from({ length: SHARD_SHAPES }, (_, index) => this.shardGeometry(index))];
179
+ const { geometry, material } = this.markParts();
180
+ return [...geometries.flatMap((shared) => [new this.THREE.Mesh(shared, opaque), new this.THREE.Mesh(shared, glass)]), new this.THREE.Mesh(geometry, material)];
181
+ }
165
182
  /**
166
183
  * 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
167
184
  * 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.
168
185
  */
169
186
  addMarks(mesh, f) {
170
187
  if (!f.marks?.length) return;
171
- this.markGeometry ?? (this.markGeometry = new this.THREE.CircleGeometry(1, 10));
172
- this.markMaterial ?? (this.markMaterial = new this.THREE.MeshBasicMaterial({ color: 723465, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
188
+ const { geometry, material } = this.markParts();
173
189
  const sc = mesh.scale;
174
190
  for (const mark of f.marks) {
175
- const disc = new this.THREE.Mesh(this.markGeometry, this.markMaterial);
191
+ const disc = new this.THREE.Mesh(geometry, material);
176
192
  const radius = Math.max(4e-3, mark.diameter) * 0.6;
177
193
  const n = this.v.set(mark.normal[0], mark.normal[1], mark.normal[2]).normalize();
178
194
  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);
@@ -1517,6 +1533,7 @@ var Visuals = class {
1517
1533
  this.b = tmpV(THREE);
1518
1534
  this.m4 = new THREE.Matrix4();
1519
1535
  this.ray = new THREE.Raycaster();
1536
+ this.ray.layers.enableAll();
1520
1537
  this.box = new THREE.Box3();
1521
1538
  this.t = tmpV(THREE);
1522
1539
  this.lbox = new THREE.Box3();
@@ -2507,8 +2524,26 @@ var Renderer = class {
2507
2524
  __publicField(this, "queue", []);
2508
2525
  /** Presentation work an event left for later frames (fragments still to spawn, loose groups still to split), run in order within the frame budget. */
2509
2526
  __publicField(this, "slices", []);
2510
- /** When the work of this frame must stop; `Infinity` outside `drainQueue`, where a call finishes what it started. */
2527
+ /** When the work of this frame must stop; `Infinity` outside a drain, where a call finishes what it started. */
2511
2528
  __publicField(this, "sliceDeadline", Infinity);
2529
+ /** The clock the drain reads; a budget lends its own while it runs the queue. */
2530
+ __publicField(this, "drainClock", () => performance.now());
2531
+ /** true once a frame budget owns the draining: `update` no longer drains by itself. */
2532
+ __publicField(this, "attached", false);
2533
+ __publicField(this, "eventQueue", {
2534
+ id: DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS,
2535
+ pending: () => this.queue.length + this.slices.length,
2536
+ run: (deadline, clock) => this.runBudgeted(deadline, clock),
2537
+ flush: () => {
2538
+ for (let guard = 0; guard < 16 && this.queue.length + this.slices.length > 0; guard++) this.runBudgeted(Infinity, () => 0);
2539
+ },
2540
+ attach: () => {
2541
+ this.attached = true;
2542
+ },
2543
+ detach: () => {
2544
+ this.attached = false;
2545
+ }
2546
+ });
2512
2547
  __publicField(this, "unsubscribe");
2513
2548
  __publicField(this, "visuals");
2514
2549
  __publicField(this, "fragments");
@@ -2554,6 +2589,7 @@ var Renderer = class {
2554
2589
  const maxParticles = Math.max(8, Math.floor((o.maxParticles ?? 2500) * (this.low ? 0.5 : 1)));
2555
2590
  this.visuals = new Visuals(THREE, this.low, 600);
2556
2591
  this.ground = new GroundSpots(THREE, o.scene, this.groundAt, 48);
2592
+ if (!o.vfx) warnOnce("game-destruction-three.own-effects-world", "game-destruction-three made an effects world of its own because the host lent none (options.vfx); pass the host effects so the effects share one budget");
2557
2593
  this.vfx = o.vfx ?? createVfx({ profile: this.quality, gravity: this.gravity, groundAt: this.groundAt, sprites: { capacity: maxParticles, density: this.low ? 0.6 : 1 } });
2558
2594
  this.ownVfx = o.vfx ? void 0 : createVfxRenderer(THREE, { scene: o.scene, camera: o.camera ?? new THREE.PerspectiveCamera(), vfx: this.vfx, pointScale: o.pointScale ?? 700 });
2559
2595
  this.stopLanding = this.vfx.onLand(this.owner, (x, _y, z, color, tag) => {
@@ -2791,7 +2827,7 @@ var Renderer = class {
2791
2827
  if (this.disposed) return;
2792
2828
  dt = clamp(dt, 0, 0.1);
2793
2829
  this.time += dt;
2794
- this.drainQueue();
2830
+ if (!this.attached) this.drainQueue();
2795
2831
  for (const b of this.bounds.values()) b.onFrame?.(dt);
2796
2832
  for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
2797
2833
  this.updateActive(dt);
@@ -2802,36 +2838,50 @@ var Renderer = class {
2802
2838
  this.ownVfx?.update(dt);
2803
2839
  this.syncTransforms();
2804
2840
  }
2841
+ deferredQueue() {
2842
+ return this.eventQueue;
2843
+ }
2805
2844
  drainQueue() {
2845
+ this.runBudgeted(performance.now() + this.eventBudgetMs, () => performance.now());
2846
+ }
2847
+ /** Applies the waiting events in order until the deadline, after the slices earlier events left; always at least one unit when something waits. Returns the units run. */
2848
+ runBudgeted(deadline, clock) {
2806
2849
  const batch = this.queue.splice(0, this.queue.length);
2807
- const deadline = performance.now() + this.eventBudgetMs;
2850
+ const outerClock = this.drainClock;
2851
+ this.drainClock = clock;
2808
2852
  this.sliceDeadline = deadline;
2853
+ let ran = 0;
2809
2854
  try {
2810
- this.runSlices(deadline);
2855
+ ran = this.runSlices(deadline);
2811
2856
  for (let i = 0; i < batch.length; i++) {
2812
- if (i > 0 && performance.now() >= deadline) {
2857
+ if ((i > 0 || ran > 0 && this.attached) && clock() >= deadline) {
2813
2858
  this.queue.unshift(...batch.slice(i));
2814
- return;
2859
+ return ran;
2815
2860
  }
2816
2861
  try {
2817
2862
  this.apply(batch[i]);
2818
2863
  } catch (err) {
2819
2864
  if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", batch[i].type, err);
2820
2865
  }
2866
+ ran++;
2821
2867
  }
2822
2868
  } finally {
2823
2869
  this.sliceDeadline = Infinity;
2870
+ this.drainClock = outerClock;
2824
2871
  }
2872
+ return ran;
2825
2873
  }
2826
2874
  /** Runs the work left by earlier frames until the deadline; at least one piece per frame, so it always finishes. */
2827
2875
  runSlices(deadline) {
2828
- for (let ran = 0; this.slices.length > 0 && (ran === 0 || performance.now() < deadline); ran++) {
2876
+ let ran = 0;
2877
+ for (; this.slices.length > 0 && (ran === 0 || this.drainClock() < deadline); ran++) {
2829
2878
  try {
2830
2879
  this.slices.shift()();
2831
2880
  } catch (err) {
2832
2881
  if (typeof console !== "undefined") console.error("game-destruction-three: failed to run deferred work", err);
2833
2882
  }
2834
2883
  }
2884
+ return ran;
2835
2885
  }
2836
2886
  /**
2837
2887
  * 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
@@ -3389,7 +3439,7 @@ var Renderer = class {
3389
3439
  const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
3390
3440
  const step = this.low ? 2 : 1;
3391
3441
  for (let i = from; i < ev.fragments.length; i += step) {
3392
- if (i - from >= MIN_FRAGMENTS_PER_SLICE * step && performance.now() >= this.sliceDeadline) {
3442
+ if (i - from >= MIN_FRAGMENTS_PER_SLICE * step && this.drainClock() >= this.sliceDeadline) {
3393
3443
  this.slices.push(() => this.spawnFragments(ev, i, look));
3394
3444
  return;
3395
3445
  }
@@ -3413,7 +3463,7 @@ var Renderer = class {
3413
3463
  if (!st || st.groups.length < 2) return;
3414
3464
  const loose = st.groups.length - 1;
3415
3465
  for (let i = 0; i < loose; i++) {
3416
- if (i > 0 && performance.now() >= this.sliceDeadline) {
3466
+ if (i > 0 && this.drainClock() >= this.sliceDeadline) {
3417
3467
  this.slices.push(() => {
3418
3468
  this.staticDirty.add(objectId);
3419
3469
  this.splitLoose(objectId);
@@ -3792,6 +3842,7 @@ var Renderer = class {
3792
3842
  const materials = [kit.discMaterial().clone(), kit.patchMaterial(8421504).clone(), kit.lineMaterial(0).clone()];
3793
3843
  const group = new T.Group();
3794
3844
  group.add(new T.Mesh(kit.discGeometry(), materials[0]), new T.Mesh(kit.discGeometry(), materials[1]), new T.LineSegments(geometry, materials[2]));
3845
+ group.add(...this.fragments.warmMeshes());
3795
3846
  for (const probe of group.children) probe.frustumCulled = false;
3796
3847
  this.warmProbes = { group, geometry, materials };
3797
3848
  return { group, dispose: () => {