@adatechnology/game-destruction-three 0.4.0 → 0.5.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
@@ -2,6 +2,10 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
+ // src/renderer.ts
6
+ import { createVfx, effectForEvent, EVENT_EFFECTS_MAP } from "@adatechnology/game-vfx";
7
+ import { createVfxRenderer } from "@adatechnology/game-vfx-three";
8
+
5
9
  // src/math.ts
6
10
  function createRng(seed) {
7
11
  let a = seed >>> 0;
@@ -29,9 +33,6 @@ function hashString(s) {
29
33
  }
30
34
  var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
31
35
  var lerp = (a, b, t) => a + (b - a) * t;
32
- function hexToRgb(hex) {
33
- return [(hex >> 16 & 255) / 255, (hex >> 8 & 255) / 255, (hex & 255) / 255];
34
- }
35
36
  function mixHex(a, b, t) {
36
37
  const ar = a >> 16 & 255;
37
38
  const ag = a >> 8 & 255;
@@ -372,345 +373,6 @@ var GroundSpots = class {
372
373
  }
373
374
  };
374
375
 
375
- // src/particles.ts
376
- var KINDS = {
377
- fire: { additive: true, life: [0.45, 0.9], size: [0.1, 0.2], grow: 0.3, gravity: 0, rise: 2.4, drag: 1.2, c0: 16760896, c1: 12591104, alpha: 0.9, fadeIn: 0.12, cone: 0.7, ground: "none", flicker: 0.35 },
378
- smoke: { additive: false, life: [1.8, 3.2], size: [0.14, 0.24], grow: 3.2, gravity: 0, rise: 0.7, drag: 0.8, c0: 6710886, c1: 10132122, alpha: 0.35, fadeIn: 0.15, cone: 0.6, ground: "none" },
379
- sparks: { additive: true, life: [0.25, 0.6], size: [0.018, 0.03], grow: 0.5, gravity: 1, rise: 0, drag: 0.3, c0: 16773792, c1: 16738832, alpha: 1, fadeIn: 0.02, cone: 0.55, ground: "bounce" },
380
- dust: { additive: false, life: [1, 2], size: [0.08, 0.14], grow: 2.5, gravity: 0.05, rise: 0, drag: 2.5, c0: 9274745, c1: 9274745, alpha: 0.4, fadeIn: 0.1, cone: 0.4, ground: "none" },
381
- shards: { additive: true, life: [0.8, 1.4], size: [0.02, 0.035], grow: 0.8, gravity: 1, rise: 0, drag: 0.2, c0: 14677247, c1: 10473727, alpha: 0.9, fadeIn: 0.03, cone: 0.5, ground: "bounce", flicker: 0.5 },
382
- splinters: { additive: false, life: [0.8, 1.5], size: [0.02, 0.035], grow: 1, gravity: 1, rise: 0, drag: 0.3, c0: 10516042, c1: 8017200, alpha: 1, fadeIn: 0.02, cone: 0.5, ground: "bounce" },
383
- chips: { additive: false, life: [0.8, 1.5], size: [0.025, 0.04], grow: 1, gravity: 1, rise: 0, drag: 0.3, c0: 9077624, c1: 7235679, alpha: 1, fadeIn: 0.02, cone: 0.5, ground: "bounce" },
384
- fibers: { additive: false, life: [1.5, 2.5], size: [0.02, 0.03], grow: 1, gravity: 0.15, rise: 0, drag: 2, c0: 12101768, c1: 12101768, alpha: 0.9, fadeIn: 0.05, cone: 0.4, ground: "stop", flutter: 2.5 },
385
- leaves: { additive: false, life: [1.5, 2.8], size: [0.03, 0.05], grow: 1, gravity: 0.15, rise: 0, drag: 2, c0: 6003260, c1: 4882224, alpha: 0.95, fadeIn: 0.05, cone: 0.4, ground: "stop", flutter: 2.8 },
386
- drips: { additive: false, life: [2, 3], size: [0.025, 0.035], grow: 1, gravity: 1, rise: 0, drag: 0, c0: 6974058, c1: 6974058, alpha: 1, fadeIn: 0.01, cone: 0.2, ground: "land" },
387
- ash: { additive: false, life: [2, 4], size: [0.02, 0.04], grow: 1.2, gravity: 0.08, rise: 0, drag: 1.5, c0: 2762275, c1: 1710102, alpha: 0.8, fadeIn: 0.1, cone: 0.5, ground: "stop", flutter: 0.6 },
388
- steam: { additive: false, life: [1, 2], size: [0.12, 0.2], grow: 3, gravity: 0, rise: 0.9, drag: 1, c0: 16777215, c1: 14739694, alpha: 0.35, fadeIn: 0.15, cone: 0.6, ground: "none" },
389
- flash: { additive: true, life: [0.12, 0.2], size: [1.2, 1.8], grow: 1.6, gravity: 0, rise: 0, drag: 0, c0: 16774080, c1: 16748592, alpha: 1, fadeIn: 0.1, cone: 0, ground: "none" },
390
- foam: { additive: false, life: [1, 1.8], size: [0.06, 0.1], grow: 1.5, gravity: 0.6, rise: 0, drag: 1.5, c0: 16054527, c1: 14542576, alpha: 0.8, fadeIn: 0.05, cone: 0.5, ground: "stop" }
391
- };
392
- var KIND_IDS = Object.keys(KINDS);
393
- var KIND_INDEX = {};
394
- KIND_IDS.forEach((k, i) => KIND_INDEX[k] = i);
395
- var KIND_LIST = KIND_IDS.map((k) => KINDS[k]);
396
- var VERT = `
397
- uniform float uScale;
398
- attribute float size;
399
- attribute float alpha;
400
- attribute vec3 pcolor;
401
- varying float vAlpha;
402
- varying vec3 vColor;
403
- void main() {
404
- vAlpha = alpha;
405
- vColor = pcolor;
406
- vec4 mv = modelViewMatrix * vec4(position, 1.0);
407
- gl_PointSize = min(size * uScale / max(0.1, -mv.z), 256.0);
408
- gl_Position = projectionMatrix * mv;
409
- }`;
410
- var FRAG = `
411
- varying float vAlpha;
412
- varying vec3 vColor;
413
- void main() {
414
- float d = length(gl_PointCoord - vec2(0.5)) * 2.0;
415
- float a = (1.0 - smoothstep(0.35, 1.0, d)) * vAlpha;
416
- if (a < 0.004) discard;
417
- gl_FragColor = vec4(vColor, a);
418
- }`;
419
- var Pool = class {
420
- constructor(THREE, capacity, additive, pointScale) {
421
- __publicField(this, "capacity", capacity);
422
- __publicField(this, "points");
423
- __publicField(this, "geometry");
424
- __publicField(this, "material");
425
- __publicField(this, "live", 0);
426
- __publicField(this, "next", 0);
427
- __publicField(this, "high", 0);
428
- __publicField(this, "pos");
429
- __publicField(this, "col");
430
- __publicField(this, "alp");
431
- __publicField(this, "siz");
432
- __publicField(this, "vel");
433
- __publicField(this, "age");
434
- __publicField(this, "life");
435
- __publicField(this, "size0");
436
- __publicField(this, "alive");
437
- __publicField(this, "kind");
438
- __publicField(this, "seed");
439
- __publicField(this, "alphaMul");
440
- __publicField(this, "tag");
441
- __publicField(this, "colorHex");
442
- __publicField(this, "rgbStart");
443
- __publicField(this, "rgbEnd");
444
- __publicField(this, "bounced");
445
- const n = Math.max(1, capacity);
446
- this.pos = new Float32Array(n * 3);
447
- this.col = new Float32Array(n * 3);
448
- this.alp = new Float32Array(n);
449
- this.siz = new Float32Array(n);
450
- this.vel = new Float32Array(n * 3);
451
- this.age = new Float32Array(n);
452
- this.life = new Float32Array(n);
453
- this.size0 = new Float32Array(n);
454
- this.alive = new Uint8Array(n);
455
- this.kind = new Uint8Array(n);
456
- this.seed = new Float32Array(n);
457
- this.alphaMul = new Float32Array(n);
458
- this.tag = new Int32Array(n);
459
- this.colorHex = new Int32Array(n);
460
- this.rgbStart = new Float32Array(n * 3);
461
- this.rgbEnd = new Float32Array(n * 3);
462
- this.bounced = new Uint8Array(n);
463
- this.geometry = new THREE.BufferGeometry();
464
- const attr = (a, size) => {
465
- const at = new THREE.BufferAttribute(a, size);
466
- at.setUsage(THREE.DynamicDrawUsage);
467
- return at;
468
- };
469
- this.geometry.setAttribute("position", attr(this.pos, 3));
470
- this.geometry.setAttribute("pcolor", attr(this.col, 3));
471
- this.geometry.setAttribute("alpha", attr(this.alp, 1));
472
- this.geometry.setAttribute("size", attr(this.siz, 1));
473
- this.geometry.setDrawRange(0, 0);
474
- this.material = new THREE.ShaderMaterial({
475
- uniforms: { uScale: { value: pointScale } },
476
- vertexShader: VERT,
477
- fragmentShader: FRAG,
478
- transparent: true,
479
- depthWrite: false,
480
- blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending
481
- });
482
- this.points = new THREE.Points(this.geometry, this.material);
483
- this.points.frustumCulled = false;
484
- this.points.name = additive ? "destruction-particles-additive" : "destruction-particles";
485
- this.points.renderOrder = additive ? 11 : 10;
486
- }
487
- spawn(kind, x, y, z, vx, vy, vz, def, rnd, o) {
488
- if (this.capacity <= 0) return;
489
- const i = this.next;
490
- this.next = (this.next + 1) % this.capacity;
491
- if (this.alive[i]) this.live--;
492
- this.alive[i] = 1;
493
- this.live++;
494
- if (i + 1 > this.high) this.high = i + 1;
495
- this.kind[i] = kind;
496
- this.pos[i * 3] = x;
497
- this.pos[i * 3 + 1] = y;
498
- this.pos[i * 3 + 2] = z;
499
- this.vel[i * 3] = vx;
500
- this.vel[i * 3 + 1] = vy;
501
- this.vel[i * 3 + 2] = vz;
502
- this.age[i] = 0;
503
- this.life[i] = lerp(def.life[0], def.life[1], rnd()) * (o?.life ?? 1);
504
- this.size0[i] = lerp(def.size[0], def.size[1], rnd()) * (o?.size ?? 1);
505
- this.seed[i] = rnd();
506
- this.alphaMul[i] = o?.alpha ?? 1;
507
- this.tag[i] = o?.tag ?? 0;
508
- this.colorHex[i] = o?.color ?? -1;
509
- this.rgbStart.set(hexToRgb(o?.color ?? def.c0), i * 3);
510
- this.rgbEnd.set(hexToRgb(o?.color ?? def.c1), i * 3);
511
- this.bounced[i] = 0;
512
- this.siz[i] = this.size0[i];
513
- this.alp[i] = 0;
514
- }
515
- kill(i) {
516
- if (!this.alive[i]) return;
517
- this.alive[i] = 0;
518
- this.live--;
519
- this.alp[i] = 0;
520
- this.siz[i] = 0;
521
- }
522
- clear() {
523
- this.alive.fill(0);
524
- this.alp.fill(0);
525
- this.siz.fill(0);
526
- this.live = 0;
527
- this.high = 0;
528
- this.next = 0;
529
- this.geometry.setDrawRange(0, 0);
530
- }
531
- /** Move a live particle (used by external pushes such as blasts). */
532
- push(cx, cy, cz, radius, speed) {
533
- for (let i = 0; i < this.high; i++) {
534
- if (!this.alive[i]) continue;
535
- const dx = this.pos[i * 3] - cx;
536
- const dy = this.pos[i * 3 + 1] - cy;
537
- const dz = this.pos[i * 3 + 2] - cz;
538
- const d = Math.hypot(dx, dy, dz);
539
- if (d > radius || d < 1e-4) continue;
540
- const f = (1 - d / radius) * speed;
541
- this.vel[i * 3] += dx / d * f;
542
- this.vel[i * 3 + 1] += dy / d * f;
543
- this.vel[i * 3 + 2] += dz / d * f;
544
- }
545
- }
546
- update(dt, host) {
547
- let top = 0;
548
- for (let i = 0; i < this.high; i++) {
549
- if (!this.alive[i]) continue;
550
- const def = KIND_LIST[this.kind[i]];
551
- const age = this.age[i] += dt;
552
- const life = this.life[i];
553
- if (age >= life) {
554
- this.kill(i);
555
- continue;
556
- }
557
- const t = age / life;
558
- const i3 = i * 3;
559
- let vx = this.vel[i3];
560
- let vy = this.vel[i3 + 1];
561
- let vz = this.vel[i3 + 2];
562
- vy += (def.rise - def.gravity * host.gravity) * dt;
563
- if (def.flutter) {
564
- const ph = age * 7 + this.seed[i] * 40;
565
- vx += Math.sin(ph) * def.flutter * dt;
566
- vz += Math.cos(ph * 1.3) * def.flutter * dt;
567
- }
568
- if (def.drag > 0) {
569
- const k = Math.exp(-def.drag * dt);
570
- vx *= k;
571
- vy *= k;
572
- vz *= k;
573
- }
574
- const x = this.pos[i3] + vx * dt;
575
- let y = this.pos[i3 + 1] + vy * dt;
576
- const z = this.pos[i3 + 2] + vz * dt;
577
- if (def.ground !== "none") {
578
- const gy = host.groundAt(x, z) + 5e-3;
579
- if (y < gy) {
580
- if (def.ground === "land") {
581
- const tag = this.tag[i];
582
- if (tag !== 0) host.onLand(x, gy, z, this.colorHex[i] >= 0 ? this.colorHex[i] : def.c0, tag);
583
- this.kill(i);
584
- continue;
585
- }
586
- y = gy;
587
- if (def.ground === "bounce" && !this.bounced[i]) {
588
- this.bounced[i] = 1;
589
- vy = Math.abs(vy) * 0.35;
590
- vx *= 0.6;
591
- vz *= 0.6;
592
- } else {
593
- vx = vy = vz = 0;
594
- }
595
- }
596
- }
597
- this.vel[i3] = vx;
598
- this.vel[i3 + 1] = vy;
599
- this.vel[i3 + 2] = vz;
600
- this.pos[i3] = x;
601
- this.pos[i3 + 1] = y;
602
- this.pos[i3 + 2] = z;
603
- this.siz[i] = this.size0[i] * lerp(1, def.grow, t);
604
- this.col[i3] = lerp(this.rgbStart[i3], this.rgbEnd[i3], t);
605
- this.col[i3 + 1] = lerp(this.rgbStart[i3 + 1], this.rgbEnd[i3 + 1], t);
606
- this.col[i3 + 2] = lerp(this.rgbStart[i3 + 2], this.rgbEnd[i3 + 2], t);
607
- let env = t < def.fadeIn ? t / def.fadeIn : 1 - (t - def.fadeIn) / (1 - def.fadeIn);
608
- if (def.flicker) env *= 1 - def.flicker * (0.5 + 0.5 * Math.sin(age * 38 + this.seed[i] * 50));
609
- this.alp[i] = clamp(env, 0, 1) * def.alpha * this.alphaMul[i];
610
- top = i + 1;
611
- }
612
- this.high = top;
613
- this.geometry.setDrawRange(0, top);
614
- for (const name of ["position", "pcolor", "alpha", "size"]) {
615
- ;
616
- this.geometry.getAttribute(name).needsUpdate = true;
617
- }
618
- }
619
- dispose() {
620
- this.geometry.dispose();
621
- this.material.dispose();
622
- }
623
- };
624
- var ParticleSystem = class {
625
- constructor(THREE, o) {
626
- __publicField(this, "o", o);
627
- __publicField(this, "add");
628
- __publicField(this, "normal");
629
- __publicField(this, "rng", createRng(2654435761));
630
- __publicField(this, "attached", false);
631
- __publicField(this, "host");
632
- const addCap = Math.floor(o.capacity * 0.4);
633
- this.add = new Pool(THREE, addCap, true, o.pointScale);
634
- this.normal = new Pool(THREE, o.capacity - addCap, false, o.pointScale);
635
- this.host = { groundAt: o.groundAt, gravity: o.gravity, onLand: o.onLand };
636
- }
637
- get capacity() {
638
- return this.add.capacity + this.normal.capacity;
639
- }
640
- get live() {
641
- return this.add.live + this.normal.live;
642
- }
643
- ensureAttached() {
644
- if (this.attached) return;
645
- this.attached = true;
646
- this.o.scene.add(this.add.points, this.normal.points);
647
- }
648
- /** Remove the Points from the scene (they come back on the next emit). */
649
- detach() {
650
- if (!this.attached) return;
651
- this.attached = false;
652
- this.o.scene.remove(this.add.points, this.normal.points);
653
- }
654
- /** Spawn one particle with an explicit velocity. */
655
- spawn(kind, p, v, opts) {
656
- const def = KINDS[kind];
657
- this.ensureAttached();
658
- (def.additive ? this.add : this.normal).spawn(KIND_INDEX[kind], p[0], p[1], p[2], v[0], v[1], v[2], def, this.rng, opts);
659
- }
660
- /** Spawn `count` particles leaving `point` around `normal` at about `speed` m/s. */
661
- burst(kind, point, normal, count, speed = 1, opts) {
662
- const def = KINDS[kind];
663
- const n = Math.max(0, Math.round(count * this.o.density));
664
- const rnd = this.rng;
665
- const nl = Math.hypot(normal[0], normal[1], normal[2]) || 1;
666
- const nx = normal[0] / nl;
667
- const ny = normal[1] / nl;
668
- const nz = normal[2] / nl;
669
- for (let k = 0; k < n; k++) {
670
- const u = rnd() * 2 - 1;
671
- const ph = rnd() * Math.PI * 2;
672
- const s = Math.sqrt(1 - u * u);
673
- const rx = s * Math.cos(ph);
674
- const ry = u;
675
- const rz = s * Math.sin(ph);
676
- let dx = nx * def.cone + rx * (1 - def.cone);
677
- let dy = ny * def.cone + ry * (1 - def.cone);
678
- let dz = nz * def.cone + rz * (1 - def.cone);
679
- dx += rx * 0.35;
680
- dy += ry * 0.35;
681
- dz += rz * 0.35;
682
- const l = Math.hypot(dx, dy, dz) || 1;
683
- const sp = speed * (0.35 + 0.65 * rnd());
684
- const jitter = 0.01 + (def.size[1] > 0.5 ? 0.15 : 0.02);
685
- this.spawn(
686
- kind,
687
- [point[0] + (rnd() - 0.5) * jitter, point[1] + (rnd() - 0.5) * jitter, point[2] + (rnd() - 0.5) * jitter],
688
- [dx / l * sp, dy / l * sp, dz / l * sp],
689
- opts
690
- );
691
- }
692
- }
693
- /** Blast wave: shove live particles away from a point. */
694
- push(point, radius, speed) {
695
- this.add.push(point[0], point[1], point[2], radius, speed);
696
- this.normal.push(point[0], point[1], point[2], radius, speed);
697
- }
698
- update(dt) {
699
- if (this.add.live > 0 || this.add.points.geometry.drawRange.count > 0) this.add.update(dt, this.host);
700
- if (this.normal.live > 0 || this.normal.points.geometry.drawRange.count > 0) this.normal.update(dt, this.host);
701
- }
702
- clear() {
703
- this.add.clear();
704
- this.normal.clear();
705
- this.detach();
706
- }
707
- dispose() {
708
- this.detach();
709
- this.add.dispose();
710
- this.normal.dispose();
711
- }
712
- };
713
-
714
376
  // src/pieces.ts
715
377
  var SUB_DT = 1 / 120;
716
378
  var MU = 0.7;
@@ -2091,8 +1753,13 @@ var Visuals = class {
2091
1753
  };
2092
1754
 
2093
1755
  // src/renderer.ts
1756
+ var rendererCount = 0;
1757
+ var IMPACT_TAGS = ["metal", "glass", "wood"];
2094
1758
  var GLOW_FROM_C = 430;
2095
1759
  var QUEUE_CAP = 4e3;
1760
+ var FIRE_CEILING_SECONDS = 3600;
1761
+ var GENERIC_REFERENCE_RADIUS = 15;
1762
+ var PRIMED_FIRE_RADIUS = 0.2;
2096
1763
  var FLUID_COLORS = {
2097
1764
  water: 9226495,
2098
1765
  fuel: 14262322,
@@ -2172,7 +1839,6 @@ var Renderer = class {
2172
1839
  __publicField(this, "queue", []);
2173
1840
  __publicField(this, "unsubscribe");
2174
1841
  __publicField(this, "visuals");
2175
- __publicField(this, "particles");
2176
1842
  __publicField(this, "fragments");
2177
1843
  __publicField(this, "ground");
2178
1844
  __publicField(this, "pieceSys");
@@ -2183,10 +1849,12 @@ var Renderer = class {
2183
1849
  __publicField(this, "homeTags", []);
2184
1850
  __publicField(this, "throttles", /* @__PURE__ */ new Map());
2185
1851
  __publicField(this, "rng", createRng(12648430));
2186
- __publicField(this, "light", null);
2187
- __publicField(this, "lightT", 0);
2188
- __publicField(this, "lightDur", 0.3);
2189
- __publicField(this, "lightPeak", 0);
1852
+ __publicField(this, "vfx");
1853
+ /** Names this renderer to the world's drips, so a shared world tells each renderer where its own landed. */
1854
+ __publicField(this, "owner", `destruction-${++rendererCount}`);
1855
+ __publicField(this, "stopLanding");
1856
+ /** The effects renderer made here when the host gave no shared world; it is stepped in `update`. */
1857
+ __publicField(this, "ownVfx");
2190
1858
  __publicField(this, "time", 0);
2191
1859
  __publicField(this, "scanTimer", 0);
2192
1860
  __publicField(this, "disposed", false);
@@ -2209,17 +1877,11 @@ var Renderer = class {
2209
1877
  const maxParticles = Math.max(8, Math.floor((o.maxParticles ?? 2500) * (this.low ? 0.5 : 1)));
2210
1878
  this.visuals = new Visuals(THREE, this.low, 600);
2211
1879
  this.ground = new GroundSpots(THREE, o.scene, this.groundAt, 48);
2212
- this.particles = new ParticleSystem(THREE, {
2213
- capacity: maxParticles,
2214
- scene: o.scene,
2215
- density: this.low ? 0.6 : 1,
2216
- pointScale: o.pointScale ?? 700,
2217
- groundAt: this.groundAt,
2218
- gravity: this.gravity,
2219
- onLand: (x, _y, z, color, tag) => {
2220
- const home = this.homeTags[tag - 1] ?? "";
2221
- this.ground.add("puddle", x, z, 0.05 + this.rng() * 0.04, color, home);
2222
- }
1880
+ this.vfx = o.vfx ?? createVfx({ profile: this.quality, gravity: this.gravity, groundAt: this.groundAt, sprites: { capacity: maxParticles, density: this.low ? 0.6 : 1 } });
1881
+ this.ownVfx = o.vfx ? void 0 : createVfxRenderer(THREE, { scene: o.scene, camera: o.camera ?? new THREE.PerspectiveCamera(), vfx: this.vfx, pointScale: o.pointScale ?? 700 });
1882
+ this.stopLanding = this.vfx.onLand(this.owner, (x, _y, z, color, tag) => {
1883
+ const home = this.homeTags[tag - 1] ?? "";
1884
+ this.ground.add("puddle", x, z, 0.05 + this.rng() * 0.04, color, home);
2223
1885
  });
2224
1886
  this.fragments = new FragmentSystem(THREE, {
2225
1887
  scene: o.scene,
@@ -2436,8 +2098,7 @@ var Renderer = class {
2436
2098
  this.pieceSys.update(dt);
2437
2099
  this.fragments.update(dt);
2438
2100
  this.ground.update(dt);
2439
- this.updateLight(dt);
2440
- this.particles.update(dt);
2101
+ this.ownVfx?.update(dt);
2441
2102
  this.syncTransforms();
2442
2103
  }
2443
2104
  drainQueue() {
@@ -2491,12 +2152,12 @@ var Renderer = class {
2491
2152
  break;
2492
2153
  case "ricochet": {
2493
2154
  const d = ev.direction;
2494
- this.particles.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
2155
+ this.vfx.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
2495
2156
  this.sound("ricochet", ev.point, 0.7);
2496
2157
  break;
2497
2158
  }
2498
2159
  case "debris":
2499
- this.particles.burst(ev.kind, ev.point, ev.normal, ev.count, ev.speed > 0 ? ev.speed : DEFAULT_SPEED[ev.kind] ?? 1, this.debrisOptions(ev.kind, ev.object, ev.part));
2160
+ this.vfx.burst(ev.kind, ev.point, ev.normal, ev.count, ev.speed > 0 ? ev.speed : DEFAULT_SPEED[ev.kind] ?? 1, this.debrisOptions(ev.kind, ev.object, ev.part));
2500
2161
  break;
2501
2162
  case "fracture":
2502
2163
  this.onFracture(ev);
@@ -2506,6 +2167,7 @@ var Renderer = class {
2506
2167
  break;
2507
2168
  case "collapse":
2508
2169
  this.splitLoose(ev.object);
2170
+ this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
2509
2171
  break;
2510
2172
  case "disassemble_progress": {
2511
2173
  const k = `unscrew:${ev.joint}`;
@@ -2516,17 +2178,15 @@ var Renderer = class {
2516
2178
  break;
2517
2179
  }
2518
2180
  case "ignite":
2519
- this.fires.set(`${ev.object}|${ev.part}`, { object: ev.object, part: ev.part, point: ev.point, accFire: 0, accSmoke: 0 });
2181
+ this.startFire(ev);
2520
2182
  this.activate(ev.object, ev.part);
2521
- this.particles.burst("fire", ev.point, [0, 1, 0], 6, 1);
2522
2183
  this.sound("ignite", ev.point, 0.7);
2523
2184
  this.sound("burn_loop", ev.point, 0.5);
2524
2185
  break;
2525
2186
  case "primed":
2526
2187
  if (ev.trigger !== "explosion") {
2527
- this.fires.set(`${ev.object}|${ev.part}`, { object: ev.object, part: ev.part, point: ev.point, accFire: 0, accSmoke: 0 });
2188
+ this.vfx.fire({ point: ev.point, radius: PRIMED_FIRE_RADIUS, life: ev.delay });
2528
2189
  this.activate(ev.object, ev.part);
2529
- this.particles.burst("fire", ev.point, [0, 1, 0], 6, 1);
2530
2190
  this.sound("ignite", ev.point, 0.7);
2531
2191
  }
2532
2192
  break;
@@ -2538,21 +2198,25 @@ var Renderer = class {
2538
2198
  break;
2539
2199
  case "expose":
2540
2200
  this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
2201
+ if (ev.bars.length > 0) {
2202
+ const [a, b] = ev.bars[0];
2203
+ this.playEvent({ kind: "expose" }, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2]);
2204
+ }
2541
2205
  break;
2542
2206
  case "extinguish": {
2543
2207
  const f = this.fires.get(`${ev.object}|${ev.part}`);
2544
2208
  if (f) {
2545
- this.fires.delete(`${ev.object}|${ev.part}`);
2546
- this.particles.burst("steam", f.point, [0, 1, 0], 14, 1.2);
2209
+ this.stopFire(`${ev.object}|${ev.part}`);
2210
+ this.vfx.burst("steam", f.point, [0, 1, 0], 14, 1.2);
2547
2211
  }
2548
2212
  this.activate(ev.object, ev.part);
2549
2213
  this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
2550
2214
  break;
2551
2215
  }
2552
2216
  case "burnout": {
2553
- this.fires.delete(`${ev.object}|${ev.part}`);
2554
- this.particles.burst("ash", ev.point, [0, 1, 0], 16, 1);
2555
- this.particles.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
2217
+ this.stopFire(`${ev.object}|${ev.part}`);
2218
+ this.vfx.burst("ash", ev.point, [0, 1, 0], 16, 1);
2219
+ this.vfx.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
2556
2220
  const pv = this.partVis(ev.object, ev.part);
2557
2221
  if (pv) {
2558
2222
  const def = this.world.materials.get(pv.material);
@@ -2575,7 +2239,7 @@ var Renderer = class {
2575
2239
  const color = (pv && this.partColor(pv, def)) ?? fallbackColor(ev.material, def);
2576
2240
  let tag = this.homeTags.indexOf(ev.object) + 1;
2577
2241
  if (tag === 0) tag = this.homeTags.push(ev.object);
2578
- this.particles.spawn("drips", ev.point, [(this.rng() - 0.5) * 0.1, -0.1, (this.rng() - 0.5) * 0.1], { color, tag });
2242
+ this.vfx.spawn("drips", ev.point, [(this.rng() - 0.5) * 0.1, -0.1, (this.rng() - 0.5) * 0.1], { color, tag, owner: this.owner });
2579
2243
  break;
2580
2244
  }
2581
2245
  case "leak": {
@@ -2666,7 +2330,7 @@ var Renderer = class {
2666
2330
  if (!this.visuals.shown(mv)) continue;
2667
2331
  this.visuals.applyDent(mv, wp, wn, ev.dent);
2668
2332
  }
2669
- if (def?.tags?.includes("metal")) this.particles.burst("sparks", ev.worldPoint, [wn.x, wn.y, wn.z], 4 + Math.min(8, ev.dent.depth * 150), 3.2);
2333
+ 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);
2670
2334
  this.sound(impactKey(def), ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1));
2671
2335
  }
2672
2336
  onHole(ev) {
@@ -2688,7 +2352,7 @@ var Renderer = class {
2688
2352
  };
2689
2353
  draw(ev.worldPoint, n);
2690
2354
  if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
2691
- if (def?.tags?.includes("metal")) this.particles.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
2355
+ if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
2692
2356
  this.sound(impactKey(def), ev.worldPoint, 0.6);
2693
2357
  }
2694
2358
  onCrack(ev) {
@@ -2712,6 +2376,20 @@ var Renderer = class {
2712
2376
  const side = point[axis] >= (box.min[axis] + box.max[axis]) / 2 ? 1 : -1;
2713
2377
  return new this.THREE.Vector3(axis === "x" ? side : 0, axis === "y" ? side : 0, axis === "z" ? side : 0);
2714
2378
  }
2379
+ /** The material an impact effect is chosen by: the tags of the material (`metal`, `glass`, `wood`), else stone and the like are `concrete`. */
2380
+ impactMaterial(def) {
2381
+ for (const tag of IMPACT_TAGS) if (def?.tags?.includes(tag)) return tag;
2382
+ return "concrete";
2383
+ }
2384
+ /**
2385
+ * Plays the effect the event map names for a destruction event (`data/event-effects.json` of `game-vfx`, or `RendererOptions.eventEffects`) at a point.
2386
+ * Answers whether one played, so the caller can leave out the burst it replaces.
2387
+ */
2388
+ playEvent(event, point) {
2389
+ const id = effectForEvent(event, this.o.eventEffects ?? EVENT_EFFECTS_MAP);
2390
+ if (!id) return false;
2391
+ return this.vfx.play(id, point, { seed: hashNumbers(point[0], point[1], point[2], this.time) }) !== null;
2392
+ }
2715
2393
  /** A coat came off: a patch in the colour of the core under it, below the bullet holes and cracks. */
2716
2394
  onSpall(ev) {
2717
2395
  const pv = this.partVis(ev.object, ev.part);
@@ -2720,6 +2398,7 @@ var Renderer = class {
2720
2398
  const mv = this.nearestMesh(pv, wp);
2721
2399
  if (!mv) return;
2722
2400
  const def = this.world.materials.get(pv.material);
2401
+ this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
2723
2402
  this.visuals.addSpall(mv, wp, this.faceNormal(mv, wp), Math.sqrt(ev.area / Math.PI), fallbackColor(pv.material, def));
2724
2403
  }
2725
2404
  /**
@@ -2727,6 +2406,7 @@ var Renderer = class {
2727
2406
  * (cutting the wall's faces) is not drawn yet: it needs the per-vertex binding of the house renderer.
2728
2407
  */
2729
2408
  onStage(ev) {
2409
+ const played = this.playEvent({ kind: "stage", stage: ev.stage }, ev.opening?.center ?? this.rootPoint(ev.object));
2730
2410
  if (!ev.fragments || !ev.opening) return;
2731
2411
  const pv = this.partVis(ev.object, ev.part);
2732
2412
  const def = pv ? this.world.materials.get(pv.material) : void 0;
@@ -2738,7 +2418,7 @@ var Renderer = class {
2738
2418
  const fdef = this.world.materials.get(fr.material);
2739
2419
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), false), ev.object, maxAlive);
2740
2420
  }
2741
- this.particles.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
2421
+ if (!played) this.vfx.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
2742
2422
  this.sound(impactKey(def), ev.opening.center, 0.8);
2743
2423
  }
2744
2424
  onFracture(ev) {
@@ -2768,24 +2448,24 @@ var Renderer = class {
2768
2448
  const sizeK = clamp(ev.fragments.length / 10, 0.5, 2);
2769
2449
  switch (ev.style) {
2770
2450
  case "shatter":
2771
- this.particles.burst("shards", ev.point, up, 18 * sizeK, 3);
2451
+ this.vfx.burst("shards", ev.point, up, 18 * sizeK, 3);
2772
2452
  break;
2773
2453
  case "crumble":
2774
- this.particles.burst("dust", ev.point, up, 16 * sizeK, 1.2, { color: pv ? this.partColor(pv, def) ?? void 0 : void 0 });
2775
- this.particles.burst("chips", ev.point, up, 6, 3);
2454
+ this.vfx.burst("dust", ev.point, up, 16 * sizeK, 1.2, { color: pv ? this.partColor(pv, def) ?? void 0 : void 0 });
2455
+ this.vfx.burst("chips", ev.point, up, 6, 3);
2776
2456
  break;
2777
2457
  case "splinter":
2778
- this.particles.burst("splinters", ev.point, up, 14 * sizeK, 3);
2779
- this.particles.burst("dust", ev.point, up, 5, 0.8, { color: 11901556 });
2458
+ this.vfx.burst("splinters", ev.point, up, 14 * sizeK, 3);
2459
+ this.vfx.burst("dust", ev.point, up, 5, 0.8, { color: 11901556 });
2780
2460
  break;
2781
2461
  case "tear":
2782
- this.particles.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color });
2462
+ this.vfx.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color });
2783
2463
  break;
2784
2464
  case "snap":
2785
- this.particles.burst("chips", ev.point, up, 8 * sizeK, 3);
2465
+ this.vfx.burst("chips", ev.point, up, 8 * sizeK, 3);
2786
2466
  break;
2787
2467
  default:
2788
- this.particles.burst("sparks", ev.point, up, 10, 4);
2468
+ this.vfx.burst("sparks", ev.point, up, 10, 4);
2789
2469
  }
2790
2470
  this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1));
2791
2471
  }
@@ -2864,27 +2544,10 @@ var Renderer = class {
2864
2544
  nb.piece = piece;
2865
2545
  this.o.onPiece?.(piece);
2866
2546
  }
2867
- onExplode(ev) {
2547
+ onExplode(ev, options = {}) {
2868
2548
  const p = ev.point;
2869
- const k = clamp(ev.radius / 3, 0.4, 2.5);
2870
- this.particles.burst("flash", p, [0, 1, 0], 1, 0, { size: k });
2871
- this.particles.burst("fire", p, [0, 1, 0], 34 * k, 4 * k, { size: 1 + k * 0.5 });
2872
- this.particles.burst("smoke", p, [0, 1, 0], 24 * k, 2.2, { color: SMOKE_TONES.black, size: 1 + k * 0.6 });
2873
- this.particles.burst("dust", p, [0, 1, 0], 22 * k, 3.5 * k);
2874
- this.particles.burst("sparks", p, [0, 1, 0], 16 * k, 8);
2875
- this.particles.push(p, ev.radius, 4);
2876
- if (this.o.explosionLight !== false) {
2877
- if (!this.light) {
2878
- this.light = new this.THREE.PointLight(16754784, 0, 10, 2);
2879
- this.light.name = "destruction-explosion-light";
2880
- }
2881
- if (!this.light.parent) this.scene.add(this.light);
2882
- this.light.position.set(p[0], p[1] + 0.5, p[2]);
2883
- this.light.distance = Math.max(6, ev.radius * 3);
2884
- this.lightPeak = clamp(Math.sqrt(Math.max(1, ev.energy)) * 0.6, 10, 400);
2885
- this.lightDur = 0.35;
2886
- this.lightT = this.lightDur;
2887
- }
2549
+ if (options.visual !== false) this.playExplosion(ev, options);
2550
+ this.vfx.push(p, ev.radius, 4);
2888
2551
  const c = this.v1;
2889
2552
  for (const pc of this.pieceSys.list) {
2890
2553
  this.pieceSys.centerOf(pc, c);
@@ -2903,11 +2566,23 @@ var Renderer = class {
2903
2566
  this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(ev.energy) * 0.08));
2904
2567
  this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1));
2905
2568
  }
2906
- updateLight(dt) {
2907
- if (!this.light || this.lightT <= 0) return;
2908
- this.lightT = Math.max(0, this.lightT - dt);
2909
- const t = this.lightT / this.lightDur;
2910
- this.light.intensity = this.lightPeak * t * t;
2569
+ playExplosion(ev, options) {
2570
+ const named = options.effect ?? ev.effect;
2571
+ const direction = options.direction;
2572
+ const scale = clamp(ev.radius / GENERIC_REFERENCE_RADIUS, 0.2, 3);
2573
+ if (named !== void 0 && this.vfx.explosion(named, ev.point, direction ? { direction } : {}) !== null) return;
2574
+ this.vfx.explosion("generic", ev.point, { scale, ...direction ? { direction } : {} });
2575
+ }
2576
+ startFire(ev) {
2577
+ const key = `${ev.object}|${ev.part}`;
2578
+ this.stopFire(key);
2579
+ this.fires.set(key, { object: ev.object, part: ev.part, point: ev.point, follow: { x: ev.point[0], y: ev.point[1], z: ev.point[2] } });
2580
+ }
2581
+ stopFire(key) {
2582
+ const fire = this.fires.get(key);
2583
+ if (!fire) return;
2584
+ if (fire.handle !== void 0) this.vfx.stop(fire.handle);
2585
+ this.fires.delete(key);
2911
2586
  }
2912
2587
  // ----------------------------------------------------------------------------------------------------------------------------------
2913
2588
  // Fire, heat, char, melt, leaks
@@ -2945,8 +2620,8 @@ var Renderer = class {
2945
2620
  }
2946
2621
  const fire = this.fires.get(key);
2947
2622
  if (fire) {
2948
- if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def, dt);
2949
- else this.fires.delete(key);
2623
+ if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def);
2624
+ else this.stopFire(key);
2950
2625
  }
2951
2626
  const hot = def?.visual?.glows === true && ps.temperature > GLOW_FROM_C;
2952
2627
  if (!ps.burning && !hot && Math.abs(ps.char - pv.charApplied) < 0.01 && Math.abs(ps.melt - pv.meltApplied) < 0.01) {
@@ -2967,43 +2642,25 @@ var Renderer = class {
2967
2642
  this.ground.add("puddle", c.x, c.z, clamp(footprint * 0.55, 0.05, 1), this.partColor(pv, def) ?? fallbackColor(pv.material, def), objectId);
2968
2643
  } else {
2969
2644
  this.ground.add("ash", c.x, c.z, clamp(footprint * 0.35, 0.04, 0.5), 1907224, objectId);
2970
- this.particles.burst("ash", [c.x, bb.max.y, c.z], [0, 1, 0], 10, 0.8);
2645
+ this.vfx.burst("ash", [c.x, bb.max.y, c.z], [0, 1, 0], 10, 0.8);
2971
2646
  }
2972
2647
  }
2973
- emitFire(f, pv, def, dt) {
2648
+ emitFire(f, pv, def) {
2974
2649
  const bb = this.box;
2975
2650
  bb.makeEmpty();
2976
2651
  const tmp = new this.THREE.Box3();
2977
2652
  for (const mv of pv.meshes) if (this.visuals.shown(mv)) bb.union(this.visuals.worldBox(mv, tmp));
2978
- let cx = f.point[0];
2979
- let cy = f.point[1];
2980
- let cz = f.point[2];
2981
- let sx = 0.2;
2982
- let sy = 0.2;
2983
- let sz = 0.2;
2984
2653
  if (!bb.isEmpty()) {
2985
2654
  const c = bb.getCenter(this.v1);
2986
- const s = bb.getSize(this.v2);
2987
- cx = c.x;
2988
- cy = bb.min.y;
2989
- cz = c.z;
2990
- sx = s.x;
2991
- sy = s.y;
2992
- sz = s.z;
2993
- }
2994
- const vol = clamp(Math.cbrt(Math.max(sx * sy * sz, 1e-5)), 0.05, 1.5);
2995
- f.accFire += (18 + 70 * vol) * dt;
2996
- f.accSmoke += (6 + 24 * vol) * dt;
2997
- const tone = SMOKE_TONES[def?.thermal.smoke ?? "grey"];
2998
- const rnd = this.rng;
2999
- while (f.accFire >= 1) {
3000
- f.accFire -= 1;
3001
- this.particles.spawn("fire", [cx + (rnd() - 0.5) * sx, cy + sy * (0.3 + 0.7 * rnd()), cz + (rnd() - 0.5) * sz], [(rnd() - 0.5) * 0.4, 0.5 + rnd() * 0.9, (rnd() - 0.5) * 0.4], { size: 0.6 + vol * 0.8 });
3002
- }
3003
- while (f.accSmoke >= 1) {
3004
- f.accSmoke -= 1;
3005
- this.particles.spawn("smoke", [cx + (rnd() - 0.5) * sx, cy + sy * (0.8 + 0.4 * rnd()), cz + (rnd() - 0.5) * sz], [(rnd() - 0.5) * 0.3 + 0.1, 0.4 + rnd() * 0.5, (rnd() - 0.5) * 0.3], { color: tone, size: 0.6 + vol });
2655
+ f.follow.x = c.x;
2656
+ f.follow.y = bb.min.y;
2657
+ f.follow.z = c.z;
3006
2658
  }
2659
+ if (f.handle !== void 0) return;
2660
+ const size = bb.isEmpty() ? this.v2.set(0.2, 0.2, 0.2) : bb.getSize(this.v2);
2661
+ const radius = clamp(Math.max(size.x, size.z) * 0.5, 0.12, 1.2);
2662
+ const smokeColor = SMOKE_TONES[def?.thermal.smoke ?? "grey"];
2663
+ f.handle = this.vfx.fire({ point: [f.follow.x, f.follow.y, f.follow.z], radius, life: FIRE_CEILING_SECONDS, follow: f.follow, ...smokeColor !== void 0 ? { smokeColor } : {} });
3007
2664
  }
3008
2665
  updateLeaks(dt) {
3009
2666
  for (let i = this.leaks.length - 1; i >= 0; i--) {
@@ -3021,8 +2678,8 @@ var Renderer = class {
3021
2678
  while (l.acc >= 1) {
3022
2679
  l.acc -= 1;
3023
2680
  const vel = [d[0] / dl * sp + (this.rng() - 0.5) * 0.25, d[1] / dl * sp + (this.rng() - 0.5) * 0.25, d[2] / dl * sp + (this.rng() - 0.5) * 0.25];
3024
- if (l.fluid === "gas") this.particles.spawn("steam", l.point, vel, { color, size: 0.5, alpha: 0.5 });
3025
- else this.particles.spawn("drips", l.point, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0 });
2681
+ if (l.fluid === "gas") this.vfx.spawn("steam", l.point, vel, { color, size: 0.5, alpha: 0.5 });
2682
+ else this.vfx.spawn("drips", l.point, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0, owner: this.owner });
3026
2683
  }
3027
2684
  }
3028
2685
  }
@@ -3035,10 +2692,10 @@ var Renderer = class {
3035
2692
  // Emit, clear, restore, stats, dispose
3036
2693
  // ----------------------------------------------------------------------------------------------------------------------------------
3037
2694
  emit(kind, point, normal, count, speed) {
3038
- this.particles.burst(kind, point, normal, count, speed ?? DEFAULT_SPEED[kind] ?? 1, kind === "smoke" ? { color: SMOKE_TONES.grey } : void 0);
2695
+ this.vfx.burst(kind, point, normal, count, speed ?? DEFAULT_SPEED[kind] ?? 1, kind === "smoke" ? { color: SMOKE_TONES.grey } : void 0);
3039
2696
  }
3040
- explode(point, energy, radius) {
3041
- this.onExplode({ type: "explode", object: "", part: "", time: this.time, point, energy, radius });
2697
+ explode(point, energy, radius, options) {
2698
+ this.onExplode({ type: "explode", object: "", part: "", time: this.time, point, energy, radius }, options);
3042
2699
  }
3043
2700
  restore(objectId) {
3044
2701
  const homes = /* @__PURE__ */ new Set([objectId]);
@@ -3046,7 +2703,7 @@ var Renderer = class {
3046
2703
  if (self?.isSplit) for (const pv of self.parts.values()) homes.add(pv.home);
3047
2704
  for (const home of homes) this.restoreHome(home);
3048
2705
  for (const [key, a] of [...this.active]) if (homes.has(a.object)) this.active.delete(key);
3049
- for (const [key, f] of [...this.fires]) if (homes.has(f.object)) this.fires.delete(key);
2706
+ for (const [key, f] of [...this.fires]) if (homes.has(f.object)) this.stopFire(key);
3050
2707
  for (let i = this.leaks.length - 1; i >= 0; i--) if (homes.has(this.leaks[i].object)) this.leaks.splice(i, 1);
3051
2708
  for (const home of homes) {
3052
2709
  this.fragments.removeHome(home);
@@ -3075,7 +2732,7 @@ var Renderer = class {
3075
2732
  clear() {
3076
2733
  this.queue.length = 0;
3077
2734
  this.active.clear();
3078
- this.fires.clear();
2735
+ for (const key of [...this.fires.keys()]) this.stopFire(key);
3079
2736
  this.leaks.length = 0;
3080
2737
  for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
3081
2738
  for (const b of [...this.bounds.values()]) {
@@ -3089,15 +2746,16 @@ var Renderer = class {
3089
2746
  this.fragments.clear();
3090
2747
  this.ground.clear();
3091
2748
  this.cages.clear();
3092
- this.particles.clear();
3093
- if (this.light) {
3094
- this.light.intensity = 0;
3095
- this.lightT = 0;
3096
- this.scene.remove(this.light);
3097
- }
2749
+ this.vfx.sprites().clear();
2750
+ this.ownVfx?.world.clear();
3098
2751
  }
3099
2752
  stats() {
3100
- return { fragments: this.fragments.count, particles: this.particles.live, pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
2753
+ return { fragments: this.fragments.count, particles: this.vfx.sprites().live + this.ownedVfxParticles(), pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
2754
+ }
2755
+ ownedVfxParticles() {
2756
+ if (!this.ownVfx) return 0;
2757
+ const { puffs, sparks, glows, chunks } = this.vfx.stats();
2758
+ return puffs + sparks + glows + chunks;
3101
2759
  }
3102
2760
  rebarCount() {
3103
2761
  return this.cages.count;
@@ -3110,10 +2768,9 @@ var Renderer = class {
3110
2768
  this.fragments.dispose();
3111
2769
  this.ground.dispose();
3112
2770
  this.cages.dispose();
3113
- this.particles.dispose();
2771
+ this.stopLanding();
3114
2772
  this.visuals.dispose();
3115
- this.light?.dispose?.();
3116
- this.light = null;
2773
+ this.ownVfx?.dispose();
3117
2774
  this.disposed = true;
3118
2775
  }
3119
2776
  };