@adatechnology/game-destruction-three 0.11.1 → 0.12.1

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.
@@ -1357,11 +1357,231 @@ var GameDestructionThree = (() => {
1357
1357
  glow.outIntensity = glow.i * Math.pow(1 - age, glow.curve) * flicker;
1358
1358
  }
1359
1359
  }
1360
+ var LEHMER_MULTIPLIER = 16807;
1361
+ var LEHMER_MODULUS = 2147483647;
1362
+ var HASH_MIX = 2654435761;
1363
+ function normalizeLehmerSeed(seed) {
1364
+ const whole = Math.floor(Math.abs(seed)) % (LEHMER_MODULUS - 1);
1365
+ return whole === 0 ? 1 : whole;
1366
+ }
1367
+ function lehmer(seed) {
1368
+ let state = normalizeLehmerSeed(seed);
1369
+ const next = () => {
1370
+ state = state * LEHMER_MULTIPLIER % LEHMER_MODULUS;
1371
+ return (state - 1) / (LEHMER_MODULUS - 1);
1372
+ };
1373
+ return {
1374
+ next,
1375
+ between: (min2, max2) => min2 + (max2 - min2) * next(),
1376
+ direction(out) {
1377
+ const up = next() * 2 - 1;
1378
+ const angle = next() * Math.PI * 2;
1379
+ const ring2 = Math.sqrt(1 - up * up);
1380
+ out.x = ring2 * Math.cos(angle);
1381
+ out.y = up;
1382
+ out.z = ring2 * Math.sin(angle);
1383
+ return out;
1384
+ },
1385
+ get state() {
1386
+ return state;
1387
+ }
1388
+ };
1389
+ }
1390
+ function mixEventSeed(eventSeed) {
1391
+ let mixed = Math.imul((eventSeed | 0) ^ HASH_MIX, 2246822507);
1392
+ mixed ^= mixed >>> 13;
1393
+ mixed = Math.imul(mixed, 3266489909);
1394
+ mixed ^= mixed >>> 16;
1395
+ return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1;
1396
+ }
1397
+ function streamFor(eventSeed) {
1398
+ return lehmer(mixEventSeed(eventSeed));
1399
+ }
1400
+ var SKIN = 5e-3;
1401
+ var MAX_PAD = 0.45;
1402
+ function rotation(q) {
1403
+ const [x, y, z, w] = q ?? [0, 0, 0, 1];
1404
+ const n = Math.hypot(x, y, z, w) || 1;
1405
+ const qx = x / n;
1406
+ const qy = y / n;
1407
+ const qz = z / n;
1408
+ const qw = w / n;
1409
+ return new Float64Array([
1410
+ 1 - 2 * (qy * qy + qz * qz),
1411
+ 2 * (qx * qy - qz * qw),
1412
+ 2 * (qx * qz + qy * qw),
1413
+ 2 * (qx * qy + qz * qw),
1414
+ 1 - 2 * (qx * qx + qz * qz),
1415
+ 2 * (qy * qz - qx * qw),
1416
+ 2 * (qx * qz - qy * qw),
1417
+ 2 * (qy * qz + qx * qw),
1418
+ 1 - 2 * (qx * qx + qy * qy)
1419
+ ]);
1420
+ }
1421
+ var Enclosures = class {
1422
+ constructor(seed = 20973) {
1423
+ __publicField2(this, "spaces", /* @__PURE__ */ new Map());
1424
+ __publicField2(this, "list", []);
1425
+ __publicField2(this, "random");
1426
+ __publicField2(this, "out", { x: 0, y: 0, z: 0, nx: 0, ny: 0, nz: 0 });
1427
+ __publicField2(this, "l0", new Float64Array(3));
1428
+ __publicField2(this, "l1", new Float64Array(3));
1429
+ __publicField2(this, "q", new Float64Array(3));
1430
+ this.random = lehmer(seed);
1431
+ }
1432
+ get empty() {
1433
+ return this.list.length === 0;
1434
+ }
1435
+ get size() {
1436
+ return this.list.length;
1437
+ }
1438
+ /** Declares, moves or (with `null`) ends the space `id`. */
1439
+ set(id, enclosure) {
1440
+ if (!enclosure) {
1441
+ if (this.spaces.delete(id)) this.list = [...this.spaces.values()];
1442
+ return;
1443
+ }
1444
+ const [hx, hy, hz] = enclosure.halfSize;
1445
+ const space = {
1446
+ cx: enclosure.center[0],
1447
+ cy: enclosure.center[1],
1448
+ cz: enclosure.center[2],
1449
+ hx: Math.abs(hx),
1450
+ hy: Math.abs(hy),
1451
+ hz: Math.abs(hz),
1452
+ r: rotation(enclosure.orientation),
1453
+ radius: Math.hypot(hx, hy, hz),
1454
+ openings: enclosure.openings ?? [],
1455
+ leak: Math.max(0, enclosure.leak ?? 0)
1456
+ };
1457
+ const old = this.spaces.get(id);
1458
+ this.spaces.set(id, space);
1459
+ const at3 = old ? this.list.indexOf(old) : -1;
1460
+ if (at3 >= 0) this.list[at3] = space;
1461
+ else this.list.push(space);
1462
+ }
1463
+ get(id) {
1464
+ const s = this.spaces.get(id);
1465
+ if (!s) return void 0;
1466
+ return { center: [s.cx, s.cy, s.cz], halfSize: [s.hx, s.hy, s.hz], openings: s.openings, leak: s.leak };
1467
+ }
1468
+ ids() {
1469
+ return [...this.spaces.keys()];
1470
+ }
1471
+ clear() {
1472
+ this.spaces.clear();
1473
+ this.list = [];
1474
+ }
1475
+ /** Whether a point is inside any space (on the inside of its walls). */
1476
+ inside(x, y, z) {
1477
+ for (const s of this.list) {
1478
+ this.toLocal(s, x, y, z, this.l0);
1479
+ if (Math.abs(this.l0[0]) <= s.hx && Math.abs(this.l0[1]) <= s.hy && Math.abs(this.l0[2]) <= s.hz) return true;
1480
+ }
1481
+ return false;
1482
+ }
1483
+ toLocal(s, x, y, z, out) {
1484
+ const dx = x - s.cx;
1485
+ const dy = y - s.cy;
1486
+ const dz = z - s.cz;
1487
+ const r = s.r;
1488
+ out[0] = r[0] * dx + r[3] * dy + r[6] * dz;
1489
+ out[1] = r[1] * dx + r[4] * dy + r[7] * dz;
1490
+ out[2] = r[2] * dx + r[5] * dy + r[8] * dz;
1491
+ }
1492
+ /** Passes at `rate` per second. */
1493
+ chance(rate, dt) {
1494
+ return rate > 0 && this.random.next() < 1 - Math.exp(-rate * dt);
1495
+ }
1496
+ /** The opening the wall point `q` (local) is in lets it through now. */
1497
+ through(s, q, dt) {
1498
+ for (const o of s.openings) {
1499
+ 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;
1500
+ if (o.rate === void 0 || this.chance(o.rate, dt)) return true;
1501
+ }
1502
+ return this.chance(s.leak, dt);
1503
+ }
1504
+ confine(x0, y0, z0, x1, y1, z1, pad, dt) {
1505
+ const list = this.list;
1506
+ if (list.length === 0) return false;
1507
+ const p = Math.min(Math.max(0, pad), MAX_PAD);
1508
+ const l0 = this.l0;
1509
+ const l1 = this.l1;
1510
+ const q = this.q;
1511
+ for (const s of list) {
1512
+ const reach = s.radius + p + 0.05;
1513
+ const dx = x1 - s.cx;
1514
+ const dy = y1 - s.cy;
1515
+ const dz = z1 - s.cz;
1516
+ if (dx * dx + dy * dy + dz * dz > reach * reach) {
1517
+ const ex = x0 - s.cx;
1518
+ const ey = y0 - s.cy;
1519
+ const ez = z0 - s.cz;
1520
+ if (ex * ex + ey * ey + ez * ez > reach * reach) continue;
1521
+ }
1522
+ this.toLocal(s, x0, y0, z0, l0);
1523
+ this.toLocal(s, x1, y1, z1, l1);
1524
+ const h = [s.hx, s.hy, s.hz];
1525
+ const in0 = Math.abs(l0[0]) <= s.hx && Math.abs(l0[1]) <= s.hy && Math.abs(l0[2]) <= s.hz;
1526
+ let axis2 = 0;
1527
+ let sign = 1;
1528
+ if (in0) {
1529
+ if (Math.abs(l1[0]) <= s.hx && Math.abs(l1[1]) <= s.hy && Math.abs(l1[2]) <= s.hz) continue;
1530
+ for (let a = 0; a < 3; a++) q[a] = Math.min(h[a], Math.max(-h[a], l1[a]));
1531
+ if (this.through(s, q, dt)) continue;
1532
+ let worst = -Infinity;
1533
+ for (let a = 0; a < 3; a++) {
1534
+ const over = Math.abs(l1[a]) - h[a];
1535
+ if (over > worst) {
1536
+ worst = over;
1537
+ axis2 = a;
1538
+ }
1539
+ }
1540
+ sign = l1[axis2] < 0 ? -1 : 1;
1541
+ for (let a = 0; a < 3; a++) l1[a] = Math.min(h[a] - SKIN, Math.max(-(h[a] - SKIN), l1[a]));
1542
+ sign = -sign;
1543
+ } else {
1544
+ const padded0 = Math.abs(l0[0]) <= s.hx + p && Math.abs(l0[1]) <= s.hy + p && Math.abs(l0[2]) <= s.hz + p;
1545
+ const bound = padded0 ? 0 : p;
1546
+ if (!(Math.abs(l1[0]) <= s.hx + bound && Math.abs(l1[1]) <= s.hy + bound && Math.abs(l1[2]) <= s.hz + bound)) continue;
1547
+ for (let a = 0; a < 3; a++) q[a] = Math.min(h[a], Math.max(-h[a], l1[a]));
1548
+ if (this.through(s, q, dt)) continue;
1549
+ let worst = -Infinity;
1550
+ for (let a = 0; a < 3; a++) {
1551
+ const over = Math.abs(l0[a]) - (h[a] + bound);
1552
+ if (over > worst) {
1553
+ worst = over;
1554
+ axis2 = a;
1555
+ }
1556
+ }
1557
+ sign = l0[axis2] < 0 ? -1 : 1;
1558
+ l1[axis2] = sign * (h[axis2] + bound + SKIN);
1559
+ }
1560
+ const r = s.r;
1561
+ const out = this.out;
1562
+ out.x = s.cx + r[0] * l1[0] + r[1] * l1[1] + r[2] * l1[2];
1563
+ out.y = s.cy + r[3] * l1[0] + r[4] * l1[1] + r[5] * l1[2];
1564
+ out.z = s.cz + r[6] * l1[0] + r[7] * l1[1] + r[8] * l1[2];
1565
+ out.nx = r[axis2] * sign;
1566
+ out.ny = r[3 + axis2] * sign;
1567
+ out.nz = r[6 + axis2] * sign;
1568
+ return true;
1569
+ }
1570
+ return false;
1571
+ }
1572
+ };
1573
+ function slideAlong(out, vx2, vy2, vz2) {
1574
+ const into = vx2 * out.nx + vy2 * out.ny + vz2 * out.nz;
1575
+ if (into >= 0) return [vx2, vy2, vz2];
1576
+ return [vx2 - into * out.nx, vy2 - into * out.ny, vz2 - into * out.nz];
1577
+ }
1360
1578
  var FLOOR_FRACTION = 0.4;
1361
1579
  var FLOOR_BOUNCE = -0.2;
1362
1580
  var ARRIVAL_FRAME = 30;
1363
1581
  var ARRIVAL_BASE = 0.3;
1364
1582
  var GROWTH_CAP_SECONDS = 3.5;
1583
+ var CARRIED_KINDS = [true, true, false, true, false];
1584
+ var PAD_OF_SIZE = 0.35;
1365
1585
  function createPuffPool(capacity) {
1366
1586
  const placeholder = { next: () => 0, between: (min2) => min2, direction: (out) => out, state: 0 };
1367
1587
  return new OrderedPool(capacity, () => ({
@@ -1456,13 +1676,24 @@ var GameDestructionThree = (() => {
1456
1676
  if (puff3.vy < 0) puff3.vy *= FLOOR_BOUNCE;
1457
1677
  }
1458
1678
  }
1459
- function updatePuffs(pool, dt, wind) {
1679
+ function updatePuffs(pool, dt, wind, confiner) {
1460
1680
  pool.compact(ageOrKeep2, dt);
1681
+ const confining = confiner !== void 0 && !confiner.empty;
1461
1682
  for (const puff3 of pool.items) {
1462
1683
  const age = puff3.t / puff3.life;
1463
1684
  const heat = puff3.h0 > 0 ? puff3.h0 * Math.pow(Math.max(0, 1 - puff3.t / puff3.hl), 1.25) : 0;
1685
+ const x0 = puff3.x;
1686
+ const y0 = puff3.y;
1687
+ const z0 = puff3.z;
1464
1688
  if (puff3.ring) moveOnRing(puff3, puff3.ring, wind);
1465
1689
  else moveFree(puff3, dt, age, heat, wind);
1690
+ if (confining && CARRIED_KINDS[puff3.kind] && confiner.confine(x0, y0, z0, puff3.x, puff3.y, puff3.z, puff3.size * PAD_OF_SIZE, dt)) {
1691
+ const out = confiner.out;
1692
+ puff3.x = out.x;
1693
+ puff3.y = out.y;
1694
+ puff3.z = out.z;
1695
+ [puff3.vx, puff3.vy, puff3.vz] = slideAlong(out, puff3.vx, puff3.vy, puff3.vz);
1696
+ }
1466
1697
  const size = puff3.s0 + (puff3.s1 - puff3.s0) * (1 - Math.exp(-puff3.t * puff3.gk)) + puff3.sg * Math.min(puff3.t, GROWTH_CAP_SECONDS);
1467
1698
  puff3.size = size * Math.min(1, puff3.t * ARRIVAL_FRAME + ARRIVAL_BASE);
1468
1699
  puff3.age = age;
@@ -1988,10 +2219,10 @@ var GameDestructionThree = (() => {
1988
2219
  ])
1989
2220
  ]
1990
2221
  };
1991
- function localToWorld(translation, rotation, local2) {
2222
+ function localToWorld(translation, rotation2, local2) {
1992
2223
  const [x, y, z] = local2;
1993
- const [a, c, e] = [Math.cos(rotation[0]), Math.cos(rotation[1]), Math.cos(rotation[2])];
1994
- const [b, d, f] = [Math.sin(rotation[0]), Math.sin(rotation[1]), Math.sin(rotation[2])];
2224
+ const [a, c, e] = [Math.cos(rotation2[0]), Math.cos(rotation2[1]), Math.cos(rotation2[2])];
2225
+ const [b, d, f] = [Math.sin(rotation2[0]), Math.sin(rotation2[1]), Math.sin(rotation2[2])];
1995
2226
  const [ae, af, be, bf] = [a * e, a * f, b * e, b * f];
1996
2227
  return [
1997
2228
  c * e * x + -c * f * y + d * z + translation[0],
@@ -3654,46 +3885,6 @@ var GameDestructionThree = (() => {
3654
3885
  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];
3655
3886
  var WORKSHOP_EFFECTS = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]));
3656
3887
  var GAME_EFFECTS = { ...WORKSHOP_EFFECTS, ...Object.fromEntries([...EVENT_EFFECTS, ...VIEWMODEL_EFFECTS, ...VEHICLE_EFFECTS].map((effect) => [effect.id, effect])) };
3657
- var LEHMER_MULTIPLIER = 16807;
3658
- var LEHMER_MODULUS = 2147483647;
3659
- var HASH_MIX = 2654435761;
3660
- function normalizeLehmerSeed(seed) {
3661
- const whole = Math.floor(Math.abs(seed)) % (LEHMER_MODULUS - 1);
3662
- return whole === 0 ? 1 : whole;
3663
- }
3664
- function lehmer(seed) {
3665
- let state = normalizeLehmerSeed(seed);
3666
- const next = () => {
3667
- state = state * LEHMER_MULTIPLIER % LEHMER_MODULUS;
3668
- return (state - 1) / (LEHMER_MODULUS - 1);
3669
- };
3670
- return {
3671
- next,
3672
- between: (min2, max2) => min2 + (max2 - min2) * next(),
3673
- direction(out) {
3674
- const up = next() * 2 - 1;
3675
- const angle = next() * Math.PI * 2;
3676
- const ring2 = Math.sqrt(1 - up * up);
3677
- out.x = ring2 * Math.cos(angle);
3678
- out.y = up;
3679
- out.z = ring2 * Math.sin(angle);
3680
- return out;
3681
- },
3682
- get state() {
3683
- return state;
3684
- }
3685
- };
3686
- }
3687
- function mixEventSeed(eventSeed) {
3688
- let mixed = Math.imul((eventSeed | 0) ^ HASH_MIX, 2246822507);
3689
- mixed ^= mixed >>> 13;
3690
- mixed = Math.imul(mixed, 3266489909);
3691
- mixed ^= mixed >>> 16;
3692
- return (mixed >>> 0) % (LEHMER_MODULUS - 1) + 1;
3693
- }
3694
- function streamFor(eventSeed) {
3695
- return lehmer(mixEventSeed(eventSeed));
3696
- }
3697
3888
  var TABLE = {
3698
3889
  concrete: { dust: 9078138, chip: 7236195, spark: 2 },
3699
3890
  metal: { dust: 0, chip: 0, spark: 18 },
@@ -4997,8 +5188,8 @@ var GameDestructionThree = (() => {
4997
5188
  function addScorch(store, random, x, z, radius) {
4998
5189
  const variant = Math.floor(random.next() * SCORCH_VARIANTS);
4999
5190
  const lift = 0.03 + nextLift(store) * 1e-3;
5000
- const rotation = random.next() * 6;
5001
- store.add({ kind: "scorch", x, z, radius, variant, lift, rotation });
5191
+ const rotation2 = random.next() * 6;
5192
+ store.add({ kind: "scorch", x, z, radius, variant, lift, rotation: rotation2 });
5002
5193
  }
5003
5194
  function addRimRocks(store, ground, random, x, z, radius) {
5004
5195
  const count = Math.round(6 + radius * 7);
@@ -5027,8 +5218,8 @@ var GameDestructionThree = (() => {
5027
5218
  function addCrater(store, ground, random, spec) {
5028
5219
  ground?.dent(random, spec.x, spec.z, spec.radius, spec.depth);
5029
5220
  if (!spec.marks) return;
5030
- const rotation = random.next() * 6;
5031
- store.add({ kind: "dirt", x: spec.x, z: spec.z, radius: spec.radius * 1.02, lift: 0.045, rotation });
5221
+ const rotation2 = random.next() * 6;
5222
+ store.add({ kind: "dirt", x: spec.x, z: spec.z, radius: spec.radius * 1.02, lift: 0.045, rotation: rotation2 });
5032
5223
  addRimRocks(store, ground, random, spec.x, spec.z, spec.radius);
5033
5224
  }
5034
5225
  function addBloodSplat(store, ground, random, spec) {
@@ -5584,6 +5775,7 @@ var GameDestructionThree = (() => {
5584
5775
  state.gas = Math.max(0, state.gas - dt / DECAY.gasSeconds);
5585
5776
  state.beat *= Math.exp(-DECAY.beat * dt);
5586
5777
  }
5778
+ var PAD_OF_SIZE2 = 0.35;
5587
5779
  var clamp4 = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
5588
5780
  var lerp4 = (a, b, t) => a + (b - a) * t;
5589
5781
  function rgb(hex) {
@@ -5743,9 +5935,20 @@ var GameDestructionThree = (() => {
5743
5935
  vy2 *= k;
5744
5936
  vz2 *= k;
5745
5937
  }
5746
- const x = this.position[i3] + vx2 * dt;
5938
+ let x = this.position[i3] + vx2 * dt;
5747
5939
  let y = this.position[i3 + 1] + vy2 * dt;
5748
- const z = this.position[i3 + 2] + vz2 * dt;
5940
+ let z = this.position[i3 + 2] + vz2 * dt;
5941
+ const confiner = host.confiner;
5942
+ if (def.carried && confiner && !confiner.empty) {
5943
+ const pad = this.size0[i] * lerp4(1, def.grow, t) * PAD_OF_SIZE2;
5944
+ if (confiner.confine(this.position[i3], this.position[i3 + 1], this.position[i3 + 2], x, y, z, pad, dt)) {
5945
+ const out = confiner.out;
5946
+ x = out.x;
5947
+ y = out.y;
5948
+ z = out.z;
5949
+ [vx2, vy2, vz2] = slideAlong(out, vx2, vy2, vz2);
5950
+ }
5951
+ }
5749
5952
  if (def.ground !== "none") {
5750
5953
  const own = this.floor[i];
5751
5954
  const gy = Number.isNaN(own) ? host.groundAt(x, z) + 5e-3 : own;
@@ -5831,7 +6034,8 @@ var GameDestructionThree = (() => {
5831
6034
  alpha: 0.35,
5832
6035
  fadeIn: 0.15,
5833
6036
  cone: 0.6,
5834
- ground: "none"
6037
+ ground: "none",
6038
+ carried: true
5835
6039
  },
5836
6040
  sparks: {
5837
6041
  id: "sparks",
@@ -5875,7 +6079,8 @@ var GameDestructionThree = (() => {
5875
6079
  alpha: 0.4,
5876
6080
  fadeIn: 0.1,
5877
6081
  cone: 0.4,
5878
- ground: "none"
6082
+ ground: "none",
6083
+ carried: true
5879
6084
  },
5880
6085
  shards: {
5881
6086
  id: "shards",
@@ -6033,7 +6238,8 @@ var GameDestructionThree = (() => {
6033
6238
  fadeIn: 0.1,
6034
6239
  cone: 0.5,
6035
6240
  ground: "stop",
6036
- flutter: 0.6
6241
+ flutter: 0.6,
6242
+ carried: true
6037
6243
  },
6038
6244
  steam: {
6039
6245
  id: "steam",
@@ -6055,7 +6261,8 @@ var GameDestructionThree = (() => {
6055
6261
  alpha: 0.35,
6056
6262
  fadeIn: 0.15,
6057
6263
  cone: 0.6,
6058
- ground: "none"
6264
+ ground: "none",
6265
+ carried: true
6059
6266
  },
6060
6267
  flash: {
6061
6268
  id: "flash",
@@ -6188,7 +6395,8 @@ var GameDestructionThree = (() => {
6188
6395
  const host = {
6189
6396
  groundAt: this.options.groundAt,
6190
6397
  gravity: this.options.gravity,
6191
- land: (owner, x, y, z, color, tag) => this.listeners.get(owner)?.(x, y, z, color, tag)
6398
+ land: (owner, x, y, z, color, tag) => this.listeners.get(owner)?.(x, y, z, color, tag),
6399
+ ...this.options.confiner ? { confiner: this.options.confiner } : {}
6192
6400
  };
6193
6401
  if (this.additive.live > 0 || this.additive.drawCount > 0) this.additive.update(dt, host);
6194
6402
  if (this.normal.live > 0 || this.normal.drawCount > 0) this.normal.update(dt, host);
@@ -6394,8 +6602,8 @@ var GameDestructionThree = (() => {
6394
6602
  * 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
6395
6603
  * world axes). The surface they were on is gone (a brick came out of a wall with its bullet holes). Returns how many went.
6396
6604
  */
6397
- clearInside(center, half, rotation = [0, 0, 0, 1]) {
6398
- const qx = -rotation[0], qy = -rotation[1], qz = -rotation[2], qw = rotation[3];
6605
+ clearInside(center, half, rotation2 = [0, 0, 0, 1]) {
6606
+ const qx = -rotation2[0], qy = -rotation2[1], qz = -rotation2[2], qw = rotation2[3];
6399
6607
  let gone = 0;
6400
6608
  for (const decal2 of this.decals) {
6401
6609
  if (!decal2.on) continue;
@@ -6516,10 +6724,14 @@ var GameDestructionThree = (() => {
6516
6724
  var DEFAULT_FADE_IN = 0.1;
6517
6725
  var FLOATS_PER_VECTOR = 3;
6518
6726
  var BYTE_MAX2 = 255;
6727
+ var CARRIED_GRAVITY = 3;
6728
+ var PAD_OF_SIZE3 = 0.35;
6519
6729
  var SpritePool = class {
6520
- constructor(capacity, context) {
6730
+ /** `carried`: the normal-blend pool (smoke, dust, steam), whose light sprites go round the closed spaces; the additive one (flames, sparks) does not. */
6731
+ constructor(capacity, context, carried = false) {
6521
6732
  __publicField2(this, "capacity", capacity);
6522
6733
  __publicField2(this, "context", context);
6734
+ __publicField2(this, "carried", carried);
6523
6735
  __publicField2(this, "count", 0);
6524
6736
  __publicField2(this, "position");
6525
6737
  __publicField2(this, "velocity");
@@ -6619,6 +6831,7 @@ var GameDestructionThree = (() => {
6619
6831
  }
6620
6832
  update(dt, leaveDecal) {
6621
6833
  const { position, velocity } = this;
6834
+ const confiner = this.context.confiner;
6622
6835
  for (let index = this.count - 1; index >= 0; index--) {
6623
6836
  this.life[index] -= dt;
6624
6837
  if (this.life[index] <= 0) {
@@ -6632,9 +6845,25 @@ var GameDestructionThree = (() => {
6632
6845
  velocity[offset] *= factor;
6633
6846
  velocity[offset + 1] *= factor;
6634
6847
  velocity[offset + 2] *= factor;
6848
+ const x0 = position[offset];
6849
+ const y0 = position[offset + 1];
6850
+ const z0 = position[offset + 2];
6635
6851
  position[offset] += velocity[offset] * dt;
6636
6852
  position[offset + 1] += velocity[offset + 1] * dt;
6637
6853
  position[offset + 2] += velocity[offset + 2] * dt;
6854
+ if (this.carried && confiner && !confiner.empty && Math.abs(this.gravity[index]) < CARRIED_GRAVITY) {
6855
+ const size = this.startSize[index] * (1 + (this.growth[index] - 1) * age);
6856
+ if (confiner.confine(x0, y0, z0, position[offset], position[offset + 1], position[offset + 2], size * PAD_OF_SIZE3, dt)) {
6857
+ const out = confiner.out;
6858
+ position[offset] = out.x;
6859
+ position[offset + 1] = out.y;
6860
+ position[offset + 2] = out.z;
6861
+ const [vx2, vy2, vz2] = slideAlong(out, velocity[offset], velocity[offset + 1], velocity[offset + 2]);
6862
+ velocity[offset] = vx2;
6863
+ velocity[offset + 1] = vy2;
6864
+ velocity[offset + 2] = vz2;
6865
+ }
6866
+ }
6638
6867
  const mode = this.groundMode[index];
6639
6868
  if (mode) {
6640
6869
  const groundHeight = this.context.groundAt(position[offset], position[offset + 2]);
@@ -6911,9 +7140,9 @@ var GameDestructionThree = (() => {
6911
7140
  __publicField2(this, "placement");
6912
7141
  __publicField2(this, "accumulators", /* @__PURE__ */ new Map());
6913
7142
  const profile = options.profile ?? PROFILES.high;
6914
- this.context = { random: options.random, groundAt: options.groundAt ?? workshopGround, quality: { level: 0, mult: 1 }, trace: options.trace };
7143
+ this.context = { random: options.random, groundAt: options.groundAt ?? workshopGround, quality: { level: 0, mult: 1 }, trace: options.trace, ...options.confiner ? { confiner: options.confiner } : {} };
6915
7144
  this.add = new SpritePool(profile.sprites.add, this.context);
6916
- this.normal = new SpritePool(profile.sprites.normal, this.context);
7145
+ this.normal = new SpritePool(profile.sprites.normal, this.context, true);
6917
7146
  this.streaks = new StreakPool(profile.streaks, this.context);
6918
7147
  this.debris = new DebrisPool(this.context);
6919
7148
  this.decals = new DecalPool(this.context);
@@ -7101,12 +7330,14 @@ var GameDestructionThree = (() => {
7101
7330
  const groundAt = options.groundAt ?? (ground ? (x, z) => ground.heightAt(x, z) : () => 0);
7102
7331
  const handles = { next: 0, events: 0 };
7103
7332
  const low = options.profile === "low";
7333
+ const enclosures = new Enclosures(20973 + (options.seed ?? 0));
7104
7334
  const spriteSystem = new SpriteKindSystem({
7105
7335
  capacity: options.sprites?.capacity ?? (low ? 1250 : 2500),
7106
7336
  density: options.sprites?.density ?? (low ? 0.6 : 1),
7107
7337
  seed: 2654435761 + (options.seed ?? 0),
7108
7338
  groundAt,
7109
7339
  gravity: options.gravity ?? DEFAULT_GRAVITY,
7340
+ confiner: enclosures,
7110
7341
  ...options.sprites?.kinds ? { kinds: options.sprites.kinds } : {}
7111
7342
  });
7112
7343
  const core = {
@@ -7156,7 +7387,7 @@ var GameDestructionThree = (() => {
7156
7387
  return [camera2.x, camera2.y, camera2.z];
7157
7388
  }, forward: (local2) => [local2[0] ?? 0, local2[1] ?? 0, local2[2] ?? 0] };
7158
7389
  function workshop() {
7159
- return engine ?? (engine = new WorkshopEngine({ random: sharedRandom, profile, groundAt }));
7390
+ return engine ?? (engine = new WorkshopEngine({ random: sharedRandom, profile, groundAt, confiner: enclosures }));
7160
7391
  }
7161
7392
  function effectPlayer() {
7162
7393
  return player ?? (player = createEffectPlayer({ engine: workshop(), effects, view }));
@@ -7215,6 +7446,10 @@ var GameDestructionThree = (() => {
7215
7446
  puff3.vz += dz * strength;
7216
7447
  }
7217
7448
  },
7449
+ enclose(id, enclosure) {
7450
+ enclosures.set(id, enclosure);
7451
+ },
7452
+ enclosures: () => enclosures,
7218
7453
  stop(handle) {
7219
7454
  player?.stop(handle);
7220
7455
  const fires = core.fires;
@@ -7242,7 +7477,7 @@ var GameDestructionThree = (() => {
7242
7477
  core.clock.time += dt;
7243
7478
  core.schedule.update(core.clock.time);
7244
7479
  updateRings(core.rings, dt);
7245
- updatePuffs(core.puffs, dt, wind);
7480
+ updatePuffs(core.puffs, dt, wind, enclosures);
7246
7481
  updateSparks(core.sparks, { dt, groundAt });
7247
7482
  updateGlows(core.glows, dt);
7248
7483
  updateShocks(core.shocks, core.puffs.items, dt);
@@ -7727,6 +7962,11 @@ void main(){
7727
7962
  var MAX_CONFORM_STEPS = 48;
7728
7963
  var MIN_CONFORM_STEPS = 16;
7729
7964
  var BLOOD_GROW_RATE = 1.2;
7965
+ var BLOOD_START_GROWTH = 0.05;
7966
+ var DEFAULT_MAX_GROUND_MARKS = 64;
7967
+ var DEFAULT_MAX_ROCKS = 512;
7968
+ var DEFAULT_MAX_BLOOD = 256;
7969
+ var MAX_SPARE_GROUND_GEOMETRIES = 16;
7730
7970
  function createMarkLayer(THREE, options) {
7731
7971
  const group = new THREE.Group();
7732
7972
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -7750,110 +7990,168 @@ void main(){
7750
7990
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
7751
7991
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
7752
7992
  owned.push(bloodMaterial, bloodGeometry);
7753
- const rockMaterials = /* @__PURE__ */ new Map();
7754
- const blood = [];
7755
- let meshCount = 0;
7756
- function rockMaterial(color) {
7757
- let material = rockMaterials.get(color);
7758
- if (!material) {
7759
- material = new THREE.MeshStandardMaterial({ color, roughness: 0.9, metalness: 0, flatShading: true });
7760
- rockMaterials.set(color, material);
7761
- owned.push(material);
7993
+ const maxGroundMarks = options.maxGroundMarks ?? DEFAULT_MAX_GROUND_MARKS;
7994
+ const instanced = {};
7995
+ function rockMaterial() {
7996
+ if (!instanced.rockMaterial) {
7997
+ instanced.rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.9, metalness: 0, flatShading: true });
7998
+ owned.push(instanced.rockMaterial);
7762
7999
  }
7763
- return material;
8000
+ return instanced.rockMaterial;
8001
+ }
8002
+ function bloodMesh() {
8003
+ return instanced.blood ?? (instanced.blood = attach(createInstanced(bloodGeometry, bloodMaterial, options.maxBlood ?? DEFAULT_MAX_BLOOD)));
8004
+ }
8005
+ function rockMesh() {
8006
+ return instanced.rock ?? (instanced.rock = attach(createInstanced(options.rockGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
8007
+ }
8008
+ function plateMesh() {
8009
+ return instanced.plate ?? (instanced.plate = attach(createInstanced(options.plateGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
8010
+ }
8011
+ function attach(mesh2, castShadow = false) {
8012
+ mesh2.castShadow = castShadow;
8013
+ group.add(mesh2);
8014
+ return mesh2;
8015
+ }
8016
+ const groundMarks = [];
8017
+ const spareGeometries = [];
8018
+ const blood = [];
8019
+ const placed = { blood: 0, rock: 0, plate: 0 };
8020
+ const matrix = new THREE.Matrix4();
8021
+ const position = new THREE.Vector3();
8022
+ const quaternion = new THREE.Quaternion();
8023
+ const euler = new THREE.Euler();
8024
+ const scale = new THREE.Vector3();
8025
+ const color = new THREE.Color();
8026
+ const up = new THREE.Vector3(0, 1, 0);
8027
+ function createInstanced(geometry, material, capacity) {
8028
+ const mesh2 = new THREE.InstancedMesh(geometry, material, capacity);
8029
+ mesh2.count = 0;
8030
+ mesh2.frustumCulled = false;
8031
+ mesh2.receiveShadow = true;
8032
+ return mesh2;
8033
+ }
8034
+ function takeSpare(vertexCount) {
8035
+ const found = spareGeometries.findIndex((geometry) => geometry.getAttribute("position").count === vertexCount);
8036
+ return found < 0 ? void 0 : spareGeometries.splice(found, 1)[0];
8037
+ }
8038
+ function evictOldestGroundMark() {
8039
+ const oldest = groundMarks.shift();
8040
+ group.remove(oldest.mesh);
8041
+ spareGeometries.push(oldest.mesh.geometry);
8042
+ if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
7764
8043
  }
7765
8044
  function conform(mark, material) {
7766
8045
  const radius = mark.radius;
7767
8046
  const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
7768
- const geometry = new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
7769
- const position = geometry.attributes.position;
8047
+ if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
8048
+ const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
8049
+ const grid = geometry.attributes.position;
8050
+ const cell = 2 / steps;
7770
8051
  const cos2 = Math.cos(mark.rotation);
7771
8052
  const sin2 = Math.sin(mark.rotation);
7772
- for (let index = 0; index < position.count; index++) {
7773
- const localX = position.getX(index) * radius;
7774
- const localZ = position.getZ(index) * radius;
8053
+ for (let index = 0; index < grid.count; index++) {
8054
+ const localX = Math.fround(index % (steps + 1) * cell - 1) * radius;
8055
+ const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
7775
8056
  const worldX = mark.x + localX * cos2 - localZ * sin2;
7776
8057
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
7777
- position.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
8058
+ grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
7778
8059
  }
8060
+ grid.needsUpdate = true;
7779
8061
  geometry.computeVertexNormals();
8062
+ geometry.attributes.normal.needsUpdate = true;
8063
+ geometry.boundingSphere = null;
8064
+ geometry.boundingBox = null;
7780
8065
  const mesh2 = new THREE.Mesh(geometry, material);
7781
8066
  mesh2.receiveShadow = true;
7782
- return mesh2;
8067
+ group.add(mesh2);
8068
+ groundMarks.push({ mesh: mesh2, steps });
7783
8069
  }
7784
8070
  function addRock(mark) {
7785
- const mesh2 = new THREE.Mesh(mark.plate ? options.plateGeometry : options.rockGeometry, rockMaterial(mark.color));
7786
- mesh2.position.set(mark.x, mark.y, mark.z);
7787
- mesh2.scale.set(mark.scale[0], mark.scale[1], mark.scale[2]);
7788
- mesh2.rotation.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]);
7789
- mesh2.castShadow = true;
7790
- mesh2.receiveShadow = true;
7791
- return mesh2;
8071
+ const mesh2 = mark.plate ? plateMesh() : rockMesh();
8072
+ const slot = mark.plate ? "plate" : "rock";
8073
+ const index = placed[slot]++ % mesh2.instanceMatrix.count;
8074
+ quaternion.setFromEuler(euler.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]));
8075
+ 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])));
8076
+ mesh2.setColorAt(index, color.set(mark.color));
8077
+ mesh2.count = Math.min(placed[slot], mesh2.instanceMatrix.count);
8078
+ mesh2.instanceMatrix.needsUpdate = true;
8079
+ mesh2.instanceColor.needsUpdate = true;
8080
+ }
8081
+ function writeBlood(index, drawn, growth) {
8082
+ quaternion.setFromAxisAngle(up, drawn.rotation);
8083
+ const mesh2 = bloodMesh();
8084
+ 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)));
8085
+ mesh2.instanceMatrix.needsUpdate = true;
7792
8086
  }
7793
8087
  function addBlood(mark) {
7794
- const mesh2 = new THREE.Mesh(bloodGeometry, bloodMaterial);
7795
- mesh2.position.set(mark.x, mark.y, mark.z);
7796
- mesh2.scale.set(mark.scaleX, 1, mark.scaleZ);
7797
- mesh2.rotation.y = mark.rotation;
7798
- mesh2.receiveShadow = true;
7799
- mesh2.scale.multiplyScalar(0.05);
7800
- blood.push({ mesh: mesh2, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
7801
- return mesh2;
8088
+ const index = placed.blood++ % bloodMesh().instanceMatrix.count;
8089
+ const drawn = { position: [mark.x, mark.y, mark.z], rotation: mark.rotation, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 };
8090
+ blood[index] = drawn;
8091
+ writeBlood(index, drawn, BLOOD_START_GROWTH);
8092
+ bloodMesh().count = Math.min(placed.blood, bloodMesh().instanceMatrix.count);
7802
8093
  }
7803
8094
  function build(mark) {
7804
- if (mark.kind === "scorch") return conform(mark, scorchMaterials[mark.variant]);
7805
- if (mark.kind === "dirt") return conform(mark, dirtMaterial);
7806
- if (mark.kind === "rock") return addRock(mark);
7807
- return addBlood(mark);
7808
- }
7809
- function clearMeshes() {
7810
- for (const child of [...group.children]) {
7811
- group.remove(child);
7812
- const geometry = child.geometry;
7813
- if (geometry !== options.rockGeometry && geometry !== options.plateGeometry && geometry !== bloodGeometry) geometry.dispose();
7814
- }
8095
+ if (mark.kind === "scorch") conform(mark, scorchMaterials[mark.variant]);
8096
+ else if (mark.kind === "dirt") conform(mark, dirtMaterial);
8097
+ else if (mark.kind === "rock") addRock(mark);
8098
+ else addBlood(mark);
8099
+ }
8100
+ function clearMarks() {
8101
+ for (const drawn of groundMarks) group.remove(drawn.mesh);
8102
+ for (const drawn of groundMarks) drawn.mesh.geometry.dispose();
8103
+ groundMarks.length = 0;
8104
+ for (const geometry of spareGeometries) geometry.dispose();
8105
+ spareGeometries.length = 0;
7815
8106
  blood.length = 0;
7816
- meshCount = 0;
8107
+ placed.blood = placed.rock = placed.plate = 0;
8108
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) if (mesh2) mesh2.count = 0;
7817
8109
  }
7818
8110
  return {
7819
8111
  group,
7820
8112
  handle(event) {
7821
- if (event.type === "clear") {
7822
- clearMeshes();
7823
- return;
7824
- }
7825
- group.add(build(event.mark));
7826
- meshCount++;
8113
+ if (event.type === "clear") clearMarks();
8114
+ else build(event.mark);
8115
+ },
8116
+ prepare() {
8117
+ bloodMesh();
8118
+ rockMesh();
8119
+ plateMesh();
7827
8120
  },
7828
8121
  update(dt) {
7829
- for (const drawn of blood) {
7830
- if (drawn.age > 1) continue;
8122
+ for (let index = 0; index < blood.length; index++) {
8123
+ const drawn = blood[index];
8124
+ if (!drawn || drawn.age > 1) continue;
7831
8125
  drawn.age += dt * BLOOD_GROW_RATE;
7832
- const ease3 = 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5);
7833
- drawn.mesh.scale.set(drawn.scaleX * Math.max(0.05, ease3), 1, drawn.scaleZ * Math.max(0.05, ease3));
8126
+ writeBlood(index, drawn, Math.max(BLOOD_START_GROWTH, 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5)));
7834
8127
  }
7835
8128
  },
7836
- count: () => meshCount,
8129
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh2) => mesh2 && mesh2.count > 0).length,
7837
8130
  dispose() {
7838
- clearMeshes();
8131
+ clearMarks();
8132
+ for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) mesh2?.dispose();
7839
8133
  for (const item of owned) item.dispose();
7840
8134
  }
7841
8135
  };
7842
8136
  }
7843
8137
  var SPHERE_DEFAULT_WIDTH = 8;
7844
8138
  var SPHERE_DEFAULT_HEIGHT = 6;
8139
+ var MAX_PARKED_MESHES_PER_KIND = 256;
7845
8140
  function geometryOf(THREE, particle) {
7846
8141
  switch (particle.shape) {
7847
8142
  case "cone":
7848
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
8143
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, false);
7849
8144
  case "cone_open":
7850
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
8145
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, true);
7851
8146
  case "sphere":
7852
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
8147
+ return new THREE.SphereGeometry(1, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
7853
8148
  case "icosahedron":
7854
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
8149
+ return new THREE.IcosahedronGeometry(1, particle.segments);
7855
8150
  }
7856
8151
  }
8152
+ function isCone(particle) {
8153
+ return particle.shape === "cone" || particle.shape === "cone_open";
8154
+ }
7857
8155
  function materialOf(THREE, particle) {
7858
8156
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
7859
8157
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -7864,51 +8162,82 @@ void main(){
7864
8162
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
7865
8163
  );
7866
8164
  }
8165
+ function geometryKeyOf(particle) {
8166
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
8167
+ }
7867
8168
  function createMeshLayer(THREE, pool) {
7868
8169
  const group = new THREE.Group();
7869
8170
  group.name = "workshop-meshes";
7870
8171
  const drawn = /* @__PURE__ */ new Map();
8172
+ const geometries = /* @__PURE__ */ new Map();
8173
+ const parked = /* @__PURE__ */ new Map();
7871
8174
  const frame = new THREE.Quaternion();
7872
8175
  const euler = new THREE.Euler();
7873
8176
  const turn = new THREE.Quaternion();
7874
8177
  const offset = new THREE.Vector3();
7875
- function release(mesh2) {
7876
- mesh2.removeFromParent();
7877
- mesh2.geometry.dispose();
7878
- mesh2.material.dispose();
8178
+ let stamp = 0;
8179
+ function geometryFor(particle) {
8180
+ const key = geometryKeyOf(particle);
8181
+ let geometry = geometries.get(key);
8182
+ if (!geometry) {
8183
+ geometry = geometryOf(THREE, particle);
8184
+ geometries.set(key, geometry);
8185
+ }
8186
+ return geometry;
8187
+ }
8188
+ function acquire(particle) {
8189
+ const parkingKey = `${geometryKeyOf(particle)}|${particle.lit}|${particle.additive}`;
8190
+ const mesh2 = parked.get(parkingKey)?.pop() ?? new THREE.Mesh(geometryFor(particle), materialOf(THREE, particle));
8191
+ mesh2.material.color.set(particle.color);
8192
+ mesh2.frustumCulled = false;
8193
+ mesh2.renderOrder = particle.additive ? 5 : 3;
8194
+ group.add(mesh2);
8195
+ return { mesh: mesh2, parkingKey, stamp };
8196
+ }
8197
+ function park(entry) {
8198
+ entry.mesh.removeFromParent();
8199
+ let free = parked.get(entry.parkingKey);
8200
+ if (!free) {
8201
+ free = [];
8202
+ parked.set(entry.parkingKey, free);
8203
+ }
8204
+ if (free.length < MAX_PARKED_MESHES_PER_KIND) free.push(entry.mesh);
8205
+ else entry.mesh.material.dispose();
7879
8206
  }
7880
8207
  return {
7881
8208
  group,
7882
8209
  sync() {
7883
- const live = /* @__PURE__ */ new Set();
8210
+ stamp++;
7884
8211
  for (const particle of pool.particles) {
7885
- live.add(particle.id);
7886
- let mesh2 = drawn.get(particle.id);
7887
- if (!mesh2) {
7888
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
7889
- mesh2.frustumCulled = false;
7890
- mesh2.renderOrder = particle.additive ? 5 : 3;
7891
- drawn.set(particle.id, mesh2);
7892
- group.add(mesh2);
8212
+ let entry = drawn.get(particle.id);
8213
+ if (!entry) {
8214
+ entry = acquire(particle);
8215
+ drawn.set(particle.id, entry);
7893
8216
  }
8217
+ entry.stamp = stamp;
8218
+ const mesh2 = entry.mesh;
7894
8219
  frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
7895
8220
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
7896
8221
  mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
7897
8222
  turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
7898
8223
  mesh2.quaternion.copy(frame).multiply(turn);
7899
- mesh2.scale.setScalar(particle.scale);
8224
+ mesh2.scale.set(particle.radius * particle.scale, (isCone(particle) ? particle.height : particle.radius) * particle.scale, particle.radius * particle.scale);
7900
8225
  mesh2.material.opacity = particle.opacity;
7901
8226
  }
7902
- for (const [id, mesh2] of drawn) {
7903
- if (live.has(id)) continue;
7904
- release(mesh2);
8227
+ for (const [id, entry] of drawn) {
8228
+ if (entry.stamp === stamp) continue;
8229
+ park(entry);
7905
8230
  drawn.delete(id);
7906
8231
  }
7907
8232
  return pool.count;
7908
8233
  },
7909
8234
  dispose() {
7910
- for (const mesh2 of drawn.values()) release(mesh2);
8235
+ for (const entry of drawn.values()) park(entry);
7911
8236
  drawn.clear();
8237
+ for (const free of parked.values()) for (const mesh2 of free) mesh2.material.dispose();
8238
+ parked.clear();
8239
+ for (const geometry of geometries.values()) geometry.dispose();
8240
+ geometries.clear();
7912
8241
  group.removeFromParent();
7913
8242
  }
7914
8243
  };
@@ -8476,7 +8805,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8476
8805
  const shocks = createShockLayer(THREE, pools.shocks);
8477
8806
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
8478
8807
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
8479
- const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry });
8808
+ const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
8480
8809
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
8481
8810
  const stopMarks = world.onMarks((event) => marks.handle(event));
8482
8811
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -8502,6 +8831,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8502
8831
  const position = options.camera.position;
8503
8832
  world.setCamera([position.x, position.y, position.z]);
8504
8833
  }
8834
+ function attachWorkshopLayers() {
8835
+ const engine = world.peekWorkshop();
8836
+ if (!engine || engine === workshopEngine) return;
8837
+ workshopLayers?.dispose();
8838
+ if (workshopLayers) root.remove(workshopLayers.group);
8839
+ workshopEngine = engine;
8840
+ workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
8841
+ root.add(workshopLayers.group);
8842
+ applyLayers();
8843
+ }
8505
8844
  function syncAll() {
8506
8845
  lastDrawn.puffs = puffs.sync();
8507
8846
  lastDrawn.sparks = sparks.sync();
@@ -8510,15 +8849,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8510
8849
  lastDrawn.shocks = shocks.sync();
8511
8850
  lastDrawn.sprites = sprites.sync();
8512
8851
  lights.sync();
8513
- const engine = world.peekWorkshop();
8514
- if (engine && engine !== workshopEngine) {
8515
- workshopLayers?.dispose();
8516
- if (workshopLayers) root.remove(workshopLayers.group);
8517
- workshopEngine = engine;
8518
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
8519
- root.add(workshopLayers.group);
8520
- applyLayers();
8521
- }
8852
+ attachWorkshopLayers();
8522
8853
  const workshopCalls = workshopLayers?.sync() ?? 0;
8523
8854
  const glow = pools.lights.glow;
8524
8855
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -8546,6 +8877,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8546
8877
  });
8547
8878
  },
8548
8879
  stop: (handle) => world.stop(handle),
8880
+ // a module's closed space (a shut car's cabin): the world's smoke, dust and steam go round it
8881
+ enclose: (id, enclosure) => world.enclose(id, enclosure),
8549
8882
  configure(settings) {
8550
8883
  const band = settings.lod_band;
8551
8884
  if (typeof band === "string" && band in LOD_BANDS) world.forceLod(LOD_BANDS[band] ?? null);
@@ -8571,6 +8904,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8571
8904
  },
8572
8905
  warmUpProbes() {
8573
8906
  releaseAnchors();
8907
+ marks.prepare();
8908
+ world.workshop();
8909
+ attachWorkshopLayers();
8574
8910
  const meshes = createMeshLayerProbes(THREE);
8575
8911
  for (const mesh2 of meshes) mesh2.frustumCulled = false;
8576
8912
  const group = new THREE.Group();
@@ -8691,6 +9027,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8691
9027
  var SHEET_SWAY = 3;
8692
9028
  var BIG_LENGTH = 0.2;
8693
9029
  var BIG_WIDTH = 0.03;
9030
+ var FRAGMENT_SEED = 1374496523;
8694
9031
  var isBig = (size) => {
8695
9032
  const [, middle, longest] = [...size].sort((a, b) => a - b);
8696
9033
  return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
@@ -8710,7 +9047,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8710
9047
  __publicField(this, "m");
8711
9048
  __publicField(this, "v");
8712
9049
  __publicField(this, "w");
8713
- __publicField(this, "rng", createRng(1374496523));
9050
+ __publicField(this, "rng", createRng(FRAGMENT_SEED));
8714
9051
  /** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
8715
9052
  __publicField(this, "tick", 0);
8716
9053
  __publicField(this, "markGeometry", null);
@@ -8811,6 +9148,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8811
9148
  mesh2.scale.copy(sc);
8812
9149
  mesh2.castShadow = true;
8813
9150
  mesh2.userData.destructionDebris = true;
9151
+ mesh2.name = "fragment";
9152
+ markKitOrigin(mesh2, { owner: "game-destruction-three", kind: "fragment", id: f.material });
8814
9153
  this.o.scene.add(mesh2);
8815
9154
  const live = {
8816
9155
  mesh: mesh2,
@@ -8827,7 +9166,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8827
9166
  dying: false,
8828
9167
  air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
8829
9168
  body: null,
8830
- born: this.tick
9169
+ born: this.tick,
9170
+ life: Math.max(0, typeof this.o.life === "function" ? this.o.life(f, home) : this.o.life)
8831
9171
  };
8832
9172
  if (f.shape !== "shard" && isBig(f.size)) {
8833
9173
  live.body = {
@@ -8836,8 +9176,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8836
9176
  live.vel.x = x;
8837
9177
  live.vel.z = z;
8838
9178
  live.vel.y = Math.max(live.vel.y, 0.3);
8839
- live.asleep = false;
8840
- live.rest = 0;
9179
+ this.rouse(live);
8841
9180
  }
8842
9181
  };
8843
9182
  }
@@ -8872,16 +9211,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8872
9211
  d.y += k * 0.4;
8873
9212
  f.vel.add(d);
8874
9213
  f.ang.add(this.w.set(this.rng() - 0.5, this.rng() - 0.5, this.rng() - 0.5).multiplyScalar(k));
8875
- f.asleep = false;
8876
- f.rest = 0;
9214
+ this.rouse(f);
8877
9215
  }
8878
9216
  }
9217
+ /** A fragment that came to rest stops having its matrix recomputed on every frame; whatever moves it again calls this first. */
9218
+ rouse(f) {
9219
+ f.asleep = false;
9220
+ f.rest = 0;
9221
+ f.mesh.matrixAutoUpdate = true;
9222
+ }
8879
9223
  /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
8880
9224
  wakeRegion(x, z, radius) {
8881
9225
  for (const f of this.list) {
8882
9226
  if (!f.asleep || Math.hypot(f.mesh.position.x - x, f.mesh.position.z - z) > radius) continue;
8883
- f.asleep = false;
8884
- f.rest = 0;
9227
+ this.rouse(f);
8885
9228
  }
8886
9229
  }
8887
9230
  update(dt) {
@@ -8890,6 +9233,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8890
9233
  for (let i = this.list.length - 1; i >= 0; i--) {
8891
9234
  const f = this.list[i];
8892
9235
  if (f.dying) {
9236
+ f.mesh.matrixAutoUpdate = true;
8893
9237
  f.fade -= dt / 0.6;
8894
9238
  if (f.fade <= 0) {
8895
9239
  this.o.scene.remove(f.mesh);
@@ -8900,7 +9244,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8900
9244
  }
8901
9245
  if (f.asleep) {
8902
9246
  f.rest += dt;
8903
- if (this.o.life > 0 && f.rest > this.o.life && !f.body) f.dying = true;
9247
+ if (f.life > 0 && f.rest > f.life && !f.body) {
9248
+ f.dying = true;
9249
+ f.mesh.matrixAutoUpdate = true;
9250
+ }
8904
9251
  continue;
8905
9252
  }
8906
9253
  this.step(f, dt, g);
@@ -8949,6 +9296,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8949
9296
  f.still += dt;
8950
9297
  if (f.still > 0.4) {
8951
9298
  f.asleep = true;
9299
+ f.mesh.updateMatrix();
9300
+ f.mesh.matrixAutoUpdate = false;
8952
9301
  f.vel.set(0, 0, 0);
8953
9302
  f.ang.set(0, 0, 0);
8954
9303
  }
@@ -8998,6 +9347,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8998
9347
  for (const f of this.list) this.o.scene.remove(f.mesh);
8999
9348
  this.list.length = 0;
9000
9349
  }
9350
+ /** `clear`, the random stream back to its seed and the tick to 0: what comes next flies as on a new system. */
9351
+ reset() {
9352
+ this.clear();
9353
+ this.rng = createRng(FRAGMENT_SEED);
9354
+ this.tick = 0;
9355
+ }
9001
9356
  dispose() {
9002
9357
  this.clear();
9003
9358
  this.box.dispose();
@@ -9096,6 +9451,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9096
9451
  var SUB_DT = 1 / 120;
9097
9452
  var MU = 0.7;
9098
9453
  var SLEEP_AFTER = 0.5;
9454
+ var CALM_DRIFT = 0.01;
9455
+ var CALM_TURN = 0.05;
9456
+ var CALM_SLEEP_AFTER = 1.5;
9099
9457
  var HARD_LANDING = 3;
9100
9458
  var SLOP = 2e-3;
9101
9459
  var NARROW = 0.3;
@@ -9104,6 +9462,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9104
9462
  var TIP_LYING = 0.25;
9105
9463
  var TIP_SECONDS = 3;
9106
9464
  var STEP = 0.03;
9465
+ var WARM_SLOTS = 8 + 12 * 7;
9466
+ var SOLVER_PASSES = 10;
9467
+ var SIDE_BOUNCE = 0.1;
9468
+ var EDGE_POINTS = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
9107
9469
  var boxTopAt = (s, x, z) => {
9108
9470
  const e = s.e;
9109
9471
  const dx = x - s.c[0], dz = z - s.c[2];
@@ -9174,6 +9536,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9174
9536
  localCenter: [localCenter.x, localCenter.y, localCenter.z],
9175
9537
  centerV: localCenter.clone(),
9176
9538
  still: 0,
9539
+ calmAt: null,
9540
+ calm: 0,
9177
9541
  lastHit: -1,
9178
9542
  onGround: false
9179
9543
  };
@@ -9192,6 +9556,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9192
9556
  clear() {
9193
9557
  this.list.length = 0;
9194
9558
  }
9559
+ /** `clear` and the clock back to 0. */
9560
+ reset() {
9561
+ this.clear();
9562
+ this.time = 0;
9563
+ }
9195
9564
  /** World position of the centre of mass. */
9196
9565
  centerOf(b, out) {
9197
9566
  return out.copy(b.centerV).applyQuaternion(b.root.quaternion).add(b.root.position);
@@ -9216,6 +9585,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9216
9585
  }
9217
9586
  b.sleeping = false;
9218
9587
  b.still = 0;
9588
+ b.calmAt = null;
9219
9589
  }
9220
9590
  /** I^-1 * (x, y, z) in world space for a box with the body's half extents and mass (needs `this.m` to hold the body's rotation). */
9221
9591
  mulInvInertia(b, x, y, z) {
@@ -9233,12 +9603,51 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9233
9603
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
9234
9604
  return [e[0] * lx + e[4] * ly + e[8] * lz, e[1] * lx + e[5] * ly + e[9] * lz, e[2] * lx + e[6] * ly + e[10] * lz];
9235
9605
  }
9606
+ /** The points of the box that touch the ground, in its own frame: the 8 corners, then (with `edges`) `EDGE_POINTS` along each of its 12 edges. */
9607
+ samples(b, edges) {
9608
+ const [hx, hy, hz] = b.halfExtents;
9609
+ const out = [];
9610
+ for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz : -hz]);
9611
+ if (!edges) return out;
9612
+ const h = [hx, hy, hz];
9613
+ for (let axis2 = 0; axis2 < 3; axis2++) {
9614
+ const a1 = (axis2 + 1) % 3;
9615
+ const a2 = (axis2 + 2) % 3;
9616
+ for (let k = 0; k < 4; k++) {
9617
+ for (const f of EDGE_POINTS) {
9618
+ const p = [0, 0, 0];
9619
+ p[axis2] = f * h[axis2];
9620
+ p[a1] = k & 1 ? h[a1] : -h[a1];
9621
+ p[a2] = k & 2 ? h[a2] : -h[a2];
9622
+ out.push(p);
9623
+ }
9624
+ }
9625
+ }
9626
+ return out;
9627
+ }
9628
+ /**
9629
+ * An impulse along the unit normal `n` at `r` (from the centre of mass, world) that stops the point closing in along `n` (with `bounce`); `e` holds the body's rotation.
9630
+ */
9631
+ contactImpulse(b, e, r, n, bounce2) {
9632
+ const v = b.velocity;
9633
+ const w = b.angularVelocity;
9634
+ const vn = (v[0] + w[1] * r[2] - w[2] * r[1]) * n[0] + (v[1] + w[2] * r[0] - w[0] * r[2]) * n[1] + (v[2] + w[0] * r[1] - w[1] * r[0]) * n[2];
9635
+ if (vn >= 0) return;
9636
+ const a = this.mulInvInertiaWith(b, e, r[1] * n[2] - r[2] * n[1], r[2] * n[0] - r[0] * n[2], r[0] * n[1] - r[1] * n[0]);
9637
+ const k = (a[1] * r[2] - a[2] * r[1]) * n[0] + (a[2] * r[0] - a[0] * r[2]) * n[1] + (a[0] * r[1] - a[1] * r[0]) * n[2];
9638
+ const j = -(1 + bounce2) * vn / (1 / b.mass + k);
9639
+ for (let i = 0; i < 3; i++) {
9640
+ v[i] += j * n[i] / b.mass;
9641
+ w[i] += a[i] * j;
9642
+ }
9643
+ }
9236
9644
  /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
9237
9645
  wakeRegion(x, z, radius) {
9238
9646
  for (const b of this.list) {
9239
9647
  if (!b.sleeping || b.held || Math.hypot(b.root.position.x - x, b.root.position.z - z) > radius) continue;
9240
9648
  b.sleeping = false;
9241
9649
  b.still = 0;
9650
+ b.calmAt = null;
9242
9651
  }
9243
9652
  }
9244
9653
  update(dt) {
@@ -9254,6 +9663,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9254
9663
  b.angularVelocity = [0, 0, 0];
9255
9664
  b.sleeping = false;
9256
9665
  b.still = 0;
9666
+ b.calmAt = null;
9257
9667
  continue;
9258
9668
  }
9259
9669
  if (b.sleeping) continue;
@@ -9278,7 +9688,6 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9278
9688
  const q = this.q.copy(b.root.quaternion);
9279
9689
  const v = b.velocity;
9280
9690
  const w = b.angularVelocity;
9281
- const [hx, hy, hz] = b.halfExtents;
9282
9691
  const invM = 1 / b.mass;
9283
9692
  const steps = clamp6(Math.ceil(dt / SUB_DT), 1, 8);
9284
9693
  const h = dt / steps;
@@ -9287,7 +9696,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9287
9696
  let contactedLast = false;
9288
9697
  let touched = false;
9289
9698
  let unstable = false;
9699
+ const corners = this.samples(b, false);
9700
+ this.m.makeRotationFromQuaternion(q);
9701
+ let gLow = Infinity;
9702
+ let gHigh = -Infinity;
9703
+ for (const [sx, sy, sz] of corners) {
9704
+ const e0 = this.m.elements;
9705
+ const y = pos[1] + e0[1] * sx + e0[5] * sy + e0[9] * sz;
9706
+ const x = pos[0] + e0[0] * sx + e0[4] * sy + e0[8] * sz;
9707
+ const z = pos[2] + e0[2] * sx + e0[6] * sy + e0[10] * sz;
9708
+ const g = Math.max(this.o.groundAt(x, z), this.o.staticTopAt?.(x, z, y, STEP) ?? -Infinity);
9709
+ gLow = Math.min(gLow, g);
9710
+ gHigh = Math.max(gHigh, g);
9711
+ }
9712
+ const local2 = gHigh - gLow > STEP ? this.samples(b, true) : corners;
9713
+ const prev = local2.map(() => [0, 0, 0]);
9290
9714
  for (let s = 0; s < steps; s++) {
9715
+ this.m.makeRotationFromQuaternion(q);
9716
+ const e0 = this.m.elements;
9717
+ for (let i = 0; i < local2.length; i++) {
9718
+ const [sx, sy, sz] = local2[i];
9719
+ prev[i][0] = pos[0] + e0[0] * sx + e0[4] * sy + e0[8] * sz;
9720
+ prev[i][1] = pos[1] + e0[1] * sx + e0[5] * sy + e0[9] * sz;
9721
+ prev[i][2] = pos[2] + e0[2] * sx + e0[6] * sy + e0[10] * sz;
9722
+ }
9291
9723
  v[1] -= this.o.gravity * h;
9292
9724
  pos[0] += v[0] * h;
9293
9725
  pos[1] += v[1] * h;
@@ -9304,20 +9736,60 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9304
9736
  this.m.makeRotationFromQuaternion(q);
9305
9737
  const e = this.m.elements;
9306
9738
  const rs = [];
9739
+ const ids = [];
9307
9740
  let maxPen = 0;
9308
- for (let i = 0; i < 8; i++) {
9309
- const sx = i & 1 ? hx : -hx;
9310
- const sy = i & 2 ? hy : -hy;
9311
- const sz = i & 4 ? hz : -hz;
9741
+ const rise = STEP + Math.max(0, -v[1]) * h * 2;
9742
+ let side = null;
9743
+ let lowest = Infinity;
9744
+ for (let i = 0; i < local2.length; i++) {
9745
+ const [sx, sy, sz] = local2[i];
9312
9746
  const r = [e[0] * sx + e[4] * sy + e[8] * sz, e[1] * sx + e[5] * sy + e[9] * sz, e[2] * sx + e[6] * sy + e[10] * sz];
9747
+ const px = pos[0] + r[0];
9748
+ const pz = pos[2] + r[2];
9313
9749
  const cy = pos[1] + r[1];
9314
- const gy = this.supportAt(b, pos[0] + r[0], cy, pos[2] + r[2], STEP + Math.max(0, -v[1]) * h * 2);
9750
+ const was = prev[i];
9751
+ const raw = this.o.groundAt(px, pz);
9752
+ if (raw > cy - SLOP && was[1] < raw - STEP) {
9753
+ if (this.o.groundAt(was[0], was[2]) >= raw - STEP) {
9754
+ const floor2 = b.floor;
9755
+ if (floor2 !== void 0 && floor2 - cy > -SLOP && floor2 < raw - STEP) {
9756
+ rs.push(r);
9757
+ ids.push(i);
9758
+ maxPen = Math.max(maxPen, floor2 - cy);
9759
+ }
9760
+ continue;
9761
+ }
9762
+ const dx = px - was[0];
9763
+ const dz = pz - was[2];
9764
+ const len = Math.hypot(dx, dz);
9765
+ if (len > 1e-6) {
9766
+ let lo = 0;
9767
+ let hi = 1;
9768
+ for (let it = 0; it < 6; it++) {
9769
+ const mid = (lo + hi) / 2;
9770
+ if (this.o.groundAt(was[0] + dx * mid, was[2] + dz * mid) >= raw - STEP) hi = mid;
9771
+ else lo = mid;
9772
+ }
9773
+ const back = (1 - lo) * len + 1e-3;
9774
+ if (!side || back > side.back) side = { back, dir: [dx / len, dz / len], r };
9775
+ }
9776
+ continue;
9777
+ }
9778
+ const gy = i < 8 ? this.supportAt(b, px, cy, pz, rise) : Math.max(raw, this.o.staticTopAt?.(px, pz, cy, rise) ?? -Infinity);
9315
9779
  const pen = gy - cy;
9316
9780
  if (pen > -SLOP) {
9317
9781
  rs.push(r);
9782
+ ids.push(i);
9318
9783
  if (pen > maxPen) maxPen = pen;
9784
+ lowest = Math.min(lowest, gy);
9319
9785
  }
9320
9786
  }
9787
+ if (lowest < Infinity) b.floor = lowest;
9788
+ if (side) {
9789
+ pos[0] -= side.dir[0] * side.back;
9790
+ pos[2] -= side.dir[1] * side.back;
9791
+ this.contactImpulse(b, e, side.r, [-side.dir[0], 0, -side.dir[1]], SIDE_BOUNCE);
9792
+ }
9321
9793
  contactedLast = rs.length > 0;
9322
9794
  if (contactedLast) touched = true;
9323
9795
  if (!contactedLast) continue;
@@ -9337,45 +9809,88 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9337
9809
  const lz = pos[2] + sz / rs.length;
9338
9810
  landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
9339
9811
  }
9340
- for (let it = 0; it < 4; it++) {
9341
- for (const r of rs) {
9342
- let vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
9343
- let vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
9344
- let vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
9345
- const vn = vy2;
9346
- if (vn >= 0) continue;
9812
+ const goal = rs.map((r) => {
9813
+ const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
9814
+ return vy2 < 0 ? -restitution * vy2 : 0;
9815
+ });
9816
+ const vx0 = v[0];
9817
+ const vz0 = v[2];
9818
+ if (!b.warm || b.warm.length !== WARM_SLOTS * 3) b.warm = new Float64Array(WARM_SLOTS * 3);
9819
+ const warm = b.warm;
9820
+ const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 3] : 0);
9821
+ const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 3 + 1], warm[id * 3 + 2]] : [0, 0]);
9822
+ for (let k = 0; k < rs.length; k++) {
9823
+ const r = rs[k];
9824
+ const jn = accN[k];
9825
+ const [fx, fz] = accT[k];
9826
+ if (jn === 0 && fx === 0 && fz === 0) continue;
9827
+ v[0] += fx * invM;
9828
+ v[1] += jn * invM;
9829
+ v[2] += fz * invM;
9830
+ const dw = this.mulInvInertiaWith(b, e, r[1] * fz - r[2] * jn, r[2] * fx - r[0] * fz, r[0] * jn - r[1] * fx);
9831
+ w[0] += dw[0];
9832
+ w[1] += dw[1];
9833
+ w[2] += dw[2];
9834
+ }
9835
+ for (let it = 0; it < SOLVER_PASSES; it++) {
9836
+ for (let k = 0; k < rs.length; k++) {
9837
+ const r = rs[k];
9838
+ const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
9347
9839
  const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
9348
9840
  const denom = invM + (a[2] * r[0] - a[0] * r[2]);
9349
- const j = -(1 + (it === 0 ? restitution : 0)) * vn / denom;
9350
- v[1] += j * invM;
9351
- const dw = this.mulInvInertiaWith(b, e, -r[2] * j, 0, r[0] * j);
9352
- w[0] += dw[0];
9353
- w[1] += dw[1];
9354
- w[2] += dw[2];
9355
- vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
9356
- vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
9357
- vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
9841
+ const total = Math.max(0, accN[k] + (goal[k] - vy2) / denom);
9842
+ const j = total - accN[k];
9843
+ accN[k] = total;
9844
+ if (j !== 0) {
9845
+ v[1] += j * invM;
9846
+ w[0] += a[0] * j;
9847
+ w[1] += a[1] * j;
9848
+ w[2] += a[2] * j;
9849
+ }
9850
+ const vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
9851
+ const vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
9358
9852
  const vt = Math.hypot(vx2, vz2);
9359
- if (vt > 1e-5) {
9853
+ if (vt < 1e-6 && accT[k][0] === 0 && accT[k][1] === 0) continue;
9854
+ let fx = accT[k][0];
9855
+ let fz = accT[k][1];
9856
+ if (vt > 1e-6) {
9360
9857
  const tx = vx2 / vt;
9361
9858
  const tz = vz2 / vt;
9362
- const rx = r[1] * tz;
9363
- const ry = r[2] * tx - r[0] * tz;
9364
- const rz = -r[1] * tx;
9365
- const at3 = this.mulInvInertiaWith(b, e, rx, ry, rz);
9859
+ const at3 = this.mulInvInertiaWith(b, e, r[1] * tz, r[2] * tx - r[0] * tz, -r[1] * tx);
9366
9860
  const cx = at3[1] * r[2] - at3[2] * r[1];
9367
9861
  const cz = at3[0] * r[1] - at3[1] * r[0];
9368
- const denomT = invM + cx * tx + cz * tz;
9369
- const jt = Math.min(vt / denomT, MU * j);
9370
- v[0] -= tx * jt * invM;
9371
- v[2] -= tz * jt * invM;
9372
- const dwt = this.mulInvInertiaWith(b, e, rx * jt, ry * jt, rz * jt);
9373
- w[0] -= dwt[0];
9374
- w[1] -= dwt[1];
9375
- w[2] -= dwt[2];
9862
+ const jt = vt / (invM + cx * tx + cz * tz);
9863
+ fx -= tx * jt;
9864
+ fz -= tz * jt;
9865
+ }
9866
+ const most = MU * accN[k];
9867
+ const f = Math.hypot(fx, fz);
9868
+ if (f > most) {
9869
+ fx *= most / f;
9870
+ fz *= most / f;
9376
9871
  }
9872
+ const dx = fx - accT[k][0];
9873
+ const dz = fz - accT[k][1];
9874
+ accT[k] = [fx, fz];
9875
+ if (dx === 0 && dz === 0) continue;
9876
+ v[0] += dx * invM;
9877
+ v[2] += dz * invM;
9878
+ const dw = this.mulInvInertiaWith(b, e, r[1] * dz, r[2] * dx - r[0] * dz, -r[1] * dx);
9879
+ w[0] += dw[0];
9880
+ w[1] += dw[1];
9881
+ w[2] += dw[2];
9377
9882
  }
9378
9883
  }
9884
+ pos[0] += (v[0] - vx0) * h;
9885
+ pos[2] += (v[2] - vz0) * h;
9886
+ warm.fill(0);
9887
+ for (let k = 0; k < rs.length; k++) {
9888
+ const id = ids[k];
9889
+ if (id >= WARM_SLOTS) continue;
9890
+ warm[id * 3] = accN[k];
9891
+ warm[id * 3 + 1] = accT[k][0];
9892
+ warm[id * 3 + 2] = accT[k][1];
9893
+ }
9379
9894
  let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
9380
9895
  for (const r of rs) {
9381
9896
  x0 = Math.min(x0, r[0]);
@@ -9390,7 +9905,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9390
9905
  unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
9391
9906
  if (b.tipping) {
9392
9907
  b.tipping.seconds -= h;
9393
- if (height < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
9908
+ const [hx, hy, hz] = b.halfExtents;
9909
+ const upHalf = Math.abs(e[1]) * hx + Math.abs(e[5]) * hy + Math.abs(e[9]) * hz;
9910
+ if (upHalf < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
9394
9911
  else {
9395
9912
  w[0] += b.tipping.x * TIP_ACCELERATION * h;
9396
9913
  w[2] += b.tipping.z * TIP_ACCELERATION * h;
@@ -9422,16 +9939,26 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9422
9939
  this.o.onHardLanding(b, landPoint, landing);
9423
9940
  }
9424
9941
  const still = touched && !unstable && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
9425
- if (still) {
9426
- b.still += dt;
9427
- if (b.still >= SLEEP_AFTER) {
9428
- b.sleeping = true;
9429
- v[0] = v[1] = v[2] = 0;
9430
- w[0] = w[1] = w[2] = 0;
9431
- }
9432
- } else {
9433
- b.still = 0;
9434
- }
9942
+ if (still) b.still += dt;
9943
+ else b.still = 0;
9944
+ if (b.still >= SLEEP_AFTER || this.staysInPlace(b, touched && !b.tipping, pos, q, dt)) {
9945
+ b.sleeping = true;
9946
+ b.calmAt = null;
9947
+ v[0] = v[1] = v[2] = 0;
9948
+ w[0] = w[1] = w[2] = 0;
9949
+ }
9950
+ }
9951
+ /** Whether the piece has kept to the same spot on the ground for `CALM_SLEEP_AFTER` (see `CALM_DRIFT`). */
9952
+ staysInPlace(b, touched, center, q, dt) {
9953
+ const at3 = b.calmAt;
9954
+ const turned = at3 ? 2 * Math.acos(Math.min(1, Math.abs(at3.quaternion[0] * q.x + at3.quaternion[1] * q.y + at3.quaternion[2] * q.z + at3.quaternion[3] * q.w))) : Infinity;
9955
+ if (!touched || !at3 || Math.hypot(center[0] - at3.center[0], center[1] - at3.center[1], center[2] - at3.center[2]) > CALM_DRIFT || turned > CALM_TURN) {
9956
+ b.calmAt = touched ? { center: [center[0], center[1], center[2]], quaternion: [q.x, q.y, q.z, q.w] } : null;
9957
+ b.calm = 0;
9958
+ return false;
9959
+ }
9960
+ b.calm += dt;
9961
+ return b.calm >= CALM_SLEEP_AFTER;
9435
9962
  }
9436
9963
  /** Half the height of the box of a piece as it is turned now. */
9437
9964
  halfHeight(b) {
@@ -9449,8 +9976,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9449
9976
  const a = this.list[i];
9450
9977
  for (let j = i + 1; j < n; j++) {
9451
9978
  const b = this.list[j];
9452
- if (a.held && b.held) continue;
9453
- if (a.sleeping && b.sleeping) continue;
9979
+ const fixedA = a.held || a.sleeping;
9980
+ const fixedB = b.held || b.sleeping;
9981
+ if (fixedA && fixedB) continue;
9454
9982
  const ra = Math.hypot(a.halfExtents[0], a.halfExtents[1], a.halfExtents[2]);
9455
9983
  const rb = Math.hypot(b.halfExtents[0], b.halfExtents[1], b.halfExtents[2]);
9456
9984
  this.centerOf(a, ca);
@@ -9468,19 +9996,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9468
9996
  d = 1;
9469
9997
  }
9470
9998
  const push = Math.min(maxMove, (rh - d) * 0.5);
9471
- const wa = a.held ? 0 : b.held ? 1 : b.mass / (a.mass + b.mass);
9472
- const wb = b.held ? 0 : a.held ? 1 : a.mass / (a.mass + b.mass);
9999
+ const wa = fixedA ? 0 : fixedB ? 1 : b.mass / (a.mass + b.mass);
10000
+ const wb = fixedB ? 0 : fixedA ? 1 : a.mass / (a.mass + b.mass);
9473
10001
  const ux = dx / (d || 1);
9474
10002
  const uz = dz / (d || 1);
9475
- if (!a.held) {
10003
+ if (!fixedA) {
9476
10004
  a.root.position.x -= ux * push * 2 * wa;
9477
10005
  a.root.position.z -= uz * push * 2 * wa;
9478
- a.sleeping = false;
9479
10006
  }
9480
- if (!b.held) {
10007
+ if (!fixedB) {
9481
10008
  b.root.position.x += ux * push * 2 * wb;
9482
10009
  b.root.position.z += uz * push * 2 * wb;
9483
- b.sleeping = false;
9484
10010
  }
9485
10011
  }
9486
10012
  }
@@ -10889,6 +11415,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10889
11415
  }
10890
11416
  return out;
10891
11417
  };
11418
+ var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
10892
11419
 
10893
11420
  // src/spec.ts
10894
11421
  function containsOf(value) {
@@ -10947,6 +11474,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10947
11474
  const fill = options.fillOf?.(o, material);
10948
11475
  const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
10949
11476
  const contains = containsOf(ud.destructionContains);
11477
+ const powder = typeof ud.destructionPowder?.color === "number" ? { color: ud.destructionPowder.color } : void 0;
10950
11478
  const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
10951
11479
  const existing = explicit ? byKey.get(explicit) : void 0;
10952
11480
  if (existing) {
@@ -10960,9 +11488,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10960
11488
  existing.skin = existing.skin && skin;
10961
11489
  if (fill !== void 0) existing.fill = fill;
10962
11490
  if (contains) existing.contains = contains;
11491
+ if (powder) existing.powder = powder;
10963
11492
  if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
10964
11493
  } else {
10965
- const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
11494
+ const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...powder ? { powder } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
10966
11495
  entries.push(e);
10967
11496
  if (explicit) byKey.set(explicit, e);
10968
11497
  }
@@ -11037,6 +11566,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11037
11566
  }
11038
11567
  if (e.tags) part.tags = [...e.tags];
11039
11568
  if (e.contains) part.contains = { ...e.contains };
11569
+ if (e.powder) part.powder = { ...e.powder };
11040
11570
  const ao = options.anchorBelow ?? -1;
11041
11571
  if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
11042
11572
  parts.push(part);
@@ -11214,6 +11744,22 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11214
11744
  var SKIN_SLACK = 5e-4;
11215
11745
  var SKIN_LAYER_SLACK = 4;
11216
11746
  var NEAREST_MARGIN = 1.5;
11747
+ var basePositions = (host) => {
11748
+ const p0 = host.mesh.userData._p0;
11749
+ if (p0) return p0;
11750
+ const op = positionAttr(host.origGeometry);
11751
+ return op ? op.array : null;
11752
+ };
11753
+ var collapseForeign = (mesh2, arr) => {
11754
+ for (const [v0, v1] of mesh2.userData.collapsedRanges ?? []) {
11755
+ for (let k = v0 + 1; k < v1; k++) {
11756
+ arr[k * 3] = arr[v0 * 3];
11757
+ arr[k * 3 + 1] = arr[v0 * 3 + 1];
11758
+ arr[k * 3 + 2] = arr[v0 * 3 + 2];
11759
+ }
11760
+ }
11761
+ };
11762
+ var DECAL_SNAP = 0.03;
11217
11763
  var Visuals = class {
11218
11764
  constructor(THREE, lowQuality, decalCap) {
11219
11765
  __publicField(this, "THREE", THREE);
@@ -11223,6 +11769,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11223
11769
  __publicField(this, "a");
11224
11770
  __publicField(this, "b");
11225
11771
  __publicField(this, "m4");
11772
+ __publicField(this, "ray");
11226
11773
  __publicField(this, "box");
11227
11774
  __publicField(this, "t");
11228
11775
  __publicField(this, "lbox");
@@ -11233,6 +11780,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11233
11780
  this.a = tmpV(THREE);
11234
11781
  this.b = tmpV(THREE);
11235
11782
  this.m4 = new THREE.Matrix4();
11783
+ this.ray = new THREE.Raycaster();
11236
11784
  this.box = new THREE.Box3();
11237
11785
  this.t = tmpV(THREE);
11238
11786
  this.lbox = new THREE.Box3();
@@ -11386,7 +11934,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11386
11934
  host.ownGeo = true;
11387
11935
  const g = host.mesh.geometry.clone();
11388
11936
  host.mesh.geometry = g;
11389
- host.work = new Float32Array(positionAttr(g).array);
11937
+ host.work = new Float32Array(basePositions(host) ?? positionAttr(g).array);
11390
11938
  return true;
11391
11939
  }
11392
11940
  // ------------------------------------------------------------------------------------------------------------------------------------
@@ -11556,12 +12104,38 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11556
12104
  // ------------------------------------------------------------------------------------------------------------------------------------
11557
12105
  // Decals
11558
12106
  // ------------------------------------------------------------------------------------------------------------------------------------
12107
+ /**
12108
+ * The point on the drawn face of `mesh` under a world point, within `DECAL_SNAP` metres along the normal: the world's box of a part and the face
12109
+ * the renderer drew (a coat of plaster, a merged house mesh) can be a few millimetres apart, and a mark behind the drawn face does not show.
12110
+ */
12111
+ onFace(mesh2, worldPoint, worldNormal) {
12112
+ if (!mesh2.isMesh || !mesh2.geometry?.attributes?.position) return worldPoint;
12113
+ const n = worldNormal.clone().normalize();
12114
+ this.ray.set(worldPoint.clone().addScaledVector(n, DECAL_SNAP), n.clone().negate());
12115
+ this.ray.near = 0;
12116
+ this.ray.far = 2 * DECAL_SNAP;
12117
+ const hits = [];
12118
+ mesh2.raycast(this.ray, hits);
12119
+ for (const c of mesh2.parent?.children ?? []) {
12120
+ if (c === mesh2 || c.name !== "breach-shell") continue;
12121
+ c.raycast(this.ray, hits);
12122
+ const behind = c.userData.behind?.();
12123
+ if (behind) behind.raycast(this.ray, hits);
12124
+ }
12125
+ let best = null;
12126
+ for (const h of hits) {
12127
+ if (h.object.userData.cutAt?.(h.point)) continue;
12128
+ if (h.face && h.face.normal.clone().transformDirection(h.object.matrixWorld).dot(n) <= 0.3) continue;
12129
+ if (!best || Math.abs(h.distance - DECAL_SNAP) < Math.abs(best.distance - DECAL_SNAP)) best = h;
12130
+ }
12131
+ return best ? best.point : worldPoint;
12132
+ }
11559
12133
  /** Decal pose in mesh space: position and a rotation taking +Z to the surface normal. */
11560
12134
  decalPose(mesh2, worldPoint, worldNormal, lift) {
11561
12135
  mesh2.updateWorldMatrix(true, false);
11562
12136
  const s = mesh2.matrixWorld.getMaxScaleOnAxis() || 1;
11563
12137
  const ln = worldNormal.clone().transformDirection(this.m4.copy(mesh2.matrixWorld).invert()).normalize();
11564
- const pos = mesh2.worldToLocal(worldPoint.clone()).addScaledVector(ln, lift / s);
12138
+ const pos = mesh2.worldToLocal(this.onFace(mesh2, worldPoint, worldNormal).clone()).addScaledVector(ln, lift / s);
11565
12139
  const quat2 = new this.THREE.Quaternion().setFromUnitVectors(new this.THREE.Vector3(0, 0, 1), ln);
11566
12140
  return { s, pos, quat: quat2 };
11567
12141
  }
@@ -11585,8 +12159,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11585
12159
  * Takes away the decals of a mesh that sit inside a box (world `center`, `half` extents along the axes of the world `rotation`): the surface they
11586
12160
  * marked is gone (a unit came out of the wall with its bullet holes and spalls).
11587
12161
  */
11588
- clearDecals(mv, center, half, rotation) {
11589
- const inv = rotation.clone().invert();
12162
+ clearDecals(mv, center, half, rotation2) {
12163
+ const inv = rotation2.clone().invert();
11590
12164
  const p = new this.THREE.Vector3();
11591
12165
  for (let i = mv.decals.length - 1; i >= 0; i--) {
11592
12166
  const d = mv.decals[i];
@@ -11800,6 +12374,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11800
12374
  if (!pos || !work) return;
11801
12375
  const arr = pos.array;
11802
12376
  arr.set(work);
12377
+ collapseForeign(host.mesh, arr);
11803
12378
  for (const s of host.sharers) {
11804
12379
  if (s.merged && s.merged.host === host) {
11805
12380
  if (s.collapsed) this.collapseRange(arr, s.merged.range);
@@ -12004,9 +12579,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12004
12579
  mv.touched = false;
12005
12580
  if (!host.ownGeo) return;
12006
12581
  const work = host.work;
12007
- const op = positionAttr(host.origGeometry);
12582
+ const op = basePositions(host);
12008
12583
  const [v0, v1] = merged.range;
12009
- if (op) work.set(op.array.subarray(v0 * 3, v1 * 3), v0 * 3);
12584
+ if (op) work.set(op.subarray(v0 * 3, v1 * 3), v0 * 3);
12010
12585
  const attr = host.mesh.geometry.getAttribute("color");
12011
12586
  if (attr) {
12012
12587
  const orig = host.origGeometry.getAttribute("color");
@@ -12020,6 +12595,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12020
12595
  if (host.sharers.every((s) => !s.touched)) {
12021
12596
  host.mesh.geometry.dispose();
12022
12597
  host.mesh.geometry = host.origGeometry;
12598
+ const base = basePositions(host), back = positionAttr(host.origGeometry);
12599
+ if (base && back) {
12600
+ const arr = back.array;
12601
+ arr.set(base);
12602
+ collapseForeign(host.mesh, arr);
12603
+ back.needsUpdate = true;
12604
+ }
12023
12605
  host.work = null;
12024
12606
  host.ownGeo = false;
12025
12607
  if (host.ownMat) {
@@ -12079,9 +12661,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12079
12661
  var rendererCount = 0;
12080
12662
  var COLLAPSE_CLOUD_KG = 1e3;
12081
12663
  var CHIP_DECAL_EDGE = 0.05;
12664
+ var OPAQUE_CRACK = 0.1;
12665
+ var WOOD_RIM = 1.5;
12082
12666
  var IMPACT_TAGS = ["metal", "glass", "wood"];
12083
12667
  var GLOW_FROM_C = 430;
12084
12668
  var QUEUE_CAP = 4e3;
12669
+ var MIN_FRAGMENTS_PER_SLICE = 8;
12085
12670
  var STATIC_REFRESH = 0.5;
12086
12671
  var STATIC_SPAN = 3;
12087
12672
  var FEET_BAND = 0.03;
@@ -12157,6 +12742,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12157
12742
  const r = v;
12158
12743
  return !!r.mesh && Array.isArray(r.range);
12159
12744
  }
12745
+ var RNG_SEED = 12648430;
12160
12746
  function createDestructionRenderer(THREE, options) {
12161
12747
  return new Renderer(THREE, options);
12162
12748
  }
@@ -12172,6 +12758,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12172
12758
  __publicField(this, "gravity");
12173
12759
  __publicField(this, "autoSplit");
12174
12760
  __publicField(this, "maxFragments");
12761
+ __publicField(this, "eventBudgetMs");
12175
12762
  __publicField(this, "bounds", /* @__PURE__ */ new Map());
12176
12763
  /** World boxes (min xyz, max xyz) of the drawn meshes of the bound objects that are not pieces, for the pieces to land on. */
12177
12764
  __publicField(this, "staticBoxes", /* @__PURE__ */ new Map());
@@ -12182,6 +12769,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12182
12769
  __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
12183
12770
  __publicField(this, "staticAt", 0);
12184
12771
  __publicField(this, "queue", []);
12772
+ /** Presentation work an event left for later frames (fragments still to spawn, loose groups still to split), run in order within the frame budget. */
12773
+ __publicField(this, "slices", []);
12774
+ /** When the work of this frame must stop; `Infinity` outside `drainQueue`, where a call finishes what it started. */
12775
+ __publicField(this, "sliceDeadline", Infinity);
12185
12776
  __publicField(this, "unsubscribe");
12186
12777
  __publicField(this, "visuals");
12187
12778
  __publicField(this, "fragments");
@@ -12195,7 +12786,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12195
12786
  __publicField(this, "leaks", []);
12196
12787
  __publicField(this, "homeTags", []);
12197
12788
  __publicField(this, "throttles", /* @__PURE__ */ new Map());
12198
- __publicField(this, "rng", createRng(12648430));
12789
+ __publicField(this, "rng", createRng(RNG_SEED));
12199
12790
  __publicField(this, "vfx");
12200
12791
  /** Names this renderer to the world's drips, so a shared world tells each renderer where its own landed. */
12201
12792
  __publicField(this, "owner", `destruction-${++rendererCount}`);
@@ -12219,6 +12810,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12219
12810
  this.gravity = o.gravity ?? 9.81;
12220
12811
  this.autoSplit = o.autoSplit !== false;
12221
12812
  this.maxFragments = o.maxFragments ?? 160;
12813
+ this.eventBudgetMs = o.eventBudgetMs ?? Infinity;
12222
12814
  this.box = new THREE.Box3();
12223
12815
  this.tmpQ = new THREE.Quaternion();
12224
12816
  this.v1 = new THREE.Vector3();
@@ -12304,6 +12896,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12304
12896
  if (leftovers.length) this.matchByBox(objectId, root, leftovers, add2);
12305
12897
  }
12306
12898
  const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
12899
+ if (options.onFrame) bound.onFrame = options.onFrame;
12307
12900
  const hosts = /* @__PURE__ */ new Map();
12308
12901
  for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? [], hosts));
12309
12902
  for (const [pid, meshes] of byPart) {
@@ -12463,6 +13056,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12463
13056
  dt = clamp6(dt, 0, 0.1);
12464
13057
  this.time += dt;
12465
13058
  this.drainQueue();
13059
+ for (const b of this.bounds.values()) b.onFrame?.(dt);
12466
13060
  for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
12467
13061
  this.updateActive(dt);
12468
13062
  this.updateLeaks(dt);
@@ -12474,11 +13068,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12474
13068
  }
12475
13069
  drainQueue() {
12476
13070
  const batch = this.queue.splice(0, this.queue.length);
12477
- for (const ev of batch) {
13071
+ const deadline = performance.now() + this.eventBudgetMs;
13072
+ this.sliceDeadline = deadline;
13073
+ try {
13074
+ this.runSlices(deadline);
13075
+ for (let i = 0; i < batch.length; i++) {
13076
+ if (i > 0 && performance.now() >= deadline) {
13077
+ this.queue.unshift(...batch.slice(i));
13078
+ return;
13079
+ }
13080
+ try {
13081
+ this.apply(batch[i]);
13082
+ } catch (err) {
13083
+ if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", batch[i].type, err);
13084
+ }
13085
+ }
13086
+ } finally {
13087
+ this.sliceDeadline = Infinity;
13088
+ }
13089
+ }
13090
+ /** Runs the work left by earlier frames until the deadline; at least one piece per frame, so it always finishes. */
13091
+ runSlices(deadline) {
13092
+ for (let ran = 0; this.slices.length > 0 && (ran === 0 || performance.now() < deadline); ran++) {
12478
13093
  try {
12479
- this.apply(ev);
13094
+ this.slices.shift()();
12480
13095
  } catch (err) {
12481
- if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", ev.type, err);
13096
+ if (typeof console !== "undefined") console.error("game-destruction-three: failed to run deferred work", err);
12482
13097
  }
12483
13098
  }
12484
13099
  }
@@ -12876,12 +13491,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12876
13491
  const seed = hashNumbers(ev.time, point[0], point[1], point[2]);
12877
13492
  this.visuals.addCrack(mv, wp, normal, 7, Math.max(0.05, ev.hole.diameter * 5), 15267071, seed);
12878
13493
  } else {
13494
+ const fresh = def?.penetrationClass === "wood" ? def.visual?.innerColor : void 0;
13495
+ if (fresh !== void 0) this.visuals.addSpall(mv, wp, normal, Math.max(6e-3, ev.hole.diameter * WOOD_RIM), fresh);
12879
13496
  this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
12880
13497
  }
12881
13498
  };
12882
13499
  draw(ev.worldPoint, n);
12883
13500
  if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
12884
13501
  if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
13502
+ if (ev.powder) {
13503
+ const puff3 = { color: ev.powder.color, size: 0.6, life: 1.6, alpha: 1.3 };
13504
+ this.vfx.burst("dust", ev.worldPoint, [n.x, n.y, n.z], this.low ? 5 : 10, 0.7, puff3);
13505
+ if (ev.hole.through && ev.exitPoint) this.vfx.burst("dust", ev.exitPoint, [-n.x, -n.y, -n.z], this.low ? 10 : 20, 1.3, puff3);
13506
+ }
12885
13507
  this.sound(impactKey(def), ev.worldPoint, 0.6);
12886
13508
  }
12887
13509
  onCrack(ev) {
@@ -12892,8 +13514,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12892
13514
  const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
12893
13515
  const mv = this.nearestMesh(pv, wp);
12894
13516
  if (mv) {
12895
- const color = def?.visual?.transparent ? 15267071 : 2762532;
12896
- this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, ev.crack.length, color, hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]));
13517
+ const glass = !!def?.visual?.transparent;
13518
+ const own = this.partColor(pv, def) ?? fallbackColor(pv.material, def);
13519
+ const color = glass ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
13520
+ const length = glass ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
13521
+ this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, length, color, hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]));
12897
13522
  }
12898
13523
  this.sound(impactKey(def), ev.worldPoint, 0.5);
12899
13524
  }
@@ -12931,6 +13556,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12931
13556
  const core = def?.visual?.innerColor;
12932
13557
  if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
12933
13558
  else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
13559
+ if (mv.mesh.userData.drawnSpalls?.includes(ev.part)) return;
12934
13560
  this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
12935
13561
  }
12936
13562
  /**
@@ -12968,11 +13594,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12968
13594
  const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
12969
13595
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone, false), ev.object, maxAlive);
12970
13596
  }
13597
+ if (ev.torn) {
13598
+ this.sound(impactKey(def), ev.center, 0.5);
13599
+ return;
13600
+ }
12971
13601
  const T = this.THREE;
12972
13602
  const center = new T.Vector3(ev.center[0], ev.center[1], ev.center[2]);
12973
13603
  const half = new T.Vector3(ev.size[0] / 2 + CHIP_DECAL_EDGE, ev.size[1] / 2 + CHIP_DECAL_EDGE, ev.size[2] / 2 + CHIP_DECAL_EDGE);
12974
- const rotation = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
12975
- for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation);
13604
+ const rotation2 = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
13605
+ for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation2);
12976
13606
  this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
12977
13607
  this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
12978
13608
  this.sound(impactKey(def), ev.center, 0.7);
@@ -12992,15 +13622,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12992
13622
  if (m && m.transparent) opacity = clamp6(m.opacity, 0.2, 0.8);
12993
13623
  this.visuals.hide(pv, "broken");
12994
13624
  }
12995
- const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
12996
- const step = this.low ? 2 : 1;
12997
- for (let i = 0; i < ev.fragments.length; i += step) {
12998
- const fr = ev.fragments[i];
12999
- const fdef = this.world.materials.get(fr.material);
13000
- const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def || !fdef ? color : fallbackColor(fr.material, fdef);
13001
- const mat = this.fragments.materialFor(fr.material, tone, transparent || fdef?.visual?.transparent === true, opacity);
13002
- this.fragments.spawn(fr, mat, ev.object, maxAlive);
13003
- }
13625
+ this.spawnFragments(ev, 0, { def, color, opacity, transparent });
13004
13626
  const up = [0, 1, 0];
13005
13627
  const sizeK = clamp6(ev.fragments.length / 10, 0.5, 2);
13006
13628
  switch (ev.style) {
@@ -13026,6 +13648,22 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13026
13648
  }
13027
13649
  this.sound(fractureKey(def, ev.style), ev.point, clamp6(0.4 + ev.fragments.length * 0.03, 0.4, 1));
13028
13650
  }
13651
+ /** Spawns the fragments of a fracture from index `from`; what does not fit in this frame's budget is spawned by the next frames. */
13652
+ spawnFragments(ev, from, look) {
13653
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
13654
+ const step = this.low ? 2 : 1;
13655
+ for (let i = from; i < ev.fragments.length; i += step) {
13656
+ if (i - from >= MIN_FRAGMENTS_PER_SLICE * step && performance.now() >= this.sliceDeadline) {
13657
+ this.slices.push(() => this.spawnFragments(ev, i, look));
13658
+ return;
13659
+ }
13660
+ const fr = ev.fragments[i];
13661
+ const fdef = this.world.materials.get(fr.material);
13662
+ const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === look.def || !fdef ? look.color : fallbackColor(fr.material, fdef);
13663
+ const mat = this.fragments.materialFor(fr.material, tone, look.transparent || fdef?.visual?.transparent === true, look.opacity);
13664
+ this.fragments.spawn(fr, mat, ev.object, maxAlive);
13665
+ }
13666
+ }
13029
13667
  onDetach(ev) {
13030
13668
  this.sound("detach", this.rootPoint(ev.object), 0.5);
13031
13669
  this.splitLoose(ev.object);
@@ -13039,6 +13677,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13039
13677
  if (!st || st.groups.length < 2) return;
13040
13678
  const loose = st.groups.length - 1;
13041
13679
  for (let i = 0; i < loose; i++) {
13680
+ if (i > 0 && performance.now() >= this.sliceDeadline) {
13681
+ this.slices.push(() => {
13682
+ this.staticDirty.add(objectId);
13683
+ this.splitLoose(objectId);
13684
+ });
13685
+ return;
13686
+ }
13042
13687
  let newId;
13043
13688
  try {
13044
13689
  newId = this.world.splitGroup(objectId, 1);
@@ -13055,8 +13700,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13055
13700
  old.mass = Math.max(0.05, this.massOf(objectId));
13056
13701
  old.sleeping = false;
13057
13702
  } else if (kitOriginOf(b.root)?.kind === "furniture") {
13058
- this.makePiece(objectId);
13703
+ const piece = this.makePiece(objectId);
13059
13704
  this.tipIfTopHeavy(b);
13705
+ this.o.onLoosened?.(objectId, piece, "apart");
13060
13706
  }
13061
13707
  }
13062
13708
  /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
@@ -13165,7 +13811,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13165
13811
  const piece = this.makePiece(b.id, { mass });
13166
13812
  const body = this.pieceSys.get(b.id);
13167
13813
  if (!body) continue;
13168
- this.o.onLoosened?.(b.id, piece);
13814
+ this.o.onLoosened?.(b.id, piece, "blast");
13169
13815
  const spread = half[0];
13170
13816
  const momentum = hit.speed * mass;
13171
13817
  this.pieceSys.impulse(body, [hit.direction[0] * momentum, hit.direction[1] * momentum, hit.direction[2] * momentum], [world.x + (this.rng() - 0.5) * spread, world.y + spread * 0.5, world.z + (this.rng() - 0.5) * spread]);
@@ -13360,6 +14006,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13360
14006
  }
13361
14007
  clear() {
13362
14008
  this.queue.length = 0;
14009
+ this.slices.length = 0;
13363
14010
  this.active.clear();
13364
14011
  for (const key of [...this.fires.keys()]) this.stopFire(key);
13365
14012
  this.leaks.length = 0;
@@ -13379,6 +14026,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13379
14026
  this.vfx.sprites().clear();
13380
14027
  this.ownVfx?.world.clear();
13381
14028
  }
14029
+ reset() {
14030
+ this.clear();
14031
+ this.rng = createRng(RNG_SEED);
14032
+ this.time = 0;
14033
+ this.staticAt = 0;
14034
+ this.throttles.clear();
14035
+ this.fragments.reset();
14036
+ this.pieceSys.reset();
14037
+ }
13382
14038
  wakeRegion(x, z, radius) {
13383
14039
  this.fragments.wakeRegion(x, z, radius);
13384
14040
  this.pieceSys.wakeRegion(x, z, radius);
@@ -13683,7 +14339,18 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13683
14339
  drag("drag_masonry", [sceneNoise({ type: "bandpass", f: 260, q: 0.9, d: 0.3, vol: 0.62 }), ...sceneMany(3, () => sceneNoise({ type: "bandpass", f: [600, 1200], q: 2, d: 0.04, vol: 0.14, delay: [0, 0.2] }))]),
13684
14340
  drag("drag_soft", [sceneNoise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
13685
14341
  ];
13686
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
14342
+ var leaf = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a door, lid or drawer worked by hand"] });
14343
+ var LEAF_SOUNDS = [
14344
+ leaf("latch", [sceneNoise({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.35 }), sceneTone({ f: 1900, f2: 1500, d: 0.025, vol: 0.06, delay: 0.035 })]),
14345
+ leaf("creak_wood", [sceneTone({ f: [190, 240], f2: 310, d: 0.5, vol: 0.05 }), sceneNoise({ type: "bandpass", f: 520, q: 6, a: 0.05, d: 0.45, vol: 0.12 })]),
14346
+ leaf("creak_metal", [sceneTone({ f: [620, 760], f2: 900, d: 0.35, vol: 0.035 }), sceneNoise({ type: "bandpass", f: 1400, q: 8, a: 0.04, d: 0.3, vol: 0.08 })]),
14347
+ leaf("shut_wood", [sceneTone({ f: 120, f2: 60, d: 0.14, vol: 0.5 }), sceneNoise({ type: "bandpass", f: 600, q: 1.2, d: 0.1, vol: 0.55, wet: 0.25 }), sceneNoise({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.25, delay: 0.02 })]),
14348
+ leaf("shut_metal", [sceneTone({ f: 160, f2: 85, d: 0.18, vol: 0.55 }), sceneNoise({ type: "bandpass", f: 900, q: 1.6, d: 0.12, vol: 0.45, wet: 0.3 }), sceneTone({ f: 1250, f2: 1150, d: 0.12, vol: 0.06 })]),
14349
+ leaf("shut_soft", [sceneTone({ f: 95, f2: 55, d: 0.12, vol: 0.35 }), sceneNoise({ type: "lowpass", f: 500, d: 0.1, vol: 0.3 })]),
14350
+ leaf("rattle", sceneMany(5, () => sceneNoise({ type: "bandpass", f: [1600, 2600], q: 3, d: 0.035, vol: 0.32, delay: [0, 0.45] }))),
14351
+ leaf("slide", [sceneNoise({ type: "bandpass", f: 700, q: 2, a: 0.04, d: 0.35, vol: 0.22 }), sceneNoise({ type: "bandpass", f: 2200, q: 5, d: 0.3, vol: 0.05 })])
14352
+ ];
14353
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
13687
14354
 
13688
14355
  // src/audio/sound.ts
13689
14356
  var DESTRUCTION_SOUND_KEYS = {
@@ -13698,6 +14365,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13698
14365
  drag_glass: { sound: "drag_glass" },
13699
14366
  drag_masonry: { sound: "drag_masonry" },
13700
14367
  drag_soft: { sound: "drag_soft" },
14368
+ // a leaf worked by hand (doors, lids, drawers): the latch, the hinge turning, the leaf meeting the frame, a locked one shaken
14369
+ leaf_latch: { sound: "latch" },
14370
+ leaf_creak_wood: { sound: "creak_wood" },
14371
+ leaf_creak_metal: { sound: "creak_metal" },
14372
+ leaf_shut_wood: { sound: "shut_wood" },
14373
+ leaf_shut_metal: { sound: "shut_metal" },
14374
+ leaf_shut_glass: { sound: "shut_metal", volumeScale: 0.6 },
14375
+ leaf_shut_masonry: { sound: "shut_wood" },
14376
+ leaf_shut_soft: { sound: "shut_soft" },
14377
+ leaf_rattle: { sound: "rattle" },
14378
+ leaf_slide: { sound: "slide" },
14379
+ // the keys of the item clips (furniture and vehicle doors, hoods, trunks, drawers)
14380
+ door_latch: { sound: "latch" },
14381
+ hood_latch: { sound: "latch" },
14382
+ trunk_latch: { sound: "latch" },
14383
+ door_open: { sound: "latch" },
14384
+ door_shut: { sound: "shut_metal" },
14385
+ hood_shut: { sound: "shut_metal" },
14386
+ trunk_shut: { sound: "shut_metal" },
14387
+ door_close: { sound: "shut_wood", volumeScale: 0.7 },
14388
+ drawer_open: { sound: "slide" },
14389
+ drawer_close: { sound: "slide" },
14390
+ lid_open: { sound: "latch" },
14391
+ lid_close: { sound: "shut_soft" },
14392
+ flap_open: { sound: "latch" },
14393
+ flap_close: { sound: "shut_soft" },
13701
14394
  shatter_glass: { sound: "glass_shatter" },
13702
14395
  crumble: { sound: "crumble" },
13703
14396
  splinter: { sound: "crack" },