@adatechnology/game-test-room 0.29.0 → 0.29.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.
@@ -3722,23 +3722,42 @@ var GameTestRoom = (() => {
3722
3722
  const v = table[mat.penetrationClass ?? mat.id] ?? table[mat.id];
3723
3723
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
3724
3724
  };
3725
- var capacityScale = (p) => {
3725
+ var BURIED_BLAST_REACH = 0.5;
3726
+ var GIVE_AT_CUT_HP = 0.5;
3727
+ var capacityScale = (p, borne, through) => {
3726
3728
  const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
3727
3729
  if (p.st.stripped) return residual;
3728
- const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
3730
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, sectionKept(p, residual, borne, through));
3729
3731
  return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
3730
3732
  };
3731
- var sectionLoss = (p) => {
3733
+ var sectionKept = (p, residual, borne, through) => {
3734
+ let kept = 1;
3735
+ forEachSection(p, borne, (c, along, gone) => {
3736
+ const share = through?.(c.center, along) ?? 1;
3737
+ if (share <= 0) return;
3738
+ kept = Math.min(kept, Math.max(p.spec.reinforcement ? residual : 0, 1 - gone) / share);
3739
+ });
3740
+ return kept;
3741
+ };
3742
+ var sectionLoss = (p, borne) => {
3743
+ let worst = 0;
3744
+ forEachSection(p, borne, (_c, _along, gone) => {
3745
+ worst = Math.max(worst, gone);
3746
+ });
3747
+ return Math.min(1, worst);
3748
+ };
3749
+ var forEachSection = (p, borne, visit) => {
3732
3750
  const cells = p.st.chipped;
3733
- if (!cells?.length) return 0;
3751
+ if (!cells?.length) return;
3734
3752
  const [u, v] = planeAxes(p.thin);
3735
3753
  const role = p.spec.structure?.role;
3736
3754
  const up = rotateVec(p.q, [0, 0, 0].map((_, k) => k === v ? 1 : 0));
3737
3755
  const upright = Math.abs(up[1]) > 0.7 ? v : Math.abs(rotateVec(p.q, [0, 0, 0].map((_, k) => k === u ? 1 : 0))[1]) > 0.7 ? u : -1;
3738
3756
  const along = (role === "wall" || role === "partition" || role === "column") && upright >= 0 ? upright : p.dims[u] >= p.dims[v] ? u : v;
3739
3757
  const across = along === u ? v : u;
3740
- let worst = 0;
3758
+ const lying = along !== upright || upright < 0;
3741
3759
  for (const c of cells) {
3760
+ if (lying && borne?.(c.center, along)) continue;
3742
3761
  const spans = [];
3743
3762
  for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) spans.push([d.center[across] - d.size[across] / 2, d.center[across] + d.size[across] / 2]);
3744
3763
  spans.sort((a, b) => a[0] - b[0]);
@@ -3748,9 +3767,8 @@ var GameTestRoom = (() => {
3748
3767
  gone += b - Math.max(a, end3);
3749
3768
  end3 = b;
3750
3769
  }
3751
- worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
3770
+ visit(c, along, Math.min(1, gone / Math.max(p.dims[across], 1e-6)));
3752
3771
  }
3753
- return Math.min(1, worst);
3754
3772
  };
3755
3773
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
3756
3774
  var clone = (v) => structuredClone(v);
@@ -4061,6 +4079,26 @@ var GameTestRoom = (() => {
4061
4079
  byTouching.set(support, { version: poseVersion, box: box2 });
4062
4080
  return box2;
4063
4081
  };
4082
+ const throughOf = (p) => {
4083
+ const load = p.st.load ?? 0;
4084
+ if (load <= 0) return void 0;
4085
+ const above2 = Math.max(0, load - p.st.mass);
4086
+ const up = rotateVec(p.q, [0, 1, 0]);
4087
+ return (local2, along) => {
4088
+ const axis2 = [0, 0, 0];
4089
+ axis2[along] = 1;
4090
+ const sign = Math.sign(rotateVec(p.q, axis2)[1]);
4091
+ if (Math.abs(up[1]) < 0.7 && Math.abs(rotateVec(p.q, axis2)[1]) < 0.7) return 1;
4092
+ const half = p.dims[along] / 2;
4093
+ const share = clamp2((half - sign * local2[along]) / Math.max(p.dims[along], 1e-6), 0, 1);
4094
+ return (above2 + p.st.mass * share) / load;
4095
+ };
4096
+ };
4097
+ const borneOn = (p, supports) => {
4098
+ if (supports.length === 0) return void 0;
4099
+ const boxes = supports.map((support) => contactBox(p, support));
4100
+ return (local2, along) => boxes.some(([lo, hi]) => local2[along] >= lo[along] - SUPPORT_TOLERANCE && local2[along] <= hi[along] + SUPPORT_TOLERANCE);
4101
+ };
4064
4102
  const measureContact = (p, support) => {
4065
4103
  const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
4066
4104
  const lo = [0, 0, 0], hi = [0, 0, 0];
@@ -4268,7 +4306,7 @@ var GameTestRoom = (() => {
4268
4306
  o.unsup[i] = o.anyAnchor && !isLost(p) && !p.orphan;
4269
4307
  continue;
4270
4308
  }
4271
- p.st.stress = p.st.load * structureSafety / (p.cap * capacityScale(p));
4309
+ p.st.stress = p.st.load * structureSafety / (p.cap * capacityScale(p, borneOn(p, (lowerOf.get(i) ?? []).map((k) => parts[k])), throughOf(p)));
4272
4310
  if (p.bend) {
4273
4311
  p.bendLoad = p.st.mass + bending[i];
4274
4312
  const bent = restFlexure(o, p, lowerOf.get(i) ?? []);
@@ -5358,6 +5396,27 @@ var GameTestRoom = (() => {
5358
5396
  debris3(o, p, p.mat.breakDebris, center, [0, -1, 0], 6, 1, src);
5359
5397
  return loose.length;
5360
5398
  };
5399
+ const giveAtCut = (o, p, src) => {
5400
+ if (!p.spec.reinforcement || !p.spec.units || !p.st.chipped?.length || sectionLoss(p) <= 0) return false;
5401
+ const opened = [];
5402
+ for (const j of o.byPart.get(p.spec.id) ?? []) {
5403
+ if (j.st.open) continue;
5404
+ const onIt = j.bears === "a_on_b" && j.spec.b === p.spec.id || j.bears === "b_on_a" && j.spec.a === p.spec.id;
5405
+ if (!onIt) continue;
5406
+ j.st.open = true;
5407
+ j.st.openedBy = "break";
5408
+ opened.push(j);
5409
+ }
5410
+ if (opened.length === 0) return false;
5411
+ p.st.hp = Math.max(p.st.hp, p.st.maxHp * GIVE_AT_CUT_HP);
5412
+ markDamaged(p);
5413
+ solveStructure(o);
5414
+ recomputeGroups(o);
5415
+ detachEvents(o, opened, "break", src);
5416
+ exposeRebar(o, p, src);
5417
+ collapseCheck(o, src);
5418
+ return true;
5419
+ };
5361
5420
  const stripPart = (o, p, reinforcement, src) => {
5362
5421
  const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
5363
5422
  p.st.stripped = true;
@@ -5415,6 +5474,7 @@ var GameTestRoom = (() => {
5415
5474
  hingeDrop(o, p, src);
5416
5475
  return;
5417
5476
  }
5477
+ if (!force && giveAtCut(o, p, src)) return;
5418
5478
  const ex = p.spec.explodes;
5419
5479
  const style = force ?? (p.snap ? "snap" : p.mat.fracture === "none" ? "snap" : p.mat.fracture);
5420
5480
  const { c, q } = partXf(o, p);
@@ -5761,7 +5821,9 @@ var GameTestRoom = (() => {
5761
5821
  const dx = local2[0] - p.pos[0];
5762
5822
  const dy = local2[1] - p.pos[1];
5763
5823
  const dz = local2[2] - p.pos[2];
5764
- if (dx * dx + dy * dy + dz * dz < reach * reach) near.push(p);
5824
+ if (dx * dx + dy * dy + dz * dz >= reach * reach) continue;
5825
+ if (p.spec.anchored && p.spec.structure?.role === "foundation" && distToPart(o, p, point) > BURIED_BLAST_REACH) continue;
5826
+ near.push(p);
5765
5827
  }
5766
5828
  blastProbe.partsTested += o.parts.length;
5767
5829
  blastProbe.partsNear += near.length;
@@ -7270,7 +7332,7 @@ var GameTestRoom = (() => {
7270
7332
  return bank;
7271
7333
  }
7272
7334
 
7273
- // ../game-audio/dist/chunk-UUWKSG4J.js
7335
+ // ../game-audio/dist/chunk-BUYBYP4L.js
7274
7336
  var DISTANCE_CURVES = {
7275
7337
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
7276
7338
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -9665,6 +9727,8 @@ var GameTestRoom = (() => {
9665
9727
  ["revolver_38_357", 2, "sfx_pistol"],
9666
9728
  ["revolver_44_50ae", 3, "sfx_pistol"],
9667
9729
  ["smg_9mm_auto", 1, "sfx_pistol"],
9730
+ ["launcher_40mm", 2],
9731
+ ["rocket_launch", 2],
9668
9732
  ["shotgun_12", 1, "sfx_shotgun"],
9669
9733
  ["rifle_5_56", 1, "sfx_rifle"],
9670
9734
  ["rifle_7_62x39", 1, "sfx_rifle"],
@@ -17204,8 +17268,8 @@ var GameTestRoom = (() => {
17204
17268
  id: "dust",
17205
17269
  additive: false,
17206
17270
  life: [
17207
- 1,
17208
- 2
17271
+ 2,
17272
+ 5
17209
17273
  ],
17210
17274
  size: [
17211
17275
  0.08,
@@ -17272,8 +17336,8 @@ var GameTestRoom = (() => {
17272
17336
  id: "chips",
17273
17337
  additive: false,
17274
17338
  life: [
17275
- 0.8,
17276
- 1.5
17339
+ 2,
17340
+ 4
17277
17341
  ],
17278
17342
  size: [
17279
17343
  0.025,
@@ -19915,7 +19979,7 @@ var GameTestRoom = (() => {
19915
19979
  return sources;
19916
19980
  }
19917
19981
 
19918
- // ../game-vfx-three/dist/chunk-CPRCWTSQ.js
19982
+ // ../game-vfx-three/dist/chunk-IOGYK5RO.js
19919
19983
  var GLOW_VERTEX = `attribute vec4 aP;attribute vec4 aC;varying vec2 vU;varying vec4 vC;
19920
19984
  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]);
19921
19985
  vec3 wp=aP.xyz+(R*position.x+Up*position.y)*aP.w;vU=position.xy;vC=aC;gl_Position=projectionMatrix*viewMatrix*vec4(wp,1.);}`;
@@ -19962,10 +20026,15 @@ void main(){
19962
20026
  if(n<diss*1.08-.04)discard;
19963
20027
  float lam=max(dot(N,uLight),0.);
19964
20028
  float band=lam>.55?1.:lam>.15?.68:.42;
19965
- vec3 col=vC*(vec3(.5,.56,.7)*band*1.05+vec3(.2,.19,.18)+uGround*(.4-.4*N.y));
20029
+ // 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)
20030
+ float dark=1.-smoothstep(.02,.12,dot(vC,vec3(.2126,.7152,.0722)));
20031
+ vec3 alb=max(vC,vec3(.055)*dark);
20032
+ vec3 col=alb*(vec3(.5,.56,.7)*band*1.05+vec3(.2,.19,.18)+uGround*(.4-.4*N.y));
20033
+ float rim=pow(1.-clamp(abs(dot(N,normalize(cameraPosition-vWP))),0.,1.),2.);
20034
+ col+=dark*rim*(uSky*.9+alb);
19966
20035
  vec3 tg=uGlowPos-vWP;float dg=length(tg)+.001;
19967
20036
  float gi=uGlowI/(1.+dg*dg*.05)*(.3+.7*max(dot(N,tg/dg),0.));
19968
- col+=uGlowCol*gi*vC*1.6;
20037
+ col+=uGlowCol*gi*alb*1.6;
19969
20038
  float h=heat*(.72+.56*n)+heat*(-N.y)*.18;
19970
20039
  if(h>.05){
19971
20040
  float hq=floor(h*5.+.5)/5.;
@@ -22143,7 +22212,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22143
22212
  var SLAM_BOUNCE = 0.2;
22144
22213
  var SLAM_FLOOR_NORMAL = 0.7;
22145
22214
  var SUB_DT = 1 / 120;
22146
- var MU = 0.7;
22215
+ var MU_STATIC = 0.7;
22216
+ var MU_DYNAMIC = 0.6;
22217
+ var SLIP_SPEED = 0.02;
22147
22218
  var SLEEP_AFTER = 0.5;
22148
22219
  var CALM_DRIFT = 0.01;
22149
22220
  var CALM_TURN = 0.05;
@@ -22162,6 +22233,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22162
22233
  var EDGE_POINTS = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
22163
22234
  var SUPPORT_CHECKS2 = 16;
22164
22235
  var SUPPORT_GAP2 = 0.02;
22236
+ var WAKE_ABOVE_SPEED = 0.05;
22237
+ var WAKE_ABOVE_SPIN = 0.1;
22238
+ var topFace = 1;
22165
22239
  var boxTopAt = (s, x, z) => {
22166
22240
  const e = s.e;
22167
22241
  const dx = x - s.c[0], dz = z - s.c[2];
@@ -22177,11 +22251,24 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22177
22251
  let a = (-h - p) / d, b = (h - p) / d;
22178
22252
  if (a > b) [a, b] = [b, a];
22179
22253
  t0 = Math.max(t0, a);
22180
- t1 = Math.min(t1, b);
22254
+ if (b < t1) {
22255
+ t1 = b;
22256
+ topFace = k;
22257
+ }
22181
22258
  if (t0 > t1) return -Infinity;
22182
22259
  }
22183
22260
  return s.c[1] + t1;
22184
22261
  };
22262
+ var UP16 = [0, 1, 0];
22263
+ var faceNormal = (s, axis2) => {
22264
+ const e = s.e;
22265
+ const sign = e[axis2 * 4 + 1] < 0 ? -1 : 1;
22266
+ return [sign * e[axis2 * 4], sign * e[axis2 * 4 + 1], sign * e[axis2 * 4 + 2]];
22267
+ };
22268
+ var pairFriction = (a, b) => ({
22269
+ static: Math.sqrt((a?.static ?? MU_STATIC) * (b?.static ?? MU_STATIC)),
22270
+ dynamic: Math.sqrt((a?.dynamic ?? MU_DYNAMIC) * (b?.dynamic ?? MU_DYNAMIC))
22271
+ });
22185
22272
  var PieceSystem = class {
22186
22273
  constructor(THREE, o) {
22187
22274
  __publicField3(this, "THREE", THREE);
@@ -22195,6 +22282,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22195
22282
  __publicField3(this, "ax");
22196
22283
  __publicField3(this, "solids", []);
22197
22284
  __publicField3(this, "supportTurn", 0);
22285
+ __publicField3(this, "supportNormal", UP16);
22286
+ __publicField3(this, "supportFriction", null);
22198
22287
  this.p = new THREE.Vector3();
22199
22288
  this.q = new THREE.Quaternion();
22200
22289
  this.dq = new THREE.Quaternion();
@@ -22300,8 +22389,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22300
22389
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
22301
22390
  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];
22302
22391
  }
22303
- /** 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. */
22392
+ /** 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`. */
22304
22393
  samples(b, edges) {
22394
+ if (b.contacts) return b.contacts;
22305
22395
  const [hx, hy, hz2] = b.halfExtents;
22306
22396
  const out = [];
22307
22397
  for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz2 : -hz2]);
@@ -22366,6 +22456,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22366
22456
  }
22367
22457
  if (b.sleeping) continue;
22368
22458
  this.integrate(b, dt);
22459
+ if (b.sleeping) continue;
22460
+ const v = b.velocity;
22461
+ const w = b.angularVelocity;
22462
+ 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);
22369
22463
  }
22370
22464
  this.separate(dt);
22371
22465
  }
@@ -22396,15 +22490,48 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22396
22490
  }
22397
22491
  return false;
22398
22492
  }
22493
+ /** 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. */
22494
+ topAt(x, z, y, step, self) {
22495
+ let top = -Infinity;
22496
+ for (const s of this.solids) {
22497
+ 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;
22498
+ const t = boxTopAt(s, x, z);
22499
+ if (t > top && t <= y + step) top = t;
22500
+ }
22501
+ return top;
22502
+ }
22503
+ /** 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. */
22504
+ wakeAbove(b) {
22505
+ const below = this.solids.find((s) => s.body === b);
22506
+ if (!below) return;
22507
+ 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]);
22508
+ for (const s of this.solids) {
22509
+ const o = s.body;
22510
+ if (!o.sleeping || o.held || o === b) continue;
22511
+ 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;
22512
+ o.sleeping = false;
22513
+ o.still = 0;
22514
+ o.calmAt = null;
22515
+ }
22516
+ }
22399
22517
  /** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
22400
22518
  supportAt(self, x, y, z, step) {
22401
22519
  let top = this.o.groundAt(x, z);
22520
+ let on = null;
22521
+ let face = 1;
22402
22522
  for (const s of this.solids) {
22403
22523
  if (s.body === self || s.body.held || Math.abs(x - s.c[0]) > s.r || Math.abs(z - s.c[2]) > s.r) continue;
22404
22524
  const t = boxTopAt(s, x, z);
22405
- if (t > top && t <= y + step) top = t;
22525
+ if (t > top && t <= y + step) {
22526
+ top = t;
22527
+ on = s;
22528
+ face = topFace;
22529
+ }
22406
22530
  }
22407
22531
  const fixed2 = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
22532
+ if (fixed2 >= top) on = null;
22533
+ this.supportNormal = on ? faceNormal(on, face) : UP16;
22534
+ this.supportFriction = on?.body.friction ?? null;
22408
22535
  return Math.max(top, fixed2);
22409
22536
  }
22410
22537
  /** 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. */
@@ -22487,7 +22614,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22487
22614
  const e = this.m.elements;
22488
22615
  const rs = [];
22489
22616
  const ids = [];
22617
+ const ns = [];
22618
+ const mus = [];
22490
22619
  let maxPen = 0;
22620
+ let penNormal = UP16;
22491
22621
  const rise = STEP + Math.max(0, -v[1]) * h * 2;
22492
22622
  let side = null;
22493
22623
  let lowest = Infinity;
@@ -22505,6 +22635,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22505
22635
  if (floor2 !== void 0 && floor2 - cy > -SLOP && floor2 < raw - STEP) {
22506
22636
  rs.push(r);
22507
22637
  ids.push(i);
22638
+ ns.push(UP16);
22639
+ mus.push(pairFriction(b.friction, null));
22508
22640
  maxPen = Math.max(maxPen, floor2 - cy);
22509
22641
  }
22510
22642
  continue;
@@ -22525,12 +22657,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22525
22657
  }
22526
22658
  continue;
22527
22659
  }
22528
- const gy = i < 8 ? this.supportAt(b, px, cy, pz, rise) : Math.max(raw, this.o.staticTopAt?.(px, pz, cy, rise) ?? -Infinity);
22660
+ this.supportNormal = UP16;
22661
+ this.supportFriction = null;
22662
+ const gy = i < 8 || b.contacts ? this.supportAt(b, px, cy, pz, rise) : Math.max(raw, this.o.staticTopAt?.(px, pz, cy, rise) ?? -Infinity);
22529
22663
  const pen = gy - cy;
22530
22664
  if (pen > -SLOP) {
22531
22665
  rs.push(r);
22532
22666
  ids.push(i);
22533
- if (pen > maxPen) maxPen = pen;
22667
+ ns.push(this.supportNormal);
22668
+ mus.push(pairFriction(b.friction, this.supportFriction));
22669
+ if (pen > maxPen) {
22670
+ maxPen = pen;
22671
+ penNormal = this.supportNormal;
22672
+ }
22534
22673
  lowest = Math.min(lowest, gy);
22535
22674
  }
22536
22675
  }
@@ -22543,7 +22682,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22543
22682
  contactedLast = rs.length > 0;
22544
22683
  if (contactedLast) touched = true;
22545
22684
  if (!contactedLast) continue;
22546
- pos[1] += Math.max(0, maxPen - SLOP * 0.5);
22685
+ const out = Math.max(0, maxPen - SLOP * 0.5) * penNormal[1];
22686
+ pos[0] += penNormal[0] * out;
22687
+ pos[1] += penNormal[1] * out;
22688
+ pos[2] += penNormal[2] * out;
22547
22689
  let impact22 = 0;
22548
22690
  for (const r of rs) impact22 = Math.max(impact22, -(v[1] + w[2] * r[0] - w[0] * r[2]));
22549
22691
  const restitution = impact22 > 2 ? 0.25 : 0;
@@ -22559,76 +22701,72 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22559
22701
  const lz = pos[2] + sz / rs.length;
22560
22702
  landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
22561
22703
  }
22562
- const goal = rs.map((r) => {
22563
- const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
22564
- return vy2 < 0 ? -restitution * vy2 : 0;
22704
+ 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]];
22705
+ const dot5 = (a, c2) => a[0] * c2[0] + a[1] * c2[1] + a[2] * c2[2];
22706
+ const goal = rs.map((r, k) => {
22707
+ const vn = dot5(pointV(r), ns[k]);
22708
+ return vn < 0 ? -restitution * vn : 0;
22709
+ });
22710
+ const slipping = rs.map((r, k) => {
22711
+ const pv = pointV(r);
22712
+ const vn = dot5(pv, ns[k]);
22713
+ return Math.hypot(pv[0] - vn * ns[k][0], pv[1] - vn * ns[k][1], pv[2] - vn * ns[k][2]) > SLIP_SPEED;
22565
22714
  });
22566
22715
  const vx0 = v[0];
22567
22716
  const vz0 = v[2];
22568
- if (!b.warm || b.warm.length !== WARM_SLOTS * 3) b.warm = new Float64Array(WARM_SLOTS * 3);
22717
+ if (!b.warm || b.warm.length !== WARM_SLOTS * 4) b.warm = new Float64Array(WARM_SLOTS * 4);
22569
22718
  const warm = b.warm;
22570
- const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 3] : 0);
22571
- const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 3 + 1], warm[id * 3 + 2]] : [0, 0]);
22572
- for (let k = 0; k < rs.length; k++) {
22573
- const r = rs[k];
22574
- const jn = accN[k];
22575
- const [fx, fz] = accT[k];
22576
- if (jn === 0 && fx === 0 && fz === 0) continue;
22577
- v[0] += fx * invM;
22578
- v[1] += jn * invM;
22579
- v[2] += fz * invM;
22580
- const dw = this.mulInvInertiaWith(b, e, r[1] * fz - r[2] * jn, r[2] * fx - r[0] * fz, r[0] * jn - r[1] * fx);
22719
+ const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 4] : 0);
22720
+ const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 4 + 1], warm[id * 4 + 2], warm[id * 4 + 3]] : [0, 0, 0]);
22721
+ const push = (r, j) => {
22722
+ v[0] += j[0] * invM;
22723
+ v[1] += j[1] * invM;
22724
+ v[2] += j[2] * invM;
22725
+ 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]);
22581
22726
  w[0] += dw[0];
22582
22727
  w[1] += dw[1];
22583
22728
  w[2] += dw[2];
22729
+ };
22730
+ const reach = (r, d) => {
22731
+ 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]);
22732
+ 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];
22733
+ };
22734
+ for (let k = 0; k < rs.length; k++) {
22735
+ const n = ns[k];
22736
+ const t = accT[k];
22737
+ 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]]);
22584
22738
  }
22585
22739
  for (let it = 0; it < SOLVER_PASSES; it++) {
22586
22740
  for (let k = 0; k < rs.length; k++) {
22587
22741
  const r = rs[k];
22588
- const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
22589
- const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
22590
- const denom = invM + (a[2] * r[0] - a[0] * r[2]);
22591
- const total = Math.max(0, accN[k] + (goal[k] - vy2) / denom);
22742
+ const n = ns[k];
22743
+ const vn = dot5(pointV(r), n);
22744
+ const total = Math.max(0, accN[k] + (goal[k] - vn) / reach(r, n));
22592
22745
  const j = total - accN[k];
22593
22746
  accN[k] = total;
22594
- if (j !== 0) {
22595
- v[1] += j * invM;
22596
- w[0] += a[0] * j;
22597
- w[1] += a[1] * j;
22598
- w[2] += a[2] * j;
22747
+ if (j !== 0) push(r, [n[0] * j, n[1] * j, n[2] * j]);
22748
+ const pv = pointV(r);
22749
+ const pn = dot5(pv, n);
22750
+ const vt = [pv[0] - pn * n[0], pv[1] - pn * n[1], pv[2] - pn * n[2]];
22751
+ const speed = Math.hypot(vt[0], vt[1], vt[2]);
22752
+ const f = [accT[k][0], accT[k][1], accT[k][2]];
22753
+ if (speed > 1e-6) {
22754
+ const d = [vt[0] / speed, vt[1] / speed, vt[2] / speed];
22755
+ const jt = speed / reach(r, d);
22756
+ f[0] -= d[0] * jt;
22757
+ f[1] -= d[1] * jt;
22758
+ f[2] -= d[2] * jt;
22759
+ } else if (f[0] === 0 && f[1] === 0 && f[2] === 0) continue;
22760
+ const most = (slipping[k] ? mus[k].dynamic : mus[k].static) * accN[k];
22761
+ const fl = Math.hypot(f[0], f[1], f[2]);
22762
+ if (fl > most) {
22763
+ f[0] *= most / fl;
22764
+ f[1] *= most / fl;
22765
+ f[2] *= most / fl;
22599
22766
  }
22600
- const vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
22601
- const vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
22602
- const vt = Math.hypot(vx2, vz2);
22603
- if (vt < 1e-6 && accT[k][0] === 0 && accT[k][1] === 0) continue;
22604
- let fx = accT[k][0];
22605
- let fz = accT[k][1];
22606
- if (vt > 1e-6) {
22607
- const tx = vx2 / vt;
22608
- const tz = vz2 / vt;
22609
- const at3 = this.mulInvInertiaWith(b, e, r[1] * tz, r[2] * tx - r[0] * tz, -r[1] * tx);
22610
- const cx = at3[1] * r[2] - at3[2] * r[1];
22611
- const cz = at3[0] * r[1] - at3[1] * r[0];
22612
- const jt = vt / (invM + cx * tx + cz * tz);
22613
- fx -= tx * jt;
22614
- fz -= tz * jt;
22615
- }
22616
- const most = MU * accN[k];
22617
- const f = Math.hypot(fx, fz);
22618
- if (f > most) {
22619
- fx *= most / f;
22620
- fz *= most / f;
22621
- }
22622
- const dx = fx - accT[k][0];
22623
- const dz = fz - accT[k][1];
22624
- accT[k] = [fx, fz];
22625
- if (dx === 0 && dz === 0) continue;
22626
- v[0] += dx * invM;
22627
- v[2] += dz * invM;
22628
- const dw = this.mulInvInertiaWith(b, e, r[1] * dz, r[2] * dx - r[0] * dz, -r[1] * dx);
22629
- w[0] += dw[0];
22630
- w[1] += dw[1];
22631
- w[2] += dw[2];
22767
+ const dj = [f[0] - accT[k][0], f[1] - accT[k][1], f[2] - accT[k][2]];
22768
+ accT[k] = f;
22769
+ if (dj[0] !== 0 || dj[1] !== 0 || dj[2] !== 0) push(r, dj);
22632
22770
  }
22633
22771
  }
22634
22772
  pos[0] += (v[0] - vx0) * h;
@@ -22637,9 +22775,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22637
22775
  for (let k = 0; k < rs.length; k++) {
22638
22776
  const id = ids[k];
22639
22777
  if (id >= WARM_SLOTS) continue;
22640
- warm[id * 3] = accN[k];
22641
- warm[id * 3 + 1] = accT[k][0];
22642
- warm[id * 3 + 2] = accT[k][1];
22778
+ warm[id * 4] = accN[k];
22779
+ warm[id * 4 + 1] = accT[k][0];
22780
+ warm[id * 4 + 2] = accT[k][1];
22781
+ warm[id * 4 + 3] = accT[k][2];
22643
22782
  }
22644
22783
  let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
22645
22784
  for (const r of rs) {
@@ -22711,6 +22850,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22711
22850
  b.calm += dt;
22712
22851
  return b.calm >= CALM_SLEEP_AFTER;
22713
22852
  }
22853
+ /** Whether the centre `c` of `upper` is over the top of `lower`'s box (as it was at the start of the update). */
22854
+ onTopOf(upper, lower, c) {
22855
+ const s = this.solids.find((one) => one.body === lower);
22856
+ if (!s) return false;
22857
+ const top = boxTopAt(s, c.x, c.z);
22858
+ return top > -Infinity && c.y >= top - 0.01;
22859
+ }
22714
22860
  /** Half the height of the box of a piece as it is turned now. */
22715
22861
  halfHeight(b) {
22716
22862
  const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
@@ -22736,6 +22882,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22736
22882
  this.centerOf(b, cb);
22737
22883
  if (Math.abs(ca.y - cb.y) > ra + rb) continue;
22738
22884
  if (Math.abs(ca.y - cb.y) >= (this.halfHeight(a) + this.halfHeight(b)) * 0.9) continue;
22885
+ if (this.onTopOf(b, a, cb) || this.onTopOf(a, b, ca)) continue;
22739
22886
  const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
22740
22887
  let dx = cb.x - ca.x;
22741
22888
  let dz = cb.z - ca.z;
@@ -24126,6 +24273,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24126
24273
  var IMPACT_TAGS = ["metal", "glass", "wood"];
24127
24274
  var GLOW_FROM_C = 430;
24128
24275
  var QUEUE_CAP = 4e3;
24276
+ var TRICKLE_SECONDS = [2, 5];
24277
+ var TRICKLE_EVERY = 0.25;
24278
+ var TRICKLE_MAX = 24;
24129
24279
  var STRUCTURAL_EVENTS = /* @__PURE__ */ new Set([
24130
24280
  "fracture",
24131
24281
  "detach",
@@ -24149,6 +24299,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24149
24299
  var FEET_BAND = 0.03;
24150
24300
  var TIP_MIN_HEIGHT = 0.3;
24151
24301
  var TIP_NARROW = 0.3;
24302
+ var CONTACT_MESHES = 12;
24303
+ var BROKEN_GAP = 0.03;
24304
+ var BROKEN_SAMPLES = [[0.5, 0.5], [0.1, 0.1], [0.9, 0.1], [0.1, 0.9], [0.9, 0.9]];
24305
+ var UNCHECKED_KINDS = /* @__PURE__ */ new Set(["structure", "room", "ground"]);
24152
24306
  var FIRE_CEILING_SECONDS = 3600;
24153
24307
  var GENERIC_REFERENCE_RADIUS = 15;
24154
24308
  var SWEEP_CORE = 0.15;
@@ -24244,9 +24398,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24244
24398
  __publicField3(this, "staticAll", true);
24245
24399
  __publicField3(this, "staticRound", []);
24246
24400
  __publicField3(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
24401
+ __publicField3(this, "broken", /* @__PURE__ */ new Set());
24247
24402
  __publicField3(this, "staticAt", 0);
24248
24403
  __publicField3(this, "queue", []);
24249
24404
  __publicField3(this, "coverWhenWentOff", /* @__PURE__ */ new WeakMap());
24405
+ __publicField3(this, "trickles", []);
24250
24406
  __publicField3(this, "slices", []);
24251
24407
  __publicField3(this, "sliceDeadline", Infinity);
24252
24408
  __publicField3(this, "drainClock", () => performance.now());
@@ -24581,10 +24737,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24581
24737
  dt = clamp6(dt, 0, 0.1);
24582
24738
  this.time += dt;
24583
24739
  if (!this.attached) this.drainQueue();
24740
+ this.checkBroken();
24584
24741
  for (const b of this.bounds.values()) b.onFrame?.(dt);
24585
24742
  for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
24586
24743
  this.updateActive(dt);
24587
24744
  this.updateLeaks(dt);
24745
+ this.updateTrickles(dt);
24588
24746
  this.pieceSys.update(dt);
24589
24747
  this.fragments.update(dt);
24590
24748
  this.ground.update(dt);
@@ -24783,6 +24941,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24783
24941
  }
24784
24942
  apply(ev) {
24785
24943
  this.staticDirty.add(ev.object);
24944
+ if (ev.type === "fracture" || ev.type === "detach" || ev.type === "collapse" || ev.type === "burnout" || ev.type === "destroyed") this.broken.add(ev.object);
24786
24945
  switch (ev.type) {
24787
24946
  case "damage":
24788
24947
  if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
@@ -25107,9 +25266,34 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25107
25266
  const core = def?.visual?.innerColor;
25108
25267
  if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
25109
25268
  else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
25269
+ if (core !== void 0) this.trickleFrom(ev.point, [normal.x, normal.y, normal.z], core);
25110
25270
  if (mv.mesh.userData.drawnSpalls?.includes(ev.part)) return;
25111
25271
  this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
25112
25272
  }
25273
+ /** 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. */
25274
+ trickleFrom(point, normal, color) {
25275
+ if (this.trickles.length >= TRICKLE_MAX) this.trickles.shift();
25276
+ const total = TRICKLE_SECONDS[0] + (TRICKLE_SECONDS[1] - TRICKLE_SECONDS[0]) * this.rng();
25277
+ this.trickles.push({ point: [point[0], point[1], point[2]], normal, color, left: total, total, next: TRICKLE_EVERY });
25278
+ }
25279
+ updateTrickles(dt) {
25280
+ for (let i = this.trickles.length - 1; i >= 0; i--) {
25281
+ const t = this.trickles[i];
25282
+ t.left -= dt;
25283
+ t.next -= dt;
25284
+ if (t.left <= 0) {
25285
+ this.trickles.splice(i, 1);
25286
+ continue;
25287
+ }
25288
+ if (t.next > 0) continue;
25289
+ const share = t.left / t.total;
25290
+ t.next = TRICKLE_EVERY / Math.max(share, 0.25);
25291
+ const out = [t.normal[0] * 0.2, -1, t.normal[2] * 0.2];
25292
+ const options = t.color !== void 0 ? { color: t.color } : void 0;
25293
+ this.vfx.burst("chips", t.point, out, this.low ? 1 : 2, 0.3, options);
25294
+ if (this.rng() < 0.5) this.vfx.burst("dust", t.point, out, 1, 0.2, options);
25295
+ }
25296
+ }
25113
25297
  /**
25114
25298
  * 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
25115
25299
  * (cutting the wall's faces) is not drawn yet: it needs the per-vertex binding of the house renderer.
@@ -25145,6 +25329,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25145
25329
  const tone3 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
25146
25330
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone3, false), ev.object, maxAlive);
25147
25331
  }
25332
+ if (def?.visual?.innerColor !== void 0 || pv?.material === "concrete" || pv?.material === "brick") this.trickleFrom(ev.center, [0, 0, 0], color);
25148
25333
  if (ev.torn) {
25149
25334
  this.impactSound(pv?.material ?? "", ev.center, 0.5, ev.blow);
25150
25335
  return;
@@ -25334,26 +25519,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25334
25519
  this.o.onPiece?.(piece);
25335
25520
  }
25336
25521
  /**
25337
- * 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
25338
- * 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.
25522
+ * 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
25523
+ * 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,
25524
+ * 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`).
25339
25525
  */
25340
25526
  loosenIfFootless(objectId) {
25341
25527
  const b = this.bounds.get(objectId);
25342
25528
  if (!b || !this.autoSplit) return;
25343
25529
  const left = this.meshesOf(b).filter((mv) => this.visuals.shown(mv));
25344
25530
  if (left.length === 0) return;
25531
+ const structural = UNCHECKED_KINDS.has(this.kindOf(b));
25345
25532
  const body = b.piece ?? this.pieceSys.get(objectId);
25346
25533
  if (body) {
25347
25534
  const { half, center } = this.measure(b.root, left);
25348
25535
  body.halfExtents = half;
25349
25536
  this.pieceSys.setCenter(body, center);
25350
25537
  body.mass = Math.max(0.05, this.massOf(objectId));
25538
+ if (!structural) this.setContacts(b, body, left);
25351
25539
  body.sleeping = false;
25352
25540
  return;
25353
25541
  }
25354
- if (kitOriginOf(b.root)?.kind !== "furniture") return;
25542
+ if (structural) return;
25355
25543
  const boxOf = (mv) => this.visuals.worldBox(mv, new this.THREE.Box3());
25356
25544
  const whole = [...b.parts.values()].flatMap((pv) => pv.meshes).map(boxOf).filter((one) => !one.isEmpty());
25545
+ 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;
25357
25546
  const floor2 = Math.min(...whole.map((one) => one.min.y));
25358
25547
  const feetOf = (boxes) => boxes.filter((one) => one.min.y < floor2 + FEET_BAND).reduce((feet, one) => feet.union(one), new this.THREE.Box3());
25359
25548
  const was = feetOf(whole);
@@ -25369,9 +25558,116 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25369
25558
  if (!narrow && centreX > now.min.x && centreX < now.max.x && centreZ > now.min.z && centreZ < now.max.z) return;
25370
25559
  }
25371
25560
  const piece = this.makePiece(objectId);
25561
+ this.setContacts(b, b.piece ?? piece, left);
25372
25562
  this.tipIfTopHeavy(b);
25373
25563
  this.o.onLoosened?.(objectId, piece, "apart");
25374
25564
  }
25565
+ shownBoxes(objectId, partId) {
25566
+ const b = this.bounds.get(objectId);
25567
+ if (!b) return [];
25568
+ const box2 = new this.THREE.Box3();
25569
+ const out = [];
25570
+ const pv = partId === void 0 ? null : b.parts.get(partId);
25571
+ if (partId !== void 0 && (!pv || pv.gone !== "none")) return out;
25572
+ for (const mv of pv ? pv.meshes : this.meshesOf(b)) {
25573
+ if (!this.visuals.shown(mv)) continue;
25574
+ this.visuals.worldBox(mv, box2);
25575
+ if (!box2.isEmpty()) out.push([box2.min.x, box2.min.y, box2.min.z, box2.max.x, box2.max.y, box2.max.z]);
25576
+ }
25577
+ return out;
25578
+ }
25579
+ /**
25580
+ * 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
25581
+ * fixed top nor a piece within `BROKEN_GAP` under it is made a piece on the spot (see `onBrokenChecked`).
25582
+ */
25583
+ checkBroken() {
25584
+ if (this.broken.size === 0) return;
25585
+ const ids = [...this.broken];
25586
+ this.broken.clear();
25587
+ for (const id of ids) {
25588
+ const released = this.releaseIfFloating(id);
25589
+ this.o.onBrokenChecked?.(id, released);
25590
+ }
25591
+ }
25592
+ releaseIfFloating(objectId) {
25593
+ const b = this.bounds.get(objectId);
25594
+ if (!b || b.piece || this.pieceSys.get(objectId) || !this.autoSplit || UNCHECKED_KINDS.has(this.kindOf(b))) return false;
25595
+ const boxes = this.shownBoxes(objectId);
25596
+ if (boxes.length === 0) return false;
25597
+ const whole = new this.THREE.Box3();
25598
+ const wholeBoxes = [];
25599
+ for (const pv of b.parts.values()) {
25600
+ for (const mv of pv.meshes) {
25601
+ this.visuals.worldBox(mv, whole);
25602
+ if (!whole.isEmpty()) wholeBoxes.push([whole.min.x, whole.min.y, whole.min.z, whole.max.x, whole.max.y, whole.max.z]);
25603
+ }
25604
+ }
25605
+ if (!this.standsOn(objectId, wholeBoxes) || this.standsOn(objectId, boxes)) return false;
25606
+ const piece = this.makePiece(objectId);
25607
+ this.setContacts(b, b.piece ?? piece, this.meshesOf(b).filter((mv) => this.visuals.shown(mv)));
25608
+ this.tipIfTopHeavy(b);
25609
+ this.o.onLoosened?.(objectId, piece, "apart");
25610
+ return true;
25611
+ }
25612
+ /** 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. */
25613
+ standsOn(objectId, boxes) {
25614
+ if (boxes.length === 0) return false;
25615
+ const low = Math.min(...boxes.map((one) => one[1]));
25616
+ for (const [x0, y0, z0, x1, , z1] of boxes) {
25617
+ if (y0 > low + FEET_BAND) continue;
25618
+ for (const [fx, fz] of BROKEN_SAMPLES) {
25619
+ const x = x0 + (x1 - x0) * fx;
25620
+ const z = z0 + (z1 - z0) * fz;
25621
+ 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));
25622
+ if (under >= y0 - BROKEN_GAP) return true;
25623
+ }
25624
+ }
25625
+ return false;
25626
+ }
25627
+ /** 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. */
25628
+ kindOf(b) {
25629
+ for (let o = b.root; o; o = o.parent) {
25630
+ const kind = kitOriginOf(o)?.kind;
25631
+ if (kind) return kind;
25632
+ }
25633
+ return "";
25634
+ }
25635
+ /** `staticTopAt` without the boxes of `objectId` itself. */
25636
+ staticTopAtBut(objectId, x, z, y, step) {
25637
+ let top = -Infinity;
25638
+ for (const [id, a] of this.staticNow()) {
25639
+ if (id === objectId || a.length === 0 || x < a[0] || x > a[3] || z < a[2] || z > a[5]) continue;
25640
+ for (let i = 6; i < a.length; i += 6) {
25641
+ if (x < a[i] || x > a[i + 3] || z < a[i + 2] || z > a[i + 5]) continue;
25642
+ const t = a[i + 4];
25643
+ if (t > top && t <= y + step) top = t;
25644
+ }
25645
+ }
25646
+ return top;
25647
+ }
25648
+ /**
25649
+ * 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
25650
+ * what holds it and it goes over until the top meets the floor. Past `CONTACT_MESHES` meshes it keeps the corners of its box.
25651
+ */
25652
+ setContacts(b, body, meshes) {
25653
+ if (meshes.length > CONTACT_MESHES) {
25654
+ body.contacts = void 0;
25655
+ return;
25656
+ }
25657
+ const inv = specFrameInverse(this.THREE, b.root);
25658
+ const m4 = new this.THREE.Matrix4();
25659
+ const lb = new this.THREE.Box3();
25660
+ const [cx, cy, cz] = body.localCenter;
25661
+ const points = [];
25662
+ for (const mv of meshes) {
25663
+ mv.mesh.updateWorldMatrix(true, false);
25664
+ this.visuals.localBox(mv, lb);
25665
+ if (lb.isEmpty()) continue;
25666
+ lb.applyMatrix4(m4.multiplyMatrices(inv, mv.mesh.matrixWorld));
25667
+ 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]);
25668
+ }
25669
+ body.contacts = points.length > 0 ? points : void 0;
25670
+ }
25375
25671
  /**
25376
25672
  * 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
25377
25673
  * high and its feet are thin, so it gets the start of the fall, across the line of its feet.
@@ -25663,6 +25959,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25663
25959
  this.active.clear();
25664
25960
  for (const key2 of [...this.fires.keys()]) this.stopFire(key2);
25665
25961
  this.leaks.length = 0;
25962
+ this.trickles.length = 0;
25666
25963
  for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
25667
25964
  for (const b of [...this.bounds.values()]) {
25668
25965
  if (b.isSplit) {
@@ -25924,6 +26221,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25924
26221
  volume: 0,
25925
26222
  sources: ["sdk: the roar under the crackles of a fire"]
25926
26223
  };
26224
+ var SMOKE_HISS_LOOP = {
26225
+ id: "smoke_grenade_hiss",
26226
+ bus: "world",
26227
+ order: "chain_loop",
26228
+ source: { kind: "noise", loop: true },
26229
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 1400 }, q: 0.7 }, { type: "lowpass", frequency: { set: 7e3 }, q: 0.5 }] },
26230
+ volume: 0,
26231
+ sources: ["sdk: a smoke canister venting (the gas through its holes, steady for its whole burn)"]
26232
+ };
26233
+ var GRENADE_PIN_PULL = blast(
26234
+ "grenade_pin_pull",
26235
+ [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 })],
26236
+ ["sdk: the ring pulled from the fuse, then the spoon flying off"],
26237
+ FIRE_FALLOFF
26238
+ );
25927
26239
  var FIRE_FUEL_OF = {
25928
26240
  wood_soft: "wood",
25929
26241
  wood_hard: "wood",
@@ -26111,7 +26423,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
26111
26423
  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 })]),
26112
26424
  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] }))])
26113
26425
  ];
26114
- 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] };
26426
+ 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] };
26115
26427
  var IMPACT_SOUND_KEYS = {
26116
26428
  impact_metal_ballistic: { sound: "metal_bullet" },
26117
26429
  impact_metal_blunt: { sound: "metal_blunt" },
@@ -48938,6 +49250,8 @@ void main(){
48938
49250
  var SMOKE_PUFF_SPEED = 1.7;
48939
49251
  var SMOKE_PUFF_SIZE = 5;
48940
49252
  var SMOKE_PUFF_ALPHA = 1.5;
49253
+ var SMOKE_HISS = { loop: "smoke_grenade_hiss", volume: 0.7, halfAt: 6, max: 1 };
49254
+ var GRENADE_PIN_SOUND = "grenade_pin_pull";
48941
49255
  var CONE_BLAST_RADIUS = 0.5;
48942
49256
  var summarizeBlast = (results) => {
48943
49257
  const reports = results.flatMap((r) => r.blast ? [r.blast] : []);
@@ -48957,12 +49271,31 @@ void main(){
48957
49271
  const random = context.random ?? Math.random;
48958
49272
  const meshes = [];
48959
49273
  const up = new THREE.Vector3(0, 1, 0);
49274
+ let smokeHiss = null;
49275
+ const hissSmoke = () => {
49276
+ if (!context.audio) return;
49277
+ if (smokeSources.length === 0) {
49278
+ smokeHiss?.stop();
49279
+ smokeHiss = null;
49280
+ return;
49281
+ }
49282
+ const at3 = camera2.position;
49283
+ let level = 0;
49284
+ for (const source of smokeSources) {
49285
+ const distance4 = Math.hypot(source.point[0] - at3.x, source.point[1] - at3.y, source.point[2] - at3.z);
49286
+ level += SMOKE_HISS.volume / (1 + (distance4 / SMOKE_HISS.halfAt) ** 2);
49287
+ }
49288
+ smokeHiss ?? (smokeHiss = context.audio.loop(SMOKE_HISS.loop));
49289
+ smokeHiss.set({ volume: Math.min(SMOKE_HISS.max, level) });
49290
+ };
48960
49291
  const drop = (objects) => {
48961
49292
  for (const object of objects) object.removeFromParent();
48962
49293
  releaseObject3D(objects, { keep: scene2 });
48963
49294
  };
48964
49295
  const clear = () => {
48965
49296
  smokeSources.length = 0;
49297
+ smokeHiss?.stop();
49298
+ smokeHiss = null;
48966
49299
  projectiles.length = 0;
48967
49300
  drop(meshes);
48968
49301
  meshes.length = 0;
@@ -49016,6 +49349,7 @@ void main(){
49016
49349
  age: 0,
49017
49350
  ...use.explosive !== void 0 ? { body: createExplosiveBody(use, arsenal.explosives, arsenal.rules.explosives), aligned: isRocket } : {}
49018
49351
  });
49352
+ if (!isRocket && !isMolotov && use.explosive !== void 0) context.audio?.play(GRENADE_PIN_SOUND, { position: use.origin });
49019
49353
  const key2 = isRocket ? "act.launch" : isMolotov ? "act.throw.molotov" : "act.throw.grenade";
49020
49354
  action(isRocket ? "launch_projectile" : "throw_projectile", translate(key2, { weapon: context.toolName(), fuse: format(use.fuse, 1), arm: format(use.armDistance ?? 0, 0) }), {
49021
49355
  data: { weapon: context.toolId(), fuse: use.fuse, explosive: use.explosive }
@@ -49269,6 +49603,7 @@ void main(){
49269
49603
  }
49270
49604
  if (source.left <= 0) smokeSources.splice(index, 1);
49271
49605
  }
49606
+ hissSmoke();
49272
49607
  };
49273
49608
  const update = (dt) => {
49274
49609
  burnSmoke(dt);
@@ -55687,6 +56022,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
55687
56022
  "act.player_thrown_wounded": "The blast threw the player at {speed} m/s, {distance} m from it: badly wounded, stunned for {stun} s ({reach}% got through the cover)",
55688
56023
  "act.player_stunned": "The blast stunned the player for {stun} s, {distance} m from it: dizzy, ears ringing",
55689
56024
  "act.came_apart": "{name} came apart: what stayed is loose and falls",
56025
+ "check.broken_floating": "ERROR: what was left of {name} stood on nothing after the break; it was let go to fall",
55690
56026
  "act.shoved": "{name} stands free: the shots moved it",
55691
56027
  "act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
55692
56028
  "act.use_item": "{name}: {use}",
@@ -56065,6 +56401,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
56065
56401
  "act.player_thrown_wounded": "A explos\xE3o arremessou o jogador a {speed} m/s, a {distance} m dela: ferido gravemente, atordoado por {stun} s ({reach}% passou pela cobertura)",
56066
56402
  "act.player_stunned": "A explos\xE3o atordoou o jogador por {stun} s, a {distance} m dela: tonto, com os ouvidos zumbindo",
56067
56403
  "act.came_apart": "{name} se desmontou: o que ficou est\xE1 solto e cai",
56404
+ "check.broken_floating": "ERRO: o que sobrou de {name} ficou sem apoio depois de quebrar; foi solto para cair",
56068
56405
  "act.shoved": "{name} estava solto no ch\xE3o: os tiros o moveram",
56069
56406
  "act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
56070
56407
  "act.use_item": "{name}: {use}",
@@ -57795,6 +58132,14 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57795
58132
  rendererPieces.set(objectId, piece);
57796
58133
  action("loosened", t(cause === "blast" ? "act.loosened" : "act.came_apart", { name: e.name }), { objectId, itemId: e.so.itemId });
57797
58134
  },
58135
+ // after a break the collision of the object is measured again from what is still drawn; what stood on nothing was let go, which is a bug the room says aloud
58136
+ onBrokenChecked: (objectId, released) => {
58137
+ bodyBoxes.delete(objectId);
58138
+ if (standingIds.includes(objectId)) leaveFootprint(objectId);
58139
+ if (!released) return;
58140
+ const e = entries2.get(objectId);
58141
+ log.add({ channel: "check", type: "fail", text: t("check.broken_floating", { name: e?.name ?? objectId }), objectId, ...e ? { itemId: e.so.itemId } : {}, data: { invariant: "broken_floating" } });
58142
+ },
57798
58143
  onPiece: (piece) => {
57799
58144
  rendererPieces.set(piece.objectId, piece);
57800
58145
  loose.push(piece);
@@ -58458,9 +58803,14 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58458
58803
  bodyBoxes.delete(id);
58459
58804
  return null;
58460
58805
  }
58461
- bodyBounds.setFromObject(e.so.root);
58462
- if (bodyBounds.isEmpty()) return null;
58463
- const box2 = { minX: bodyBounds.min.x, maxX: bodyBounds.max.x, minZ: bodyBounds.min.z, maxZ: bodyBounds.max.z, base: bodyBounds.min.y, top: bodyBounds.max.y, stamp: frameStamp };
58806
+ const parts = renderer.shownBoxes(id);
58807
+ if (parts.length === 0) {
58808
+ bodyBoxes.delete(id);
58809
+ return null;
58810
+ }
58811
+ bodyBounds.makeEmpty();
58812
+ for (const p of parts) bodyBounds.expandByPoint(tmpV2.set(p[0], p[1], p[2])).expandByPoint(tmpV2.set(p[3], p[4], p[5]));
58813
+ const box2 = { minX: bodyBounds.min.x, maxX: bodyBounds.max.x, minZ: bodyBounds.min.z, maxZ: bodyBounds.max.z, base: bodyBounds.min.y, top: bodyBounds.max.y, stamp: frameStamp, parts };
58464
58814
  bodyBoxes.set(id, box2);
58465
58815
  return box2;
58466
58816
  };
@@ -58477,7 +58827,15 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58477
58827
  const reach = c.radius + r;
58478
58828
  return (x - footPos.x) ** 2 + (z - footPos.z) ** 2 < reach * reach ? { ...b, top: Math.min(b.top, footPos.y + (c.height ?? b.top - footPos.y)) } : null;
58479
58829
  }
58480
- return x > b.minX - r && x < b.maxX + r && z > b.minZ - r && z < b.maxZ + r ? b : null;
58830
+ if (!(x > b.minX - r && x < b.maxX + r && z > b.minZ - r && z < b.maxZ + r)) return null;
58831
+ let base = Infinity;
58832
+ let top = -Infinity;
58833
+ for (const p of b.parts) {
58834
+ if (!(x > p[0] - r && x < p[3] + r && z > p[2] - r && z < p[5] + r)) continue;
58835
+ base = Math.min(base, p[1]);
58836
+ top = Math.max(top, p[4]);
58837
+ }
58838
+ return top > base ? { ...b, base, top } : null;
58481
58839
  };
58482
58840
  blockers.add((x, z, r) => {
58483
58841
  for (const e of entries2.values()) {
@@ -59535,7 +59893,8 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
59535
59893
  emitHost,
59536
59894
  startFire,
59537
59895
  say,
59538
- projectileModel: options.projectileModel
59896
+ projectileModel: options.projectileModel,
59897
+ audio
59539
59898
  });
59540
59899
  const toolOf = (hand) => hand === "L" ? offTool && offToolId ? { id: offToolId, state: offTool } : null : { id: toolId, state: tool };
59541
59900
  const dualReload = createDualReload(