@adatechnology/game-destruction-three 0.17.3 → 0.18.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.
@@ -43,10 +43,12 @@ var GameDestructionThree = (() => {
43
43
  FIRE_FALLOFF: () => FIRE_FALLOFF,
44
44
  FIRE_LOOP: () => FIRE_LOOP,
45
45
  FIRE_SOUNDS: () => FIRE_SOUNDS,
46
+ GRENADE_PIN_PULL: () => GRENADE_PIN_PULL,
46
47
  IMPACT_FAMILY_TAGS: () => IMPACT_FAMILY_TAGS,
47
48
  IMPACT_SOUNDS: () => IMPACT_SOUNDS,
48
49
  IMPACT_SOUND_KEYS: () => IMPACT_SOUND_KEYS,
49
50
  SCENE_SOUNDS: () => SCENE_SOUNDS,
51
+ SMOKE_HISS_LOOP: () => SMOKE_HISS_LOOP,
50
52
  blastFireSampleBank: () => blastFireSampleBank,
51
53
  breakSoundKeys: () => breakSoundKeys,
52
54
  createDestructionRenderer: () => createDestructionRenderer,
@@ -6222,8 +6224,8 @@ var GameDestructionThree = (() => {
6222
6224
  id: "dust",
6223
6225
  additive: false,
6224
6226
  life: [
6225
- 1,
6226
- 2
6227
+ 2,
6228
+ 5
6227
6229
  ],
6228
6230
  size: [
6229
6231
  0.08,
@@ -6290,8 +6292,8 @@ var GameDestructionThree = (() => {
6290
6292
  id: "chips",
6291
6293
  additive: false,
6292
6294
  life: [
6293
- 0.8,
6294
- 1.5
6295
+ 2,
6296
+ 4
6295
6297
  ],
6296
6298
  size: [
6297
6299
  0.025,
@@ -7738,7 +7740,7 @@ var GameDestructionThree = (() => {
7738
7740
  };
7739
7741
  var EFFECT_STEP_SECONDS = 1 / 50;
7740
7742
 
7741
- // ../game-vfx-three/dist/chunk-CPRCWTSQ.js
7743
+ // ../game-vfx-three/dist/chunk-IOGYK5RO.js
7742
7744
  var GLOW_VERTEX = `attribute vec4 aP;attribute vec4 aC;varying vec2 vU;varying vec4 vC;
7743
7745
  void main(){vec3 R=vec3(viewMatrix[0][0],viewMatrix[1][0],viewMatrix[2][0]);vec3 Up=vec3(viewMatrix[0][1],viewMatrix[1][1],viewMatrix[2][1]);
7744
7746
  vec3 wp=aP.xyz+(R*position.x+Up*position.y)*aP.w;vU=position.xy;vC=aC;gl_Position=projectionMatrix*viewMatrix*vec4(wp,1.);}`;
@@ -7785,10 +7787,15 @@ void main(){
7785
7787
  if(n<diss*1.08-.04)discard;
7786
7788
  float lam=max(dot(N,uLight),0.);
7787
7789
  float band=lam>.55?1.:lam>.15?.68:.42;
7788
- vec3 col=vC*(vec3(.5,.56,.7)*band*1.05+vec3(.2,.19,.18)+uGround*(.4-.4*N.y));
7790
+ // soot: a dark smoke still scatters some light, so it never goes flat black, and its rim takes the sky so its volume reads (only the dark puffs; the rest as the Lab)
7791
+ float dark=1.-smoothstep(.02,.12,dot(vC,vec3(.2126,.7152,.0722)));
7792
+ vec3 alb=max(vC,vec3(.055)*dark);
7793
+ vec3 col=alb*(vec3(.5,.56,.7)*band*1.05+vec3(.2,.19,.18)+uGround*(.4-.4*N.y));
7794
+ float rim=pow(1.-clamp(abs(dot(N,normalize(cameraPosition-vWP))),0.,1.),2.);
7795
+ col+=dark*rim*(uSky*.9+alb);
7789
7796
  vec3 tg=uGlowPos-vWP;float dg=length(tg)+.001;
7790
7797
  float gi=uGlowI/(1.+dg*dg*.05)*(.3+.7*max(dot(N,tg/dg),0.));
7791
- col+=uGlowCol*gi*vC*1.6;
7798
+ col+=uGlowCol*gi*alb*1.6;
7792
7799
  float h=heat*(.72+.56*n)+heat*(-N.y)*.18;
7793
7800
  if(h>.05){
7794
7801
  float hq=floor(h*5.+.5)/5.;
@@ -9940,7 +9947,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9940
9947
  var SLAM_BOUNCE = 0.2;
9941
9948
  var SLAM_FLOOR_NORMAL = 0.7;
9942
9949
  var SUB_DT = 1 / 120;
9943
- var MU = 0.7;
9950
+ var MU_STATIC = 0.7;
9951
+ var MU_DYNAMIC = 0.6;
9952
+ var SLIP_SPEED = 0.02;
9944
9953
  var SLEEP_AFTER = 0.5;
9945
9954
  var CALM_DRIFT = 0.01;
9946
9955
  var CALM_TURN = 0.05;
@@ -9959,6 +9968,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9959
9968
  var EDGE_POINTS = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
9960
9969
  var SUPPORT_CHECKS2 = 16;
9961
9970
  var SUPPORT_GAP2 = 0.02;
9971
+ var WAKE_ABOVE_SPEED = 0.05;
9972
+ var WAKE_ABOVE_SPIN = 0.1;
9973
+ var topFace = 1;
9962
9974
  var boxTopAt = (s, x, z) => {
9963
9975
  const e = s.e;
9964
9976
  const dx = x - s.c[0], dz = z - s.c[2];
@@ -9974,11 +9986,24 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9974
9986
  let a = (-h - p) / d, b = (h - p) / d;
9975
9987
  if (a > b) [a, b] = [b, a];
9976
9988
  t0 = Math.max(t0, a);
9977
- t1 = Math.min(t1, b);
9989
+ if (b < t1) {
9990
+ t1 = b;
9991
+ topFace = k;
9992
+ }
9978
9993
  if (t0 > t1) return -Infinity;
9979
9994
  }
9980
9995
  return s.c[1] + t1;
9981
9996
  };
9997
+ var UP16 = [0, 1, 0];
9998
+ var faceNormal = (s, axis2) => {
9999
+ const e = s.e;
10000
+ const sign = e[axis2 * 4 + 1] < 0 ? -1 : 1;
10001
+ return [sign * e[axis2 * 4], sign * e[axis2 * 4 + 1], sign * e[axis2 * 4 + 2]];
10002
+ };
10003
+ var pairFriction = (a, b) => ({
10004
+ static: Math.sqrt((a?.static ?? MU_STATIC) * (b?.static ?? MU_STATIC)),
10005
+ dynamic: Math.sqrt((a?.dynamic ?? MU_DYNAMIC) * (b?.dynamic ?? MU_DYNAMIC))
10006
+ });
9982
10007
  var PieceSystem = class {
9983
10008
  constructor(THREE, o) {
9984
10009
  __publicField(this, "THREE", THREE);
@@ -9992,6 +10017,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9992
10017
  __publicField(this, "ax");
9993
10018
  __publicField(this, "solids", []);
9994
10019
  __publicField(this, "supportTurn", 0);
10020
+ /** The surface normal and the friction of what the last `supportAt` found under the point (the ground and fixed tops are level: up, and no piece). */
10021
+ __publicField(this, "supportNormal", UP16);
10022
+ __publicField(this, "supportFriction", null);
9995
10023
  this.p = new THREE.Vector3();
9996
10024
  this.q = new THREE.Quaternion();
9997
10025
  this.dq = new THREE.Quaternion();
@@ -10097,8 +10125,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10097
10125
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
10098
10126
  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];
10099
10127
  }
10100
- /** 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. */
10128
+ /** 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; or the piece's own `contacts`. */
10101
10129
  samples(b, edges) {
10130
+ if (b.contacts) return b.contacts;
10102
10131
  const [hx, hy, hz2] = b.halfExtents;
10103
10132
  const out = [];
10104
10133
  for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz2 : -hz2]);
@@ -10163,6 +10192,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10163
10192
  }
10164
10193
  if (b.sleeping) continue;
10165
10194
  this.integrate(b, dt);
10195
+ if (b.sleeping) continue;
10196
+ const v = b.velocity;
10197
+ const w = b.angularVelocity;
10198
+ if (Math.hypot(v[0], v[1], v[2]) > WAKE_ABOVE_SPEED || Math.hypot(w[0], w[1], w[2]) > WAKE_ABOVE_SPIN) this.wakeAbove(b);
10166
10199
  }
10167
10200
  this.separate(dt);
10168
10201
  }
@@ -10193,15 +10226,48 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10193
10226
  }
10194
10227
  return false;
10195
10228
  }
10229
+ /** The highest top of a piece (other than `self`) under `(x, z)` that a point at height `y` stands on (at most `step` above it), -Infinity when none; the boxes of the last update. */
10230
+ topAt(x, z, y, step, self) {
10231
+ let top = -Infinity;
10232
+ for (const s of this.solids) {
10233
+ if (s.body.objectId === self || s.body.held || Math.abs(x - s.c[0]) > s.r || Math.abs(z - s.c[2]) > s.r) continue;
10234
+ const t = boxTopAt(s, x, z);
10235
+ if (t > top && t <= y + step) top = t;
10236
+ }
10237
+ return top;
10238
+ }
10239
+ /** What lay asleep on `b` (a box over its top) wakes at once when `b` moves: a can on a table that goes over goes with it. */
10240
+ wakeAbove(b) {
10241
+ const below = this.solids.find((s) => s.body === b);
10242
+ if (!below) return;
10243
+ const top = below.c[1] + below.half[0] * Math.abs(below.e[1]) + below.half[1] * Math.abs(below.e[5]) + below.half[2] * Math.abs(below.e[9]);
10244
+ for (const s of this.solids) {
10245
+ const o = s.body;
10246
+ if (!o.sleeping || o.held || o === b) continue;
10247
+ if (s.c[1] < below.c[1] || s.c[1] - s.r > top + STEP || Math.hypot(s.c[0] - below.c[0], s.c[2] - below.c[2]) > s.r + below.r) continue;
10248
+ o.sleeping = false;
10249
+ o.still = 0;
10250
+ o.calmAt = null;
10251
+ }
10252
+ }
10196
10253
  /** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
10197
10254
  supportAt(self, x, y, z, step) {
10198
10255
  let top = this.o.groundAt(x, z);
10256
+ let on = null;
10257
+ let face = 1;
10199
10258
  for (const s of this.solids) {
10200
10259
  if (s.body === self || s.body.held || Math.abs(x - s.c[0]) > s.r || Math.abs(z - s.c[2]) > s.r) continue;
10201
10260
  const t = boxTopAt(s, x, z);
10202
- if (t > top && t <= y + step) top = t;
10261
+ if (t > top && t <= y + step) {
10262
+ top = t;
10263
+ on = s;
10264
+ face = topFace;
10265
+ }
10203
10266
  }
10204
10267
  const fixed = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
10268
+ if (fixed >= top) on = null;
10269
+ this.supportNormal = on ? faceNormal(on, face) : UP16;
10270
+ this.supportFriction = on?.body.friction ?? null;
10205
10271
  return Math.max(top, fixed);
10206
10272
  }
10207
10273
  /** Stops a piece that ran into something on its way from `start` to `pos` (`sweep`): moves it back to where it met it and takes the speed into it. */
@@ -10284,7 +10350,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10284
10350
  const e = this.m.elements;
10285
10351
  const rs = [];
10286
10352
  const ids = [];
10353
+ const ns = [];
10354
+ const mus = [];
10287
10355
  let maxPen = 0;
10356
+ let penNormal = UP16;
10288
10357
  const rise = STEP + Math.max(0, -v[1]) * h * 2;
10289
10358
  let side = null;
10290
10359
  let lowest = Infinity;
@@ -10302,6 +10371,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10302
10371
  if (floor2 !== void 0 && floor2 - cy > -SLOP && floor2 < raw - STEP) {
10303
10372
  rs.push(r);
10304
10373
  ids.push(i);
10374
+ ns.push(UP16);
10375
+ mus.push(pairFriction(b.friction, null));
10305
10376
  maxPen = Math.max(maxPen, floor2 - cy);
10306
10377
  }
10307
10378
  continue;
@@ -10322,12 +10393,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10322
10393
  }
10323
10394
  continue;
10324
10395
  }
10325
- const gy = i < 8 ? this.supportAt(b, px, cy, pz, rise) : Math.max(raw, this.o.staticTopAt?.(px, pz, cy, rise) ?? -Infinity);
10396
+ this.supportNormal = UP16;
10397
+ this.supportFriction = null;
10398
+ const gy = i < 8 || b.contacts ? this.supportAt(b, px, cy, pz, rise) : Math.max(raw, this.o.staticTopAt?.(px, pz, cy, rise) ?? -Infinity);
10326
10399
  const pen = gy - cy;
10327
10400
  if (pen > -SLOP) {
10328
10401
  rs.push(r);
10329
10402
  ids.push(i);
10330
- if (pen > maxPen) maxPen = pen;
10403
+ ns.push(this.supportNormal);
10404
+ mus.push(pairFriction(b.friction, this.supportFriction));
10405
+ if (pen > maxPen) {
10406
+ maxPen = pen;
10407
+ penNormal = this.supportNormal;
10408
+ }
10331
10409
  lowest = Math.min(lowest, gy);
10332
10410
  }
10333
10411
  }
@@ -10340,7 +10418,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10340
10418
  contactedLast = rs.length > 0;
10341
10419
  if (contactedLast) touched = true;
10342
10420
  if (!contactedLast) continue;
10343
- pos[1] += Math.max(0, maxPen - SLOP * 0.5);
10421
+ const out = Math.max(0, maxPen - SLOP * 0.5) * penNormal[1];
10422
+ pos[0] += penNormal[0] * out;
10423
+ pos[1] += penNormal[1] * out;
10424
+ pos[2] += penNormal[2] * out;
10344
10425
  let impact3 = 0;
10345
10426
  for (const r of rs) impact3 = Math.max(impact3, -(v[1] + w[2] * r[0] - w[0] * r[2]));
10346
10427
  const restitution = impact3 > 2 ? 0.25 : 0;
@@ -10356,76 +10437,72 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10356
10437
  const lz = pos[2] + sz / rs.length;
10357
10438
  landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
10358
10439
  }
10359
- const goal = rs.map((r) => {
10360
- const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
10361
- return vy2 < 0 ? -restitution * vy2 : 0;
10440
+ const pointV = (r) => [v[0] + w[1] * r[2] - w[2] * r[1], v[1] + w[2] * r[0] - w[0] * r[2], v[2] + w[0] * r[1] - w[1] * r[0]];
10441
+ const dot = (a, c2) => a[0] * c2[0] + a[1] * c2[1] + a[2] * c2[2];
10442
+ const goal = rs.map((r, k) => {
10443
+ const vn = dot(pointV(r), ns[k]);
10444
+ return vn < 0 ? -restitution * vn : 0;
10445
+ });
10446
+ const slipping = rs.map((r, k) => {
10447
+ const pv = pointV(r);
10448
+ const vn = dot(pv, ns[k]);
10449
+ return Math.hypot(pv[0] - vn * ns[k][0], pv[1] - vn * ns[k][1], pv[2] - vn * ns[k][2]) > SLIP_SPEED;
10362
10450
  });
10363
10451
  const vx0 = v[0];
10364
10452
  const vz0 = v[2];
10365
- if (!b.warm || b.warm.length !== WARM_SLOTS * 3) b.warm = new Float64Array(WARM_SLOTS * 3);
10453
+ if (!b.warm || b.warm.length !== WARM_SLOTS * 4) b.warm = new Float64Array(WARM_SLOTS * 4);
10366
10454
  const warm = b.warm;
10367
- const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 3] : 0);
10368
- const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 3 + 1], warm[id * 3 + 2]] : [0, 0]);
10369
- for (let k = 0; k < rs.length; k++) {
10370
- const r = rs[k];
10371
- const jn = accN[k];
10372
- const [fx, fz] = accT[k];
10373
- if (jn === 0 && fx === 0 && fz === 0) continue;
10374
- v[0] += fx * invM;
10375
- v[1] += jn * invM;
10376
- v[2] += fz * invM;
10377
- const dw = this.mulInvInertiaWith(b, e, r[1] * fz - r[2] * jn, r[2] * fx - r[0] * fz, r[0] * jn - r[1] * fx);
10455
+ const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 4] : 0);
10456
+ const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 4 + 1], warm[id * 4 + 2], warm[id * 4 + 3]] : [0, 0, 0]);
10457
+ const push = (r, j) => {
10458
+ v[0] += j[0] * invM;
10459
+ v[1] += j[1] * invM;
10460
+ v[2] += j[2] * invM;
10461
+ const dw = this.mulInvInertiaWith(b, e, r[1] * j[2] - r[2] * j[1], r[2] * j[0] - r[0] * j[2], r[0] * j[1] - r[1] * j[0]);
10378
10462
  w[0] += dw[0];
10379
10463
  w[1] += dw[1];
10380
10464
  w[2] += dw[2];
10465
+ };
10466
+ const reach = (r, d) => {
10467
+ const a = this.mulInvInertiaWith(b, e, r[1] * d[2] - r[2] * d[1], r[2] * d[0] - r[0] * d[2], r[0] * d[1] - r[1] * d[0]);
10468
+ return invM + (a[1] * r[2] - a[2] * r[1]) * d[0] + (a[2] * r[0] - a[0] * r[2]) * d[1] + (a[0] * r[1] - a[1] * r[0]) * d[2];
10469
+ };
10470
+ for (let k = 0; k < rs.length; k++) {
10471
+ const n = ns[k];
10472
+ const t = accT[k];
10473
+ if (accN[k] !== 0 || t[0] !== 0 || t[1] !== 0 || t[2] !== 0) push(rs[k], [n[0] * accN[k] + t[0], n[1] * accN[k] + t[1], n[2] * accN[k] + t[2]]);
10381
10474
  }
10382
10475
  for (let it = 0; it < SOLVER_PASSES; it++) {
10383
10476
  for (let k = 0; k < rs.length; k++) {
10384
10477
  const r = rs[k];
10385
- const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
10386
- const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
10387
- const denom = invM + (a[2] * r[0] - a[0] * r[2]);
10388
- const total = Math.max(0, accN[k] + (goal[k] - vy2) / denom);
10478
+ const n = ns[k];
10479
+ const vn = dot(pointV(r), n);
10480
+ const total = Math.max(0, accN[k] + (goal[k] - vn) / reach(r, n));
10389
10481
  const j = total - accN[k];
10390
10482
  accN[k] = total;
10391
- if (j !== 0) {
10392
- v[1] += j * invM;
10393
- w[0] += a[0] * j;
10394
- w[1] += a[1] * j;
10395
- w[2] += a[2] * j;
10396
- }
10397
- const vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
10398
- const vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
10399
- const vt = Math.hypot(vx2, vz2);
10400
- if (vt < 1e-6 && accT[k][0] === 0 && accT[k][1] === 0) continue;
10401
- let fx = accT[k][0];
10402
- let fz = accT[k][1];
10403
- if (vt > 1e-6) {
10404
- const tx = vx2 / vt;
10405
- const tz = vz2 / vt;
10406
- const at3 = this.mulInvInertiaWith(b, e, r[1] * tz, r[2] * tx - r[0] * tz, -r[1] * tx);
10407
- const cx = at3[1] * r[2] - at3[2] * r[1];
10408
- const cz = at3[0] * r[1] - at3[1] * r[0];
10409
- const jt = vt / (invM + cx * tx + cz * tz);
10410
- fx -= tx * jt;
10411
- fz -= tz * jt;
10412
- }
10413
- const most = MU * accN[k];
10414
- const f = Math.hypot(fx, fz);
10415
- if (f > most) {
10416
- fx *= most / f;
10417
- fz *= most / f;
10483
+ if (j !== 0) push(r, [n[0] * j, n[1] * j, n[2] * j]);
10484
+ const pv = pointV(r);
10485
+ const pn = dot(pv, n);
10486
+ const vt = [pv[0] - pn * n[0], pv[1] - pn * n[1], pv[2] - pn * n[2]];
10487
+ const speed = Math.hypot(vt[0], vt[1], vt[2]);
10488
+ const f = [accT[k][0], accT[k][1], accT[k][2]];
10489
+ if (speed > 1e-6) {
10490
+ const d = [vt[0] / speed, vt[1] / speed, vt[2] / speed];
10491
+ const jt = speed / reach(r, d);
10492
+ f[0] -= d[0] * jt;
10493
+ f[1] -= d[1] * jt;
10494
+ f[2] -= d[2] * jt;
10495
+ } else if (f[0] === 0 && f[1] === 0 && f[2] === 0) continue;
10496
+ const most = (slipping[k] ? mus[k].dynamic : mus[k].static) * accN[k];
10497
+ const fl = Math.hypot(f[0], f[1], f[2]);
10498
+ if (fl > most) {
10499
+ f[0] *= most / fl;
10500
+ f[1] *= most / fl;
10501
+ f[2] *= most / fl;
10418
10502
  }
10419
- const dx = fx - accT[k][0];
10420
- const dz = fz - accT[k][1];
10421
- accT[k] = [fx, fz];
10422
- if (dx === 0 && dz === 0) continue;
10423
- v[0] += dx * invM;
10424
- v[2] += dz * invM;
10425
- const dw = this.mulInvInertiaWith(b, e, r[1] * dz, r[2] * dx - r[0] * dz, -r[1] * dx);
10426
- w[0] += dw[0];
10427
- w[1] += dw[1];
10428
- w[2] += dw[2];
10503
+ const dj = [f[0] - accT[k][0], f[1] - accT[k][1], f[2] - accT[k][2]];
10504
+ accT[k] = f;
10505
+ if (dj[0] !== 0 || dj[1] !== 0 || dj[2] !== 0) push(r, dj);
10429
10506
  }
10430
10507
  }
10431
10508
  pos[0] += (v[0] - vx0) * h;
@@ -10434,9 +10511,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10434
10511
  for (let k = 0; k < rs.length; k++) {
10435
10512
  const id = ids[k];
10436
10513
  if (id >= WARM_SLOTS) continue;
10437
- warm[id * 3] = accN[k];
10438
- warm[id * 3 + 1] = accT[k][0];
10439
- warm[id * 3 + 2] = accT[k][1];
10514
+ warm[id * 4] = accN[k];
10515
+ warm[id * 4 + 1] = accT[k][0];
10516
+ warm[id * 4 + 2] = accT[k][1];
10517
+ warm[id * 4 + 3] = accT[k][2];
10440
10518
  }
10441
10519
  let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
10442
10520
  for (const r of rs) {
@@ -10508,6 +10586,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10508
10586
  b.calm += dt;
10509
10587
  return b.calm >= CALM_SLEEP_AFTER;
10510
10588
  }
10589
+ /** Whether the centre `c` of `upper` is over the top of `lower`'s box (as it was at the start of the update). */
10590
+ onTopOf(upper, lower, c) {
10591
+ const s = this.solids.find((one) => one.body === lower);
10592
+ if (!s) return false;
10593
+ const top = boxTopAt(s, c.x, c.z);
10594
+ return top > -Infinity && c.y >= top - 0.01;
10595
+ }
10511
10596
  /** Half the height of the box of a piece as it is turned now. */
10512
10597
  halfHeight(b) {
10513
10598
  const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
@@ -10533,6 +10618,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10533
10618
  this.centerOf(b, cb);
10534
10619
  if (Math.abs(ca.y - cb.y) > ra + rb) continue;
10535
10620
  if (Math.abs(ca.y - cb.y) >= (this.halfHeight(a) + this.halfHeight(b)) * 0.9) continue;
10621
+ if (this.onTopOf(b, a, cb) || this.onTopOf(a, b, ca)) continue;
10536
10622
  const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
10537
10623
  let dx = cb.x - ca.x;
10538
10624
  let dz = cb.z - ca.z;
@@ -13463,6 +13549,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13463
13549
  var IMPACT_TAGS = ["metal", "glass", "wood"];
13464
13550
  var GLOW_FROM_C = 430;
13465
13551
  var QUEUE_CAP = 4e3;
13552
+ var TRICKLE_SECONDS = [2, 5];
13553
+ var TRICKLE_EVERY = 0.25;
13554
+ var TRICKLE_MAX = 24;
13466
13555
  var STRUCTURAL_EVENTS = /* @__PURE__ */ new Set([
13467
13556
  "fracture",
13468
13557
  "detach",
@@ -13486,6 +13575,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13486
13575
  var FEET_BAND = 0.03;
13487
13576
  var TIP_MIN_HEIGHT = 0.3;
13488
13577
  var TIP_NARROW = 0.3;
13578
+ var CONTACT_MESHES = 12;
13579
+ var BROKEN_GAP = 0.03;
13580
+ var BROKEN_SAMPLES = [[0.5, 0.5], [0.1, 0.1], [0.9, 0.1], [0.1, 0.9], [0.9, 0.9]];
13581
+ var UNCHECKED_KINDS = /* @__PURE__ */ new Set(["structure", "room", "ground"]);
13489
13582
  var FIRE_CEILING_SECONDS = 3600;
13490
13583
  var GENERIC_REFERENCE_RADIUS = 15;
13491
13584
  var SWEEP_CORE = 0.15;
@@ -13585,10 +13678,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13585
13678
  __publicField(this, "staticRound", []);
13586
13679
  /** Boxes of the pieces' meshes (as `staticBoxes`), with the time they were measured. */
13587
13680
  __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
13681
+ /** Objects broken since the last update: `checkBroken` looks at what is left of them. */
13682
+ __publicField(this, "broken", /* @__PURE__ */ new Set());
13588
13683
  __publicField(this, "staticAt", 0);
13589
13684
  __publicField(this, "queue", []);
13590
13685
  /** The cover of each body around a part of the world that went off, read as the event is emitted: before the blast tore or moved anything. */
13591
13686
  __publicField(this, "coverWhenWentOff", /* @__PURE__ */ new WeakMap());
13687
+ /** Places a blow broke a hard face at: grit and dust keep coming off them for a few seconds, less and less (`TRICKLE_SECONDS`). */
13688
+ __publicField(this, "trickles", []);
13592
13689
  /** Presentation work an event left for later frames (fragments still to spawn, loose groups still to split), run in order within the frame budget. */
13593
13690
  __publicField(this, "slices", []);
13594
13691
  /** When the work of this frame must stop; `Infinity` outside a drain, where a call finishes what it started. */
@@ -13931,10 +14028,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13931
14028
  dt = clamp6(dt, 0, 0.1);
13932
14029
  this.time += dt;
13933
14030
  if (!this.attached) this.drainQueue();
14031
+ this.checkBroken();
13934
14032
  for (const b of this.bounds.values()) b.onFrame?.(dt);
13935
14033
  for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
13936
14034
  this.updateActive(dt);
13937
14035
  this.updateLeaks(dt);
14036
+ this.updateTrickles(dt);
13938
14037
  this.pieceSys.update(dt);
13939
14038
  this.fragments.update(dt);
13940
14039
  this.ground.update(dt);
@@ -14133,6 +14232,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14133
14232
  }
14134
14233
  apply(ev) {
14135
14234
  this.staticDirty.add(ev.object);
14235
+ if (ev.type === "fracture" || ev.type === "detach" || ev.type === "collapse" || ev.type === "burnout" || ev.type === "destroyed") this.broken.add(ev.object);
14136
14236
  switch (ev.type) {
14137
14237
  case "damage":
14138
14238
  if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
@@ -14457,9 +14557,34 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14457
14557
  const core = def?.visual?.innerColor;
14458
14558
  if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
14459
14559
  else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
14560
+ if (core !== void 0) this.trickleFrom(ev.point, [normal.x, normal.y, normal.z], core);
14460
14561
  if (mv.mesh.userData.drawnSpalls?.includes(ev.part)) return;
14461
14562
  this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
14462
14563
  }
14564
+ /** Grit and dust go on falling from where a blow broke a hard face (masonry, concrete) for `TRICKLE_SECONDS`, a pinch at a time and less and less. */
14565
+ trickleFrom(point, normal, color) {
14566
+ if (this.trickles.length >= TRICKLE_MAX) this.trickles.shift();
14567
+ const total = TRICKLE_SECONDS[0] + (TRICKLE_SECONDS[1] - TRICKLE_SECONDS[0]) * this.rng();
14568
+ this.trickles.push({ point: [point[0], point[1], point[2]], normal, color, left: total, total, next: TRICKLE_EVERY });
14569
+ }
14570
+ updateTrickles(dt) {
14571
+ for (let i = this.trickles.length - 1; i >= 0; i--) {
14572
+ const t = this.trickles[i];
14573
+ t.left -= dt;
14574
+ t.next -= dt;
14575
+ if (t.left <= 0) {
14576
+ this.trickles.splice(i, 1);
14577
+ continue;
14578
+ }
14579
+ if (t.next > 0) continue;
14580
+ const share = t.left / t.total;
14581
+ t.next = TRICKLE_EVERY / Math.max(share, 0.25);
14582
+ const out = [t.normal[0] * 0.2, -1, t.normal[2] * 0.2];
14583
+ const options = t.color !== void 0 ? { color: t.color } : void 0;
14584
+ this.vfx.burst("chips", t.point, out, this.low ? 1 : 2, 0.3, options);
14585
+ if (this.rng() < 0.5) this.vfx.burst("dust", t.point, out, 1, 0.2, options);
14586
+ }
14587
+ }
14463
14588
  /**
14464
14589
  * Stage 2 of a staged part: the unit-sized fragments come out and the part stays drawn (hiding it is what `fracture` does). The opening itself
14465
14590
  * (cutting the wall's faces) is not drawn yet: it needs the per-vertex binding of the house renderer.
@@ -14495,6 +14620,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14495
14620
  const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
14496
14621
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone, false), ev.object, maxAlive);
14497
14622
  }
14623
+ if (def?.visual?.innerColor !== void 0 || pv?.material === "concrete" || pv?.material === "brick") this.trickleFrom(ev.center, [0, 0, 0], color);
14498
14624
  if (ev.torn) {
14499
14625
  this.impactSound(pv?.material ?? "", ev.center, 0.5, ev.blow);
14500
14626
  return;
@@ -14684,26 +14810,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14684
14810
  this.o.onPiece?.(piece);
14685
14811
  }
14686
14812
  /**
14687
- * Furniture that lost what it stood on falls: with every leg of a table broken the top is loose, and with the legs of one side gone what is left topples. Feet are the
14688
- * meshes at the bottom of the object as it was whole; while the ones left still span the same floor (a cupboard with a broken door, a table missing one leg) it stands.
14813
+ * What lost what it stood on falls (furniture, a stand, a crate, a car; not a structure): with every leg of a table broken the top is loose, and with the legs of one side
14814
+ * gone what is left topples. Feet are the meshes at the bottom of the object as it was whole; while the ones left still span the same floor (a cupboard with a broken door,
14815
+ * a table missing one leg) it stands. A piece that lost a part is the box of what is left, touching the ground by the corners of each part (`setContacts`).
14689
14816
  */
14690
14817
  loosenIfFootless(objectId) {
14691
14818
  const b = this.bounds.get(objectId);
14692
14819
  if (!b || !this.autoSplit) return;
14693
14820
  const left = this.meshesOf(b).filter((mv) => this.visuals.shown(mv));
14694
14821
  if (left.length === 0) return;
14822
+ const structural = UNCHECKED_KINDS.has(this.kindOf(b));
14695
14823
  const body = b.piece ?? this.pieceSys.get(objectId);
14696
14824
  if (body) {
14697
14825
  const { half, center } = this.measure(b.root, left);
14698
14826
  body.halfExtents = half;
14699
14827
  this.pieceSys.setCenter(body, center);
14700
14828
  body.mass = Math.max(0.05, this.massOf(objectId));
14829
+ if (!structural) this.setContacts(b, body, left);
14701
14830
  body.sleeping = false;
14702
14831
  return;
14703
14832
  }
14704
- if (kitOriginOf(b.root)?.kind !== "furniture") return;
14833
+ if (structural) return;
14705
14834
  const boxOf = (mv) => this.visuals.worldBox(mv, new this.THREE.Box3());
14706
14835
  const whole = [...b.parts.values()].flatMap((pv) => pv.meshes).map(boxOf).filter((one) => !one.isEmpty());
14836
+ if (!this.standsOn(objectId, whole.map((one) => [one.min.x, one.min.y, one.min.z, one.max.x, one.max.y, one.max.z]))) return;
14707
14837
  const floor2 = Math.min(...whole.map((one) => one.min.y));
14708
14838
  const feetOf = (boxes) => boxes.filter((one) => one.min.y < floor2 + FEET_BAND).reduce((feet, one) => feet.union(one), new this.THREE.Box3());
14709
14839
  const was = feetOf(whole);
@@ -14719,9 +14849,116 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
14719
14849
  if (!narrow && centreX > now.min.x && centreX < now.max.x && centreZ > now.min.z && centreZ < now.max.z) return;
14720
14850
  }
14721
14851
  const piece = this.makePiece(objectId);
14852
+ this.setContacts(b, b.piece ?? piece, left);
14722
14853
  this.tipIfTopHeavy(b);
14723
14854
  this.o.onLoosened?.(objectId, piece, "apart");
14724
14855
  }
14856
+ shownBoxes(objectId, partId) {
14857
+ const b = this.bounds.get(objectId);
14858
+ if (!b) return [];
14859
+ const box = new this.THREE.Box3();
14860
+ const out = [];
14861
+ const pv = partId === void 0 ? null : b.parts.get(partId);
14862
+ if (partId !== void 0 && (!pv || pv.gone !== "none")) return out;
14863
+ for (const mv of pv ? pv.meshes : this.meshesOf(b)) {
14864
+ if (!this.visuals.shown(mv)) continue;
14865
+ this.visuals.worldBox(mv, box);
14866
+ if (!box.isEmpty()) out.push([box.min.x, box.min.y, box.min.z, box.max.x, box.max.y, box.max.z]);
14867
+ }
14868
+ return out;
14869
+ }
14870
+ /**
14871
+ * What is left of each object broken since the last update stands on something, or falls: an object that is not a piece whose lowest drawn box has neither the ground, a
14872
+ * fixed top nor a piece within `BROKEN_GAP` under it is made a piece on the spot (see `onBrokenChecked`).
14873
+ */
14874
+ checkBroken() {
14875
+ if (this.broken.size === 0) return;
14876
+ const ids = [...this.broken];
14877
+ this.broken.clear();
14878
+ for (const id of ids) {
14879
+ const released = this.releaseIfFloating(id);
14880
+ this.o.onBrokenChecked?.(id, released);
14881
+ }
14882
+ }
14883
+ releaseIfFloating(objectId) {
14884
+ const b = this.bounds.get(objectId);
14885
+ if (!b || b.piece || this.pieceSys.get(objectId) || !this.autoSplit || UNCHECKED_KINDS.has(this.kindOf(b))) return false;
14886
+ const boxes = this.shownBoxes(objectId);
14887
+ if (boxes.length === 0) return false;
14888
+ const whole = new this.THREE.Box3();
14889
+ const wholeBoxes = [];
14890
+ for (const pv of b.parts.values()) {
14891
+ for (const mv of pv.meshes) {
14892
+ this.visuals.worldBox(mv, whole);
14893
+ if (!whole.isEmpty()) wholeBoxes.push([whole.min.x, whole.min.y, whole.min.z, whole.max.x, whole.max.y, whole.max.z]);
14894
+ }
14895
+ }
14896
+ if (!this.standsOn(objectId, wholeBoxes) || this.standsOn(objectId, boxes)) return false;
14897
+ const piece = this.makePiece(objectId);
14898
+ this.setContacts(b, b.piece ?? piece, this.meshesOf(b).filter((mv) => this.visuals.shown(mv)));
14899
+ this.tipIfTopHeavy(b);
14900
+ this.o.onLoosened?.(objectId, piece, "apart");
14901
+ return true;
14902
+ }
14903
+ /** Whether the lowest of `boxes` (world boxes of the object `objectId`) has the ground, a fixed top or a piece within `BROKEN_GAP` under it. */
14904
+ standsOn(objectId, boxes) {
14905
+ if (boxes.length === 0) return false;
14906
+ const low = Math.min(...boxes.map((one) => one[1]));
14907
+ for (const [x0, y0, z0, x1, , z1] of boxes) {
14908
+ if (y0 > low + FEET_BAND) continue;
14909
+ for (const [fx, fz] of BROKEN_SAMPLES) {
14910
+ const x = x0 + (x1 - x0) * fx;
14911
+ const z = z0 + (z1 - z0) * fz;
14912
+ const under = Math.max(this.groundAt(x, z), this.staticTopAtBut(objectId, x, z, y0, BROKEN_GAP), this.pieceSys.topAt(x, z, y0, BROKEN_GAP, objectId));
14913
+ if (under >= y0 - BROKEN_GAP) return true;
14914
+ }
14915
+ }
14916
+ return false;
14917
+ }
14918
+ /** The kind of kit piece a bound object is, by the mark on its root or the nearest one above it (a house binds a group inside its marked root); '' when unmarked. */
14919
+ kindOf(b) {
14920
+ for (let o = b.root; o; o = o.parent) {
14921
+ const kind = kitOriginOf(o)?.kind;
14922
+ if (kind) return kind;
14923
+ }
14924
+ return "";
14925
+ }
14926
+ /** `staticTopAt` without the boxes of `objectId` itself. */
14927
+ staticTopAtBut(objectId, x, z, y, step) {
14928
+ let top = -Infinity;
14929
+ for (const [id, a] of this.staticNow()) {
14930
+ if (id === objectId || a.length === 0 || x < a[0] || x > a[3] || z < a[2] || z > a[5]) continue;
14931
+ for (let i = 6; i < a.length; i += 6) {
14932
+ if (x < a[i] || x > a[i + 3] || z < a[i + 2] || z > a[i + 5]) continue;
14933
+ const t = a[i + 4];
14934
+ if (t > top && t <= y + step) top = t;
14935
+ }
14936
+ }
14937
+ return top;
14938
+ }
14939
+ /**
14940
+ * The piece touches the ground by the corners of each part left of it, not by the box around them all: on one leg, or on the legs of one side, its centre of mass is off
14941
+ * what holds it and it goes over until the top meets the floor. Past `CONTACT_MESHES` meshes it keeps the corners of its box.
14942
+ */
14943
+ setContacts(b, body, meshes) {
14944
+ if (meshes.length > CONTACT_MESHES) {
14945
+ body.contacts = void 0;
14946
+ return;
14947
+ }
14948
+ const inv = specFrameInverse(this.THREE, b.root);
14949
+ const m4 = new this.THREE.Matrix4();
14950
+ const lb = new this.THREE.Box3();
14951
+ const [cx, cy, cz] = body.localCenter;
14952
+ const points = [];
14953
+ for (const mv of meshes) {
14954
+ mv.mesh.updateWorldMatrix(true, false);
14955
+ this.visuals.localBox(mv, lb);
14956
+ if (lb.isEmpty()) continue;
14957
+ lb.applyMatrix4(m4.multiplyMatrices(inv, mv.mesh.matrixWorld));
14958
+ for (let i = 0; i < 8; i++) points.push([(i & 1 ? lb.max.x : lb.min.x) - cx, (i & 2 ? lb.max.y : lb.min.y) - cy, (i & 4 ? lb.max.z : lb.min.z) - cz]);
14959
+ }
14960
+ body.contacts = points.length > 0 ? points : void 0;
14961
+ }
14725
14962
  /**
14726
14963
  * A piece of furniture standing on feet too close for its height (a board on two legs in a line, a lone leg) does not stay up: the box of a piece hides that its weight is up
14727
14964
  * high and its feet are thin, so it gets the start of the fall, across the line of its feet.
@@ -15013,6 +15250,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15013
15250
  this.active.clear();
15014
15251
  for (const key of [...this.fires.keys()]) this.stopFire(key);
15015
15252
  this.leaks.length = 0;
15253
+ this.trickles.length = 0;
15016
15254
  for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
15017
15255
  for (const b of [...this.bounds.values()]) {
15018
15256
  if (b.isSplit) {
@@ -15148,7 +15386,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15148
15386
  return { id: options.id, samples };
15149
15387
  }
15150
15388
 
15151
- // ../game-audio/dist/chunk-UUWKSG4J.js
15389
+ // ../game-audio/dist/chunk-BUYBYP4L.js
15152
15390
  var DISTANCE_CURVES = {
15153
15391
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
15154
15392
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -15326,6 +15564,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15326
15564
  ["revolver_38_357", 2, "sfx_pistol"],
15327
15565
  ["revolver_44_50ae", 3, "sfx_pistol"],
15328
15566
  ["smg_9mm_auto", 1, "sfx_pistol"],
15567
+ ["launcher_40mm", 2],
15568
+ ["rocket_launch", 2],
15329
15569
  ["shotgun_12", 1, "sfx_shotgun"],
15330
15570
  ["rifle_5_56", 1, "sfx_rifle"],
15331
15571
  ["rifle_7_62x39", 1, "sfx_rifle"],
@@ -16791,6 +17031,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16791
17031
  volume: 0,
16792
17032
  sources: ["sdk: the roar under the crackles of a fire"]
16793
17033
  };
17034
+ var SMOKE_HISS_LOOP = {
17035
+ id: "smoke_grenade_hiss",
17036
+ bus: "world",
17037
+ order: "chain_loop",
17038
+ source: { kind: "noise", loop: true },
17039
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 1400 }, q: 0.7 }, { type: "lowpass", frequency: { set: 7e3 }, q: 0.5 }] },
17040
+ volume: 0,
17041
+ sources: ["sdk: a smoke canister venting (the gas through its holes, steady for its whole burn)"]
17042
+ };
17043
+ var GRENADE_PIN_PULL = blast(
17044
+ "grenade_pin_pull",
17045
+ [sceneNoise({ type: "highpass", f: [3200, 4200], d: 0.025, vol: 0.5 }), sceneNoise({ type: "bandpass", f: [2200, 2800], q: 2, d: 0.04, vol: 0.4, delay: 0.18 })],
17046
+ ["sdk: the ring pulled from the fuse, then the spoon flying off"],
17047
+ FIRE_FALLOFF
17048
+ );
16794
17049
  var FIRE_FUEL_OF = {
16795
17050
  wood_soft: "wood",
16796
17051
  wood_hard: "wood",
@@ -16980,7 +17235,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16980
17235
  impact2("plastic_crack", [sceneNoise({ type: "bandpass", f: 1800, q: 2, d: 0.06, vol: 0.75 }), sceneTone({ f: [450, 550], f2: 250, d: 0.05, vol: 0.22 })]),
16981
17236
  impact2("mdf_crumble", [sceneNoise({ type: "lowpass", f: 700, d: 0.25, vol: 0.75, wet: 0.2 }), ...sceneMany(4, () => sceneNoise({ type: "lowpass", f: 500, d: 0.05, vol: 0.32, delay: [0.05, 0.3] }))])
16982
17237
  ];
16983
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, ...IMPACT_SOUNDS, EXPLOSION_BOOM, ...BLAST_SOUNDS, ...FIRE_SOUNDS], loops: [BURN_LOOP, FIRE_LOOP, ...FIRE_FUEL_LOOPS] };
17238
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, ...IMPACT_SOUNDS, EXPLOSION_BOOM, ...BLAST_SOUNDS, ...FIRE_SOUNDS, GRENADE_PIN_PULL], loops: [BURN_LOOP, FIRE_LOOP, ...FIRE_FUEL_LOOPS, SMOKE_HISS_LOOP] };
16984
17239
 
16985
17240
  // src/audio/sound.ts
16986
17241
  var IMPACT_SOUND_KEYS = {
@@ -17281,7 +17536,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17281
17536
  // each fuel has its own roar: a gas jet (burners and torches), a pool of fuel, plastic and rubber sizzling under the flames
17282
17537
  fire_loop_gas: { bus: "world", gain: 6, loop: { crossfade: 1 } },
17283
17538
  fire_loop_liquid: { bus: "world", gain: 3.5, loop: { crossfade: 1 } },
17284
- fire_loop_plastic: { bus: "world", gain: 1, loop: { crossfade: 0.6 } }
17539
+ fire_loop_plastic: { bus: "world", gain: 1, loop: { crossfade: 0.6 } },
17540
+ // a smoke canister venting: a gas hiss chained like the fire roar (the level comes from the room, by how near the burning canisters are)
17541
+ smoke_grenade_hiss: { bus: "world", gain: 1, loop: { crossfade: 0.8 } },
17542
+ // the ring and the spoon of a hand grenade: a small metal latch
17543
+ grenade_pin_pull: fire(0.5)
17285
17544
  };
17286
17545
  function blastFireSampleBank(manifest, options = {}) {
17287
17546
  const sounds = options.sounds ?? BLAST_FIRE_SAMPLES;