@adatechnology/game-destruction-three 0.3.3 → 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;
@@ -57,6 +58,8 @@ var FragmentSystem = class {
57
58
  __publicField(this, "v");
58
59
  __publicField(this, "w");
59
60
  __publicField(this, "rng", createRng(1374496523));
61
+ __publicField(this, "markGeometry", null);
62
+ __publicField(this, "markMaterial", null);
60
63
  this.box = new THREE.BoxGeometry(1, 1, 1);
61
64
  this.q = new THREE.Quaternion();
62
65
  this.dq = new THREE.Quaternion();
@@ -103,6 +106,25 @@ var FragmentSystem = class {
103
106
  }
104
107
  return g;
105
108
  }
109
+ /**
110
+ * 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
111
+ * 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.
112
+ */
113
+ addMarks(mesh, f) {
114
+ if (!f.marks?.length) return;
115
+ this.markGeometry ?? (this.markGeometry = new this.THREE.CircleGeometry(1, 10));
116
+ this.markMaterial ?? (this.markMaterial = new this.THREE.MeshBasicMaterial({ color: 723465, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4, polygonOffsetUnits: -4 }));
117
+ const sc = mesh.scale;
118
+ for (const mark of f.marks) {
119
+ const disc = new this.THREE.Mesh(this.markGeometry, this.markMaterial);
120
+ const radius = Math.max(4e-3, mark.diameter) * 0.6;
121
+ const n = this.v.set(mark.normal[0], mark.normal[1], mark.normal[2]).normalize();
122
+ 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);
123
+ disc.quaternion.setFromUnitVectors(this.w.set(0, 0, 1), n);
124
+ disc.scale.set(radius / sc.x, radius / sc.y, radius / sc.z);
125
+ mesh.add(disc);
126
+ }
127
+ }
106
128
  /** Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget. */
107
129
  spawn(f, material, home, maxAllowed) {
108
130
  const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
@@ -136,6 +158,8 @@ var FragmentSystem = class {
136
158
  alive--;
137
159
  }
138
160
  }
161
+ this.addMarks(mesh, f);
162
+ return mesh;
139
163
  }
140
164
  /** Add velocity to every fragment within `radius` of a point (blasts). */
141
165
  blast(point, radius, speed) {
@@ -259,6 +283,8 @@ var FragmentSystem = class {
259
283
  dispose() {
260
284
  this.clear();
261
285
  this.box.dispose();
286
+ this.markGeometry?.dispose();
287
+ this.markMaterial?.dispose();
262
288
  for (const g of this.shardGeos) g?.dispose();
263
289
  this.shardGeos = [];
264
290
  for (const m of this.materials.values()) m.dispose();
@@ -347,345 +373,6 @@ var GroundSpots = class {
347
373
  }
348
374
  };
349
375
 
350
- // src/particles.ts
351
- var KINDS = {
352
- 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 },
353
- 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" },
354
- 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" },
355
- 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" },
356
- 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 },
357
- 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" },
358
- 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" },
359
- 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 },
360
- 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 },
361
- 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" },
362
- 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 },
363
- 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" },
364
- 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" },
365
- 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" }
366
- };
367
- var KIND_IDS = Object.keys(KINDS);
368
- var KIND_INDEX = {};
369
- KIND_IDS.forEach((k, i) => KIND_INDEX[k] = i);
370
- var KIND_LIST = KIND_IDS.map((k) => KINDS[k]);
371
- var VERT = `
372
- uniform float uScale;
373
- attribute float size;
374
- attribute float alpha;
375
- attribute vec3 pcolor;
376
- varying float vAlpha;
377
- varying vec3 vColor;
378
- void main() {
379
- vAlpha = alpha;
380
- vColor = pcolor;
381
- vec4 mv = modelViewMatrix * vec4(position, 1.0);
382
- gl_PointSize = min(size * uScale / max(0.1, -mv.z), 256.0);
383
- gl_Position = projectionMatrix * mv;
384
- }`;
385
- var FRAG = `
386
- varying float vAlpha;
387
- varying vec3 vColor;
388
- void main() {
389
- float d = length(gl_PointCoord - vec2(0.5)) * 2.0;
390
- float a = (1.0 - smoothstep(0.35, 1.0, d)) * vAlpha;
391
- if (a < 0.004) discard;
392
- gl_FragColor = vec4(vColor, a);
393
- }`;
394
- var Pool = class {
395
- constructor(THREE, capacity, additive, pointScale) {
396
- __publicField(this, "capacity", capacity);
397
- __publicField(this, "points");
398
- __publicField(this, "geometry");
399
- __publicField(this, "material");
400
- __publicField(this, "live", 0);
401
- __publicField(this, "next", 0);
402
- __publicField(this, "high", 0);
403
- __publicField(this, "pos");
404
- __publicField(this, "col");
405
- __publicField(this, "alp");
406
- __publicField(this, "siz");
407
- __publicField(this, "vel");
408
- __publicField(this, "age");
409
- __publicField(this, "life");
410
- __publicField(this, "size0");
411
- __publicField(this, "alive");
412
- __publicField(this, "kind");
413
- __publicField(this, "seed");
414
- __publicField(this, "alphaMul");
415
- __publicField(this, "tag");
416
- __publicField(this, "colorHex");
417
- __publicField(this, "rgbStart");
418
- __publicField(this, "rgbEnd");
419
- __publicField(this, "bounced");
420
- const n = Math.max(1, capacity);
421
- this.pos = new Float32Array(n * 3);
422
- this.col = new Float32Array(n * 3);
423
- this.alp = new Float32Array(n);
424
- this.siz = new Float32Array(n);
425
- this.vel = new Float32Array(n * 3);
426
- this.age = new Float32Array(n);
427
- this.life = new Float32Array(n);
428
- this.size0 = new Float32Array(n);
429
- this.alive = new Uint8Array(n);
430
- this.kind = new Uint8Array(n);
431
- this.seed = new Float32Array(n);
432
- this.alphaMul = new Float32Array(n);
433
- this.tag = new Int32Array(n);
434
- this.colorHex = new Int32Array(n);
435
- this.rgbStart = new Float32Array(n * 3);
436
- this.rgbEnd = new Float32Array(n * 3);
437
- this.bounced = new Uint8Array(n);
438
- this.geometry = new THREE.BufferGeometry();
439
- const attr = (a, size) => {
440
- const at = new THREE.BufferAttribute(a, size);
441
- at.setUsage(THREE.DynamicDrawUsage);
442
- return at;
443
- };
444
- this.geometry.setAttribute("position", attr(this.pos, 3));
445
- this.geometry.setAttribute("pcolor", attr(this.col, 3));
446
- this.geometry.setAttribute("alpha", attr(this.alp, 1));
447
- this.geometry.setAttribute("size", attr(this.siz, 1));
448
- this.geometry.setDrawRange(0, 0);
449
- this.material = new THREE.ShaderMaterial({
450
- uniforms: { uScale: { value: pointScale } },
451
- vertexShader: VERT,
452
- fragmentShader: FRAG,
453
- transparent: true,
454
- depthWrite: false,
455
- blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending
456
- });
457
- this.points = new THREE.Points(this.geometry, this.material);
458
- this.points.frustumCulled = false;
459
- this.points.name = additive ? "destruction-particles-additive" : "destruction-particles";
460
- this.points.renderOrder = additive ? 11 : 10;
461
- }
462
- spawn(kind, x, y, z, vx, vy, vz, def, rnd, o) {
463
- if (this.capacity <= 0) return;
464
- const i = this.next;
465
- this.next = (this.next + 1) % this.capacity;
466
- if (this.alive[i]) this.live--;
467
- this.alive[i] = 1;
468
- this.live++;
469
- if (i + 1 > this.high) this.high = i + 1;
470
- this.kind[i] = kind;
471
- this.pos[i * 3] = x;
472
- this.pos[i * 3 + 1] = y;
473
- this.pos[i * 3 + 2] = z;
474
- this.vel[i * 3] = vx;
475
- this.vel[i * 3 + 1] = vy;
476
- this.vel[i * 3 + 2] = vz;
477
- this.age[i] = 0;
478
- this.life[i] = lerp(def.life[0], def.life[1], rnd()) * (o?.life ?? 1);
479
- this.size0[i] = lerp(def.size[0], def.size[1], rnd()) * (o?.size ?? 1);
480
- this.seed[i] = rnd();
481
- this.alphaMul[i] = o?.alpha ?? 1;
482
- this.tag[i] = o?.tag ?? 0;
483
- this.colorHex[i] = o?.color ?? -1;
484
- this.rgbStart.set(hexToRgb(o?.color ?? def.c0), i * 3);
485
- this.rgbEnd.set(hexToRgb(o?.color ?? def.c1), i * 3);
486
- this.bounced[i] = 0;
487
- this.siz[i] = this.size0[i];
488
- this.alp[i] = 0;
489
- }
490
- kill(i) {
491
- if (!this.alive[i]) return;
492
- this.alive[i] = 0;
493
- this.live--;
494
- this.alp[i] = 0;
495
- this.siz[i] = 0;
496
- }
497
- clear() {
498
- this.alive.fill(0);
499
- this.alp.fill(0);
500
- this.siz.fill(0);
501
- this.live = 0;
502
- this.high = 0;
503
- this.next = 0;
504
- this.geometry.setDrawRange(0, 0);
505
- }
506
- /** Move a live particle (used by external pushes such as blasts). */
507
- push(cx, cy, cz, radius, speed) {
508
- for (let i = 0; i < this.high; i++) {
509
- if (!this.alive[i]) continue;
510
- const dx = this.pos[i * 3] - cx;
511
- const dy = this.pos[i * 3 + 1] - cy;
512
- const dz = this.pos[i * 3 + 2] - cz;
513
- const d = Math.hypot(dx, dy, dz);
514
- if (d > radius || d < 1e-4) continue;
515
- const f = (1 - d / radius) * speed;
516
- this.vel[i * 3] += dx / d * f;
517
- this.vel[i * 3 + 1] += dy / d * f;
518
- this.vel[i * 3 + 2] += dz / d * f;
519
- }
520
- }
521
- update(dt, host) {
522
- let top = 0;
523
- for (let i = 0; i < this.high; i++) {
524
- if (!this.alive[i]) continue;
525
- const def = KIND_LIST[this.kind[i]];
526
- const age = this.age[i] += dt;
527
- const life = this.life[i];
528
- if (age >= life) {
529
- this.kill(i);
530
- continue;
531
- }
532
- const t = age / life;
533
- const i3 = i * 3;
534
- let vx = this.vel[i3];
535
- let vy = this.vel[i3 + 1];
536
- let vz = this.vel[i3 + 2];
537
- vy += (def.rise - def.gravity * host.gravity) * dt;
538
- if (def.flutter) {
539
- const ph = age * 7 + this.seed[i] * 40;
540
- vx += Math.sin(ph) * def.flutter * dt;
541
- vz += Math.cos(ph * 1.3) * def.flutter * dt;
542
- }
543
- if (def.drag > 0) {
544
- const k = Math.exp(-def.drag * dt);
545
- vx *= k;
546
- vy *= k;
547
- vz *= k;
548
- }
549
- const x = this.pos[i3] + vx * dt;
550
- let y = this.pos[i3 + 1] + vy * dt;
551
- const z = this.pos[i3 + 2] + vz * dt;
552
- if (def.ground !== "none") {
553
- const gy = host.groundAt(x, z) + 5e-3;
554
- if (y < gy) {
555
- if (def.ground === "land") {
556
- const tag = this.tag[i];
557
- if (tag !== 0) host.onLand(x, gy, z, this.colorHex[i] >= 0 ? this.colorHex[i] : def.c0, tag);
558
- this.kill(i);
559
- continue;
560
- }
561
- y = gy;
562
- if (def.ground === "bounce" && !this.bounced[i]) {
563
- this.bounced[i] = 1;
564
- vy = Math.abs(vy) * 0.35;
565
- vx *= 0.6;
566
- vz *= 0.6;
567
- } else {
568
- vx = vy = vz = 0;
569
- }
570
- }
571
- }
572
- this.vel[i3] = vx;
573
- this.vel[i3 + 1] = vy;
574
- this.vel[i3 + 2] = vz;
575
- this.pos[i3] = x;
576
- this.pos[i3 + 1] = y;
577
- this.pos[i3 + 2] = z;
578
- this.siz[i] = this.size0[i] * lerp(1, def.grow, t);
579
- this.col[i3] = lerp(this.rgbStart[i3], this.rgbEnd[i3], t);
580
- this.col[i3 + 1] = lerp(this.rgbStart[i3 + 1], this.rgbEnd[i3 + 1], t);
581
- this.col[i3 + 2] = lerp(this.rgbStart[i3 + 2], this.rgbEnd[i3 + 2], t);
582
- let env = t < def.fadeIn ? t / def.fadeIn : 1 - (t - def.fadeIn) / (1 - def.fadeIn);
583
- if (def.flicker) env *= 1 - def.flicker * (0.5 + 0.5 * Math.sin(age * 38 + this.seed[i] * 50));
584
- this.alp[i] = clamp(env, 0, 1) * def.alpha * this.alphaMul[i];
585
- top = i + 1;
586
- }
587
- this.high = top;
588
- this.geometry.setDrawRange(0, top);
589
- for (const name of ["position", "pcolor", "alpha", "size"]) {
590
- ;
591
- this.geometry.getAttribute(name).needsUpdate = true;
592
- }
593
- }
594
- dispose() {
595
- this.geometry.dispose();
596
- this.material.dispose();
597
- }
598
- };
599
- var ParticleSystem = class {
600
- constructor(THREE, o) {
601
- __publicField(this, "o", o);
602
- __publicField(this, "add");
603
- __publicField(this, "normal");
604
- __publicField(this, "rng", createRng(2654435761));
605
- __publicField(this, "attached", false);
606
- __publicField(this, "host");
607
- const addCap = Math.floor(o.capacity * 0.4);
608
- this.add = new Pool(THREE, addCap, true, o.pointScale);
609
- this.normal = new Pool(THREE, o.capacity - addCap, false, o.pointScale);
610
- this.host = { groundAt: o.groundAt, gravity: o.gravity, onLand: o.onLand };
611
- }
612
- get capacity() {
613
- return this.add.capacity + this.normal.capacity;
614
- }
615
- get live() {
616
- return this.add.live + this.normal.live;
617
- }
618
- ensureAttached() {
619
- if (this.attached) return;
620
- this.attached = true;
621
- this.o.scene.add(this.add.points, this.normal.points);
622
- }
623
- /** Remove the Points from the scene (they come back on the next emit). */
624
- detach() {
625
- if (!this.attached) return;
626
- this.attached = false;
627
- this.o.scene.remove(this.add.points, this.normal.points);
628
- }
629
- /** Spawn one particle with an explicit velocity. */
630
- spawn(kind, p, v, opts) {
631
- const def = KINDS[kind];
632
- this.ensureAttached();
633
- (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);
634
- }
635
- /** Spawn `count` particles leaving `point` around `normal` at about `speed` m/s. */
636
- burst(kind, point, normal, count, speed = 1, opts) {
637
- const def = KINDS[kind];
638
- const n = Math.max(0, Math.round(count * this.o.density));
639
- const rnd = this.rng;
640
- const nl = Math.hypot(normal[0], normal[1], normal[2]) || 1;
641
- const nx = normal[0] / nl;
642
- const ny = normal[1] / nl;
643
- const nz = normal[2] / nl;
644
- for (let k = 0; k < n; k++) {
645
- const u = rnd() * 2 - 1;
646
- const ph = rnd() * Math.PI * 2;
647
- const s = Math.sqrt(1 - u * u);
648
- const rx = s * Math.cos(ph);
649
- const ry = u;
650
- const rz = s * Math.sin(ph);
651
- let dx = nx * def.cone + rx * (1 - def.cone);
652
- let dy = ny * def.cone + ry * (1 - def.cone);
653
- let dz = nz * def.cone + rz * (1 - def.cone);
654
- dx += rx * 0.35;
655
- dy += ry * 0.35;
656
- dz += rz * 0.35;
657
- const l = Math.hypot(dx, dy, dz) || 1;
658
- const sp = speed * (0.35 + 0.65 * rnd());
659
- const jitter = 0.01 + (def.size[1] > 0.5 ? 0.15 : 0.02);
660
- this.spawn(
661
- kind,
662
- [point[0] + (rnd() - 0.5) * jitter, point[1] + (rnd() - 0.5) * jitter, point[2] + (rnd() - 0.5) * jitter],
663
- [dx / l * sp, dy / l * sp, dz / l * sp],
664
- opts
665
- );
666
- }
667
- }
668
- /** Blast wave: shove live particles away from a point. */
669
- push(point, radius, speed) {
670
- this.add.push(point[0], point[1], point[2], radius, speed);
671
- this.normal.push(point[0], point[1], point[2], radius, speed);
672
- }
673
- update(dt) {
674
- if (this.add.live > 0 || this.add.points.geometry.drawRange.count > 0) this.add.update(dt, this.host);
675
- if (this.normal.live > 0 || this.normal.points.geometry.drawRange.count > 0) this.normal.update(dt, this.host);
676
- }
677
- clear() {
678
- this.add.clear();
679
- this.normal.clear();
680
- this.detach();
681
- }
682
- dispose() {
683
- this.detach();
684
- this.add.dispose();
685
- this.normal.dispose();
686
- }
687
- };
688
-
689
376
  // src/pieces.ts
690
377
  var SUB_DT = 1 / 120;
691
378
  var MU = 0.7;
@@ -1010,6 +697,78 @@ var PieceSystem = class {
1010
697
  }
1011
698
  };
1012
699
 
700
+ // src/rebar.ts
701
+ var RebarCages = class {
702
+ constructor(THREE, scene, radius = 4e-3) {
703
+ __publicField(this, "THREE", THREE);
704
+ __publicField(this, "scene", scene);
705
+ __publicField(this, "cages", /* @__PURE__ */ new Map());
706
+ __publicField(this, "geometry");
707
+ __publicField(this, "material");
708
+ __publicField(this, "up");
709
+ __publicField(this, "dir");
710
+ __publicField(this, "mid");
711
+ __publicField(this, "q");
712
+ __publicField(this, "scale");
713
+ __publicField(this, "m");
714
+ this.geometry = new THREE.CylinderGeometry(radius, radius, 1, 6);
715
+ this.material = new THREE.MeshStandardMaterial({ color: 7628380, roughness: 0.55, metalness: 0.6 });
716
+ this.up = new THREE.Vector3(0, 1, 0);
717
+ this.dir = new THREE.Vector3();
718
+ this.mid = new THREE.Vector3();
719
+ this.q = new THREE.Quaternion();
720
+ this.scale = new THREE.Vector3();
721
+ this.m = new THREE.Matrix4();
722
+ }
723
+ /** Bars drawn now, over every part. */
724
+ get count() {
725
+ let n = 0;
726
+ for (const mesh of this.cages.values()) n += mesh.count;
727
+ return n;
728
+ }
729
+ /** Meshes of the live cages (tests and picking). */
730
+ meshes() {
731
+ return [...this.cages.values()];
732
+ }
733
+ /** Draw `bars` (world space) as the cage of `key`; an empty list removes it. */
734
+ set(key, bars) {
735
+ this.remove(key);
736
+ if (bars.length === 0) return;
737
+ const mesh = new this.THREE.InstancedMesh(this.geometry, this.material, bars.length);
738
+ bars.forEach(([a, b], i) => {
739
+ this.dir.set(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
740
+ const length = this.dir.length();
741
+ this.mid.set((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2);
742
+ this.q.setFromUnitVectors(this.up, length > 1e-9 ? this.dir.divideScalar(length) : this.up);
743
+ this.m.compose(this.mid, this.q, this.scale.set(1, Math.max(length, 1e-4), 1));
744
+ mesh.setMatrixAt(i, this.m);
745
+ });
746
+ mesh.instanceMatrix.needsUpdate = true;
747
+ mesh.castShadow = true;
748
+ this.scene.add(mesh);
749
+ this.cages.set(key, mesh);
750
+ }
751
+ remove(key) {
752
+ const mesh = this.cages.get(key);
753
+ if (!mesh) return;
754
+ this.scene.remove(mesh);
755
+ mesh.dispose();
756
+ this.cages.delete(key);
757
+ }
758
+ /** Remove the cages of every part of `object` (keys are `object|part`). */
759
+ removeObject(object) {
760
+ for (const key of [...this.cages.keys()]) if (key.startsWith(`${object}|`)) this.remove(key);
761
+ }
762
+ clear() {
763
+ for (const key of [...this.cages.keys()]) this.remove(key);
764
+ }
765
+ dispose() {
766
+ this.clear();
767
+ this.geometry.dispose();
768
+ this.material.dispose();
769
+ }
770
+ };
771
+
1013
772
  // src/spec.ts
1014
773
  import { guessMaterial, inferJoints } from "@adatechnology/game-destruction";
1015
774
  function partHintOf(value) {
@@ -1214,6 +973,7 @@ var DecalKit = class {
1214
973
  __publicField(this, "disc", null);
1215
974
  __publicField(this, "discMat", null);
1216
975
  __publicField(this, "lineMats", /* @__PURE__ */ new Map());
976
+ __publicField(this, "patchMats", /* @__PURE__ */ new Map());
1217
977
  }
1218
978
  discGeometry() {
1219
979
  return this.disc ?? (this.disc = new this.THREE.CircleGeometry(1, 14));
@@ -1229,6 +989,15 @@ var DecalKit = class {
1229
989
  polygonOffsetUnits: -4
1230
990
  }));
1231
991
  }
992
+ /** Opaque patch in the colour of what shows under a spalled coat. */
993
+ patchMaterial(color) {
994
+ let m = this.patchMats.get(color);
995
+ if (!m) {
996
+ m = new this.THREE.MeshBasicMaterial({ color, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
997
+ this.patchMats.set(color, m);
998
+ }
999
+ return m;
1000
+ }
1232
1001
  lineMaterial(color) {
1233
1002
  let m = this.lineMats.get(color);
1234
1003
  if (!m) {
@@ -1241,6 +1010,8 @@ var DecalKit = class {
1241
1010
  this.disc?.dispose();
1242
1011
  this.discMat?.dispose();
1243
1012
  for (const m of this.lineMats.values()) m.dispose();
1013
+ for (const m of this.patchMats.values()) m.dispose();
1014
+ this.patchMats.clear();
1244
1015
  this.disc = null;
1245
1016
  this.discMat = null;
1246
1017
  this.lineMats.clear();
@@ -1532,6 +1303,16 @@ var Visuals = class {
1532
1303
  disc.renderOrder = 2;
1533
1304
  this.addDecal(mv, { obj: disc });
1534
1305
  }
1306
+ /** A patch where a surface coat came off, in the colour of the core under it; drawn under the bullet holes. */
1307
+ addSpall(mv, worldPoint, worldNormal, radius, color) {
1308
+ const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 15e-4);
1309
+ const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.patchMaterial(color));
1310
+ disc.position.copy(pos);
1311
+ disc.quaternion.copy(quat);
1312
+ disc.scale.setScalar(Math.max(0.01, radius) / s);
1313
+ disc.renderOrder = 1;
1314
+ this.addDecal(mv, { obj: disc });
1315
+ }
1535
1316
  /** A star of jagged lines: `rays` rays up to `length` metres, seeded so the same event always draws the same star. */
1536
1317
  addCrack(mv, worldPoint, worldNormal, rays, length, color, seed) {
1537
1318
  const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 2e-3);
@@ -1972,8 +1753,13 @@ var Visuals = class {
1972
1753
  };
1973
1754
 
1974
1755
  // src/renderer.ts
1756
+ var rendererCount = 0;
1757
+ var IMPACT_TAGS = ["metal", "glass", "wood"];
1975
1758
  var GLOW_FROM_C = 430;
1976
1759
  var QUEUE_CAP = 4e3;
1760
+ var FIRE_CEILING_SECONDS = 3600;
1761
+ var GENERIC_REFERENCE_RADIUS = 15;
1762
+ var PRIMED_FIRE_RADIUS = 0.2;
1977
1763
  var FLUID_COLORS = {
1978
1764
  water: 9226495,
1979
1765
  fuel: 14262322,
@@ -2053,20 +1839,22 @@ var Renderer = class {
2053
1839
  __publicField(this, "queue", []);
2054
1840
  __publicField(this, "unsubscribe");
2055
1841
  __publicField(this, "visuals");
2056
- __publicField(this, "particles");
2057
1842
  __publicField(this, "fragments");
2058
1843
  __publicField(this, "ground");
2059
1844
  __publicField(this, "pieceSys");
1845
+ __publicField(this, "cages");
2060
1846
  __publicField(this, "active", /* @__PURE__ */ new Map());
2061
1847
  __publicField(this, "fires", /* @__PURE__ */ new Map());
2062
1848
  __publicField(this, "leaks", []);
2063
1849
  __publicField(this, "homeTags", []);
2064
1850
  __publicField(this, "throttles", /* @__PURE__ */ new Map());
2065
1851
  __publicField(this, "rng", createRng(12648430));
2066
- __publicField(this, "light", null);
2067
- __publicField(this, "lightT", 0);
2068
- __publicField(this, "lightDur", 0.3);
2069
- __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");
2070
1858
  __publicField(this, "time", 0);
2071
1859
  __publicField(this, "scanTimer", 0);
2072
1860
  __publicField(this, "disposed", false);
@@ -2089,17 +1877,11 @@ var Renderer = class {
2089
1877
  const maxParticles = Math.max(8, Math.floor((o.maxParticles ?? 2500) * (this.low ? 0.5 : 1)));
2090
1878
  this.visuals = new Visuals(THREE, this.low, 600);
2091
1879
  this.ground = new GroundSpots(THREE, o.scene, this.groundAt, 48);
2092
- this.particles = new ParticleSystem(THREE, {
2093
- capacity: maxParticles,
2094
- scene: o.scene,
2095
- density: this.low ? 0.6 : 1,
2096
- pointScale: o.pointScale ?? 700,
2097
- groundAt: this.groundAt,
2098
- gravity: this.gravity,
2099
- onLand: (x, _y, z, color, tag) => {
2100
- const home = this.homeTags[tag - 1] ?? "";
2101
- this.ground.add("puddle", x, z, 0.05 + this.rng() * 0.04, color, home);
2102
- }
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);
2103
1885
  });
2104
1886
  this.fragments = new FragmentSystem(THREE, {
2105
1887
  scene: o.scene,
@@ -2108,6 +1890,7 @@ var Renderer = class {
2108
1890
  max: this.maxFragments,
2109
1891
  life: o.fragmentLife ?? 12
2110
1892
  });
1893
+ this.cages = new RebarCages(THREE, o.scene);
2111
1894
  this.pieceSys = new PieceSystem(THREE, {
2112
1895
  groundAt: this.groundAt,
2113
1896
  gravity: this.gravity,
@@ -2315,8 +2098,7 @@ var Renderer = class {
2315
2098
  this.pieceSys.update(dt);
2316
2099
  this.fragments.update(dt);
2317
2100
  this.ground.update(dt);
2318
- this.updateLight(dt);
2319
- this.particles.update(dt);
2101
+ this.ownVfx?.update(dt);
2320
2102
  this.syncTransforms();
2321
2103
  }
2322
2104
  drainQueue() {
@@ -2370,12 +2152,12 @@ var Renderer = class {
2370
2152
  break;
2371
2153
  case "ricochet": {
2372
2154
  const d = ev.direction;
2373
- 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);
2374
2156
  this.sound("ricochet", ev.point, 0.7);
2375
2157
  break;
2376
2158
  }
2377
2159
  case "debris":
2378
- 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));
2379
2161
  break;
2380
2162
  case "fracture":
2381
2163
  this.onFracture(ev);
@@ -2385,6 +2167,7 @@ var Renderer = class {
2385
2167
  break;
2386
2168
  case "collapse":
2387
2169
  this.splitLoose(ev.object);
2170
+ this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
2388
2171
  break;
2389
2172
  case "disassemble_progress": {
2390
2173
  const k = `unscrew:${ev.joint}`;
@@ -2395,34 +2178,45 @@ var Renderer = class {
2395
2178
  break;
2396
2179
  }
2397
2180
  case "ignite":
2398
- this.fires.set(`${ev.object}|${ev.part}`, { object: ev.object, part: ev.part, point: ev.point, accFire: 0, accSmoke: 0 });
2181
+ this.startFire(ev);
2399
2182
  this.activate(ev.object, ev.part);
2400
- this.particles.burst("fire", ev.point, [0, 1, 0], 6, 1);
2401
2183
  this.sound("ignite", ev.point, 0.7);
2402
2184
  this.sound("burn_loop", ev.point, 0.5);
2403
2185
  break;
2404
2186
  case "primed":
2405
2187
  if (ev.trigger !== "explosion") {
2406
- 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 });
2407
2189
  this.activate(ev.object, ev.part);
2408
- this.particles.burst("fire", ev.point, [0, 1, 0], 6, 1);
2409
2190
  this.sound("ignite", ev.point, 0.7);
2410
2191
  }
2411
2192
  break;
2193
+ case "spall":
2194
+ this.onSpall(ev);
2195
+ break;
2196
+ case "stage":
2197
+ this.onStage(ev);
2198
+ break;
2199
+ case "expose":
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
+ }
2205
+ break;
2412
2206
  case "extinguish": {
2413
2207
  const f = this.fires.get(`${ev.object}|${ev.part}`);
2414
2208
  if (f) {
2415
- this.fires.delete(`${ev.object}|${ev.part}`);
2416
- 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);
2417
2211
  }
2418
2212
  this.activate(ev.object, ev.part);
2419
2213
  this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
2420
2214
  break;
2421
2215
  }
2422
2216
  case "burnout": {
2423
- this.fires.delete(`${ev.object}|${ev.part}`);
2424
- this.particles.burst("ash", ev.point, [0, 1, 0], 16, 1);
2425
- 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 });
2426
2220
  const pv = this.partVis(ev.object, ev.part);
2427
2221
  if (pv) {
2428
2222
  const def = this.world.materials.get(pv.material);
@@ -2445,7 +2239,7 @@ var Renderer = class {
2445
2239
  const color = (pv && this.partColor(pv, def)) ?? fallbackColor(ev.material, def);
2446
2240
  let tag = this.homeTags.indexOf(ev.object) + 1;
2447
2241
  if (tag === 0) tag = this.homeTags.push(ev.object);
2448
- 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 });
2449
2243
  break;
2450
2244
  }
2451
2245
  case "leak": {
@@ -2536,7 +2330,7 @@ var Renderer = class {
2536
2330
  if (!this.visuals.shown(mv)) continue;
2537
2331
  this.visuals.applyDent(mv, wp, wn, ev.dent);
2538
2332
  }
2539
- 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);
2540
2334
  this.sound(impactKey(def), ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1));
2541
2335
  }
2542
2336
  onHole(ev) {
@@ -2558,7 +2352,7 @@ var Renderer = class {
2558
2352
  };
2559
2353
  draw(ev.worldPoint, n);
2560
2354
  if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
2561
- 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);
2562
2356
  this.sound(impactKey(def), ev.worldPoint, 0.6);
2563
2357
  }
2564
2358
  onCrack(ev) {
@@ -2574,6 +2368,59 @@ var Renderer = class {
2574
2368
  }
2575
2369
  this.sound(impactKey(def), ev.worldPoint, 0.5);
2576
2370
  }
2371
+ /** Outward normal of the face of `mv` nearest to a world point: the thin axis of its world box (right for a wall; a sloped face is left to the vertex-range binding). */
2372
+ faceNormal(mv, point) {
2373
+ const box = this.visuals.worldBox(mv, this.box);
2374
+ const size = box.getSize(this.v1);
2375
+ const axis = size.x <= size.y && size.x <= size.z ? "x" : size.y <= size.z ? "y" : "z";
2376
+ const side = point[axis] >= (box.min[axis] + box.max[axis]) / 2 ? 1 : -1;
2377
+ return new this.THREE.Vector3(axis === "x" ? side : 0, axis === "y" ? side : 0, axis === "z" ? side : 0);
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
+ }
2393
+ /** A coat came off: a patch in the colour of the core under it, below the bullet holes and cracks. */
2394
+ onSpall(ev) {
2395
+ const pv = this.partVis(ev.object, ev.part);
2396
+ if (!pv) return;
2397
+ const wp = new this.THREE.Vector3(ev.point[0], ev.point[1], ev.point[2]);
2398
+ const mv = this.nearestMesh(pv, wp);
2399
+ if (!mv) return;
2400
+ const def = this.world.materials.get(pv.material);
2401
+ this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
2402
+ this.visuals.addSpall(mv, wp, this.faceNormal(mv, wp), Math.sqrt(ev.area / Math.PI), fallbackColor(pv.material, def));
2403
+ }
2404
+ /**
2405
+ * Stage 2 of a staged part: the unit-sized fragments come out and the part stays drawn (hiding it is what `fracture` does). The opening itself
2406
+ * (cutting the wall's faces) is not drawn yet: it needs the per-vertex binding of the house renderer.
2407
+ */
2408
+ onStage(ev) {
2409
+ const played = this.playEvent({ kind: "stage", stage: ev.stage }, ev.opening?.center ?? this.rootPoint(ev.object));
2410
+ if (!ev.fragments || !ev.opening) return;
2411
+ const pv = this.partVis(ev.object, ev.part);
2412
+ const def = pv ? this.world.materials.get(pv.material) : void 0;
2413
+ const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
2414
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
2415
+ const step = this.low ? 2 : 1;
2416
+ for (let i = 0; i < ev.fragments.length; i += step) {
2417
+ const fr = ev.fragments[i];
2418
+ const fdef = this.world.materials.get(fr.material);
2419
+ this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), false), ev.object, maxAlive);
2420
+ }
2421
+ if (!played) this.vfx.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
2422
+ this.sound(impactKey(def), ev.opening.center, 0.8);
2423
+ }
2577
2424
  onFracture(ev) {
2578
2425
  const b = this.bounds.get(ev.object);
2579
2426
  const pv = b?.parts.get(ev.part);
@@ -2601,24 +2448,24 @@ var Renderer = class {
2601
2448
  const sizeK = clamp(ev.fragments.length / 10, 0.5, 2);
2602
2449
  switch (ev.style) {
2603
2450
  case "shatter":
2604
- this.particles.burst("shards", ev.point, up, 18 * sizeK, 3);
2451
+ this.vfx.burst("shards", ev.point, up, 18 * sizeK, 3);
2605
2452
  break;
2606
2453
  case "crumble":
2607
- this.particles.burst("dust", ev.point, up, 16 * sizeK, 1.2, { color: pv ? this.partColor(pv, def) ?? void 0 : void 0 });
2608
- 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);
2609
2456
  break;
2610
2457
  case "splinter":
2611
- this.particles.burst("splinters", ev.point, up, 14 * sizeK, 3);
2612
- 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 });
2613
2460
  break;
2614
2461
  case "tear":
2615
- 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 });
2616
2463
  break;
2617
2464
  case "snap":
2618
- this.particles.burst("chips", ev.point, up, 8 * sizeK, 3);
2465
+ this.vfx.burst("chips", ev.point, up, 8 * sizeK, 3);
2619
2466
  break;
2620
2467
  default:
2621
- this.particles.burst("sparks", ev.point, up, 10, 4);
2468
+ this.vfx.burst("sparks", ev.point, up, 10, 4);
2622
2469
  }
2623
2470
  this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1));
2624
2471
  }
@@ -2697,27 +2544,10 @@ var Renderer = class {
2697
2544
  nb.piece = piece;
2698
2545
  this.o.onPiece?.(piece);
2699
2546
  }
2700
- onExplode(ev) {
2547
+ onExplode(ev, options = {}) {
2701
2548
  const p = ev.point;
2702
- const k = clamp(ev.radius / 3, 0.4, 2.5);
2703
- this.particles.burst("flash", p, [0, 1, 0], 1, 0, { size: k });
2704
- this.particles.burst("fire", p, [0, 1, 0], 34 * k, 4 * k, { size: 1 + k * 0.5 });
2705
- this.particles.burst("smoke", p, [0, 1, 0], 24 * k, 2.2, { color: SMOKE_TONES.black, size: 1 + k * 0.6 });
2706
- this.particles.burst("dust", p, [0, 1, 0], 22 * k, 3.5 * k);
2707
- this.particles.burst("sparks", p, [0, 1, 0], 16 * k, 8);
2708
- this.particles.push(p, ev.radius, 4);
2709
- if (this.o.explosionLight !== false) {
2710
- if (!this.light) {
2711
- this.light = new this.THREE.PointLight(16754784, 0, 10, 2);
2712
- this.light.name = "destruction-explosion-light";
2713
- }
2714
- if (!this.light.parent) this.scene.add(this.light);
2715
- this.light.position.set(p[0], p[1] + 0.5, p[2]);
2716
- this.light.distance = Math.max(6, ev.radius * 3);
2717
- this.lightPeak = clamp(Math.sqrt(Math.max(1, ev.energy)) * 0.6, 10, 400);
2718
- this.lightDur = 0.35;
2719
- this.lightT = this.lightDur;
2720
- }
2549
+ if (options.visual !== false) this.playExplosion(ev, options);
2550
+ this.vfx.push(p, ev.radius, 4);
2721
2551
  const c = this.v1;
2722
2552
  for (const pc of this.pieceSys.list) {
2723
2553
  this.pieceSys.centerOf(pc, c);
@@ -2736,11 +2566,23 @@ var Renderer = class {
2736
2566
  this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(ev.energy) * 0.08));
2737
2567
  this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1));
2738
2568
  }
2739
- updateLight(dt) {
2740
- if (!this.light || this.lightT <= 0) return;
2741
- this.lightT = Math.max(0, this.lightT - dt);
2742
- const t = this.lightT / this.lightDur;
2743
- 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);
2744
2586
  }
2745
2587
  // ----------------------------------------------------------------------------------------------------------------------------------
2746
2588
  // Fire, heat, char, melt, leaks
@@ -2778,8 +2620,8 @@ var Renderer = class {
2778
2620
  }
2779
2621
  const fire = this.fires.get(key);
2780
2622
  if (fire) {
2781
- if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def, dt);
2782
- 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);
2783
2625
  }
2784
2626
  const hot = def?.visual?.glows === true && ps.temperature > GLOW_FROM_C;
2785
2627
  if (!ps.burning && !hot && Math.abs(ps.char - pv.charApplied) < 0.01 && Math.abs(ps.melt - pv.meltApplied) < 0.01) {
@@ -2800,43 +2642,25 @@ var Renderer = class {
2800
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);
2801
2643
  } else {
2802
2644
  this.ground.add("ash", c.x, c.z, clamp(footprint * 0.35, 0.04, 0.5), 1907224, objectId);
2803
- 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);
2804
2646
  }
2805
2647
  }
2806
- emitFire(f, pv, def, dt) {
2648
+ emitFire(f, pv, def) {
2807
2649
  const bb = this.box;
2808
2650
  bb.makeEmpty();
2809
2651
  const tmp = new this.THREE.Box3();
2810
2652
  for (const mv of pv.meshes) if (this.visuals.shown(mv)) bb.union(this.visuals.worldBox(mv, tmp));
2811
- let cx = f.point[0];
2812
- let cy = f.point[1];
2813
- let cz = f.point[2];
2814
- let sx = 0.2;
2815
- let sy = 0.2;
2816
- let sz = 0.2;
2817
2653
  if (!bb.isEmpty()) {
2818
2654
  const c = bb.getCenter(this.v1);
2819
- const s = bb.getSize(this.v2);
2820
- cx = c.x;
2821
- cy = bb.min.y;
2822
- cz = c.z;
2823
- sx = s.x;
2824
- sy = s.y;
2825
- sz = s.z;
2826
- }
2827
- const vol = clamp(Math.cbrt(Math.max(sx * sy * sz, 1e-5)), 0.05, 1.5);
2828
- f.accFire += (18 + 70 * vol) * dt;
2829
- f.accSmoke += (6 + 24 * vol) * dt;
2830
- const tone = SMOKE_TONES[def?.thermal.smoke ?? "grey"];
2831
- const rnd = this.rng;
2832
- while (f.accFire >= 1) {
2833
- f.accFire -= 1;
2834
- 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 });
2835
- }
2836
- while (f.accSmoke >= 1) {
2837
- f.accSmoke -= 1;
2838
- 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;
2839
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 } : {} });
2840
2664
  }
2841
2665
  updateLeaks(dt) {
2842
2666
  for (let i = this.leaks.length - 1; i >= 0; i--) {
@@ -2854,8 +2678,8 @@ var Renderer = class {
2854
2678
  while (l.acc >= 1) {
2855
2679
  l.acc -= 1;
2856
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];
2857
- if (l.fluid === "gas") this.particles.spawn("steam", l.point, vel, { color, size: 0.5, alpha: 0.5 });
2858
- 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 });
2859
2683
  }
2860
2684
  }
2861
2685
  }
@@ -2868,10 +2692,10 @@ var Renderer = class {
2868
2692
  // Emit, clear, restore, stats, dispose
2869
2693
  // ----------------------------------------------------------------------------------------------------------------------------------
2870
2694
  emit(kind, point, normal, count, speed) {
2871
- 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);
2872
2696
  }
2873
- explode(point, energy, radius) {
2874
- 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);
2875
2699
  }
2876
2700
  restore(objectId) {
2877
2701
  const homes = /* @__PURE__ */ new Set([objectId]);
@@ -2879,11 +2703,12 @@ var Renderer = class {
2879
2703
  if (self?.isSplit) for (const pv of self.parts.values()) homes.add(pv.home);
2880
2704
  for (const home of homes) this.restoreHome(home);
2881
2705
  for (const [key, a] of [...this.active]) if (homes.has(a.object)) this.active.delete(key);
2882
- 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);
2883
2707
  for (let i = this.leaks.length - 1; i >= 0; i--) if (homes.has(this.leaks[i].object)) this.leaks.splice(i, 1);
2884
2708
  for (const home of homes) {
2885
2709
  this.fragments.removeHome(home);
2886
2710
  this.ground.removeHome(home);
2711
+ this.cages.removeObject(home);
2887
2712
  }
2888
2713
  }
2889
2714
  restoreHome(home) {
@@ -2907,7 +2732,7 @@ var Renderer = class {
2907
2732
  clear() {
2908
2733
  this.queue.length = 0;
2909
2734
  this.active.clear();
2910
- this.fires.clear();
2735
+ for (const key of [...this.fires.keys()]) this.stopFire(key);
2911
2736
  this.leaks.length = 0;
2912
2737
  for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
2913
2738
  for (const b of [...this.bounds.values()]) {
@@ -2920,15 +2745,20 @@ var Renderer = class {
2920
2745
  }
2921
2746
  this.fragments.clear();
2922
2747
  this.ground.clear();
2923
- this.particles.clear();
2924
- if (this.light) {
2925
- this.light.intensity = 0;
2926
- this.lightT = 0;
2927
- this.scene.remove(this.light);
2928
- }
2748
+ this.cages.clear();
2749
+ this.vfx.sprites().clear();
2750
+ this.ownVfx?.world.clear();
2929
2751
  }
2930
2752
  stats() {
2931
- 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;
2759
+ }
2760
+ rebarCount() {
2761
+ return this.cages.count;
2932
2762
  }
2933
2763
  dispose() {
2934
2764
  if (this.disposed) return;
@@ -2937,10 +2767,10 @@ var Renderer = class {
2937
2767
  for (const id of [...this.bounds.keys()]) this.unbind(id);
2938
2768
  this.fragments.dispose();
2939
2769
  this.ground.dispose();
2940
- this.particles.dispose();
2770
+ this.cages.dispose();
2771
+ this.stopLanding();
2941
2772
  this.visuals.dispose();
2942
- this.light?.dispose?.();
2943
- this.light = null;
2773
+ this.ownVfx?.dispose();
2944
2774
  this.disposed = true;
2945
2775
  }
2946
2776
  };