@adatechnology/game-vfx-three 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1344,11 +1344,231 @@ var GameVfxThree = (() => {
1344
1344
  glow.outIntensity = glow.i * Math.pow(1 - age, glow.curve) * flicker;
1345
1345
  }
1346
1346
  }
1347
+ var LEHMER_MULTIPLIER = 16807;
1348
+ var LEHMER_MODULUS = 2147483647;
1349
+ var HASH_MIX = 2654435761;
1350
+ function normalizeLehmerSeed(seed) {
1351
+ const whole = Math.floor(Math.abs(seed)) % (LEHMER_MODULUS - 1);
1352
+ return whole === 0 ? 1 : whole;
1353
+ }
1354
+ function lehmer(seed) {
1355
+ let state = normalizeLehmerSeed(seed);
1356
+ const next = () => {
1357
+ state = state * LEHMER_MULTIPLIER % LEHMER_MODULUS;
1358
+ return (state - 1) / (LEHMER_MODULUS - 1);
1359
+ };
1360
+ return {
1361
+ next,
1362
+ between: (min2, max2) => min2 + (max2 - min2) * next(),
1363
+ direction(out) {
1364
+ const up = next() * 2 - 1;
1365
+ const angle = next() * Math.PI * 2;
1366
+ const ring2 = Math.sqrt(1 - up * up);
1367
+ out.x = ring2 * Math.cos(angle);
1368
+ out.y = up;
1369
+ out.z = ring2 * Math.sin(angle);
1370
+ return out;
1371
+ },
1372
+ get state() {
1373
+ return state;
1374
+ }
1375
+ };
1376
+ }
1377
+ function mixEventSeed(eventSeed) {
1378
+ let mixed = Math.imul((eventSeed | 0) ^ HASH_MIX, 2246822507);
1379
+ mixed ^= mixed >>> 13;
1380
+ mixed = Math.imul(mixed, 3266489909);
1381
+ mixed ^= mixed >>> 16;
1382
+ return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1;
1383
+ }
1384
+ function streamFor(eventSeed) {
1385
+ return lehmer(mixEventSeed(eventSeed));
1386
+ }
1387
+ var SKIN = 5e-3;
1388
+ var MAX_PAD = 0.45;
1389
+ function rotation(q) {
1390
+ const [x, y, z, w] = q ?? [0, 0, 0, 1];
1391
+ const n = Math.hypot(x, y, z, w) || 1;
1392
+ const qx = x / n;
1393
+ const qy = y / n;
1394
+ const qz = z / n;
1395
+ const qw = w / n;
1396
+ return new Float64Array([
1397
+ 1 - 2 * (qy * qy + qz * qz),
1398
+ 2 * (qx * qy - qz * qw),
1399
+ 2 * (qx * qz + qy * qw),
1400
+ 2 * (qx * qy + qz * qw),
1401
+ 1 - 2 * (qx * qx + qz * qz),
1402
+ 2 * (qy * qz - qx * qw),
1403
+ 2 * (qx * qz - qy * qw),
1404
+ 2 * (qy * qz + qx * qw),
1405
+ 1 - 2 * (qx * qx + qy * qy)
1406
+ ]);
1407
+ }
1408
+ var Enclosures = class {
1409
+ constructor(seed = 20973) {
1410
+ __publicField(this, "spaces", /* @__PURE__ */ new Map());
1411
+ __publicField(this, "list", []);
1412
+ __publicField(this, "random");
1413
+ __publicField(this, "out", { x: 0, y: 0, z: 0, nx: 0, ny: 0, nz: 0 });
1414
+ __publicField(this, "l0", new Float64Array(3));
1415
+ __publicField(this, "l1", new Float64Array(3));
1416
+ __publicField(this, "q", new Float64Array(3));
1417
+ this.random = lehmer(seed);
1418
+ }
1419
+ get empty() {
1420
+ return this.list.length === 0;
1421
+ }
1422
+ get size() {
1423
+ return this.list.length;
1424
+ }
1425
+ /** Declares, moves or (with `null`) ends the space `id`. */
1426
+ set(id, enclosure) {
1427
+ if (!enclosure) {
1428
+ if (this.spaces.delete(id)) this.list = [...this.spaces.values()];
1429
+ return;
1430
+ }
1431
+ const [hx, hy, hz] = enclosure.halfSize;
1432
+ const space = {
1433
+ cx: enclosure.center[0],
1434
+ cy: enclosure.center[1],
1435
+ cz: enclosure.center[2],
1436
+ hx: Math.abs(hx),
1437
+ hy: Math.abs(hy),
1438
+ hz: Math.abs(hz),
1439
+ r: rotation(enclosure.orientation),
1440
+ radius: Math.hypot(hx, hy, hz),
1441
+ openings: enclosure.openings ?? [],
1442
+ leak: Math.max(0, enclosure.leak ?? 0)
1443
+ };
1444
+ const old = this.spaces.get(id);
1445
+ this.spaces.set(id, space);
1446
+ const at3 = old ? this.list.indexOf(old) : -1;
1447
+ if (at3 >= 0) this.list[at3] = space;
1448
+ else this.list.push(space);
1449
+ }
1450
+ get(id) {
1451
+ const s = this.spaces.get(id);
1452
+ if (!s) return void 0;
1453
+ return { center: [s.cx, s.cy, s.cz], halfSize: [s.hx, s.hy, s.hz], openings: s.openings, leak: s.leak };
1454
+ }
1455
+ ids() {
1456
+ return [...this.spaces.keys()];
1457
+ }
1458
+ clear() {
1459
+ this.spaces.clear();
1460
+ this.list = [];
1461
+ }
1462
+ /** Whether a point is inside any space (on the inside of its walls). */
1463
+ inside(x, y, z) {
1464
+ for (const s of this.list) {
1465
+ this.toLocal(s, x, y, z, this.l0);
1466
+ if (Math.abs(this.l0[0]) <= s.hx && Math.abs(this.l0[1]) <= s.hy && Math.abs(this.l0[2]) <= s.hz) return true;
1467
+ }
1468
+ return false;
1469
+ }
1470
+ toLocal(s, x, y, z, out) {
1471
+ const dx = x - s.cx;
1472
+ const dy = y - s.cy;
1473
+ const dz = z - s.cz;
1474
+ const r = s.r;
1475
+ out[0] = r[0] * dx + r[3] * dy + r[6] * dz;
1476
+ out[1] = r[1] * dx + r[4] * dy + r[7] * dz;
1477
+ out[2] = r[2] * dx + r[5] * dy + r[8] * dz;
1478
+ }
1479
+ /** Passes at `rate` per second. */
1480
+ chance(rate, dt) {
1481
+ return rate > 0 && this.random.next() < 1 - Math.exp(-rate * dt);
1482
+ }
1483
+ /** The opening the wall point `q` (local) is in lets it through now. */
1484
+ through(s, q, dt) {
1485
+ for (const o of s.openings) {
1486
+ if (Math.abs(q[0] - o.center[0]) > o.halfSize[0] || Math.abs(q[1] - o.center[1]) > o.halfSize[1] || Math.abs(q[2] - o.center[2]) > o.halfSize[2]) continue;
1487
+ if (o.rate === void 0 || this.chance(o.rate, dt)) return true;
1488
+ }
1489
+ return this.chance(s.leak, dt);
1490
+ }
1491
+ confine(x0, y0, z0, x1, y1, z1, pad, dt) {
1492
+ const list = this.list;
1493
+ if (list.length === 0) return false;
1494
+ const p = Math.min(Math.max(0, pad), MAX_PAD);
1495
+ const l0 = this.l0;
1496
+ const l1 = this.l1;
1497
+ const q = this.q;
1498
+ for (const s of list) {
1499
+ const reach = s.radius + p + 0.05;
1500
+ const dx = x1 - s.cx;
1501
+ const dy = y1 - s.cy;
1502
+ const dz = z1 - s.cz;
1503
+ if (dx * dx + dy * dy + dz * dz > reach * reach) {
1504
+ const ex = x0 - s.cx;
1505
+ const ey = y0 - s.cy;
1506
+ const ez = z0 - s.cz;
1507
+ if (ex * ex + ey * ey + ez * ez > reach * reach) continue;
1508
+ }
1509
+ this.toLocal(s, x0, y0, z0, l0);
1510
+ this.toLocal(s, x1, y1, z1, l1);
1511
+ const h = [s.hx, s.hy, s.hz];
1512
+ const in0 = Math.abs(l0[0]) <= s.hx && Math.abs(l0[1]) <= s.hy && Math.abs(l0[2]) <= s.hz;
1513
+ let axis2 = 0;
1514
+ let sign = 1;
1515
+ if (in0) {
1516
+ if (Math.abs(l1[0]) <= s.hx && Math.abs(l1[1]) <= s.hy && Math.abs(l1[2]) <= s.hz) continue;
1517
+ for (let a = 0; a < 3; a++) q[a] = Math.min(h[a], Math.max(-h[a], l1[a]));
1518
+ if (this.through(s, q, dt)) continue;
1519
+ let worst = -Infinity;
1520
+ for (let a = 0; a < 3; a++) {
1521
+ const over = Math.abs(l1[a]) - h[a];
1522
+ if (over > worst) {
1523
+ worst = over;
1524
+ axis2 = a;
1525
+ }
1526
+ }
1527
+ sign = l1[axis2] < 0 ? -1 : 1;
1528
+ for (let a = 0; a < 3; a++) l1[a] = Math.min(h[a] - SKIN, Math.max(-(h[a] - SKIN), l1[a]));
1529
+ sign = -sign;
1530
+ } else {
1531
+ const padded0 = Math.abs(l0[0]) <= s.hx + p && Math.abs(l0[1]) <= s.hy + p && Math.abs(l0[2]) <= s.hz + p;
1532
+ const bound = padded0 ? 0 : p;
1533
+ if (!(Math.abs(l1[0]) <= s.hx + bound && Math.abs(l1[1]) <= s.hy + bound && Math.abs(l1[2]) <= s.hz + bound)) continue;
1534
+ for (let a = 0; a < 3; a++) q[a] = Math.min(h[a], Math.max(-h[a], l1[a]));
1535
+ if (this.through(s, q, dt)) continue;
1536
+ let worst = -Infinity;
1537
+ for (let a = 0; a < 3; a++) {
1538
+ const over = Math.abs(l0[a]) - (h[a] + bound);
1539
+ if (over > worst) {
1540
+ worst = over;
1541
+ axis2 = a;
1542
+ }
1543
+ }
1544
+ sign = l0[axis2] < 0 ? -1 : 1;
1545
+ l1[axis2] = sign * (h[axis2] + bound + SKIN);
1546
+ }
1547
+ const r = s.r;
1548
+ const out = this.out;
1549
+ out.x = s.cx + r[0] * l1[0] + r[1] * l1[1] + r[2] * l1[2];
1550
+ out.y = s.cy + r[3] * l1[0] + r[4] * l1[1] + r[5] * l1[2];
1551
+ out.z = s.cz + r[6] * l1[0] + r[7] * l1[1] + r[8] * l1[2];
1552
+ out.nx = r[axis2] * sign;
1553
+ out.ny = r[3 + axis2] * sign;
1554
+ out.nz = r[6 + axis2] * sign;
1555
+ return true;
1556
+ }
1557
+ return false;
1558
+ }
1559
+ };
1560
+ function slideAlong(out, vx2, vy2, vz2) {
1561
+ const into = vx2 * out.nx + vy2 * out.ny + vz2 * out.nz;
1562
+ if (into >= 0) return [vx2, vy2, vz2];
1563
+ return [vx2 - into * out.nx, vy2 - into * out.ny, vz2 - into * out.nz];
1564
+ }
1347
1565
  var FLOOR_FRACTION = 0.4;
1348
1566
  var FLOOR_BOUNCE = -0.2;
1349
1567
  var ARRIVAL_FRAME = 30;
1350
1568
  var ARRIVAL_BASE = 0.3;
1351
1569
  var GROWTH_CAP_SECONDS = 3.5;
1570
+ var CARRIED_KINDS = [true, true, false, true, false];
1571
+ var PAD_OF_SIZE = 0.35;
1352
1572
  function createPuffPool(capacity) {
1353
1573
  const placeholder = { next: () => 0, between: (min2) => min2, direction: (out) => out, state: 0 };
1354
1574
  return new OrderedPool(capacity, () => ({
@@ -1443,13 +1663,24 @@ var GameVfxThree = (() => {
1443
1663
  if (puff3.vy < 0) puff3.vy *= FLOOR_BOUNCE;
1444
1664
  }
1445
1665
  }
1446
- function updatePuffs(pool, dt, wind) {
1666
+ function updatePuffs(pool, dt, wind, confiner) {
1447
1667
  pool.compact(ageOrKeep2, dt);
1668
+ const confining = confiner !== void 0 && !confiner.empty;
1448
1669
  for (const puff3 of pool.items) {
1449
1670
  const age = puff3.t / puff3.life;
1450
1671
  const heat = puff3.h0 > 0 ? puff3.h0 * Math.pow(Math.max(0, 1 - puff3.t / puff3.hl), 1.25) : 0;
1672
+ const x0 = puff3.x;
1673
+ const y0 = puff3.y;
1674
+ const z0 = puff3.z;
1451
1675
  if (puff3.ring) moveOnRing(puff3, puff3.ring, wind);
1452
1676
  else moveFree(puff3, dt, age, heat, wind);
1677
+ if (confining && CARRIED_KINDS[puff3.kind] && confiner.confine(x0, y0, z0, puff3.x, puff3.y, puff3.z, puff3.size * PAD_OF_SIZE, dt)) {
1678
+ const out = confiner.out;
1679
+ puff3.x = out.x;
1680
+ puff3.y = out.y;
1681
+ puff3.z = out.z;
1682
+ [puff3.vx, puff3.vy, puff3.vz] = slideAlong(out, puff3.vx, puff3.vy, puff3.vz);
1683
+ }
1453
1684
  const size = puff3.s0 + (puff3.s1 - puff3.s0) * (1 - Math.exp(-puff3.t * puff3.gk)) + puff3.sg * Math.min(puff3.t, GROWTH_CAP_SECONDS);
1454
1685
  puff3.size = size * Math.min(1, puff3.t * ARRIVAL_FRAME + ARRIVAL_BASE);
1455
1686
  puff3.age = age;
@@ -1975,10 +2206,10 @@ var GameVfxThree = (() => {
1975
2206
  ])
1976
2207
  ]
1977
2208
  };
1978
- function localToWorld(translation, rotation, local2) {
2209
+ function localToWorld(translation, rotation2, local2) {
1979
2210
  const [x, y, z] = local2;
1980
- const [a, c, e] = [Math.cos(rotation[0]), Math.cos(rotation[1]), Math.cos(rotation[2])];
1981
- const [b, d, f] = [Math.sin(rotation[0]), Math.sin(rotation[1]), Math.sin(rotation[2])];
2211
+ const [a, c, e] = [Math.cos(rotation2[0]), Math.cos(rotation2[1]), Math.cos(rotation2[2])];
2212
+ const [b, d, f] = [Math.sin(rotation2[0]), Math.sin(rotation2[1]), Math.sin(rotation2[2])];
1982
2213
  const [ae, af, be, bf] = [a * e, a * f, b * e, b * f];
1983
2214
  return [
1984
2215
  c * e * x + -c * f * y + d * z + translation[0],
@@ -3641,46 +3872,6 @@ var GameVfxThree = (() => {
3641
3872
  var ALL_EFFECTS = [BLOOD_HIT, HEADSHOT, DISMEMBER, BLOATER_CLOUD, SCREAMER_SHRIEK, CORPSE_DECAY, REANIMATION, EYES_IN_FOG, MUZZLE_FLASH, IMPACT_BY_MATERIAL, PENETRATION, CASINGS_AND_MAGAZINE, MOLOTOV, GRENADE, EXPLOSIVE_BARREL, SHOPFRONT_GLASS, DOOR_STAGES, BUILDING_COLLAPSE, BLOCK_WALL, METAL_DOOR_EFFECT, IRON_RAILING, WOODEN_FENCE, HIT_POINT_BREAKAGE, PRESSURE_CYLINDERS, BUILDING_FIRE, RAIN_AND_PUDDLES, FOG_AND_ASH, SCRAPING_SPARKS, ENGINE_SMOKE, FUEL_LEAK, CAR_EXPLOSION, WINDSHIELD, HEADLIGHTS_IN_RAIN, DAMAGE_AND_BITE, INFECTION_PHASES, PANIC, BLOATER_BLINDNESS, CAMPFIRE, COUGH_AND_BREATH];
3642
3873
  var WORKSHOP_EFFECTS = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]));
3643
3874
  var GAME_EFFECTS = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS, ...VEHICLE_EFFECTS].map((effect) => [effect.id, effect])) };
3644
- var LEHMER_MULTIPLIER = 16807;
3645
- var LEHMER_MODULUS = 2147483647;
3646
- var HASH_MIX = 2654435761;
3647
- function normalizeLehmerSeed(seed) {
3648
- const whole = Math.floor(Math.abs(seed)) % (LEHMER_MODULUS - 1);
3649
- return whole === 0 ? 1 : whole;
3650
- }
3651
- function lehmer(seed) {
3652
- let state = normalizeLehmerSeed(seed);
3653
- const next = () => {
3654
- state = state * LEHMER_MULTIPLIER % LEHMER_MODULUS;
3655
- return (state - 1) / (LEHMER_MODULUS - 1);
3656
- };
3657
- return {
3658
- next,
3659
- between: (min2, max2) => min2 + (max2 - min2) * next(),
3660
- direction(out) {
3661
- const up = next() * 2 - 1;
3662
- const angle = next() * Math.PI * 2;
3663
- const ring2 = Math.sqrt(1 - up * up);
3664
- out.x = ring2 * Math.cos(angle);
3665
- out.y = up;
3666
- out.z = ring2 * Math.sin(angle);
3667
- return out;
3668
- },
3669
- get state() {
3670
- return state;
3671
- }
3672
- };
3673
- }
3674
- function mixEventSeed(eventSeed) {
3675
- let mixed = Math.imul((eventSeed | 0) ^ HASH_MIX, 2246822507);
3676
- mixed ^= mixed >>> 13;
3677
- mixed = Math.imul(mixed, 3266489909);
3678
- mixed ^= mixed >>> 16;
3679
- return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1;
3680
- }
3681
- function streamFor(eventSeed) {
3682
- return lehmer(mixEventSeed(eventSeed));
3683
- }
3684
3875
  var TABLE = {
3685
3876
  concrete: { dust: 9078138, chip: 7236195, spark: 2 },
3686
3877
  metal: { dust: 0, chip: 0, spark: 18 },
@@ -4984,8 +5175,8 @@ var GameVfxThree = (() => {
4984
5175
  function addScorch(store, random, x, z, radius) {
4985
5176
  const variant = Math.floor(random.next() * SCORCH_VARIANTS);
4986
5177
  const lift = 0.03 + nextLift(store) * 1e-3;
4987
- const rotation = random.next() * 6;
4988
- store.add({ kind: "scorch", x, z, radius, variant, lift, rotation });
5178
+ const rotation2 = random.next() * 6;
5179
+ store.add({ kind: "scorch", x, z, radius, variant, lift, rotation: rotation2 });
4989
5180
  }
4990
5181
  function addRimRocks(store, ground, random, x, z, radius) {
4991
5182
  const count = Math.round(6 + radius * 7);
@@ -5014,8 +5205,8 @@ var GameVfxThree = (() => {
5014
5205
  function addCrater(store, ground, random, spec) {
5015
5206
  ground?.dent(random, spec.x, spec.z, spec.radius, spec.depth);
5016
5207
  if (!spec.marks) return;
5017
- const rotation = random.next() * 6;
5018
- store.add({ kind: "dirt", x: spec.x, z: spec.z, radius: spec.radius * 1.02, lift: 0.045, rotation });
5208
+ const rotation2 = random.next() * 6;
5209
+ store.add({ kind: "dirt", x: spec.x, z: spec.z, radius: spec.radius * 1.02, lift: 0.045, rotation: rotation2 });
5019
5210
  addRimRocks(store, ground, random, spec.x, spec.z, spec.radius);
5020
5211
  }
5021
5212
  function addBloodSplat(store, ground, random, spec) {
@@ -5571,6 +5762,7 @@ var GameVfxThree = (() => {
5571
5762
  state.gas = Math.max(0, state.gas - dt / DECAY.gasSeconds);
5572
5763
  state.beat *= Math.exp(-DECAY.beat * dt);
5573
5764
  }
5765
+ var PAD_OF_SIZE2 = 0.35;
5574
5766
  var clamp4 = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
5575
5767
  var lerp4 = (a, b, t) => a + (b - a) * t;
5576
5768
  function rgb(hex) {
@@ -5730,9 +5922,20 @@ var GameVfxThree = (() => {
5730
5922
  vy2 *= k;
5731
5923
  vz2 *= k;
5732
5924
  }
5733
- const x = this.position[i3] + vx2 * dt;
5925
+ let x = this.position[i3] + vx2 * dt;
5734
5926
  let y = this.position[i3 + 1] + vy2 * dt;
5735
- const z = this.position[i3 + 2] + vz2 * dt;
5927
+ let z = this.position[i3 + 2] + vz2 * dt;
5928
+ const confiner = host.confiner;
5929
+ if (def.carried && confiner && !confiner.empty) {
5930
+ const pad = this.size0[i] * lerp4(1, def.grow, t) * PAD_OF_SIZE2;
5931
+ if (confiner.confine(this.position[i3], this.position[i3 + 1], this.position[i3 + 2], x, y, z, pad, dt)) {
5932
+ const out = confiner.out;
5933
+ x = out.x;
5934
+ y = out.y;
5935
+ z = out.z;
5936
+ [vx2, vy2, vz2] = slideAlong(out, vx2, vy2, vz2);
5937
+ }
5938
+ }
5736
5939
  if (def.ground !== "none") {
5737
5940
  const own = this.floor[i];
5738
5941
  const gy = Number.isNaN(own) ? host.groundAt(x, z) + 5e-3 : own;
@@ -5818,7 +6021,8 @@ var GameVfxThree = (() => {
5818
6021
  alpha: 0.35,
5819
6022
  fadeIn: 0.15,
5820
6023
  cone: 0.6,
5821
- ground: "none"
6024
+ ground: "none",
6025
+ carried: true
5822
6026
  },
5823
6027
  sparks: {
5824
6028
  id: "sparks",
@@ -5862,7 +6066,8 @@ var GameVfxThree = (() => {
5862
6066
  alpha: 0.4,
5863
6067
  fadeIn: 0.1,
5864
6068
  cone: 0.4,
5865
- ground: "none"
6069
+ ground: "none",
6070
+ carried: true
5866
6071
  },
5867
6072
  shards: {
5868
6073
  id: "shards",
@@ -6020,7 +6225,8 @@ var GameVfxThree = (() => {
6020
6225
  fadeIn: 0.1,
6021
6226
  cone: 0.5,
6022
6227
  ground: "stop",
6023
- flutter: 0.6
6228
+ flutter: 0.6,
6229
+ carried: true
6024
6230
  },
6025
6231
  steam: {
6026
6232
  id: "steam",
@@ -6042,7 +6248,8 @@ var GameVfxThree = (() => {
6042
6248
  alpha: 0.35,
6043
6249
  fadeIn: 0.15,
6044
6250
  cone: 0.6,
6045
- ground: "none"
6251
+ ground: "none",
6252
+ carried: true
6046
6253
  },
6047
6254
  flash: {
6048
6255
  id: "flash",
@@ -6175,7 +6382,8 @@ var GameVfxThree = (() => {
6175
6382
  const host = {
6176
6383
  groundAt: this.options.groundAt,
6177
6384
  gravity: this.options.gravity,
6178
- land: (owner, x, y, z, color, tag) => this.listeners.get(owner)?.(x, y, z, color, tag)
6385
+ land: (owner, x, y, z, color, tag) => this.listeners.get(owner)?.(x, y, z, color, tag),
6386
+ ...this.options.confiner ? { confiner: this.options.confiner } : {}
6179
6387
  };
6180
6388
  if (this.additive.live > 0 || this.additive.drawCount > 0) this.additive.update(dt, host);
6181
6389
  if (this.normal.live > 0 || this.normal.drawCount > 0) this.normal.update(dt, host);
@@ -6381,8 +6589,8 @@ var GameVfxThree = (() => {
6381
6589
  * Takes away the marks inside a box: world `center`, `half` extents along the axes of the world `rotation` (a quaternion, x y z w; none means the
6382
6590
  * world axes). The surface they were on is gone (a brick came out of a wall with its bullet holes). Returns how many went.
6383
6591
  */
6384
- clearInside(center, half, rotation = [0, 0, 0, 1]) {
6385
- const qx = -rotation[0], qy = -rotation[1], qz = -rotation[2], qw = rotation[3];
6592
+ clearInside(center, half, rotation2 = [0, 0, 0, 1]) {
6593
+ const qx = -rotation2[0], qy = -rotation2[1], qz = -rotation2[2], qw = rotation2[3];
6386
6594
  let gone = 0;
6387
6595
  for (const decal2 of this.decals) {
6388
6596
  if (!decal2.on) continue;
@@ -6503,10 +6711,14 @@ var GameVfxThree = (() => {
6503
6711
  var DEFAULT_FADE_IN = 0.1;
6504
6712
  var FLOATS_PER_VECTOR = 3;
6505
6713
  var BYTE_MAX2 = 255;
6714
+ var CARRIED_GRAVITY = 3;
6715
+ var PAD_OF_SIZE3 = 0.35;
6506
6716
  var SpritePool = class {
6507
- constructor(capacity, context) {
6717
+ /** `carried`: the normal-blend pool (smoke, dust, steam), whose light sprites go round the closed spaces; the additive one (flames, sparks) does not. */
6718
+ constructor(capacity, context, carried = false) {
6508
6719
  __publicField(this, "capacity", capacity);
6509
6720
  __publicField(this, "context", context);
6721
+ __publicField(this, "carried", carried);
6510
6722
  __publicField(this, "count", 0);
6511
6723
  __publicField(this, "position");
6512
6724
  __publicField(this, "velocity");
@@ -6606,6 +6818,7 @@ var GameVfxThree = (() => {
6606
6818
  }
6607
6819
  update(dt, leaveDecal) {
6608
6820
  const { position, velocity } = this;
6821
+ const confiner = this.context.confiner;
6609
6822
  for (let index = this.count - 1; index >= 0; index--) {
6610
6823
  this.life[index] -= dt;
6611
6824
  if (this.life[index] <= 0) {
@@ -6619,9 +6832,25 @@ var GameVfxThree = (() => {
6619
6832
  velocity[offset] *= factor;
6620
6833
  velocity[offset + 1] *= factor;
6621
6834
  velocity[offset + 2] *= factor;
6835
+ const x0 = position[offset];
6836
+ const y0 = position[offset + 1];
6837
+ const z0 = position[offset + 2];
6622
6838
  position[offset] += velocity[offset] * dt;
6623
6839
  position[offset + 1] += velocity[offset + 1] * dt;
6624
6840
  position[offset + 2] += velocity[offset + 2] * dt;
6841
+ if (this.carried && confiner && !confiner.empty && Math.abs(this.gravity[index]) < CARRIED_GRAVITY) {
6842
+ const size = this.startSize[index] * (1 + (this.growth[index] - 1) * age);
6843
+ if (confiner.confine(x0, y0, z0, position[offset], position[offset + 1], position[offset + 2], size * PAD_OF_SIZE3, dt)) {
6844
+ const out = confiner.out;
6845
+ position[offset] = out.x;
6846
+ position[offset + 1] = out.y;
6847
+ position[offset + 2] = out.z;
6848
+ const [vx2, vy2, vz2] = slideAlong(out, velocity[offset], velocity[offset + 1], velocity[offset + 2]);
6849
+ velocity[offset] = vx2;
6850
+ velocity[offset + 1] = vy2;
6851
+ velocity[offset + 2] = vz2;
6852
+ }
6853
+ }
6625
6854
  const mode = this.groundMode[index];
6626
6855
  if (mode) {
6627
6856
  const groundHeight = this.context.groundAt(position[offset], position[offset + 2]);
@@ -6898,9 +7127,9 @@ var GameVfxThree = (() => {
6898
7127
  __publicField(this, "placement");
6899
7128
  __publicField(this, "accumulators", /* @__PURE__ */ new Map());
6900
7129
  const profile = options.profile ?? PROFILES.high;
6901
- this.context = { random: options.random, groundAt: options.groundAt ?? workshopGround, quality: { level: 0, mult: 1 }, trace: options.trace };
7130
+ this.context = { random: options.random, groundAt: options.groundAt ?? workshopGround, quality: { level: 0, mult: 1 }, trace: options.trace, ...options.confiner ? { confiner: options.confiner } : {} };
6902
7131
  this.add = new SpritePool(profile.sprites.add, this.context);
6903
- this.normal = new SpritePool(profile.sprites.normal, this.context);
7132
+ this.normal = new SpritePool(profile.sprites.normal, this.context, true);
6904
7133
  this.streaks = new StreakPool(profile.streaks, this.context);
6905
7134
  this.debris = new DebrisPool(this.context);
6906
7135
  this.decals = new DecalPool(this.context);
@@ -7088,12 +7317,14 @@ var GameVfxThree = (() => {
7088
7317
  const groundAt = options.groundAt ?? (ground ? (x, z) => ground.heightAt(x, z) : () => 0);
7089
7318
  const handles = { next: 0, events: 0 };
7090
7319
  const low = options.profile === "low";
7320
+ const enclosures = new Enclosures(20973 + (options.seed ?? 0));
7091
7321
  const spriteSystem = new SpriteKindSystem({
7092
7322
  capacity: options.sprites?.capacity ?? (low ? 1250 : 2500),
7093
7323
  density: options.sprites?.density ?? (low ? 0.6 : 1),
7094
7324
  seed: 2654435761 + (options.seed ?? 0),
7095
7325
  groundAt,
7096
7326
  gravity: options.gravity ?? DEFAULT_GRAVITY,
7327
+ confiner: enclosures,
7097
7328
  ...options.sprites?.kinds ? { kinds: options.sprites.kinds } : {}
7098
7329
  });
7099
7330
  const core = {
@@ -7143,7 +7374,7 @@ var GameVfxThree = (() => {
7143
7374
  return [camera2.x, camera2.y, camera2.z];
7144
7375
  }, forward: (local2) => [local2[0] ?? 0, local2[1] ?? 0, local2[2] ?? 0] };
7145
7376
  function workshop() {
7146
- return engine ?? (engine = new WorkshopEngine({ random: sharedRandom, profile, groundAt }));
7377
+ return engine ?? (engine = new WorkshopEngine({ random: sharedRandom, profile, groundAt, confiner: enclosures }));
7147
7378
  }
7148
7379
  function effectPlayer() {
7149
7380
  return player ?? (player = createEffectPlayer({ engine: workshop(), effects, view }));
@@ -7202,6 +7433,10 @@ var GameVfxThree = (() => {
7202
7433
  puff3.vz += dz * strength;
7203
7434
  }
7204
7435
  },
7436
+ enclose(id, enclosure) {
7437
+ enclosures.set(id, enclosure);
7438
+ },
7439
+ enclosures: () => enclosures,
7205
7440
  stop(handle) {
7206
7441
  player?.stop(handle);
7207
7442
  const fires = core.fires;
@@ -7229,7 +7464,7 @@ var GameVfxThree = (() => {
7229
7464
  core.clock.time += dt;
7230
7465
  core.schedule.update(core.clock.time);
7231
7466
  updateRings(core.rings, dt);
7232
- updatePuffs(core.puffs, dt, wind);
7467
+ updatePuffs(core.puffs, dt, wind, enclosures);
7233
7468
  updateSparks(core.sparks, { dt, groundAt });
7234
7469
  updateGlows(core.glows, dt);
7235
7470
  updateShocks(core.shocks, core.puffs.items, dt);
@@ -8523,6 +8758,11 @@ void main(){
8523
8758
  var MAX_CONFORM_STEPS = 48;
8524
8759
  var MIN_CONFORM_STEPS = 16;
8525
8760
  var BLOOD_GROW_RATE = 1.2;
8761
+ var BLOOD_START_GROWTH = 0.05;
8762
+ var DEFAULT_MAX_GROUND_MARKS = 64;
8763
+ var DEFAULT_MAX_ROCKS = 512;
8764
+ var DEFAULT_MAX_BLOOD = 256;
8765
+ var MAX_SPARE_GROUND_GEOMETRIES = 16;
8526
8766
  function createMarkLayer(THREE, options) {
8527
8767
  const group = new THREE.Group();
8528
8768
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -8546,92 +8786,146 @@ void main(){
8546
8786
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
8547
8787
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
8548
8788
  owned.push(bloodMaterial, bloodGeometry);
8549
- const rockMaterials = /* @__PURE__ */ new Map();
8550
- const blood = [];
8551
- let meshCount = 0;
8552
- function rockMaterial(color) {
8553
- let material = rockMaterials.get(color);
8554
- if (!material) {
8555
- material = new THREE.MeshStandardMaterial({ color, roughness: 0.9, metalness: 0, flatShading: true });
8556
- rockMaterials.set(color, material);
8557
- owned.push(material);
8789
+ const maxGroundMarks = options.maxGroundMarks ?? DEFAULT_MAX_GROUND_MARKS;
8790
+ const instanced = {};
8791
+ function rockMaterial() {
8792
+ if (!instanced.rockMaterial) {
8793
+ instanced.rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.9, metalness: 0, flatShading: true });
8794
+ owned.push(instanced.rockMaterial);
8558
8795
  }
8559
- return material;
8796
+ return instanced.rockMaterial;
8797
+ }
8798
+ function bloodMesh() {
8799
+ return instanced.blood ?? (instanced.blood = attach(createInstanced(bloodGeometry, bloodMaterial, options.maxBlood ?? DEFAULT_MAX_BLOOD)));
8800
+ }
8801
+ function rockMesh() {
8802
+ return instanced.rock ?? (instanced.rock = attach(createInstanced(options.rockGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
8803
+ }
8804
+ function plateMesh() {
8805
+ return instanced.plate ?? (instanced.plate = attach(createInstanced(options.plateGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
8806
+ }
8807
+ function attach(mesh2, castShadow = false) {
8808
+ mesh2.castShadow = castShadow;
8809
+ group.add(mesh2);
8810
+ return mesh2;
8811
+ }
8812
+ const groundMarks = [];
8813
+ const spareGeometries = [];
8814
+ const blood = [];
8815
+ const placed = { blood: 0, rock: 0, plate: 0 };
8816
+ const matrix = new THREE.Matrix4();
8817
+ const position = new THREE.Vector3();
8818
+ const quaternion = new THREE.Quaternion();
8819
+ const euler = new THREE.Euler();
8820
+ const scale = new THREE.Vector3();
8821
+ const color = new THREE.Color();
8822
+ const up = new THREE.Vector3(0, 1, 0);
8823
+ function createInstanced(geometry, material, capacity) {
8824
+ const mesh2 = new THREE.InstancedMesh(geometry, material, capacity);
8825
+ mesh2.count = 0;
8826
+ mesh2.frustumCulled = false;
8827
+ mesh2.receiveShadow = true;
8828
+ return mesh2;
8829
+ }
8830
+ function takeSpare(vertexCount) {
8831
+ const found = spareGeometries.findIndex((geometry) => geometry.getAttribute("position").count === vertexCount);
8832
+ return found < 0 ? void 0 : spareGeometries.splice(found, 1)[0];
8833
+ }
8834
+ function evictOldestGroundMark() {
8835
+ const oldest = groundMarks.shift();
8836
+ group.remove(oldest.mesh);
8837
+ spareGeometries.push(oldest.mesh.geometry);
8838
+ if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
8560
8839
  }
8561
8840
  function conform(mark, material) {
8562
8841
  const radius = mark.radius;
8563
8842
  const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
8564
- const geometry = new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
8565
- const position = geometry.attributes.position;
8843
+ if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
8844
+ const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
8845
+ const grid = geometry.attributes.position;
8846
+ const cell = 2 / steps;
8566
8847
  const cos2 = Math.cos(mark.rotation);
8567
8848
  const sin2 = Math.sin(mark.rotation);
8568
- for (let index = 0; index < position.count; index++) {
8569
- const localX = position.getX(index) * radius;
8570
- const localZ = position.getZ(index) * radius;
8849
+ for (let index = 0; index < grid.count; index++) {
8850
+ const localX = Math.fround(index % (steps + 1) * cell - 1) * radius;
8851
+ const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
8571
8852
  const worldX = mark.x + localX * cos2 - localZ * sin2;
8572
8853
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
8573
- position.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
8854
+ grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
8574
8855
  }
8856
+ grid.needsUpdate = true;
8575
8857
  geometry.computeVertexNormals();
8858
+ geometry.attributes.normal.needsUpdate = true;
8859
+ geometry.boundingSphere = null;
8860
+ geometry.boundingBox = null;
8576
8861
  const mesh2 = new THREE.Mesh(geometry, material);
8577
8862
  mesh2.receiveShadow = true;
8578
- return mesh2;
8863
+ group.add(mesh2);
8864
+ groundMarks.push({ mesh: mesh2, steps });
8579
8865
  }
8580
8866
  function addRock(mark) {
8581
- const mesh2 = new THREE.Mesh(mark.plate ? options.plateGeometry : options.rockGeometry, rockMaterial(mark.color));
8582
- mesh2.position.set(mark.x, mark.y, mark.z);
8583
- mesh2.scale.set(mark.scale[0], mark.scale[1], mark.scale[2]);
8584
- mesh2.rotation.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]);
8585
- mesh2.castShadow = true;
8586
- mesh2.receiveShadow = true;
8587
- return mesh2;
8867
+ const mesh2 = mark.plate ? plateMesh() : rockMesh();
8868
+ const slot = mark.plate ? "plate" : "rock";
8869
+ const index = placed[slot]++ % mesh2.instanceMatrix.count;
8870
+ quaternion.setFromEuler(euler.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]));
8871
+ mesh2.setMatrixAt(index, matrix.compose(position.set(mark.x, mark.y, mark.z), quaternion, scale.set(mark.scale[0], mark.scale[1], mark.scale[2])));
8872
+ mesh2.setColorAt(index, color.set(mark.color));
8873
+ mesh2.count = Math.min(placed[slot], mesh2.instanceMatrix.count);
8874
+ mesh2.instanceMatrix.needsUpdate = true;
8875
+ mesh2.instanceColor.needsUpdate = true;
8876
+ }
8877
+ function writeBlood(index, drawn, growth) {
8878
+ quaternion.setFromAxisAngle(up, drawn.rotation);
8879
+ const mesh2 = bloodMesh();
8880
+ mesh2.setMatrixAt(index, matrix.compose(position.set(drawn.position[0], drawn.position[1], drawn.position[2]), quaternion, scale.set(drawn.scaleX * growth, 1, drawn.scaleZ * growth)));
8881
+ mesh2.instanceMatrix.needsUpdate = true;
8588
8882
  }
8589
8883
  function addBlood(mark) {
8590
- const mesh2 = new THREE.Mesh(bloodGeometry, bloodMaterial);
8591
- mesh2.position.set(mark.x, mark.y, mark.z);
8592
- mesh2.scale.set(mark.scaleX, 1, mark.scaleZ);
8593
- mesh2.rotation.y = mark.rotation;
8594
- mesh2.receiveShadow = true;
8595
- mesh2.scale.multiplyScalar(0.05);
8596
- blood.push({ mesh: mesh2, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
8597
- return mesh2;
8884
+ const index = placed.blood++ % bloodMesh().instanceMatrix.count;
8885
+ const drawn = { position: [mark.x, mark.y, mark.z], rotation: mark.rotation, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 };
8886
+ blood[index] = drawn;
8887
+ writeBlood(index, drawn, BLOOD_START_GROWTH);
8888
+ bloodMesh().count = Math.min(placed.blood, bloodMesh().instanceMatrix.count);
8598
8889
  }
8599
8890
  function build(mark) {
8600
- if (mark.kind === "scorch") return conform(mark, scorchMaterials[mark.variant]);
8601
- if (mark.kind === "dirt") return conform(mark, dirtMaterial);
8602
- if (mark.kind === "rock") return addRock(mark);
8603
- return addBlood(mark);
8604
- }
8605
- function clearMeshes() {
8606
- for (const child of [...group.children]) {
8607
- group.remove(child);
8608
- const geometry = child.geometry;
8609
- if (geometry !== options.rockGeometry && geometry !== options.plateGeometry && geometry !== bloodGeometry) geometry.dispose();
8610
- }
8891
+ if (mark.kind === "scorch") conform(mark, scorchMaterials[mark.variant]);
8892
+ else if (mark.kind === "dirt") conform(mark, dirtMaterial);
8893
+ else if (mark.kind === "rock") addRock(mark);
8894
+ else addBlood(mark);
8895
+ }
8896
+ function clearMarks() {
8897
+ for (const drawn of groundMarks) group.remove(drawn.mesh);
8898
+ for (const drawn of groundMarks) drawn.mesh.geometry.dispose();
8899
+ groundMarks.length = 0;
8900
+ for (const geometry of spareGeometries) geometry.dispose();
8901
+ spareGeometries.length = 0;
8611
8902
  blood.length = 0;
8612
- meshCount = 0;
8903
+ placed.blood = placed.rock = placed.plate = 0;
8904
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) if (mesh2) mesh2.count = 0;
8613
8905
  }
8614
8906
  return {
8615
8907
  group,
8616
8908
  handle(event) {
8617
- if (event.type === "clear") {
8618
- clearMeshes();
8619
- return;
8620
- }
8621
- group.add(build(event.mark));
8622
- meshCount++;
8909
+ if (event.type === "clear") clearMarks();
8910
+ else build(event.mark);
8911
+ },
8912
+ prepare() {
8913
+ bloodMesh();
8914
+ rockMesh();
8915
+ plateMesh();
8623
8916
  },
8624
8917
  update(dt) {
8625
- for (const drawn of blood) {
8626
- if (drawn.age > 1) continue;
8918
+ for (let index = 0; index < blood.length; index++) {
8919
+ const drawn = blood[index];
8920
+ if (!drawn || drawn.age > 1) continue;
8627
8921
  drawn.age += dt * BLOOD_GROW_RATE;
8628
- const ease3 = 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5);
8629
- drawn.mesh.scale.set(drawn.scaleX * Math.max(0.05, ease3), 1, drawn.scaleZ * Math.max(0.05, ease3));
8922
+ writeBlood(index, drawn, Math.max(BLOOD_START_GROWTH, 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5)));
8630
8923
  }
8631
8924
  },
8632
- count: () => meshCount,
8925
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh2) => mesh2 && mesh2.count > 0).length,
8633
8926
  dispose() {
8634
- clearMeshes();
8927
+ clearMarks();
8928
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) mesh2?.dispose();
8635
8929
  for (const item of owned) item.dispose();
8636
8930
  }
8637
8931
  };
@@ -8640,18 +8934,22 @@ void main(){
8640
8934
  // src/mesh-layer.ts
8641
8935
  var SPHERE_DEFAULT_WIDTH = 8;
8642
8936
  var SPHERE_DEFAULT_HEIGHT = 6;
8937
+ var MAX_PARKED_MESHES_PER_KIND = 256;
8643
8938
  function geometryOf(THREE, particle) {
8644
8939
  switch (particle.shape) {
8645
8940
  case "cone":
8646
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
8941
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, false);
8647
8942
  case "cone_open":
8648
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
8943
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, true);
8649
8944
  case "sphere":
8650
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
8945
+ return new THREE.SphereGeometry(1, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
8651
8946
  case "icosahedron":
8652
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
8947
+ return new THREE.IcosahedronGeometry(1, particle.segments);
8653
8948
  }
8654
8949
  }
8950
+ function isCone(particle) {
8951
+ return particle.shape === "cone" || particle.shape === "cone_open";
8952
+ }
8655
8953
  function materialOf(THREE, particle) {
8656
8954
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
8657
8955
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -8662,51 +8960,82 @@ void main(){
8662
8960
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
8663
8961
  );
8664
8962
  }
8963
+ function geometryKeyOf(particle) {
8964
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
8965
+ }
8665
8966
  function createMeshLayer(THREE, pool) {
8666
8967
  const group = new THREE.Group();
8667
8968
  group.name = "workshop-meshes";
8668
8969
  const drawn = /* @__PURE__ */ new Map();
8970
+ const geometries = /* @__PURE__ */ new Map();
8971
+ const parked = /* @__PURE__ */ new Map();
8669
8972
  const frame = new THREE.Quaternion();
8670
8973
  const euler = new THREE.Euler();
8671
8974
  const turn = new THREE.Quaternion();
8672
8975
  const offset = new THREE.Vector3();
8673
- function release(mesh2) {
8674
- mesh2.removeFromParent();
8675
- mesh2.geometry.dispose();
8676
- mesh2.material.dispose();
8976
+ let stamp = 0;
8977
+ function geometryFor(particle) {
8978
+ const key = geometryKeyOf(particle);
8979
+ let geometry = geometries.get(key);
8980
+ if (!geometry) {
8981
+ geometry = geometryOf(THREE, particle);
8982
+ geometries.set(key, geometry);
8983
+ }
8984
+ return geometry;
8985
+ }
8986
+ function acquire(particle) {
8987
+ const parkingKey = `${geometryKeyOf(particle)}|${particle.lit}|${particle.additive}`;
8988
+ const mesh2 = parked.get(parkingKey)?.pop() ?? new THREE.Mesh(geometryFor(particle), materialOf(THREE, particle));
8989
+ mesh2.material.color.set(particle.color);
8990
+ mesh2.frustumCulled = false;
8991
+ mesh2.renderOrder = particle.additive ? 5 : 3;
8992
+ group.add(mesh2);
8993
+ return { mesh: mesh2, parkingKey, stamp };
8994
+ }
8995
+ function park(entry) {
8996
+ entry.mesh.removeFromParent();
8997
+ let free = parked.get(entry.parkingKey);
8998
+ if (!free) {
8999
+ free = [];
9000
+ parked.set(entry.parkingKey, free);
9001
+ }
9002
+ if (free.length < MAX_PARKED_MESHES_PER_KIND) free.push(entry.mesh);
9003
+ else entry.mesh.material.dispose();
8677
9004
  }
8678
9005
  return {
8679
9006
  group,
8680
9007
  sync() {
8681
- const live = /* @__PURE__ */ new Set();
9008
+ stamp++;
8682
9009
  for (const particle of pool.particles) {
8683
- live.add(particle.id);
8684
- let mesh2 = drawn.get(particle.id);
8685
- if (!mesh2) {
8686
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
8687
- mesh2.frustumCulled = false;
8688
- mesh2.renderOrder = particle.additive ? 5 : 3;
8689
- drawn.set(particle.id, mesh2);
8690
- group.add(mesh2);
9010
+ let entry = drawn.get(particle.id);
9011
+ if (!entry) {
9012
+ entry = acquire(particle);
9013
+ drawn.set(particle.id, entry);
8691
9014
  }
9015
+ entry.stamp = stamp;
9016
+ const mesh2 = entry.mesh;
8692
9017
  frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
8693
9018
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
8694
9019
  mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
8695
9020
  turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
8696
9021
  mesh2.quaternion.copy(frame).multiply(turn);
8697
- mesh2.scale.setScalar(particle.scale);
9022
+ mesh2.scale.set(particle.radius * particle.scale, (isCone(particle) ? particle.height : particle.radius) * particle.scale, particle.radius * particle.scale);
8698
9023
  mesh2.material.opacity = particle.opacity;
8699
9024
  }
8700
- for (const [id, mesh2] of drawn) {
8701
- if (live.has(id)) continue;
8702
- release(mesh2);
9025
+ for (const [id, entry] of drawn) {
9026
+ if (entry.stamp === stamp) continue;
9027
+ park(entry);
8703
9028
  drawn.delete(id);
8704
9029
  }
8705
9030
  return pool.count;
8706
9031
  },
8707
9032
  dispose() {
8708
- for (const mesh2 of drawn.values()) release(mesh2);
9033
+ for (const entry of drawn.values()) park(entry);
8709
9034
  drawn.clear();
9035
+ for (const free of parked.values()) for (const mesh2 of free) mesh2.material.dispose();
9036
+ parked.clear();
9037
+ for (const geometry of geometries.values()) geometry.dispose();
9038
+ geometries.clear();
8710
9039
  group.removeFromParent();
8711
9040
  }
8712
9041
  };
@@ -9291,7 +9620,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9291
9620
  const shocks = createShockLayer(THREE, pools.shocks);
9292
9621
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
9293
9622
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
9294
- const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry });
9623
+ const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
9295
9624
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
9296
9625
  const stopMarks = world.onMarks((event) => marks.handle(event));
9297
9626
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -9317,6 +9646,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9317
9646
  const position = options.camera.position;
9318
9647
  world.setCamera([position.x, position.y, position.z]);
9319
9648
  }
9649
+ function attachWorkshopLayers() {
9650
+ const engine = world.peekWorkshop();
9651
+ if (!engine || engine === workshopEngine) return;
9652
+ workshopLayers?.dispose();
9653
+ if (workshopLayers) root.remove(workshopLayers.group);
9654
+ workshopEngine = engine;
9655
+ workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
9656
+ root.add(workshopLayers.group);
9657
+ applyLayers();
9658
+ }
9320
9659
  function syncAll() {
9321
9660
  lastDrawn.puffs = puffs.sync();
9322
9661
  lastDrawn.sparks = sparks.sync();
@@ -9325,15 +9664,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9325
9664
  lastDrawn.shocks = shocks.sync();
9326
9665
  lastDrawn.sprites = sprites.sync();
9327
9666
  lights.sync();
9328
- const engine = world.peekWorkshop();
9329
- if (engine && engine !== workshopEngine) {
9330
- workshopLayers?.dispose();
9331
- if (workshopLayers) root.remove(workshopLayers.group);
9332
- workshopEngine = engine;
9333
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
9334
- root.add(workshopLayers.group);
9335
- applyLayers();
9336
- }
9667
+ attachWorkshopLayers();
9337
9668
  const workshopCalls = workshopLayers?.sync() ?? 0;
9338
9669
  const glow = pools.lights.glow;
9339
9670
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -9361,6 +9692,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9361
9692
  });
9362
9693
  },
9363
9694
  stop: (handle) => world.stop(handle),
9695
+ // a module's closed space (a shut car's cabin): the world's smoke, dust and steam go round it
9696
+ enclose: (id, enclosure) => world.enclose(id, enclosure),
9364
9697
  configure(settings) {
9365
9698
  const band = settings.lod_band;
9366
9699
  if (typeof band === "string" && band in LOD_BANDS) world.forceLod(LOD_BANDS[band] ?? null);
@@ -9386,6 +9719,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9386
9719
  },
9387
9720
  warmUpProbes() {
9388
9721
  releaseAnchors();
9722
+ marks.prepare();
9723
+ world.workshop();
9724
+ attachWorkshopLayers();
9389
9725
  const meshes = createMeshLayerProbes(THREE);
9390
9726
  for (const mesh2 of meshes) mesh2.frustumCulled = false;
9391
9727
  const group = new THREE.Group();