@adatechnology/game-vfx-three 0.3.6 → 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);
@@ -9457,6 +9692,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9457
9692
  });
9458
9693
  },
9459
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),
9460
9697
  configure(settings) {
9461
9698
  const band = settings.lod_band;
9462
9699
  if (typeof band === "string" && band in LOD_BANDS) world.forceLod(LOD_BANDS[band] ?? null);