@adatechnology/game-fluids-three 0.4.5 → 0.4.7

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.
@@ -1325,11 +1325,231 @@ var GameFluidsThree = (() => {
1325
1325
  glow.outIntensity = glow.i * Math.pow(1 - age, glow.curve) * flicker;
1326
1326
  }
1327
1327
  }
1328
+ var LEHMER_MULTIPLIER = 16807;
1329
+ var LEHMER_MODULUS = 2147483647;
1330
+ var HASH_MIX = 2654435761;
1331
+ function normalizeLehmerSeed(seed) {
1332
+ const whole = Math.floor(Math.abs(seed)) % (LEHMER_MODULUS - 1);
1333
+ return whole === 0 ? 1 : whole;
1334
+ }
1335
+ function lehmer(seed) {
1336
+ let state = normalizeLehmerSeed(seed);
1337
+ const next = () => {
1338
+ state = state * LEHMER_MULTIPLIER % LEHMER_MODULUS;
1339
+ return (state - 1) / (LEHMER_MODULUS - 1);
1340
+ };
1341
+ return {
1342
+ next,
1343
+ between: (min2, max2) => min2 + (max2 - min2) * next(),
1344
+ direction(out) {
1345
+ const up = next() * 2 - 1;
1346
+ const angle = next() * Math.PI * 2;
1347
+ const ring2 = Math.sqrt(1 - up * up);
1348
+ out.x = ring2 * Math.cos(angle);
1349
+ out.y = up;
1350
+ out.z = ring2 * Math.sin(angle);
1351
+ return out;
1352
+ },
1353
+ get state() {
1354
+ return state;
1355
+ }
1356
+ };
1357
+ }
1358
+ function mixEventSeed(eventSeed) {
1359
+ let mixed = Math.imul((eventSeed | 0) ^ HASH_MIX, 2246822507);
1360
+ mixed ^= mixed >>> 13;
1361
+ mixed = Math.imul(mixed, 3266489909);
1362
+ mixed ^= mixed >>> 16;
1363
+ return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1;
1364
+ }
1365
+ function streamFor(eventSeed) {
1366
+ return lehmer(mixEventSeed(eventSeed));
1367
+ }
1368
+ var SKIN = 5e-3;
1369
+ var MAX_PAD = 0.45;
1370
+ function rotation(q) {
1371
+ const [x, y, z, w] = q ?? [0, 0, 0, 1];
1372
+ const n = Math.hypot(x, y, z, w) || 1;
1373
+ const qx = x / n;
1374
+ const qy = y / n;
1375
+ const qz = z / n;
1376
+ const qw = w / n;
1377
+ return new Float64Array([
1378
+ 1 - 2 * (qy * qy + qz * qz),
1379
+ 2 * (qx * qy - qz * qw),
1380
+ 2 * (qx * qz + qy * qw),
1381
+ 2 * (qx * qy + qz * qw),
1382
+ 1 - 2 * (qx * qx + qz * qz),
1383
+ 2 * (qy * qz - qx * qw),
1384
+ 2 * (qx * qz - qy * qw),
1385
+ 2 * (qy * qz + qx * qw),
1386
+ 1 - 2 * (qx * qx + qy * qy)
1387
+ ]);
1388
+ }
1389
+ var Enclosures = class {
1390
+ constructor(seed = 20973) {
1391
+ __publicField2(this, "spaces", /* @__PURE__ */ new Map());
1392
+ __publicField2(this, "list", []);
1393
+ __publicField2(this, "random");
1394
+ __publicField2(this, "out", { x: 0, y: 0, z: 0, nx: 0, ny: 0, nz: 0 });
1395
+ __publicField2(this, "l0", new Float64Array(3));
1396
+ __publicField2(this, "l1", new Float64Array(3));
1397
+ __publicField2(this, "q", new Float64Array(3));
1398
+ this.random = lehmer(seed);
1399
+ }
1400
+ get empty() {
1401
+ return this.list.length === 0;
1402
+ }
1403
+ get size() {
1404
+ return this.list.length;
1405
+ }
1406
+ /** Declares, moves or (with `null`) ends the space `id`. */
1407
+ set(id, enclosure) {
1408
+ if (!enclosure) {
1409
+ if (this.spaces.delete(id)) this.list = [...this.spaces.values()];
1410
+ return;
1411
+ }
1412
+ const [hx, hy, hz] = enclosure.halfSize;
1413
+ const space = {
1414
+ cx: enclosure.center[0],
1415
+ cy: enclosure.center[1],
1416
+ cz: enclosure.center[2],
1417
+ hx: Math.abs(hx),
1418
+ hy: Math.abs(hy),
1419
+ hz: Math.abs(hz),
1420
+ r: rotation(enclosure.orientation),
1421
+ radius: Math.hypot(hx, hy, hz),
1422
+ openings: enclosure.openings ?? [],
1423
+ leak: Math.max(0, enclosure.leak ?? 0)
1424
+ };
1425
+ const old = this.spaces.get(id);
1426
+ this.spaces.set(id, space);
1427
+ const at3 = old ? this.list.indexOf(old) : -1;
1428
+ if (at3 >= 0) this.list[at3] = space;
1429
+ else this.list.push(space);
1430
+ }
1431
+ get(id) {
1432
+ const s = this.spaces.get(id);
1433
+ if (!s) return void 0;
1434
+ return { center: [s.cx, s.cy, s.cz], halfSize: [s.hx, s.hy, s.hz], openings: s.openings, leak: s.leak };
1435
+ }
1436
+ ids() {
1437
+ return [...this.spaces.keys()];
1438
+ }
1439
+ clear() {
1440
+ this.spaces.clear();
1441
+ this.list = [];
1442
+ }
1443
+ /** Whether a point is inside any space (on the inside of its walls). */
1444
+ inside(x, y, z) {
1445
+ for (const s of this.list) {
1446
+ this.toLocal(s, x, y, z, this.l0);
1447
+ if (Math.abs(this.l0[0]) <= s.hx && Math.abs(this.l0[1]) <= s.hy && Math.abs(this.l0[2]) <= s.hz) return true;
1448
+ }
1449
+ return false;
1450
+ }
1451
+ toLocal(s, x, y, z, out) {
1452
+ const dx = x - s.cx;
1453
+ const dy = y - s.cy;
1454
+ const dz = z - s.cz;
1455
+ const r = s.r;
1456
+ out[0] = r[0] * dx + r[3] * dy + r[6] * dz;
1457
+ out[1] = r[1] * dx + r[4] * dy + r[7] * dz;
1458
+ out[2] = r[2] * dx + r[5] * dy + r[8] * dz;
1459
+ }
1460
+ /** Passes at `rate` per second. */
1461
+ chance(rate, dt) {
1462
+ return rate > 0 && this.random.next() < 1 - Math.exp(-rate * dt);
1463
+ }
1464
+ /** The opening the wall point `q` (local) is in lets it through now. */
1465
+ through(s, q, dt) {
1466
+ for (const o of s.openings) {
1467
+ 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;
1468
+ if (o.rate === void 0 || this.chance(o.rate, dt)) return true;
1469
+ }
1470
+ return this.chance(s.leak, dt);
1471
+ }
1472
+ confine(x0, y0, z0, x1, y1, z1, pad, dt) {
1473
+ const list = this.list;
1474
+ if (list.length === 0) return false;
1475
+ const p = Math.min(Math.max(0, pad), MAX_PAD);
1476
+ const l0 = this.l0;
1477
+ const l1 = this.l1;
1478
+ const q = this.q;
1479
+ for (const s of list) {
1480
+ const reach = s.radius + p + 0.05;
1481
+ const dx = x1 - s.cx;
1482
+ const dy = y1 - s.cy;
1483
+ const dz = z1 - s.cz;
1484
+ if (dx * dx + dy * dy + dz * dz > reach * reach) {
1485
+ const ex = x0 - s.cx;
1486
+ const ey = y0 - s.cy;
1487
+ const ez = z0 - s.cz;
1488
+ if (ex * ex + ey * ey + ez * ez > reach * reach) continue;
1489
+ }
1490
+ this.toLocal(s, x0, y0, z0, l0);
1491
+ this.toLocal(s, x1, y1, z1, l1);
1492
+ const h = [s.hx, s.hy, s.hz];
1493
+ const in0 = Math.abs(l0[0]) <= s.hx && Math.abs(l0[1]) <= s.hy && Math.abs(l0[2]) <= s.hz;
1494
+ let axis2 = 0;
1495
+ let sign = 1;
1496
+ if (in0) {
1497
+ if (Math.abs(l1[0]) <= s.hx && Math.abs(l1[1]) <= s.hy && Math.abs(l1[2]) <= s.hz) continue;
1498
+ for (let a = 0; a < 3; a++) q[a] = Math.min(h[a], Math.max(-h[a], l1[a]));
1499
+ if (this.through(s, q, dt)) continue;
1500
+ let worst = -Infinity;
1501
+ for (let a = 0; a < 3; a++) {
1502
+ const over = Math.abs(l1[a]) - h[a];
1503
+ if (over > worst) {
1504
+ worst = over;
1505
+ axis2 = a;
1506
+ }
1507
+ }
1508
+ sign = l1[axis2] < 0 ? -1 : 1;
1509
+ for (let a = 0; a < 3; a++) l1[a] = Math.min(h[a] - SKIN, Math.max(-(h[a] - SKIN), l1[a]));
1510
+ sign = -sign;
1511
+ } else {
1512
+ const padded0 = Math.abs(l0[0]) <= s.hx + p && Math.abs(l0[1]) <= s.hy + p && Math.abs(l0[2]) <= s.hz + p;
1513
+ const bound = padded0 ? 0 : p;
1514
+ if (!(Math.abs(l1[0]) <= s.hx + bound && Math.abs(l1[1]) <= s.hy + bound && Math.abs(l1[2]) <= s.hz + bound)) continue;
1515
+ for (let a = 0; a < 3; a++) q[a] = Math.min(h[a], Math.max(-h[a], l1[a]));
1516
+ if (this.through(s, q, dt)) continue;
1517
+ let worst = -Infinity;
1518
+ for (let a = 0; a < 3; a++) {
1519
+ const over = Math.abs(l0[a]) - (h[a] + bound);
1520
+ if (over > worst) {
1521
+ worst = over;
1522
+ axis2 = a;
1523
+ }
1524
+ }
1525
+ sign = l0[axis2] < 0 ? -1 : 1;
1526
+ l1[axis2] = sign * (h[axis2] + bound + SKIN);
1527
+ }
1528
+ const r = s.r;
1529
+ const out = this.out;
1530
+ out.x = s.cx + r[0] * l1[0] + r[1] * l1[1] + r[2] * l1[2];
1531
+ out.y = s.cy + r[3] * l1[0] + r[4] * l1[1] + r[5] * l1[2];
1532
+ out.z = s.cz + r[6] * l1[0] + r[7] * l1[1] + r[8] * l1[2];
1533
+ out.nx = r[axis2] * sign;
1534
+ out.ny = r[3 + axis2] * sign;
1535
+ out.nz = r[6 + axis2] * sign;
1536
+ return true;
1537
+ }
1538
+ return false;
1539
+ }
1540
+ };
1541
+ function slideAlong(out, vx2, vy2, vz2) {
1542
+ const into = vx2 * out.nx + vy2 * out.ny + vz2 * out.nz;
1543
+ if (into >= 0) return [vx2, vy2, vz2];
1544
+ return [vx2 - into * out.nx, vy2 - into * out.ny, vz2 - into * out.nz];
1545
+ }
1328
1546
  var FLOOR_FRACTION = 0.4;
1329
1547
  var FLOOR_BOUNCE = -0.2;
1330
1548
  var ARRIVAL_FRAME = 30;
1331
1549
  var ARRIVAL_BASE = 0.3;
1332
1550
  var GROWTH_CAP_SECONDS = 3.5;
1551
+ var CARRIED_KINDS = [true, true, false, true, false];
1552
+ var PAD_OF_SIZE = 0.35;
1333
1553
  function createPuffPool(capacity) {
1334
1554
  const placeholder = { next: () => 0, between: (min2) => min2, direction: (out) => out, state: 0 };
1335
1555
  return new OrderedPool(capacity, () => ({
@@ -1424,13 +1644,24 @@ var GameFluidsThree = (() => {
1424
1644
  if (puff3.vy < 0) puff3.vy *= FLOOR_BOUNCE;
1425
1645
  }
1426
1646
  }
1427
- function updatePuffs(pool, dt, wind) {
1647
+ function updatePuffs(pool, dt, wind, confiner) {
1428
1648
  pool.compact(ageOrKeep2, dt);
1649
+ const confining = confiner !== void 0 && !confiner.empty;
1429
1650
  for (const puff3 of pool.items) {
1430
1651
  const age = puff3.t / puff3.life;
1431
1652
  const heat = puff3.h0 > 0 ? puff3.h0 * Math.pow(Math.max(0, 1 - puff3.t / puff3.hl), 1.25) : 0;
1653
+ const x0 = puff3.x;
1654
+ const y0 = puff3.y;
1655
+ const z0 = puff3.z;
1432
1656
  if (puff3.ring) moveOnRing(puff3, puff3.ring, wind);
1433
1657
  else moveFree(puff3, dt, age, heat, wind);
1658
+ if (confining && CARRIED_KINDS[puff3.kind] && confiner.confine(x0, y0, z0, puff3.x, puff3.y, puff3.z, puff3.size * PAD_OF_SIZE, dt)) {
1659
+ const out = confiner.out;
1660
+ puff3.x = out.x;
1661
+ puff3.y = out.y;
1662
+ puff3.z = out.z;
1663
+ [puff3.vx, puff3.vy, puff3.vz] = slideAlong(out, puff3.vx, puff3.vy, puff3.vz);
1664
+ }
1434
1665
  const size = puff3.s0 + (puff3.s1 - puff3.s0) * (1 - Math.exp(-puff3.t * puff3.gk)) + puff3.sg * Math.min(puff3.t, GROWTH_CAP_SECONDS);
1435
1666
  puff3.size = size * Math.min(1, puff3.t * ARRIVAL_FRAME + ARRIVAL_BASE);
1436
1667
  puff3.age = age;
@@ -1956,10 +2187,10 @@ var GameFluidsThree = (() => {
1956
2187
  ])
1957
2188
  ]
1958
2189
  };
1959
- function localToWorld(translation, rotation, local2) {
2190
+ function localToWorld(translation, rotation2, local2) {
1960
2191
  const [x, y, z] = local2;
1961
- const [a, c, e] = [Math.cos(rotation[0]), Math.cos(rotation[1]), Math.cos(rotation[2])];
1962
- const [b, d, f] = [Math.sin(rotation[0]), Math.sin(rotation[1]), Math.sin(rotation[2])];
2192
+ const [a, c, e] = [Math.cos(rotation2[0]), Math.cos(rotation2[1]), Math.cos(rotation2[2])];
2193
+ const [b, d, f] = [Math.sin(rotation2[0]), Math.sin(rotation2[1]), Math.sin(rotation2[2])];
1963
2194
  const [ae, af, be, bf] = [a * e, a * f, b * e, b * f];
1964
2195
  return [
1965
2196
  c * e * x + -c * f * y + d * z + translation[0],
@@ -3622,46 +3853,6 @@ var GameFluidsThree = (() => {
3622
3853
  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];
3623
3854
  var WORKSHOP_EFFECTS = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]));
3624
3855
  var GAME_EFFECTS = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS, ...VEHICLE_EFFECTS].map((effect) => [effect.id, effect])) };
3625
- var LEHMER_MULTIPLIER = 16807;
3626
- var LEHMER_MODULUS = 2147483647;
3627
- var HASH_MIX = 2654435761;
3628
- function normalizeLehmerSeed(seed) {
3629
- const whole = Math.floor(Math.abs(seed)) % (LEHMER_MODULUS - 1);
3630
- return whole === 0 ? 1 : whole;
3631
- }
3632
- function lehmer(seed) {
3633
- let state = normalizeLehmerSeed(seed);
3634
- const next = () => {
3635
- state = state * LEHMER_MULTIPLIER % LEHMER_MODULUS;
3636
- return (state - 1) / (LEHMER_MODULUS - 1);
3637
- };
3638
- return {
3639
- next,
3640
- between: (min2, max2) => min2 + (max2 - min2) * next(),
3641
- direction(out) {
3642
- const up = next() * 2 - 1;
3643
- const angle = next() * Math.PI * 2;
3644
- const ring2 = Math.sqrt(1 - up * up);
3645
- out.x = ring2 * Math.cos(angle);
3646
- out.y = up;
3647
- out.z = ring2 * Math.sin(angle);
3648
- return out;
3649
- },
3650
- get state() {
3651
- return state;
3652
- }
3653
- };
3654
- }
3655
- function mixEventSeed(eventSeed) {
3656
- let mixed = Math.imul((eventSeed | 0) ^ HASH_MIX, 2246822507);
3657
- mixed ^= mixed >>> 13;
3658
- mixed = Math.imul(mixed, 3266489909);
3659
- mixed ^= mixed >>> 16;
3660
- return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1;
3661
- }
3662
- function streamFor(eventSeed) {
3663
- return lehmer(mixEventSeed(eventSeed));
3664
- }
3665
3856
  var TABLE = {
3666
3857
  concrete: { dust: 9078138, chip: 7236195, spark: 2 },
3667
3858
  metal: { dust: 0, chip: 0, spark: 18 },
@@ -4965,8 +5156,8 @@ var GameFluidsThree = (() => {
4965
5156
  function addScorch(store, random, x, z, radius) {
4966
5157
  const variant = Math.floor(random.next() * SCORCH_VARIANTS);
4967
5158
  const lift = 0.03 + nextLift(store) * 1e-3;
4968
- const rotation = random.next() * 6;
4969
- store.add({ kind: "scorch", x, z, radius, variant, lift, rotation });
5159
+ const rotation2 = random.next() * 6;
5160
+ store.add({ kind: "scorch", x, z, radius, variant, lift, rotation: rotation2 });
4970
5161
  }
4971
5162
  function addRimRocks(store, ground, random, x, z, radius) {
4972
5163
  const count = Math.round(6 + radius * 7);
@@ -4995,8 +5186,8 @@ var GameFluidsThree = (() => {
4995
5186
  function addCrater(store, ground, random, spec) {
4996
5187
  ground?.dent(random, spec.x, spec.z, spec.radius, spec.depth);
4997
5188
  if (!spec.marks) return;
4998
- const rotation = random.next() * 6;
4999
- store.add({ kind: "dirt", x: spec.x, z: spec.z, radius: spec.radius * 1.02, lift: 0.045, rotation });
5189
+ const rotation2 = random.next() * 6;
5190
+ store.add({ kind: "dirt", x: spec.x, z: spec.z, radius: spec.radius * 1.02, lift: 0.045, rotation: rotation2 });
5000
5191
  addRimRocks(store, ground, random, spec.x, spec.z, spec.radius);
5001
5192
  }
5002
5193
  function addBloodSplat(store, ground, random, spec) {
@@ -5552,6 +5743,7 @@ var GameFluidsThree = (() => {
5552
5743
  state.gas = Math.max(0, state.gas - dt / DECAY.gasSeconds);
5553
5744
  state.beat *= Math.exp(-DECAY.beat * dt);
5554
5745
  }
5746
+ var PAD_OF_SIZE2 = 0.35;
5555
5747
  var clamp4 = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
5556
5748
  var lerp4 = (a, b, t) => a + (b - a) * t;
5557
5749
  function rgb(hex) {
@@ -5711,9 +5903,20 @@ var GameFluidsThree = (() => {
5711
5903
  vy2 *= k;
5712
5904
  vz2 *= k;
5713
5905
  }
5714
- const x = this.position[i3] + vx2 * dt;
5906
+ let x = this.position[i3] + vx2 * dt;
5715
5907
  let y = this.position[i3 + 1] + vy2 * dt;
5716
- const z = this.position[i3 + 2] + vz2 * dt;
5908
+ let z = this.position[i3 + 2] + vz2 * dt;
5909
+ const confiner = host.confiner;
5910
+ if (def.carried && confiner && !confiner.empty) {
5911
+ const pad = this.size0[i] * lerp4(1, def.grow, t) * PAD_OF_SIZE2;
5912
+ if (confiner.confine(this.position[i3], this.position[i3 + 1], this.position[i3 + 2], x, y, z, pad, dt)) {
5913
+ const out = confiner.out;
5914
+ x = out.x;
5915
+ y = out.y;
5916
+ z = out.z;
5917
+ [vx2, vy2, vz2] = slideAlong(out, vx2, vy2, vz2);
5918
+ }
5919
+ }
5717
5920
  if (def.ground !== "none") {
5718
5921
  const own = this.floor[i];
5719
5922
  const gy = Number.isNaN(own) ? host.groundAt(x, z) + 5e-3 : own;
@@ -5799,7 +6002,8 @@ var GameFluidsThree = (() => {
5799
6002
  alpha: 0.35,
5800
6003
  fadeIn: 0.15,
5801
6004
  cone: 0.6,
5802
- ground: "none"
6005
+ ground: "none",
6006
+ carried: true
5803
6007
  },
5804
6008
  sparks: {
5805
6009
  id: "sparks",
@@ -5843,7 +6047,8 @@ var GameFluidsThree = (() => {
5843
6047
  alpha: 0.4,
5844
6048
  fadeIn: 0.1,
5845
6049
  cone: 0.4,
5846
- ground: "none"
6050
+ ground: "none",
6051
+ carried: true
5847
6052
  },
5848
6053
  shards: {
5849
6054
  id: "shards",
@@ -6001,7 +6206,8 @@ var GameFluidsThree = (() => {
6001
6206
  fadeIn: 0.1,
6002
6207
  cone: 0.5,
6003
6208
  ground: "stop",
6004
- flutter: 0.6
6209
+ flutter: 0.6,
6210
+ carried: true
6005
6211
  },
6006
6212
  steam: {
6007
6213
  id: "steam",
@@ -6023,7 +6229,8 @@ var GameFluidsThree = (() => {
6023
6229
  alpha: 0.35,
6024
6230
  fadeIn: 0.15,
6025
6231
  cone: 0.6,
6026
- ground: "none"
6232
+ ground: "none",
6233
+ carried: true
6027
6234
  },
6028
6235
  flash: {
6029
6236
  id: "flash",
@@ -6156,7 +6363,8 @@ var GameFluidsThree = (() => {
6156
6363
  const host = {
6157
6364
  groundAt: this.options.groundAt,
6158
6365
  gravity: this.options.gravity,
6159
- land: (owner, x, y, z, color, tag) => this.listeners.get(owner)?.(x, y, z, color, tag)
6366
+ land: (owner, x, y, z, color, tag) => this.listeners.get(owner)?.(x, y, z, color, tag),
6367
+ ...this.options.confiner ? { confiner: this.options.confiner } : {}
6160
6368
  };
6161
6369
  if (this.additive.live > 0 || this.additive.drawCount > 0) this.additive.update(dt, host);
6162
6370
  if (this.normal.live > 0 || this.normal.drawCount > 0) this.normal.update(dt, host);
@@ -6362,8 +6570,8 @@ var GameFluidsThree = (() => {
6362
6570
  * 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
6363
6571
  * world axes). The surface they were on is gone (a brick came out of a wall with its bullet holes). Returns how many went.
6364
6572
  */
6365
- clearInside(center, half, rotation = [0, 0, 0, 1]) {
6366
- const qx = -rotation[0], qy = -rotation[1], qz = -rotation[2], qw = rotation[3];
6573
+ clearInside(center, half, rotation2 = [0, 0, 0, 1]) {
6574
+ const qx = -rotation2[0], qy = -rotation2[1], qz = -rotation2[2], qw = rotation2[3];
6367
6575
  let gone = 0;
6368
6576
  for (const decal2 of this.decals) {
6369
6577
  if (!decal2.on) continue;
@@ -6484,10 +6692,14 @@ var GameFluidsThree = (() => {
6484
6692
  var DEFAULT_FADE_IN = 0.1;
6485
6693
  var FLOATS_PER_VECTOR = 3;
6486
6694
  var BYTE_MAX2 = 255;
6695
+ var CARRIED_GRAVITY = 3;
6696
+ var PAD_OF_SIZE3 = 0.35;
6487
6697
  var SpritePool = class {
6488
- constructor(capacity, context) {
6698
+ /** `carried`: the normal-blend pool (smoke, dust, steam), whose light sprites go round the closed spaces; the additive one (flames, sparks) does not. */
6699
+ constructor(capacity, context, carried = false) {
6489
6700
  __publicField2(this, "capacity", capacity);
6490
6701
  __publicField2(this, "context", context);
6702
+ __publicField2(this, "carried", carried);
6491
6703
  __publicField2(this, "count", 0);
6492
6704
  __publicField2(this, "position");
6493
6705
  __publicField2(this, "velocity");
@@ -6587,6 +6799,7 @@ var GameFluidsThree = (() => {
6587
6799
  }
6588
6800
  update(dt, leaveDecal) {
6589
6801
  const { position, velocity } = this;
6802
+ const confiner = this.context.confiner;
6590
6803
  for (let index = this.count - 1; index >= 0; index--) {
6591
6804
  this.life[index] -= dt;
6592
6805
  if (this.life[index] <= 0) {
@@ -6600,9 +6813,25 @@ var GameFluidsThree = (() => {
6600
6813
  velocity[offset] *= factor;
6601
6814
  velocity[offset + 1] *= factor;
6602
6815
  velocity[offset + 2] *= factor;
6816
+ const x0 = position[offset];
6817
+ const y0 = position[offset + 1];
6818
+ const z0 = position[offset + 2];
6603
6819
  position[offset] += velocity[offset] * dt;
6604
6820
  position[offset + 1] += velocity[offset + 1] * dt;
6605
6821
  position[offset + 2] += velocity[offset + 2] * dt;
6822
+ if (this.carried && confiner && !confiner.empty && Math.abs(this.gravity[index]) < CARRIED_GRAVITY) {
6823
+ const size = this.startSize[index] * (1 + (this.growth[index] - 1) * age);
6824
+ if (confiner.confine(x0, y0, z0, position[offset], position[offset + 1], position[offset + 2], size * PAD_OF_SIZE3, dt)) {
6825
+ const out = confiner.out;
6826
+ position[offset] = out.x;
6827
+ position[offset + 1] = out.y;
6828
+ position[offset + 2] = out.z;
6829
+ const [vx2, vy2, vz2] = slideAlong(out, velocity[offset], velocity[offset + 1], velocity[offset + 2]);
6830
+ velocity[offset] = vx2;
6831
+ velocity[offset + 1] = vy2;
6832
+ velocity[offset + 2] = vz2;
6833
+ }
6834
+ }
6606
6835
  const mode = this.groundMode[index];
6607
6836
  if (mode) {
6608
6837
  const groundHeight = this.context.groundAt(position[offset], position[offset + 2]);
@@ -6879,9 +7108,9 @@ var GameFluidsThree = (() => {
6879
7108
  __publicField2(this, "placement");
6880
7109
  __publicField2(this, "accumulators", /* @__PURE__ */ new Map());
6881
7110
  const profile = options.profile ?? PROFILES.high;
6882
- this.context = { random: options.random, groundAt: options.groundAt ?? workshopGround, quality: { level: 0, mult: 1 }, trace: options.trace };
7111
+ this.context = { random: options.random, groundAt: options.groundAt ?? workshopGround, quality: { level: 0, mult: 1 }, trace: options.trace, ...options.confiner ? { confiner: options.confiner } : {} };
6883
7112
  this.add = new SpritePool(profile.sprites.add, this.context);
6884
- this.normal = new SpritePool(profile.sprites.normal, this.context);
7113
+ this.normal = new SpritePool(profile.sprites.normal, this.context, true);
6885
7114
  this.streaks = new StreakPool(profile.streaks, this.context);
6886
7115
  this.debris = new DebrisPool(this.context);
6887
7116
  this.decals = new DecalPool(this.context);
@@ -7069,12 +7298,14 @@ var GameFluidsThree = (() => {
7069
7298
  const groundAt = options.groundAt ?? (ground ? (x, z) => ground.heightAt(x, z) : () => 0);
7070
7299
  const handles = { next: 0, events: 0 };
7071
7300
  const low = options.profile === "low";
7301
+ const enclosures = new Enclosures(20973 + (options.seed ?? 0));
7072
7302
  const spriteSystem = new SpriteKindSystem({
7073
7303
  capacity: options.sprites?.capacity ?? (low ? 1250 : 2500),
7074
7304
  density: options.sprites?.density ?? (low ? 0.6 : 1),
7075
7305
  seed: 2654435761 + (options.seed ?? 0),
7076
7306
  groundAt,
7077
7307
  gravity: options.gravity ?? DEFAULT_GRAVITY,
7308
+ confiner: enclosures,
7078
7309
  ...options.sprites?.kinds ? { kinds: options.sprites.kinds } : {}
7079
7310
  });
7080
7311
  const core = {
@@ -7124,7 +7355,7 @@ var GameFluidsThree = (() => {
7124
7355
  return [camera2.x, camera2.y, camera2.z];
7125
7356
  }, forward: (local2) => [local2[0] ?? 0, local2[1] ?? 0, local2[2] ?? 0] };
7126
7357
  function workshop() {
7127
- return engine ?? (engine = new WorkshopEngine({ random: sharedRandom, profile, groundAt }));
7358
+ return engine ?? (engine = new WorkshopEngine({ random: sharedRandom, profile, groundAt, confiner: enclosures }));
7128
7359
  }
7129
7360
  function effectPlayer() {
7130
7361
  return player ?? (player = createEffectPlayer({ engine: workshop(), effects, view }));
@@ -7183,6 +7414,10 @@ var GameFluidsThree = (() => {
7183
7414
  puff3.vz += dz * strength;
7184
7415
  }
7185
7416
  },
7417
+ enclose(id, enclosure) {
7418
+ enclosures.set(id, enclosure);
7419
+ },
7420
+ enclosures: () => enclosures,
7186
7421
  stop(handle) {
7187
7422
  player?.stop(handle);
7188
7423
  const fires = core.fires;
@@ -7210,7 +7445,7 @@ var GameFluidsThree = (() => {
7210
7445
  core.clock.time += dt;
7211
7446
  core.schedule.update(core.clock.time);
7212
7447
  updateRings(core.rings, dt);
7213
- updatePuffs(core.puffs, dt, wind);
7448
+ updatePuffs(core.puffs, dt, wind, enclosures);
7214
7449
  updateSparks(core.sparks, { dt, groundAt });
7215
7450
  updateGlows(core.glows, dt);
7216
7451
  updateShocks(core.shocks, core.puffs.items, dt);
@@ -7688,6 +7923,11 @@ void main(){
7688
7923
  var MAX_CONFORM_STEPS = 48;
7689
7924
  var MIN_CONFORM_STEPS = 16;
7690
7925
  var BLOOD_GROW_RATE = 1.2;
7926
+ var BLOOD_START_GROWTH = 0.05;
7927
+ var DEFAULT_MAX_GROUND_MARKS = 64;
7928
+ var DEFAULT_MAX_ROCKS = 512;
7929
+ var DEFAULT_MAX_BLOOD = 256;
7930
+ var MAX_SPARE_GROUND_GEOMETRIES = 16;
7691
7931
  function createMarkLayer(THREE, options) {
7692
7932
  const group = new THREE.Group();
7693
7933
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -7711,110 +7951,168 @@ void main(){
7711
7951
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
7712
7952
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
7713
7953
  owned.push(bloodMaterial, bloodGeometry);
7714
- const rockMaterials = /* @__PURE__ */ new Map();
7715
- const blood = [];
7716
- let meshCount = 0;
7717
- function rockMaterial(color) {
7718
- let material = rockMaterials.get(color);
7719
- if (!material) {
7720
- material = new THREE.MeshStandardMaterial({ color, roughness: 0.9, metalness: 0, flatShading: true });
7721
- rockMaterials.set(color, material);
7722
- owned.push(material);
7954
+ const maxGroundMarks = options.maxGroundMarks ?? DEFAULT_MAX_GROUND_MARKS;
7955
+ const instanced = {};
7956
+ function rockMaterial() {
7957
+ if (!instanced.rockMaterial) {
7958
+ instanced.rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.9, metalness: 0, flatShading: true });
7959
+ owned.push(instanced.rockMaterial);
7723
7960
  }
7724
- return material;
7961
+ return instanced.rockMaterial;
7962
+ }
7963
+ function bloodMesh() {
7964
+ return instanced.blood ?? (instanced.blood = attach(createInstanced(bloodGeometry, bloodMaterial, options.maxBlood ?? DEFAULT_MAX_BLOOD)));
7965
+ }
7966
+ function rockMesh() {
7967
+ return instanced.rock ?? (instanced.rock = attach(createInstanced(options.rockGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
7968
+ }
7969
+ function plateMesh() {
7970
+ return instanced.plate ?? (instanced.plate = attach(createInstanced(options.plateGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
7971
+ }
7972
+ function attach(mesh2, castShadow = false) {
7973
+ mesh2.castShadow = castShadow;
7974
+ group.add(mesh2);
7975
+ return mesh2;
7976
+ }
7977
+ const groundMarks = [];
7978
+ const spareGeometries = [];
7979
+ const blood = [];
7980
+ const placed = { blood: 0, rock: 0, plate: 0 };
7981
+ const matrix = new THREE.Matrix4();
7982
+ const position = new THREE.Vector3();
7983
+ const quaternion = new THREE.Quaternion();
7984
+ const euler = new THREE.Euler();
7985
+ const scale2 = new THREE.Vector3();
7986
+ const color = new THREE.Color();
7987
+ const up = new THREE.Vector3(0, 1, 0);
7988
+ function createInstanced(geometry, material, capacity) {
7989
+ const mesh2 = new THREE.InstancedMesh(geometry, material, capacity);
7990
+ mesh2.count = 0;
7991
+ mesh2.frustumCulled = false;
7992
+ mesh2.receiveShadow = true;
7993
+ return mesh2;
7994
+ }
7995
+ function takeSpare(vertexCount) {
7996
+ const found = spareGeometries.findIndex((geometry) => geometry.getAttribute("position").count === vertexCount);
7997
+ return found < 0 ? void 0 : spareGeometries.splice(found, 1)[0];
7998
+ }
7999
+ function evictOldestGroundMark() {
8000
+ const oldest = groundMarks.shift();
8001
+ group.remove(oldest.mesh);
8002
+ spareGeometries.push(oldest.mesh.geometry);
8003
+ if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
7725
8004
  }
7726
8005
  function conform(mark, material) {
7727
8006
  const radius = mark.radius;
7728
8007
  const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
7729
- const geometry = new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
7730
- const position = geometry.attributes.position;
8008
+ if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
8009
+ const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
8010
+ const grid = geometry.attributes.position;
8011
+ const cell = 2 / steps;
7731
8012
  const cos2 = Math.cos(mark.rotation);
7732
8013
  const sin2 = Math.sin(mark.rotation);
7733
- for (let index = 0; index < position.count; index++) {
7734
- const localX = position.getX(index) * radius;
7735
- const localZ = position.getZ(index) * radius;
8014
+ for (let index = 0; index < grid.count; index++) {
8015
+ const localX = Math.fround(index % (steps + 1) * cell - 1) * radius;
8016
+ const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
7736
8017
  const worldX = mark.x + localX * cos2 - localZ * sin2;
7737
8018
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
7738
- position.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
8019
+ grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
7739
8020
  }
8021
+ grid.needsUpdate = true;
7740
8022
  geometry.computeVertexNormals();
8023
+ geometry.attributes.normal.needsUpdate = true;
8024
+ geometry.boundingSphere = null;
8025
+ geometry.boundingBox = null;
7741
8026
  const mesh2 = new THREE.Mesh(geometry, material);
7742
8027
  mesh2.receiveShadow = true;
7743
- return mesh2;
8028
+ group.add(mesh2);
8029
+ groundMarks.push({ mesh: mesh2, steps });
7744
8030
  }
7745
8031
  function addRock(mark) {
7746
- const mesh2 = new THREE.Mesh(mark.plate ? options.plateGeometry : options.rockGeometry, rockMaterial(mark.color));
7747
- mesh2.position.set(mark.x, mark.y, mark.z);
7748
- mesh2.scale.set(mark.scale[0], mark.scale[1], mark.scale[2]);
7749
- mesh2.rotation.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]);
7750
- mesh2.castShadow = true;
7751
- mesh2.receiveShadow = true;
7752
- return mesh2;
8032
+ const mesh2 = mark.plate ? plateMesh() : rockMesh();
8033
+ const slot = mark.plate ? "plate" : "rock";
8034
+ const index = placed[slot]++ % mesh2.instanceMatrix.count;
8035
+ quaternion.setFromEuler(euler.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]));
8036
+ mesh2.setMatrixAt(index, matrix.compose(position.set(mark.x, mark.y, mark.z), quaternion, scale2.set(mark.scale[0], mark.scale[1], mark.scale[2])));
8037
+ mesh2.setColorAt(index, color.set(mark.color));
8038
+ mesh2.count = Math.min(placed[slot], mesh2.instanceMatrix.count);
8039
+ mesh2.instanceMatrix.needsUpdate = true;
8040
+ mesh2.instanceColor.needsUpdate = true;
8041
+ }
8042
+ function writeBlood(index, drawn, growth) {
8043
+ quaternion.setFromAxisAngle(up, drawn.rotation);
8044
+ const mesh2 = bloodMesh();
8045
+ mesh2.setMatrixAt(index, matrix.compose(position.set(drawn.position[0], drawn.position[1], drawn.position[2]), quaternion, scale2.set(drawn.scaleX * growth, 1, drawn.scaleZ * growth)));
8046
+ mesh2.instanceMatrix.needsUpdate = true;
7753
8047
  }
7754
8048
  function addBlood(mark) {
7755
- const mesh2 = new THREE.Mesh(bloodGeometry, bloodMaterial);
7756
- mesh2.position.set(mark.x, mark.y, mark.z);
7757
- mesh2.scale.set(mark.scaleX, 1, mark.scaleZ);
7758
- mesh2.rotation.y = mark.rotation;
7759
- mesh2.receiveShadow = true;
7760
- mesh2.scale.multiplyScalar(0.05);
7761
- blood.push({ mesh: mesh2, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
7762
- return mesh2;
8049
+ const index = placed.blood++ % bloodMesh().instanceMatrix.count;
8050
+ const drawn = { position: [mark.x, mark.y, mark.z], rotation: mark.rotation, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 };
8051
+ blood[index] = drawn;
8052
+ writeBlood(index, drawn, BLOOD_START_GROWTH);
8053
+ bloodMesh().count = Math.min(placed.blood, bloodMesh().instanceMatrix.count);
7763
8054
  }
7764
8055
  function build(mark) {
7765
- if (mark.kind === "scorch") return conform(mark, scorchMaterials[mark.variant]);
7766
- if (mark.kind === "dirt") return conform(mark, dirtMaterial);
7767
- if (mark.kind === "rock") return addRock(mark);
7768
- return addBlood(mark);
7769
- }
7770
- function clearMeshes() {
7771
- for (const child of [...group.children]) {
7772
- group.remove(child);
7773
- const geometry = child.geometry;
7774
- if (geometry !== options.rockGeometry && geometry !== options.plateGeometry && geometry !== bloodGeometry) geometry.dispose();
7775
- }
8056
+ if (mark.kind === "scorch") conform(mark, scorchMaterials[mark.variant]);
8057
+ else if (mark.kind === "dirt") conform(mark, dirtMaterial);
8058
+ else if (mark.kind === "rock") addRock(mark);
8059
+ else addBlood(mark);
8060
+ }
8061
+ function clearMarks() {
8062
+ for (const drawn of groundMarks) group.remove(drawn.mesh);
8063
+ for (const drawn of groundMarks) drawn.mesh.geometry.dispose();
8064
+ groundMarks.length = 0;
8065
+ for (const geometry of spareGeometries) geometry.dispose();
8066
+ spareGeometries.length = 0;
7776
8067
  blood.length = 0;
7777
- meshCount = 0;
8068
+ placed.blood = placed.rock = placed.plate = 0;
8069
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) if (mesh2) mesh2.count = 0;
7778
8070
  }
7779
8071
  return {
7780
8072
  group,
7781
8073
  handle(event) {
7782
- if (event.type === "clear") {
7783
- clearMeshes();
7784
- return;
7785
- }
7786
- group.add(build(event.mark));
7787
- meshCount++;
8074
+ if (event.type === "clear") clearMarks();
8075
+ else build(event.mark);
8076
+ },
8077
+ prepare() {
8078
+ bloodMesh();
8079
+ rockMesh();
8080
+ plateMesh();
7788
8081
  },
7789
8082
  update(dt) {
7790
- for (const drawn of blood) {
7791
- if (drawn.age > 1) continue;
8083
+ for (let index = 0; index < blood.length; index++) {
8084
+ const drawn = blood[index];
8085
+ if (!drawn || drawn.age > 1) continue;
7792
8086
  drawn.age += dt * BLOOD_GROW_RATE;
7793
- const ease3 = 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5);
7794
- drawn.mesh.scale.set(drawn.scaleX * Math.max(0.05, ease3), 1, drawn.scaleZ * Math.max(0.05, ease3));
8087
+ writeBlood(index, drawn, Math.max(BLOOD_START_GROWTH, 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5)));
7795
8088
  }
7796
8089
  },
7797
- count: () => meshCount,
8090
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh2) => mesh2 && mesh2.count > 0).length,
7798
8091
  dispose() {
7799
- clearMeshes();
8092
+ clearMarks();
8093
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) mesh2?.dispose();
7800
8094
  for (const item of owned) item.dispose();
7801
8095
  }
7802
8096
  };
7803
8097
  }
7804
8098
  var SPHERE_DEFAULT_WIDTH = 8;
7805
8099
  var SPHERE_DEFAULT_HEIGHT = 6;
8100
+ var MAX_PARKED_MESHES_PER_KIND = 256;
7806
8101
  function geometryOf(THREE, particle) {
7807
8102
  switch (particle.shape) {
7808
8103
  case "cone":
7809
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
8104
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, false);
7810
8105
  case "cone_open":
7811
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
8106
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, true);
7812
8107
  case "sphere":
7813
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
8108
+ return new THREE.SphereGeometry(1, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
7814
8109
  case "icosahedron":
7815
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
8110
+ return new THREE.IcosahedronGeometry(1, particle.segments);
7816
8111
  }
7817
8112
  }
8113
+ function isCone(particle) {
8114
+ return particle.shape === "cone" || particle.shape === "cone_open";
8115
+ }
7818
8116
  function materialOf(THREE, particle) {
7819
8117
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
7820
8118
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -7825,51 +8123,82 @@ void main(){
7825
8123
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
7826
8124
  );
7827
8125
  }
8126
+ function geometryKeyOf(particle) {
8127
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
8128
+ }
7828
8129
  function createMeshLayer(THREE, pool) {
7829
8130
  const group = new THREE.Group();
7830
8131
  group.name = "workshop-meshes";
7831
8132
  const drawn = /* @__PURE__ */ new Map();
8133
+ const geometries = /* @__PURE__ */ new Map();
8134
+ const parked = /* @__PURE__ */ new Map();
7832
8135
  const frame = new THREE.Quaternion();
7833
8136
  const euler = new THREE.Euler();
7834
8137
  const turn = new THREE.Quaternion();
7835
8138
  const offset = new THREE.Vector3();
7836
- function release(mesh2) {
7837
- mesh2.removeFromParent();
7838
- mesh2.geometry.dispose();
7839
- mesh2.material.dispose();
8139
+ let stamp = 0;
8140
+ function geometryFor(particle) {
8141
+ const key = geometryKeyOf(particle);
8142
+ let geometry = geometries.get(key);
8143
+ if (!geometry) {
8144
+ geometry = geometryOf(THREE, particle);
8145
+ geometries.set(key, geometry);
8146
+ }
8147
+ return geometry;
8148
+ }
8149
+ function acquire(particle) {
8150
+ const parkingKey = `${geometryKeyOf(particle)}|${particle.lit}|${particle.additive}`;
8151
+ const mesh2 = parked.get(parkingKey)?.pop() ?? new THREE.Mesh(geometryFor(particle), materialOf(THREE, particle));
8152
+ mesh2.material.color.set(particle.color);
8153
+ mesh2.frustumCulled = false;
8154
+ mesh2.renderOrder = particle.additive ? 5 : 3;
8155
+ group.add(mesh2);
8156
+ return { mesh: mesh2, parkingKey, stamp };
8157
+ }
8158
+ function park(entry) {
8159
+ entry.mesh.removeFromParent();
8160
+ let free = parked.get(entry.parkingKey);
8161
+ if (!free) {
8162
+ free = [];
8163
+ parked.set(entry.parkingKey, free);
8164
+ }
8165
+ if (free.length < MAX_PARKED_MESHES_PER_KIND) free.push(entry.mesh);
8166
+ else entry.mesh.material.dispose();
7840
8167
  }
7841
8168
  return {
7842
8169
  group,
7843
8170
  sync() {
7844
- const live = /* @__PURE__ */ new Set();
8171
+ stamp++;
7845
8172
  for (const particle of pool.particles) {
7846
- live.add(particle.id);
7847
- let mesh2 = drawn.get(particle.id);
7848
- if (!mesh2) {
7849
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
7850
- mesh2.frustumCulled = false;
7851
- mesh2.renderOrder = particle.additive ? 5 : 3;
7852
- drawn.set(particle.id, mesh2);
7853
- group.add(mesh2);
8173
+ let entry = drawn.get(particle.id);
8174
+ if (!entry) {
8175
+ entry = acquire(particle);
8176
+ drawn.set(particle.id, entry);
7854
8177
  }
8178
+ entry.stamp = stamp;
8179
+ const mesh2 = entry.mesh;
7855
8180
  frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
7856
8181
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
7857
8182
  mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
7858
8183
  turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
7859
8184
  mesh2.quaternion.copy(frame).multiply(turn);
7860
- mesh2.scale.setScalar(particle.scale);
8185
+ mesh2.scale.set(particle.radius * particle.scale, (isCone(particle) ? particle.height : particle.radius) * particle.scale, particle.radius * particle.scale);
7861
8186
  mesh2.material.opacity = particle.opacity;
7862
8187
  }
7863
- for (const [id, mesh2] of drawn) {
7864
- if (live.has(id)) continue;
7865
- release(mesh2);
8188
+ for (const [id, entry] of drawn) {
8189
+ if (entry.stamp === stamp) continue;
8190
+ park(entry);
7866
8191
  drawn.delete(id);
7867
8192
  }
7868
8193
  return pool.count;
7869
8194
  },
7870
8195
  dispose() {
7871
- for (const mesh2 of drawn.values()) release(mesh2);
8196
+ for (const entry of drawn.values()) park(entry);
7872
8197
  drawn.clear();
8198
+ for (const free of parked.values()) for (const mesh2 of free) mesh2.material.dispose();
8199
+ parked.clear();
8200
+ for (const geometry of geometries.values()) geometry.dispose();
8201
+ geometries.clear();
7873
8202
  group.removeFromParent();
7874
8203
  }
7875
8204
  };
@@ -8437,7 +8766,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8437
8766
  const shocks = createShockLayer(THREE, pools.shocks);
8438
8767
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
8439
8768
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
8440
- const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry });
8769
+ const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
8441
8770
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
8442
8771
  const stopMarks = world.onMarks((event) => marks.handle(event));
8443
8772
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -8463,6 +8792,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8463
8792
  const position = options.camera.position;
8464
8793
  world.setCamera([position.x, position.y, position.z]);
8465
8794
  }
8795
+ function attachWorkshopLayers() {
8796
+ const engine = world.peekWorkshop();
8797
+ if (!engine || engine === workshopEngine) return;
8798
+ workshopLayers?.dispose();
8799
+ if (workshopLayers) root.remove(workshopLayers.group);
8800
+ workshopEngine = engine;
8801
+ workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
8802
+ root.add(workshopLayers.group);
8803
+ applyLayers();
8804
+ }
8466
8805
  function syncAll() {
8467
8806
  lastDrawn.puffs = puffs.sync();
8468
8807
  lastDrawn.sparks = sparks.sync();
@@ -8471,15 +8810,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8471
8810
  lastDrawn.shocks = shocks.sync();
8472
8811
  lastDrawn.sprites = sprites.sync();
8473
8812
  lights.sync();
8474
- const engine = world.peekWorkshop();
8475
- if (engine && engine !== workshopEngine) {
8476
- workshopLayers?.dispose();
8477
- if (workshopLayers) root.remove(workshopLayers.group);
8478
- workshopEngine = engine;
8479
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
8480
- root.add(workshopLayers.group);
8481
- applyLayers();
8482
- }
8813
+ attachWorkshopLayers();
8483
8814
  const workshopCalls = workshopLayers?.sync() ?? 0;
8484
8815
  const glow = pools.lights.glow;
8485
8816
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -8507,6 +8838,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8507
8838
  });
8508
8839
  },
8509
8840
  stop: (handle) => world.stop(handle),
8841
+ // a module's closed space (a shut car's cabin): the world's smoke, dust and steam go round it
8842
+ enclose: (id, enclosure) => world.enclose(id, enclosure),
8510
8843
  configure(settings) {
8511
8844
  const band = settings.lod_band;
8512
8845
  if (typeof band === "string" && band in LOD_BANDS) world.forceLod(LOD_BANDS[band] ?? null);
@@ -8532,6 +8865,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8532
8865
  },
8533
8866
  warmUpProbes() {
8534
8867
  releaseAnchors();
8868
+ marks.prepare();
8869
+ world.workshop();
8870
+ attachWorkshopLayers();
8535
8871
  const meshes = createMeshLayerProbes(THREE);
8536
8872
  for (const mesh2 of meshes) mesh2.frustumCulled = false;
8537
8873
  const group = new THREE.Group();
@@ -10574,7 +10910,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10574
10910
  const direction = new THREE.Vector3();
10575
10911
  const location = new THREE.Vector3();
10576
10912
  const scale2 = new THREE.Vector3();
10577
- const rotation = new THREE.Quaternion();
10913
+ const rotation2 = new THREE.Quaternion();
10578
10914
  const matrix = new THREE.Matrix4();
10579
10915
  return {
10580
10916
  drops,
@@ -10594,9 +10930,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10594
10930
  const o = i * 3;
10595
10931
  const speed = Math.hypot(velocity[o], velocity[o + 1], velocity[o + 2]);
10596
10932
  direction.set(velocity[o], velocity[o + 1], velocity[o + 2]).normalize();
10597
- rotation.setFromUnitVectors(up, direction);
10933
+ rotation2.setFromUnitVectors(up, direction);
10598
10934
  const size = radius[i] * METRES_PER_MM;
10599
- matrix.compose(location.set(position[o], position[o + 1], position[o + 2]), rotation, scale2.set(size, size * (1 + Math.min(MAX_STRETCH, speed * STRETCH_PER_SPEED)), size));
10935
+ matrix.compose(location.set(position[o], position[o + 1], position[o + 2]), rotation2, scale2.set(size, size * (1 + Math.min(MAX_STRETCH, speed * STRETCH_PER_SPEED)), size));
10600
10936
  drops.setMatrixAt(i, matrix);
10601
10937
  drawn++;
10602
10938
  }