@adatechnology/game-destruction-three 0.4.0 → 0.6.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;
@@ -294,9 +295,9 @@ var FragmentSystem = class {
294
295
  // src/ground.ts
295
296
  var MAX_RADIUS = 1.2;
296
297
  var GroundSpots = class {
297
- constructor(THREE, scene, groundAt, cap) {
298
+ constructor(THREE, scene2, groundAt, cap) {
298
299
  __publicField(this, "THREE", THREE);
299
- __publicField(this, "scene", scene);
300
+ __publicField(this, "scene", scene2);
300
301
  __publicField(this, "groundAt", groundAt);
301
302
  __publicField(this, "cap", cap);
302
303
  __publicField(this, "spots", []);
@@ -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;
@@ -1037,9 +699,9 @@ var PieceSystem = class {
1037
699
 
1038
700
  // src/rebar.ts
1039
701
  var RebarCages = class {
1040
- constructor(THREE, scene, radius = 4e-3) {
702
+ constructor(THREE, scene2, radius = 4e-3) {
1041
703
  __publicField(this, "THREE", THREE);
1042
- __publicField(this, "scene", scene);
704
+ __publicField(this, "scene", scene2);
1043
705
  __publicField(this, "cages", /* @__PURE__ */ new Map());
1044
706
  __publicField(this, "geometry");
1045
707
  __publicField(this, "material");
@@ -1855,10 +1517,10 @@ var Visuals = class {
1855
1517
  const sy = lerp(1, 0.25, melt);
1856
1518
  const sxz = 1 + 0.18 * melt;
1857
1519
  for (let i = range[0] * 3, v = 0; i < range[1] * 3; i += 3, v++) {
1858
- const noise = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
1520
+ const noise2 = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
1859
1521
  arr[i] = cx + (work[i] - cx) * sxz;
1860
1522
  arr[i + 2] = cz + (work[i + 2] - cz) * sxz;
1861
- arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise * melt * h * 0.04);
1523
+ arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise2 * melt * h * 0.04);
1862
1524
  }
1863
1525
  }
1864
1526
  /** The colour attribute of the host's current geometry (created white when there is none) with vertex colours switched on in its material. */
@@ -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,21 +1839,24 @@ 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");
2179
1845
  __publicField(this, "cages");
2180
1846
  __publicField(this, "active", /* @__PURE__ */ new Map());
2181
1847
  __publicField(this, "fires", /* @__PURE__ */ new Map());
1848
+ /** Primed parts burning in the effects world until they go off: their burn loop ends when they are put out. */
1849
+ __publicField(this, "primedBurning", /* @__PURE__ */ new Set());
2182
1850
  __publicField(this, "leaks", []);
2183
1851
  __publicField(this, "homeTags", []);
2184
1852
  __publicField(this, "throttles", /* @__PURE__ */ new Map());
2185
1853
  __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);
1854
+ __publicField(this, "vfx");
1855
+ /** Names this renderer to the world's drips, so a shared world tells each renderer where its own landed. */
1856
+ __publicField(this, "owner", `destruction-${++rendererCount}`);
1857
+ __publicField(this, "stopLanding");
1858
+ /** The effects renderer made here when the host gave no shared world; it is stepped in `update`. */
1859
+ __publicField(this, "ownVfx");
2190
1860
  __publicField(this, "time", 0);
2191
1861
  __publicField(this, "scanTimer", 0);
2192
1862
  __publicField(this, "disposed", false);
@@ -2209,17 +1879,11 @@ var Renderer = class {
2209
1879
  const maxParticles = Math.max(8, Math.floor((o.maxParticles ?? 2500) * (this.low ? 0.5 : 1)));
2210
1880
  this.visuals = new Visuals(THREE, this.low, 600);
2211
1881
  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
- }
1882
+ this.vfx = o.vfx ?? createVfx({ profile: this.quality, gravity: this.gravity, groundAt: this.groundAt, sprites: { capacity: maxParticles, density: this.low ? 0.6 : 1 } });
1883
+ this.ownVfx = o.vfx ? void 0 : createVfxRenderer(THREE, { scene: o.scene, camera: o.camera ?? new THREE.PerspectiveCamera(), vfx: this.vfx, pointScale: o.pointScale ?? 700 });
1884
+ this.stopLanding = this.vfx.onLand(this.owner, (x, _y, z, color, tag) => {
1885
+ const home = this.homeTags[tag - 1] ?? "";
1886
+ this.ground.add("puddle", x, z, 0.05 + this.rng() * 0.04, color, home);
2223
1887
  });
2224
1888
  this.fragments = new FragmentSystem(THREE, {
2225
1889
  scene: o.scene,
@@ -2436,8 +2100,7 @@ var Renderer = class {
2436
2100
  this.pieceSys.update(dt);
2437
2101
  this.fragments.update(dt);
2438
2102
  this.ground.update(dt);
2439
- this.updateLight(dt);
2440
- this.particles.update(dt);
2103
+ this.ownVfx?.update(dt);
2441
2104
  this.syncTransforms();
2442
2105
  }
2443
2106
  drainQueue() {
@@ -2491,12 +2154,12 @@ var Renderer = class {
2491
2154
  break;
2492
2155
  case "ricochet": {
2493
2156
  const d = ev.direction;
2494
- this.particles.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
2157
+ this.vfx.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
2495
2158
  this.sound("ricochet", ev.point, 0.7);
2496
2159
  break;
2497
2160
  }
2498
2161
  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));
2162
+ 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
2163
  break;
2501
2164
  case "fracture":
2502
2165
  this.onFracture(ev);
@@ -2506,6 +2169,7 @@ var Renderer = class {
2506
2169
  break;
2507
2170
  case "collapse":
2508
2171
  this.splitLoose(ev.object);
2172
+ this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
2509
2173
  break;
2510
2174
  case "disassemble_progress": {
2511
2175
  const k = `unscrew:${ev.joint}`;
@@ -2516,18 +2180,18 @@ var Renderer = class {
2516
2180
  break;
2517
2181
  }
2518
2182
  case "ignite":
2519
- this.fires.set(`${ev.object}|${ev.part}`, { object: ev.object, part: ev.part, point: ev.point, accFire: 0, accSmoke: 0 });
2183
+ this.startFire(ev);
2520
2184
  this.activate(ev.object, ev.part);
2521
- this.particles.burst("fire", ev.point, [0, 1, 0], 6, 1);
2522
2185
  this.sound("ignite", ev.point, 0.7);
2523
2186
  this.sound("burn_loop", ev.point, 0.5);
2524
2187
  break;
2525
2188
  case "primed":
2526
2189
  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 });
2190
+ this.vfx.fire({ point: ev.point, radius: PRIMED_FIRE_RADIUS, life: ev.delay });
2191
+ this.primedBurning.add(`${ev.object}|${ev.part}`);
2528
2192
  this.activate(ev.object, ev.part);
2529
- this.particles.burst("fire", ev.point, [0, 1, 0], 6, 1);
2530
2193
  this.sound("ignite", ev.point, 0.7);
2194
+ this.sound("burn_loop", ev.point, 0.5);
2531
2195
  }
2532
2196
  break;
2533
2197
  case "spall":
@@ -2538,21 +2202,29 @@ var Renderer = class {
2538
2202
  break;
2539
2203
  case "expose":
2540
2204
  this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
2205
+ if (ev.bars.length > 0) {
2206
+ const [a, b] = ev.bars[0];
2207
+ this.playEvent({ kind: "expose" }, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2]);
2208
+ }
2541
2209
  break;
2542
2210
  case "extinguish": {
2543
2211
  const f = this.fires.get(`${ev.object}|${ev.part}`);
2212
+ const primed = this.primedBurning.delete(`${ev.object}|${ev.part}`);
2544
2213
  if (f) {
2545
- this.fires.delete(`${ev.object}|${ev.part}`);
2546
- this.particles.burst("steam", f.point, [0, 1, 0], 14, 1.2);
2214
+ this.stopFire(`${ev.object}|${ev.part}`);
2215
+ this.vfx.burst("steam", f.point, [0, 1, 0], 14, 1.2);
2547
2216
  }
2217
+ if (f || primed) this.sound("burn_end", f?.point ?? this.rootPoint(ev.object), 0.5);
2548
2218
  this.activate(ev.object, ev.part);
2549
2219
  this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
2550
2220
  break;
2551
2221
  }
2552
2222
  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 });
2223
+ this.stopFire(`${ev.object}|${ev.part}`);
2224
+ this.primedBurning.delete(`${ev.object}|${ev.part}`);
2225
+ this.sound("burn_end", ev.point, 0.5);
2226
+ this.vfx.burst("ash", ev.point, [0, 1, 0], 16, 1);
2227
+ this.vfx.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
2556
2228
  const pv = this.partVis(ev.object, ev.part);
2557
2229
  if (pv) {
2558
2230
  const def = this.world.materials.get(pv.material);
@@ -2575,7 +2247,7 @@ var Renderer = class {
2575
2247
  const color = (pv && this.partColor(pv, def)) ?? fallbackColor(ev.material, def);
2576
2248
  let tag = this.homeTags.indexOf(ev.object) + 1;
2577
2249
  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 });
2250
+ 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
2251
  break;
2580
2252
  }
2581
2253
  case "leak": {
@@ -2592,6 +2264,7 @@ var Renderer = class {
2592
2264
  break;
2593
2265
  }
2594
2266
  case "explode":
2267
+ this.primedBurning.delete(`${ev.object}|${ev.part}`);
2595
2268
  this.onExplode(ev);
2596
2269
  break;
2597
2270
  case "destroyed":
@@ -2666,7 +2339,7 @@ var Renderer = class {
2666
2339
  if (!this.visuals.shown(mv)) continue;
2667
2340
  this.visuals.applyDent(mv, wp, wn, ev.dent);
2668
2341
  }
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);
2342
+ 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
2343
  this.sound(impactKey(def), ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1));
2671
2344
  }
2672
2345
  onHole(ev) {
@@ -2688,7 +2361,7 @@ var Renderer = class {
2688
2361
  };
2689
2362
  draw(ev.worldPoint, n);
2690
2363
  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);
2364
+ if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
2692
2365
  this.sound(impactKey(def), ev.worldPoint, 0.6);
2693
2366
  }
2694
2367
  onCrack(ev) {
@@ -2712,6 +2385,20 @@ var Renderer = class {
2712
2385
  const side = point[axis] >= (box.min[axis] + box.max[axis]) / 2 ? 1 : -1;
2713
2386
  return new this.THREE.Vector3(axis === "x" ? side : 0, axis === "y" ? side : 0, axis === "z" ? side : 0);
2714
2387
  }
2388
+ /** The material an impact effect is chosen by: the tags of the material (`metal`, `glass`, `wood`), else stone and the like are `concrete`. */
2389
+ impactMaterial(def) {
2390
+ for (const tag of IMPACT_TAGS) if (def?.tags?.includes(tag)) return tag;
2391
+ return "concrete";
2392
+ }
2393
+ /**
2394
+ * Plays the effect the event map names for a destruction event (`data/event-effects.json` of `game-vfx`, or `RendererOptions.eventEffects`) at a point.
2395
+ * Answers whether one played, so the caller can leave out the burst it replaces.
2396
+ */
2397
+ playEvent(event, point) {
2398
+ const id = effectForEvent(event, this.o.eventEffects ?? EVENT_EFFECTS_MAP);
2399
+ if (!id) return false;
2400
+ return this.vfx.play(id, point, { seed: hashNumbers(point[0], point[1], point[2], this.time) }) !== null;
2401
+ }
2715
2402
  /** A coat came off: a patch in the colour of the core under it, below the bullet holes and cracks. */
2716
2403
  onSpall(ev) {
2717
2404
  const pv = this.partVis(ev.object, ev.part);
@@ -2720,6 +2407,7 @@ var Renderer = class {
2720
2407
  const mv = this.nearestMesh(pv, wp);
2721
2408
  if (!mv) return;
2722
2409
  const def = this.world.materials.get(pv.material);
2410
+ this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
2723
2411
  this.visuals.addSpall(mv, wp, this.faceNormal(mv, wp), Math.sqrt(ev.area / Math.PI), fallbackColor(pv.material, def));
2724
2412
  }
2725
2413
  /**
@@ -2727,6 +2415,7 @@ var Renderer = class {
2727
2415
  * (cutting the wall's faces) is not drawn yet: it needs the per-vertex binding of the house renderer.
2728
2416
  */
2729
2417
  onStage(ev) {
2418
+ const played = this.playEvent({ kind: "stage", stage: ev.stage }, ev.opening?.center ?? this.rootPoint(ev.object));
2730
2419
  if (!ev.fragments || !ev.opening) return;
2731
2420
  const pv = this.partVis(ev.object, ev.part);
2732
2421
  const def = pv ? this.world.materials.get(pv.material) : void 0;
@@ -2738,7 +2427,7 @@ var Renderer = class {
2738
2427
  const fdef = this.world.materials.get(fr.material);
2739
2428
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), false), ev.object, maxAlive);
2740
2429
  }
2741
- this.particles.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
2430
+ if (!played) this.vfx.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
2742
2431
  this.sound(impactKey(def), ev.opening.center, 0.8);
2743
2432
  }
2744
2433
  onFracture(ev) {
@@ -2768,24 +2457,24 @@ var Renderer = class {
2768
2457
  const sizeK = clamp(ev.fragments.length / 10, 0.5, 2);
2769
2458
  switch (ev.style) {
2770
2459
  case "shatter":
2771
- this.particles.burst("shards", ev.point, up, 18 * sizeK, 3);
2460
+ this.vfx.burst("shards", ev.point, up, 18 * sizeK, 3);
2772
2461
  break;
2773
2462
  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);
2463
+ this.vfx.burst("dust", ev.point, up, 16 * sizeK, 1.2, { color: pv ? this.partColor(pv, def) ?? void 0 : void 0 });
2464
+ this.vfx.burst("chips", ev.point, up, 6, 3);
2776
2465
  break;
2777
2466
  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 });
2467
+ this.vfx.burst("splinters", ev.point, up, 14 * sizeK, 3);
2468
+ this.vfx.burst("dust", ev.point, up, 5, 0.8, { color: 11901556 });
2780
2469
  break;
2781
2470
  case "tear":
2782
- this.particles.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color });
2471
+ this.vfx.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color });
2783
2472
  break;
2784
2473
  case "snap":
2785
- this.particles.burst("chips", ev.point, up, 8 * sizeK, 3);
2474
+ this.vfx.burst("chips", ev.point, up, 8 * sizeK, 3);
2786
2475
  break;
2787
2476
  default:
2788
- this.particles.burst("sparks", ev.point, up, 10, 4);
2477
+ this.vfx.burst("sparks", ev.point, up, 10, 4);
2789
2478
  }
2790
2479
  this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1));
2791
2480
  }
@@ -2864,27 +2553,10 @@ var Renderer = class {
2864
2553
  nb.piece = piece;
2865
2554
  this.o.onPiece?.(piece);
2866
2555
  }
2867
- onExplode(ev) {
2556
+ onExplode(ev, options = {}) {
2868
2557
  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
- }
2558
+ if (options.visual !== false) this.playExplosion(ev, options);
2559
+ this.vfx.push(p, ev.radius, 4);
2888
2560
  const c = this.v1;
2889
2561
  for (const pc of this.pieceSys.list) {
2890
2562
  this.pieceSys.centerOf(pc, c);
@@ -2903,11 +2575,23 @@ var Renderer = class {
2903
2575
  this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(ev.energy) * 0.08));
2904
2576
  this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1));
2905
2577
  }
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;
2578
+ playExplosion(ev, options) {
2579
+ const named = options.effect ?? ev.effect;
2580
+ const direction = options.direction;
2581
+ const scale = clamp(ev.radius / GENERIC_REFERENCE_RADIUS, 0.2, 3);
2582
+ if (named !== void 0 && this.vfx.explosion(named, ev.point, direction ? { direction } : {}) !== null) return;
2583
+ this.vfx.explosion("generic", ev.point, { scale, ...direction ? { direction } : {} });
2584
+ }
2585
+ startFire(ev) {
2586
+ const key = `${ev.object}|${ev.part}`;
2587
+ this.stopFire(key);
2588
+ 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] } });
2589
+ }
2590
+ stopFire(key) {
2591
+ const fire = this.fires.get(key);
2592
+ if (!fire) return;
2593
+ if (fire.handle !== void 0) this.vfx.stop(fire.handle);
2594
+ this.fires.delete(key);
2911
2595
  }
2912
2596
  // ----------------------------------------------------------------------------------------------------------------------------------
2913
2597
  // Fire, heat, char, melt, leaks
@@ -2945,8 +2629,8 @@ var Renderer = class {
2945
2629
  }
2946
2630
  const fire = this.fires.get(key);
2947
2631
  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);
2632
+ if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def);
2633
+ else this.stopFire(key);
2950
2634
  }
2951
2635
  const hot = def?.visual?.glows === true && ps.temperature > GLOW_FROM_C;
2952
2636
  if (!ps.burning && !hot && Math.abs(ps.char - pv.charApplied) < 0.01 && Math.abs(ps.melt - pv.meltApplied) < 0.01) {
@@ -2967,43 +2651,25 @@ var Renderer = class {
2967
2651
  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
2652
  } else {
2969
2653
  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);
2654
+ this.vfx.burst("ash", [c.x, bb.max.y, c.z], [0, 1, 0], 10, 0.8);
2971
2655
  }
2972
2656
  }
2973
- emitFire(f, pv, def, dt) {
2657
+ emitFire(f, pv, def) {
2974
2658
  const bb = this.box;
2975
2659
  bb.makeEmpty();
2976
2660
  const tmp = new this.THREE.Box3();
2977
2661
  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
2662
  if (!bb.isEmpty()) {
2985
2663
  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 });
2664
+ f.follow.x = c.x;
2665
+ f.follow.y = bb.min.y;
2666
+ f.follow.z = c.z;
3006
2667
  }
2668
+ if (f.handle !== void 0) return;
2669
+ const size = bb.isEmpty() ? this.v2.set(0.2, 0.2, 0.2) : bb.getSize(this.v2);
2670
+ const radius = clamp(Math.max(size.x, size.z) * 0.5, 0.12, 1.2);
2671
+ const smokeColor = SMOKE_TONES[def?.thermal.smoke ?? "grey"];
2672
+ 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
2673
  }
3008
2674
  updateLeaks(dt) {
3009
2675
  for (let i = this.leaks.length - 1; i >= 0; i--) {
@@ -3021,8 +2687,8 @@ var Renderer = class {
3021
2687
  while (l.acc >= 1) {
3022
2688
  l.acc -= 1;
3023
2689
  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 });
2690
+ if (l.fluid === "gas") this.vfx.spawn("steam", l.point, vel, { color, size: 0.5, alpha: 0.5 });
2691
+ else this.vfx.spawn("drips", l.point, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0, owner: this.owner });
3026
2692
  }
3027
2693
  }
3028
2694
  }
@@ -3035,10 +2701,10 @@ var Renderer = class {
3035
2701
  // Emit, clear, restore, stats, dispose
3036
2702
  // ----------------------------------------------------------------------------------------------------------------------------------
3037
2703
  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);
2704
+ this.vfx.burst(kind, point, normal, count, speed ?? DEFAULT_SPEED[kind] ?? 1, kind === "smoke" ? { color: SMOKE_TONES.grey } : void 0);
3039
2705
  }
3040
- explode(point, energy, radius) {
3041
- this.onExplode({ type: "explode", object: "", part: "", time: this.time, point, energy, radius });
2706
+ explode(point, energy, radius, options) {
2707
+ this.onExplode({ type: "explode", object: "", part: "", time: this.time, point, energy, radius }, options);
3042
2708
  }
3043
2709
  restore(objectId) {
3044
2710
  const homes = /* @__PURE__ */ new Set([objectId]);
@@ -3046,7 +2712,7 @@ var Renderer = class {
3046
2712
  if (self?.isSplit) for (const pv of self.parts.values()) homes.add(pv.home);
3047
2713
  for (const home of homes) this.restoreHome(home);
3048
2714
  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);
2715
+ for (const [key, f] of [...this.fires]) if (homes.has(f.object)) this.stopFire(key);
3050
2716
  for (let i = this.leaks.length - 1; i >= 0; i--) if (homes.has(this.leaks[i].object)) this.leaks.splice(i, 1);
3051
2717
  for (const home of homes) {
3052
2718
  this.fragments.removeHome(home);
@@ -3075,7 +2741,7 @@ var Renderer = class {
3075
2741
  clear() {
3076
2742
  this.queue.length = 0;
3077
2743
  this.active.clear();
3078
- this.fires.clear();
2744
+ for (const key of [...this.fires.keys()]) this.stopFire(key);
3079
2745
  this.leaks.length = 0;
3080
2746
  for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
3081
2747
  for (const b of [...this.bounds.values()]) {
@@ -3089,15 +2755,17 @@ var Renderer = class {
3089
2755
  this.fragments.clear();
3090
2756
  this.ground.clear();
3091
2757
  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
- }
2758
+ this.primedBurning.clear();
2759
+ this.vfx.sprites().clear();
2760
+ this.ownVfx?.world.clear();
3098
2761
  }
3099
2762
  stats() {
3100
- return { fragments: this.fragments.count, particles: this.particles.live, pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
2763
+ return { fragments: this.fragments.count, particles: this.vfx.sprites().live + this.ownedVfxParticles(), pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
2764
+ }
2765
+ ownedVfxParticles() {
2766
+ if (!this.ownVfx) return 0;
2767
+ const { puffs, sparks, glows, chunks } = this.vfx.stats();
2768
+ return puffs + sparks + glows + chunks;
3101
2769
  }
3102
2770
  rebarCount() {
3103
2771
  return this.cages.count;
@@ -3110,15 +2778,212 @@ var Renderer = class {
3110
2778
  this.fragments.dispose();
3111
2779
  this.ground.dispose();
3112
2780
  this.cages.dispose();
3113
- this.particles.dispose();
2781
+ this.stopLanding();
3114
2782
  this.visuals.dispose();
3115
- this.light?.dispose?.();
3116
- this.light = null;
2783
+ this.ownVfx?.dispose();
3117
2784
  this.disposed = true;
3118
2785
  }
3119
2786
  };
2787
+
2788
+ // src/audio/recipes.ts
2789
+ import { DISTANCE_CURVES } from "@adatechnology/game-audio";
2790
+ var ATTACK = 2e-3;
2791
+ var lengthOf = (d, attack) => {
2792
+ if (typeof d === "number") return attack + d;
2793
+ if (Array.isArray(d)) return [attack + d[0], attack + d[1]];
2794
+ return { ...d, add: (d.add ?? 0) + attack };
2795
+ };
2796
+ var envelope = (attack) => [
2797
+ [0, 1e-4, "set", "fixed"],
2798
+ [attack, 1, "exp"],
2799
+ ["1d", 1e-4, "exp", "fixed"]
2800
+ ];
2801
+ function noise(o) {
2802
+ const a = o.a ?? ATTACK;
2803
+ const over = typeof o.d === "number" ? o.d : 0;
2804
+ return {
2805
+ filters: {
2806
+ mode: "serial",
2807
+ bank: [{ type: o.type ?? "lowpass", frequency: o.f2 === void 0 ? { set: o.f } : { set: o.f, to: o.f2, curve: "exp", over }, q: o.q ?? 0.7 }]
2808
+ },
2809
+ duration: lengthOf(o.d, a),
2810
+ gain: o.vol,
2811
+ ...o.delay === void 0 ? {} : { at: o.delay },
2812
+ ...o.wet === void 0 ? {} : { wet: o.wet },
2813
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
2814
+ envelope: envelope(a)
2815
+ };
2816
+ }
2817
+ function tone(o) {
2818
+ return {
2819
+ source: { kind: "osc", wave: o.wave ?? "sine", frequency: { from: o.f, to: o.f2 ?? "from", curve: "exp", over: "duration" } },
2820
+ duration: lengthOf(o.d, ATTACK),
2821
+ gain: o.vol,
2822
+ ...o.delay === void 0 ? {} : { at: o.delay },
2823
+ ...o.wet === void 0 ? {} : { wet: o.wet },
2824
+ envelope: envelope(ATTACK)
2825
+ };
2826
+ }
2827
+ var many = (count, make) => Array.from({ length: count }, make);
2828
+ var scene = (id, layers, line, vars) => ({
2829
+ id,
2830
+ bus: "world",
2831
+ pan: true,
2832
+ layers,
2833
+ falloff: DISTANCE_CURVES.art_scene,
2834
+ ...vars ? { vars } : {},
2835
+ sources: [`cena:${line}`]
2836
+ });
2837
+ var boomLayers = (big) => [
2838
+ noise({ f: 1200, f2: 120, d: 1.8, vol: 2, wet: 1.2 }),
2839
+ tone({ f: 65, f2: 24, d: 1, vol: 1.8 }),
2840
+ noise({ type: "highpass", f: 1500, d: 0.2, vol: 0.8 }),
2841
+ ...big ? [noise({ f: 500, f2: 90, a: 0.2, d: 2.4, vol: 1.2, wet: 1, delay: 0.15 })] : [],
2842
+ ...many(8, () => tone({ f: [2600, 5600], d: 0.05, vol: 0.08, delay: [0.4, 1.6] }))
2843
+ ];
2844
+ var SCENE_SOUNDS = [
2845
+ scene(
2846
+ "glass_shatter",
2847
+ [noise({ type: "highpass", f: 2800, d: 0.45, vol: 0.9, wet: 0.5 }), ...many(12, () => tone({ f: [3e3, 7500], d: [0.06, 0.14], vol: 0.18, delay: [0, 0.55] }))],
2848
+ "L1359 glass"
2849
+ ),
2850
+ scene(
2851
+ "wood_hit",
2852
+ [noise({ type: "bandpass", f: 850, q: 1, d: 0.25, vol: 0.9, wet: 0.3 }), tone({ f: 210, f2: 90, d: 0.1, vol: 0.6 }), noise({ type: "highpass", f: 2e3, d: 0.04, vol: 0.5 })],
2853
+ "L1360 wood"
2854
+ ),
2855
+ scene("thunk", [noise({ type: "bandpass", f: 700, q: 2, d: 0.07, vol: 0.4 })], "L1361 thunk"),
2856
+ scene("ping", [tone({ f: [2600, 3500], f2: 1400, d: 0.14, vol: 0.18, wet: 0.3 })], "L1362 ping"),
2857
+ scene("clang", [tone({ f: [520, 720], f2: 480, d: 0.5, vol: 0.35, wet: 0.4 }), tone({ f: 1370, f2: 1300, d: 0.3, vol: 0.15 }), noise({ type: "highpass", f: 2e3, d: 0.05, vol: 0.5 })], "L1363 clang"),
2858
+ scene("chip", [noise({ type: "bandpass", f: 1600, q: 1.2, d: 0.06, vol: 0.5 })], "L1364 chip"),
2859
+ scene(
2860
+ "crumble",
2861
+ [noise({ f: 900, f2: 200, d: 0.6, vol: 0.9, wet: 0.3 }), ...many(5, () => noise({ type: "bandpass", f: [500, 1400], q: 2, d: 0.05, vol: 0.4, delay: [0.1, 0.5] }))],
2862
+ "L1365 crumble"
2863
+ ),
2864
+ // `thud(x, z, v)`: `v` is the `scale` param (default 1)
2865
+ scene("thud", [tone({ f: 75, f2: 38, d: 0.22, vol: { param: "scale", scale: 0.6 } }), noise({ f: 420, d: 0.16, vol: { param: "scale", scale: 0.45 } })], "L1358 thud", { scale: 1 }),
2866
+ // `hiss(x, z, d)`: `seconds` is the param `d` (default 1.2)
2867
+ scene("hiss", [noise({ type: "highpass", f: 3500, a: 0.05, d: { param: "seconds" }, vol: 0.35 })], "L1366 hiss", { seconds: 1.2 }),
2868
+ scene("boom", boomLayers(false), "L1367 boom"),
2869
+ scene("boom_big", boomLayers(true), "L1367 boom (big)"),
2870
+ scene("burst", [noise({ f: 700, f2: 120, d: 0.7, vol: 1.4, wet: 0.5 }), noise({ type: "highpass", f: 3e3, a: 0.05, d: 1.2, vol: 0.4, delay: 0.1 })], "L1357 burst"),
2871
+ scene("crack", [noise({ type: "highpass", f: 1800, d: 0.05, vol: 0.8 }), tone({ f: 420, f2: 180, d: 0.06, vol: 0.4 })], "L1354 crack")
2872
+ ];
2873
+ var EXPLOSION_BOOM = {
2874
+ id: "explosion_boom",
2875
+ bus: "world",
2876
+ order: "boom",
2877
+ duration: 1.6,
2878
+ tail: 0.1,
2879
+ layers: [
2880
+ {
2881
+ source: { kind: "osc", wave: "sine", frequency: { from: 70, to: 28, curve: "exp", over: 1.2 } },
2882
+ envelope: [
2883
+ [0, 1, "set"],
2884
+ ["1d", 1e-3, "exp", "fixed"]
2885
+ ]
2886
+ }
2887
+ ],
2888
+ falloff: DISTANCE_CURVES.game_boom,
2889
+ sources: ["jogo:L68278-68285 boom"]
2890
+ };
2891
+ var BURN_LOOP = {
2892
+ id: "burn_loop",
2893
+ bus: "world",
2894
+ order: "chain_loop",
2895
+ source: { kind: "noise", loop: true },
2896
+ filters: { mode: "serial", bank: [{ type: "bandpass", frequency: { set: 1800 }, q: 0.6 }] },
2897
+ volume: 0,
2898
+ sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
2899
+ };
2900
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
2901
+
2902
+ // src/audio/sound.ts
2903
+ import { GAME_SFX_BANK } from "@adatechnology/game-audio";
2904
+ var DESTRUCTION_SOUND_KEYS = {
2905
+ art_scene: {
2906
+ impact_metal: { sound: "clang" },
2907
+ impact_wood: { sound: "wood_hit" },
2908
+ impact_glass: { sound: "ping" },
2909
+ impact_masonry: { sound: "chip" },
2910
+ impact_soft: { sound: "thud" },
2911
+ shatter_glass: { sound: "glass_shatter" },
2912
+ crumble: { sound: "crumble" },
2913
+ splinter: { sound: "crack" },
2914
+ ricochet: { sound: "ping" },
2915
+ unscrew: { sound: "chip" },
2916
+ detach: { sound: "thunk" },
2917
+ ignite: { sound: "hiss", params: { seconds: 0.5 } },
2918
+ extinguish: { sound: "hiss" },
2919
+ melt_sizzle: { sound: "hiss", params: { seconds: 0.6 } },
2920
+ explode: { sound: "boom", above: { volume: 0.9, sound: "boom_big" } }
2921
+ },
2922
+ game: {
2923
+ impact_metal: { sound: "sfx_impact" },
2924
+ impact_wood: { sound: "sfx_wood" },
2925
+ impact_glass: { sound: "sfx_glass" },
2926
+ impact_masonry: { sound: "sfx_impact" },
2927
+ impact_soft: { sound: "sfx_flesh" },
2928
+ shatter_glass: { sound: "sfx_glass" },
2929
+ crumble: { sound: "sfx_impact" },
2930
+ splinter: { sound: "sfx_wood" },
2931
+ explode: { sound: "explosion_boom" }
2932
+ }
2933
+ };
2934
+ var DEFAULT_FIRE = { perFire: 0.05, max: 0.2 };
2935
+ function createDestructionSound(engine, options = {}) {
2936
+ engine.register(DESTRUCTION_SOUND_BANK);
2937
+ engine.register(GAME_SFX_BANK);
2938
+ const fire = { ...DEFAULT_FIRE, ...options.fire };
2939
+ const rules = { ...DESTRUCTION_SOUND_KEYS.art_scene, ...options.style === "game" ? DESTRUCTION_SOUND_KEYS.game : {}, ...options.keys };
2940
+ let loop;
2941
+ let burning = 0;
2942
+ const setFire = () => {
2943
+ if (options.burnLoop === false) return;
2944
+ if (burning <= 0) {
2945
+ loop?.stop();
2946
+ loop = void 0;
2947
+ return;
2948
+ }
2949
+ loop ?? (loop = engine.loop("burn_loop"));
2950
+ loop.set({ volume: Math.min(fire.max, fire.perFire * burning) });
2951
+ };
2952
+ return {
2953
+ onSound(key, point, volume) {
2954
+ if (key === "burn_loop") {
2955
+ burning += 1;
2956
+ setFire();
2957
+ return;
2958
+ }
2959
+ if (key === "burn_end") {
2960
+ if (burning > 0) burning -= 1;
2961
+ setFire();
2962
+ return;
2963
+ }
2964
+ const rule = rules[key];
2965
+ if (!rule) return;
2966
+ const sound = rule.above && volume >= rule.above.volume ? rule.above.sound : rule.sound;
2967
+ engine.play(sound, { position: point, volume: volume * (rule.volumeScale ?? 1), ...rule.params ? { params: rule.params } : {} });
2968
+ },
2969
+ rule: (key) => rules[key] ?? void 0,
2970
+ burning: () => burning,
2971
+ dispose() {
2972
+ loop?.stop();
2973
+ loop = void 0;
2974
+ burning = 0;
2975
+ }
2976
+ };
2977
+ }
3120
2978
  export {
2979
+ BURN_LOOP,
2980
+ DEFAULT_FIRE,
2981
+ DESTRUCTION_SOUND_BANK,
2982
+ DESTRUCTION_SOUND_KEYS,
2983
+ EXPLOSION_BOOM,
2984
+ SCENE_SOUNDS,
3121
2985
  createDestructionRenderer,
2986
+ createDestructionSound,
3122
2987
  heatColor,
3123
2988
  specFromObject3D
3124
2989
  };