@adatechnology/game-test-room 0.28.2 → 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"],
@@ -11226,7 +11290,8 @@ var GameTestRoom = (() => {
11226
11290
  var EVENT_BUDGET = { particles: 400, debris: 40, blastLights: 4, fireLights: 2 };
11227
11291
  var explosion_aliases_default = {
11228
11292
  carl_gustaf_84mm: "at4_84mm",
11229
- vog_25: "m79_40mm"
11293
+ vog_25: "m79_40mm",
11294
+ semtex: "c4"
11230
11295
  };
11231
11296
  var explosions_default = {
11232
11297
  m67: {
@@ -17203,8 +17268,8 @@ var GameTestRoom = (() => {
17203
17268
  id: "dust",
17204
17269
  additive: false,
17205
17270
  life: [
17206
- 1,
17207
- 2
17271
+ 2,
17272
+ 5
17208
17273
  ],
17209
17274
  size: [
17210
17275
  0.08,
@@ -17271,8 +17336,8 @@ var GameTestRoom = (() => {
17271
17336
  id: "chips",
17272
17337
  additive: false,
17273
17338
  life: [
17274
- 0.8,
17275
- 1.5
17339
+ 2,
17340
+ 4
17276
17341
  ],
17277
17342
  size: [
17278
17343
  0.025,
@@ -19914,7 +19979,7 @@ var GameTestRoom = (() => {
19914
19979
  return sources;
19915
19980
  }
19916
19981
 
19917
- // ../game-vfx-three/dist/chunk-CPRCWTSQ.js
19982
+ // ../game-vfx-three/dist/chunk-IOGYK5RO.js
19918
19983
  var GLOW_VERTEX = `attribute vec4 aP;attribute vec4 aC;varying vec2 vU;varying vec4 vC;
19919
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]);
19920
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.);}`;
@@ -19961,10 +20026,15 @@ void main(){
19961
20026
  if(n<diss*1.08-.04)discard;
19962
20027
  float lam=max(dot(N,uLight),0.);
19963
20028
  float band=lam>.55?1.:lam>.15?.68:.42;
19964
- 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);
19965
20035
  vec3 tg=uGlowPos-vWP;float dg=length(tg)+.001;
19966
20036
  float gi=uGlowI/(1.+dg*dg*.05)*(.3+.7*max(dot(N,tg/dg),0.));
19967
- col+=uGlowCol*gi*vC*1.6;
20037
+ col+=uGlowCol*gi*alb*1.6;
19968
20038
  float h=heat*(.72+.56*n)+heat*(-N.y)*.18;
19969
20039
  if(h>.05){
19970
20040
  float hq=floor(h*5.+.5)/5.;
@@ -22142,7 +22212,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22142
22212
  var SLAM_BOUNCE = 0.2;
22143
22213
  var SLAM_FLOOR_NORMAL = 0.7;
22144
22214
  var SUB_DT = 1 / 120;
22145
- var MU = 0.7;
22215
+ var MU_STATIC = 0.7;
22216
+ var MU_DYNAMIC = 0.6;
22217
+ var SLIP_SPEED = 0.02;
22146
22218
  var SLEEP_AFTER = 0.5;
22147
22219
  var CALM_DRIFT = 0.01;
22148
22220
  var CALM_TURN = 0.05;
@@ -22161,6 +22233,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22161
22233
  var EDGE_POINTS = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
22162
22234
  var SUPPORT_CHECKS2 = 16;
22163
22235
  var SUPPORT_GAP2 = 0.02;
22236
+ var WAKE_ABOVE_SPEED = 0.05;
22237
+ var WAKE_ABOVE_SPIN = 0.1;
22238
+ var topFace = 1;
22164
22239
  var boxTopAt = (s, x, z) => {
22165
22240
  const e = s.e;
22166
22241
  const dx = x - s.c[0], dz = z - s.c[2];
@@ -22176,11 +22251,24 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22176
22251
  let a = (-h - p) / d, b = (h - p) / d;
22177
22252
  if (a > b) [a, b] = [b, a];
22178
22253
  t0 = Math.max(t0, a);
22179
- t1 = Math.min(t1, b);
22254
+ if (b < t1) {
22255
+ t1 = b;
22256
+ topFace = k;
22257
+ }
22180
22258
  if (t0 > t1) return -Infinity;
22181
22259
  }
22182
22260
  return s.c[1] + t1;
22183
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
+ });
22184
22272
  var PieceSystem = class {
22185
22273
  constructor(THREE, o) {
22186
22274
  __publicField3(this, "THREE", THREE);
@@ -22194,6 +22282,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22194
22282
  __publicField3(this, "ax");
22195
22283
  __publicField3(this, "solids", []);
22196
22284
  __publicField3(this, "supportTurn", 0);
22285
+ __publicField3(this, "supportNormal", UP16);
22286
+ __publicField3(this, "supportFriction", null);
22197
22287
  this.p = new THREE.Vector3();
22198
22288
  this.q = new THREE.Quaternion();
22199
22289
  this.dq = new THREE.Quaternion();
@@ -22299,8 +22389,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22299
22389
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
22300
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];
22301
22391
  }
22302
- /** 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`. */
22303
22393
  samples(b, edges) {
22394
+ if (b.contacts) return b.contacts;
22304
22395
  const [hx, hy, hz2] = b.halfExtents;
22305
22396
  const out = [];
22306
22397
  for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz2 : -hz2]);
@@ -22365,6 +22456,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22365
22456
  }
22366
22457
  if (b.sleeping) continue;
22367
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);
22368
22463
  }
22369
22464
  this.separate(dt);
22370
22465
  }
@@ -22395,15 +22490,48 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22395
22490
  }
22396
22491
  return false;
22397
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
+ }
22398
22517
  /** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
22399
22518
  supportAt(self, x, y, z, step) {
22400
22519
  let top = this.o.groundAt(x, z);
22520
+ let on = null;
22521
+ let face = 1;
22401
22522
  for (const s of this.solids) {
22402
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;
22403
22524
  const t = boxTopAt(s, x, z);
22404
- 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
+ }
22405
22530
  }
22406
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;
22407
22535
  return Math.max(top, fixed2);
22408
22536
  }
22409
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. */
@@ -22486,7 +22614,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22486
22614
  const e = this.m.elements;
22487
22615
  const rs = [];
22488
22616
  const ids = [];
22617
+ const ns = [];
22618
+ const mus = [];
22489
22619
  let maxPen = 0;
22620
+ let penNormal = UP16;
22490
22621
  const rise = STEP + Math.max(0, -v[1]) * h * 2;
22491
22622
  let side = null;
22492
22623
  let lowest = Infinity;
@@ -22504,6 +22635,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22504
22635
  if (floor2 !== void 0 && floor2 - cy > -SLOP && floor2 < raw - STEP) {
22505
22636
  rs.push(r);
22506
22637
  ids.push(i);
22638
+ ns.push(UP16);
22639
+ mus.push(pairFriction(b.friction, null));
22507
22640
  maxPen = Math.max(maxPen, floor2 - cy);
22508
22641
  }
22509
22642
  continue;
@@ -22524,12 +22657,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22524
22657
  }
22525
22658
  continue;
22526
22659
  }
22527
- 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);
22528
22663
  const pen = gy - cy;
22529
22664
  if (pen > -SLOP) {
22530
22665
  rs.push(r);
22531
22666
  ids.push(i);
22532
- 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
+ }
22533
22673
  lowest = Math.min(lowest, gy);
22534
22674
  }
22535
22675
  }
@@ -22542,7 +22682,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22542
22682
  contactedLast = rs.length > 0;
22543
22683
  if (contactedLast) touched = true;
22544
22684
  if (!contactedLast) continue;
22545
- 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;
22546
22689
  let impact22 = 0;
22547
22690
  for (const r of rs) impact22 = Math.max(impact22, -(v[1] + w[2] * r[0] - w[0] * r[2]));
22548
22691
  const restitution = impact22 > 2 ? 0.25 : 0;
@@ -22558,76 +22701,72 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22558
22701
  const lz = pos[2] + sz / rs.length;
22559
22702
  landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
22560
22703
  }
22561
- const goal = rs.map((r) => {
22562
- const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
22563
- 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;
22564
22714
  });
22565
22715
  const vx0 = v[0];
22566
22716
  const vz0 = v[2];
22567
- 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);
22568
22718
  const warm = b.warm;
22569
- const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 3] : 0);
22570
- const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 3 + 1], warm[id * 3 + 2]] : [0, 0]);
22571
- for (let k = 0; k < rs.length; k++) {
22572
- const r = rs[k];
22573
- const jn = accN[k];
22574
- const [fx, fz] = accT[k];
22575
- if (jn === 0 && fx === 0 && fz === 0) continue;
22576
- v[0] += fx * invM;
22577
- v[1] += jn * invM;
22578
- v[2] += fz * invM;
22579
- 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]);
22580
22726
  w[0] += dw[0];
22581
22727
  w[1] += dw[1];
22582
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]]);
22583
22738
  }
22584
22739
  for (let it = 0; it < SOLVER_PASSES; it++) {
22585
22740
  for (let k = 0; k < rs.length; k++) {
22586
22741
  const r = rs[k];
22587
- const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
22588
- const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
22589
- const denom = invM + (a[2] * r[0] - a[0] * r[2]);
22590
- 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));
22591
22745
  const j = total - accN[k];
22592
22746
  accN[k] = total;
22593
- if (j !== 0) {
22594
- v[1] += j * invM;
22595
- w[0] += a[0] * j;
22596
- w[1] += a[1] * j;
22597
- w[2] += a[2] * j;
22598
- }
22599
- const vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
22600
- const vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
22601
- const vt = Math.hypot(vx2, vz2);
22602
- if (vt < 1e-6 && accT[k][0] === 0 && accT[k][1] === 0) continue;
22603
- let fx = accT[k][0];
22604
- let fz = accT[k][1];
22605
- if (vt > 1e-6) {
22606
- const tx = vx2 / vt;
22607
- const tz = vz2 / vt;
22608
- const at3 = this.mulInvInertiaWith(b, e, r[1] * tz, r[2] * tx - r[0] * tz, -r[1] * tx);
22609
- const cx = at3[1] * r[2] - at3[2] * r[1];
22610
- const cz = at3[0] * r[1] - at3[1] * r[0];
22611
- const jt = vt / (invM + cx * tx + cz * tz);
22612
- fx -= tx * jt;
22613
- fz -= tz * jt;
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;
22614
22766
  }
22615
- const most = MU * accN[k];
22616
- const f = Math.hypot(fx, fz);
22617
- if (f > most) {
22618
- fx *= most / f;
22619
- fz *= most / f;
22620
- }
22621
- const dx = fx - accT[k][0];
22622
- const dz = fz - accT[k][1];
22623
- accT[k] = [fx, fz];
22624
- if (dx === 0 && dz === 0) continue;
22625
- v[0] += dx * invM;
22626
- v[2] += dz * invM;
22627
- const dw = this.mulInvInertiaWith(b, e, r[1] * dz, r[2] * dx - r[0] * dz, -r[1] * dx);
22628
- w[0] += dw[0];
22629
- w[1] += dw[1];
22630
- 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);
22631
22770
  }
22632
22771
  }
22633
22772
  pos[0] += (v[0] - vx0) * h;
@@ -22636,9 +22775,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22636
22775
  for (let k = 0; k < rs.length; k++) {
22637
22776
  const id = ids[k];
22638
22777
  if (id >= WARM_SLOTS) continue;
22639
- warm[id * 3] = accN[k];
22640
- warm[id * 3 + 1] = accT[k][0];
22641
- 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];
22642
22782
  }
22643
22783
  let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
22644
22784
  for (const r of rs) {
@@ -22710,6 +22850,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22710
22850
  b.calm += dt;
22711
22851
  return b.calm >= CALM_SLEEP_AFTER;
22712
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
+ }
22713
22860
  /** Half the height of the box of a piece as it is turned now. */
22714
22861
  halfHeight(b) {
22715
22862
  const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
@@ -22735,6 +22882,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
22735
22882
  this.centerOf(b, cb);
22736
22883
  if (Math.abs(ca.y - cb.y) > ra + rb) continue;
22737
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;
22738
22886
  const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
22739
22887
  let dx = cb.x - ca.x;
22740
22888
  let dz = cb.z - ca.z;
@@ -24125,6 +24273,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24125
24273
  var IMPACT_TAGS = ["metal", "glass", "wood"];
24126
24274
  var GLOW_FROM_C = 430;
24127
24275
  var QUEUE_CAP = 4e3;
24276
+ var TRICKLE_SECONDS = [2, 5];
24277
+ var TRICKLE_EVERY = 0.25;
24278
+ var TRICKLE_MAX = 24;
24128
24279
  var STRUCTURAL_EVENTS = /* @__PURE__ */ new Set([
24129
24280
  "fracture",
24130
24281
  "detach",
@@ -24148,6 +24299,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24148
24299
  var FEET_BAND = 0.03;
24149
24300
  var TIP_MIN_HEIGHT = 0.3;
24150
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"]);
24151
24306
  var FIRE_CEILING_SECONDS = 3600;
24152
24307
  var GENERIC_REFERENCE_RADIUS = 15;
24153
24308
  var SWEEP_CORE = 0.15;
@@ -24243,9 +24398,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24243
24398
  __publicField3(this, "staticAll", true);
24244
24399
  __publicField3(this, "staticRound", []);
24245
24400
  __publicField3(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
24401
+ __publicField3(this, "broken", /* @__PURE__ */ new Set());
24246
24402
  __publicField3(this, "staticAt", 0);
24247
24403
  __publicField3(this, "queue", []);
24248
24404
  __publicField3(this, "coverWhenWentOff", /* @__PURE__ */ new WeakMap());
24405
+ __publicField3(this, "trickles", []);
24249
24406
  __publicField3(this, "slices", []);
24250
24407
  __publicField3(this, "sliceDeadline", Infinity);
24251
24408
  __publicField3(this, "drainClock", () => performance.now());
@@ -24580,10 +24737,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24580
24737
  dt = clamp6(dt, 0, 0.1);
24581
24738
  this.time += dt;
24582
24739
  if (!this.attached) this.drainQueue();
24740
+ this.checkBroken();
24583
24741
  for (const b of this.bounds.values()) b.onFrame?.(dt);
24584
24742
  for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
24585
24743
  this.updateActive(dt);
24586
24744
  this.updateLeaks(dt);
24745
+ this.updateTrickles(dt);
24587
24746
  this.pieceSys.update(dt);
24588
24747
  this.fragments.update(dt);
24589
24748
  this.ground.update(dt);
@@ -24782,6 +24941,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
24782
24941
  }
24783
24942
  apply(ev) {
24784
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);
24785
24945
  switch (ev.type) {
24786
24946
  case "damage":
24787
24947
  if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
@@ -25106,9 +25266,34 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25106
25266
  const core = def?.visual?.innerColor;
25107
25267
  if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
25108
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);
25109
25270
  if (mv.mesh.userData.drawnSpalls?.includes(ev.part)) return;
25110
25271
  this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
25111
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
+ }
25112
25297
  /**
25113
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
25114
25299
  * (cutting the wall's faces) is not drawn yet: it needs the per-vertex binding of the house renderer.
@@ -25144,6 +25329,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25144
25329
  const tone3 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
25145
25330
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone3, false), ev.object, maxAlive);
25146
25331
  }
25332
+ if (def?.visual?.innerColor !== void 0 || pv?.material === "concrete" || pv?.material === "brick") this.trickleFrom(ev.center, [0, 0, 0], color);
25147
25333
  if (ev.torn) {
25148
25334
  this.impactSound(pv?.material ?? "", ev.center, 0.5, ev.blow);
25149
25335
  return;
@@ -25333,26 +25519,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25333
25519
  this.o.onPiece?.(piece);
25334
25520
  }
25335
25521
  /**
25336
- * 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
25337
- * 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`).
25338
25525
  */
25339
25526
  loosenIfFootless(objectId) {
25340
25527
  const b = this.bounds.get(objectId);
25341
25528
  if (!b || !this.autoSplit) return;
25342
25529
  const left = this.meshesOf(b).filter((mv) => this.visuals.shown(mv));
25343
25530
  if (left.length === 0) return;
25531
+ const structural = UNCHECKED_KINDS.has(this.kindOf(b));
25344
25532
  const body = b.piece ?? this.pieceSys.get(objectId);
25345
25533
  if (body) {
25346
25534
  const { half, center } = this.measure(b.root, left);
25347
25535
  body.halfExtents = half;
25348
25536
  this.pieceSys.setCenter(body, center);
25349
25537
  body.mass = Math.max(0.05, this.massOf(objectId));
25538
+ if (!structural) this.setContacts(b, body, left);
25350
25539
  body.sleeping = false;
25351
25540
  return;
25352
25541
  }
25353
- if (kitOriginOf(b.root)?.kind !== "furniture") return;
25542
+ if (structural) return;
25354
25543
  const boxOf = (mv) => this.visuals.worldBox(mv, new this.THREE.Box3());
25355
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;
25356
25546
  const floor2 = Math.min(...whole.map((one) => one.min.y));
25357
25547
  const feetOf = (boxes) => boxes.filter((one) => one.min.y < floor2 + FEET_BAND).reduce((feet, one) => feet.union(one), new this.THREE.Box3());
25358
25548
  const was = feetOf(whole);
@@ -25368,8 +25558,115 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25368
25558
  if (!narrow && centreX > now.min.x && centreX < now.max.x && centreZ > now.min.z && centreZ < now.max.z) return;
25369
25559
  }
25370
25560
  const piece = this.makePiece(objectId);
25561
+ this.setContacts(b, b.piece ?? piece, left);
25562
+ this.tipIfTopHeavy(b);
25563
+ this.o.onLoosened?.(objectId, piece, "apart");
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)));
25371
25608
  this.tipIfTopHeavy(b);
25372
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;
25373
25670
  }
25374
25671
  /**
25375
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
@@ -25662,6 +25959,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25662
25959
  this.active.clear();
25663
25960
  for (const key2 of [...this.fires.keys()]) this.stopFire(key2);
25664
25961
  this.leaks.length = 0;
25962
+ this.trickles.length = 0;
25665
25963
  for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
25666
25964
  for (const b of [...this.bounds.values()]) {
25667
25965
  if (b.isSplit) {
@@ -25923,6 +26221,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
25923
26221
  volume: 0,
25924
26222
  sources: ["sdk: the roar under the crackles of a fire"]
25925
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
+ );
25926
26239
  var FIRE_FUEL_OF = {
25927
26240
  wood_soft: "wood",
25928
26241
  wood_hard: "wood",
@@ -26110,7 +26423,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
26110
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 })]),
26111
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] }))])
26112
26425
  ];
26113
- 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] };
26114
26427
  var IMPACT_SOUND_KEYS = {
26115
26428
  impact_metal_ballistic: { sound: "metal_bullet" },
26116
26429
  impact_metal_blunt: { sound: "metal_blunt" },
@@ -26417,6 +26730,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
26417
26730
  kick: "kick",
26418
26731
  shove: "shove",
26419
26732
  knife: "blade",
26733
+ kabar: "blade",
26734
+ katana: "blade",
26735
+ brass_knuckles: "punch",
26420
26736
  machete: "blade",
26421
26737
  utility_knife: "blade",
26422
26738
  axe: "chop",
@@ -27468,6 +27784,60 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
27468
27784
  sources: [
27469
27785
  "Estimated from muzzle energy, bullet construction and the published anchors of this file (see _notes); refine with a datasheet."
27470
27786
  ]
27787
+ },
27788
+ {
27789
+ id: "7_62x25:fmj",
27790
+ caliber: "7_62x25",
27791
+ kind: "fmj",
27792
+ label: "7.62x25 Tokarev FMJ",
27793
+ core: "lead",
27794
+ jacket: "steel",
27795
+ penetration: {
27796
+ steel: 3.5,
27797
+ concrete: 30,
27798
+ brick: 60,
27799
+ wood: 100
27800
+ },
27801
+ basis: "estimated",
27802
+ sources: [
27803
+ "Estimated from muzzle energy, bullet construction and the published anchors of this file (see _notes); refine with a datasheet."
27804
+ ]
27805
+ },
27806
+ {
27807
+ id: "7_92x57:fmj",
27808
+ caliber: "7_92x57",
27809
+ kind: "fmj",
27810
+ label: "7.92x57 s.S. ball",
27811
+ core: "lead",
27812
+ jacket: "gilding_metal",
27813
+ penetration: {
27814
+ steel: 5.5,
27815
+ concrete: 125,
27816
+ brick: 250,
27817
+ wood: 330
27818
+ },
27819
+ basis: "estimated",
27820
+ sources: [
27821
+ "Estimated from muzzle energy, bullet construction and the published anchors of this file (see _notes); refine with a datasheet."
27822
+ ]
27823
+ },
27824
+ {
27825
+ id: "9x39:fmj",
27826
+ caliber: "9x39",
27827
+ kind: "fmj",
27828
+ label: "9x39 SP-5",
27829
+ core: "lead",
27830
+ jacket: "gilding_metal",
27831
+ penetration: {
27832
+ steel: 3,
27833
+ concrete: 30,
27834
+ brick: 70,
27835
+ wood: 130
27836
+ },
27837
+ basis: "estimated",
27838
+ sources: [
27839
+ "Estimated from muzzle energy, bullet construction and the published anchors of this file (see _notes); refine with a datasheet."
27840
+ ]
27471
27841
  }
27472
27842
  ]
27473
27843
  };
@@ -30140,6 +30510,72 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
30140
30510
  sources: [
30141
30511
  "sdk"
30142
30512
  ]
30513
+ },
30514
+ {
30515
+ id: "7_62x25",
30516
+ label: "7.62\xD725 mm Tokarev",
30517
+ diameter: 7.85,
30518
+ bulletGrams: 5.5,
30519
+ muzzleSpeed: 488,
30520
+ dragPer100m: 0.12,
30521
+ kind: "fmj",
30522
+ noise: 300,
30523
+ caseType: "pistol",
30524
+ rim: "rimless",
30525
+ cartridge: {
30526
+ caseDiameter: 9.95,
30527
+ overallLength: 35.2,
30528
+ caseLength: 25,
30529
+ projectileDiameter: 7.85
30530
+ },
30531
+ sources: [
30532
+ "reference",
30533
+ "derived"
30534
+ ]
30535
+ },
30536
+ {
30537
+ id: "7_92x57",
30538
+ label: "7.92\xD757 mm Mauser",
30539
+ diameter: 8.22,
30540
+ bulletGrams: 11.7,
30541
+ muzzleSpeed: 760,
30542
+ dragPer100m: 0.08,
30543
+ kind: "fmj",
30544
+ noise: 420,
30545
+ caseType: "rifle",
30546
+ rim: "rimless",
30547
+ cartridge: {
30548
+ caseDiameter: 11.95,
30549
+ overallLength: 82,
30550
+ caseLength: 57,
30551
+ projectileDiameter: 8.22
30552
+ },
30553
+ sources: [
30554
+ "reference",
30555
+ "derived"
30556
+ ]
30557
+ },
30558
+ {
30559
+ id: "9x39",
30560
+ label: "9\xD739 mm SP-5",
30561
+ diameter: 9.27,
30562
+ bulletGrams: 16.1,
30563
+ muzzleSpeed: 290,
30564
+ dragPer100m: 0.2,
30565
+ kind: "fmj",
30566
+ noise: 300,
30567
+ caseType: "rifle",
30568
+ rim: "rimless",
30569
+ cartridge: {
30570
+ caseDiameter: 11.33,
30571
+ overallLength: 56,
30572
+ caseLength: 38.7,
30573
+ projectileDiameter: 9.27
30574
+ },
30575
+ sources: [
30576
+ "reference",
30577
+ "derived"
30578
+ ]
30143
30579
  }
30144
30580
  ];
30145
30581
  var explosives_default = [
@@ -30594,6 +31030,59 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
30594
31030
  "lab",
30595
31031
  "derived"
30596
31032
  ]
31033
+ },
31034
+ {
31035
+ id: "semtex",
31036
+ name: "Semtex charge",
31037
+ kind: "blast",
31038
+ lethalRadius: 6,
31039
+ injuryRadius: 18,
31040
+ energy: 377400,
31041
+ tnt: 0.83,
31042
+ destructionLevel: 3,
31043
+ noise: 700,
31044
+ fuseLength: 0.23,
31045
+ burnRate: 131,
31046
+ burnTolerance: 0.1,
31047
+ triggers: [
31048
+ "remote",
31049
+ "timer"
31050
+ ],
31051
+ mass: 0.5,
31052
+ sources: [
31053
+ "lab",
31054
+ "reference",
31055
+ "derived"
31056
+ ]
31057
+ },
31058
+ {
31059
+ id: "smoke_grenade",
31060
+ name: "M18 colored smoke grenade",
31061
+ kind: "smoke",
31062
+ lethalRadius: 0,
31063
+ injuryRadius: 0,
31064
+ energy: 0,
31065
+ destructionLevel: 0,
31066
+ noise: 30,
31067
+ fuseRange: [
31068
+ 1,
31069
+ 2.3
31070
+ ],
31071
+ smoke: {
31072
+ color: 4177738,
31073
+ seconds: [
31074
+ 50,
31075
+ 90
31076
+ ]
31077
+ },
31078
+ triggers: [
31079
+ "timer"
31080
+ ],
31081
+ mass: 0.54,
31082
+ sources: [
31083
+ "reference",
31084
+ "derived"
31085
+ ]
30597
31086
  }
30598
31087
  ];
30599
31088
  var explosive_weapons_default = [
@@ -30695,6 +31184,54 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
30695
31184
  "lab",
30696
31185
  "reference"
30697
31186
  ]
31187
+ },
31188
+ {
31189
+ id: "semtex",
31190
+ name: "Semtex charge",
31191
+ category: "throwable",
31192
+ action: {
31193
+ kind: "throwable",
31194
+ explosive: "semtex",
31195
+ energy: 0,
31196
+ radius: 0,
31197
+ fuse: 0,
31198
+ throwSpeed: 0,
31199
+ noise: 0
31200
+ },
31201
+ mass: 0.5,
31202
+ tags: [
31203
+ "explosive",
31204
+ "placed"
31205
+ ],
31206
+ archetype: "frag_grenade",
31207
+ sources: [
31208
+ "reference",
31209
+ "derived"
31210
+ ]
31211
+ },
31212
+ {
31213
+ id: "smoke_grenade",
31214
+ name: "M18 colored smoke grenade",
31215
+ category: "throwable",
31216
+ action: {
31217
+ kind: "throwable",
31218
+ explosive: "smoke_grenade",
31219
+ energy: 0,
31220
+ radius: 0,
31221
+ fuse: 0,
31222
+ throwSpeed: 15,
31223
+ noise: 0
31224
+ },
31225
+ mass: 0.54,
31226
+ tags: [
31227
+ "explosive",
31228
+ "smoke"
31229
+ ],
31230
+ archetype: "frag_grenade",
31231
+ sources: [
31232
+ "reference",
31233
+ "derived"
31234
+ ]
30698
31235
  }
30699
31236
  ];
30700
31237
  var firearms_default = [
@@ -38216,6 +38753,1477 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
38216
38753
  }
38217
38754
  ],
38218
38755
  fitted: []
38756
+ },
38757
+ {
38758
+ id: "aks_74u",
38759
+ name: "AKS-74U",
38760
+ category: "rifle",
38761
+ action: {
38762
+ kind: "firearm",
38763
+ caliber: "5_45x39",
38764
+ rpm: 700,
38765
+ modes: [
38766
+ "auto",
38767
+ "semi"
38768
+ ],
38769
+ magazine: 30,
38770
+ reload: 2.6,
38771
+ muzzleSpeed: 735,
38772
+ spreadDeg: 0.6,
38773
+ bloomDeg: 0.25,
38774
+ recoveryDegPerS: 5,
38775
+ recoil: [
38776
+ 0.66,
38777
+ 0.4
38778
+ ],
38779
+ range: 400,
38780
+ family: "magazine",
38781
+ timings: {
38782
+ draw: 0.35,
38783
+ reload: 2.6,
38784
+ reloadEmpty: 3.4,
38785
+ clear: 1.2
38786
+ }
38787
+ },
38788
+ mass: 2.7,
38789
+ dimensions: {
38790
+ length: 0.73,
38791
+ barrel: 0.206,
38792
+ emptyMass: 2.7,
38793
+ lengthCollapsed: 0.49
38794
+ },
38795
+ archetype: "rifle_5_56",
38796
+ variantOf: "ak_74",
38797
+ sources: [
38798
+ "reference",
38799
+ "derived",
38800
+ "sdk"
38801
+ ],
38802
+ mounts: [
38803
+ {
38804
+ kind: "muzzle",
38805
+ interfaces: [
38806
+ "fixed_muzzle_device"
38807
+ ],
38808
+ sources: [
38809
+ "derived"
38810
+ ]
38811
+ },
38812
+ {
38813
+ kind: "magazine",
38814
+ interfaces: [
38815
+ "mag_curved"
38816
+ ],
38817
+ sources: [
38818
+ "gallery"
38819
+ ]
38820
+ },
38821
+ {
38822
+ kind: "sling",
38823
+ interfaces: [
38824
+ "swivel"
38825
+ ],
38826
+ sources: [
38827
+ "derived"
38828
+ ]
38829
+ }
38830
+ ],
38831
+ fitted: [
38832
+ {
38833
+ id: "muzzle_brake",
38834
+ mount: "muzzle"
38835
+ }
38836
+ ],
38837
+ stock: {
38838
+ kind: "fold",
38839
+ foldAxis: "left"
38840
+ }
38841
+ },
38842
+ {
38843
+ id: "fn_scar_l",
38844
+ name: "FN SCAR-L",
38845
+ maker: "FN",
38846
+ category: "dmr",
38847
+ action: {
38848
+ kind: "firearm",
38849
+ caliber: "5_56x45",
38850
+ rpm: 625,
38851
+ modes: [
38852
+ "auto",
38853
+ "semi"
38854
+ ],
38855
+ magazine: 30,
38856
+ reload: 2.6,
38857
+ muzzleSpeed: 870,
38858
+ spreadDeg: 0.25,
38859
+ bloomDeg: 0.3,
38860
+ recoveryDegPerS: 3,
38861
+ recoil: [
38862
+ 1.43,
38863
+ 0.24
38864
+ ],
38865
+ range: 800,
38866
+ family: "magazine",
38867
+ timings: {
38868
+ draw: 0.35,
38869
+ reload: 2.6,
38870
+ clear: 1.2
38871
+ }
38872
+ },
38873
+ mass: 3.29,
38874
+ dimensions: {
38875
+ length: 0.889,
38876
+ barrel: 0.351,
38877
+ emptyMass: 3.29,
38878
+ lengthCollapsed: 0.635
38879
+ },
38880
+ archetype: "rifle_bolt_308",
38881
+ variantOf: "fn_scar_h",
38882
+ sources: [
38883
+ "reference",
38884
+ "derived",
38885
+ "sdk"
38886
+ ],
38887
+ mounts: [
38888
+ {
38889
+ kind: "rail_top",
38890
+ interfaces: [
38891
+ "picatinny"
38892
+ ],
38893
+ sources: [
38894
+ "gallery"
38895
+ ]
38896
+ },
38897
+ {
38898
+ kind: "rail_left",
38899
+ interfaces: [
38900
+ "picatinny"
38901
+ ],
38902
+ sources: [
38903
+ "gallery"
38904
+ ]
38905
+ },
38906
+ {
38907
+ kind: "rail_right",
38908
+ interfaces: [
38909
+ "picatinny"
38910
+ ],
38911
+ sources: [
38912
+ "gallery"
38913
+ ]
38914
+ },
38915
+ {
38916
+ kind: "rail_bottom",
38917
+ interfaces: [
38918
+ "picatinny"
38919
+ ],
38920
+ sources: [
38921
+ "gallery"
38922
+ ]
38923
+ },
38924
+ {
38925
+ kind: "magazine",
38926
+ interfaces: [
38927
+ "mag_box"
38928
+ ],
38929
+ sources: [
38930
+ "gallery"
38931
+ ]
38932
+ },
38933
+ {
38934
+ kind: "sling",
38935
+ interfaces: [
38936
+ "swivel"
38937
+ ],
38938
+ sources: [
38939
+ "derived"
38940
+ ]
38941
+ }
38942
+ ],
38943
+ fitted: [
38944
+ {
38945
+ id: "acog_4x",
38946
+ mount: "rail_top"
38947
+ }
38948
+ ],
38949
+ stock: {
38950
+ kind: "fold",
38951
+ foldAxis: "right"
38952
+ }
38953
+ },
38954
+ {
38955
+ id: "m1_garand",
38956
+ name: "M1 Garand",
38957
+ category: "rifle",
38958
+ action: {
38959
+ kind: "firearm",
38960
+ caliber: "30_06",
38961
+ rpm: 120,
38962
+ modes: [
38963
+ "semi"
38964
+ ],
38965
+ magazine: 8,
38966
+ reload: 2.2,
38967
+ muzzleSpeed: 865,
38968
+ spreadDeg: 0.8,
38969
+ bloomDeg: 0.35,
38970
+ recoveryDegPerS: 4.5,
38971
+ recoil: [
38972
+ 2.28,
38973
+ 1.22
38974
+ ],
38975
+ range: 350,
38976
+ family: "top_load_semi",
38977
+ timings: {
38978
+ draw: 0.35,
38979
+ reload: 2.2,
38980
+ clear: 1.2
38981
+ }
38982
+ },
38983
+ mass: 4.31,
38984
+ dimensions: {
38985
+ length: 1.1,
38986
+ barrel: 0.61,
38987
+ emptyMass: 4.31
38988
+ },
38989
+ archetype: "rifle_7_62x39",
38990
+ variantOf: "sks",
38991
+ sources: [
38992
+ "reference",
38993
+ "derived",
38994
+ "sdk"
38995
+ ],
38996
+ mounts: [
38997
+ {
38998
+ kind: "sling",
38999
+ interfaces: [
39000
+ "swivel"
39001
+ ],
39002
+ sources: [
39003
+ "derived"
39004
+ ]
39005
+ }
39006
+ ],
39007
+ fitted: []
39008
+ },
39009
+ {
39010
+ id: "accuracy_l96a1",
39011
+ name: "Accuracy L96A1",
39012
+ maker: "Accuracy",
39013
+ category: "sniper",
39014
+ action: {
39015
+ kind: "firearm",
39016
+ caliber: "7_62x51",
39017
+ rpm: 73.17,
39018
+ modes: [
39019
+ "bolt"
39020
+ ],
39021
+ magazine: 10,
39022
+ reload: 3.2,
39023
+ muzzleSpeed: 840,
39024
+ spreadDeg: 0.25,
39025
+ bloomDeg: 0.3,
39026
+ recoveryDegPerS: 3,
39027
+ recoil: [
39028
+ 1.64,
39029
+ 0.27
39030
+ ],
39031
+ range: 800,
39032
+ family: "bolt_magazine",
39033
+ timings: {
39034
+ draw: 0.35,
39035
+ reload: 3.2,
39036
+ cycle: 0.82,
39037
+ clear: 1.2
39038
+ }
39039
+ },
39040
+ mass: 6.5,
39041
+ dimensions: {
39042
+ length: 1.18,
39043
+ barrel: 0.66,
39044
+ emptyMass: 6.5
39045
+ },
39046
+ archetype: "rifle_bolt_308",
39047
+ variantOf: "accuracy_awm",
39048
+ sources: [
39049
+ "reference",
39050
+ "derived",
39051
+ "sdk"
39052
+ ],
39053
+ mounts: [
39054
+ {
39055
+ kind: "muzzle",
39056
+ interfaces: [
39057
+ "fixed_muzzle_device"
39058
+ ],
39059
+ sources: [
39060
+ "derived"
39061
+ ]
39062
+ },
39063
+ {
39064
+ kind: "rail_top",
39065
+ interfaces: [
39066
+ "dovetail"
39067
+ ],
39068
+ sources: [
39069
+ "derived"
39070
+ ]
39071
+ },
39072
+ {
39073
+ kind: "rail_bottom",
39074
+ interfaces: [
39075
+ "bipod_stud"
39076
+ ],
39077
+ sources: [
39078
+ "gallery"
39079
+ ]
39080
+ },
39081
+ {
39082
+ kind: "magazine",
39083
+ interfaces: [
39084
+ "mag_box"
39085
+ ],
39086
+ sources: [
39087
+ "gallery"
39088
+ ]
39089
+ },
39090
+ {
39091
+ kind: "sling",
39092
+ interfaces: [
39093
+ "swivel"
39094
+ ],
39095
+ sources: [
39096
+ "derived"
39097
+ ]
39098
+ }
39099
+ ],
39100
+ fitted: [
39101
+ {
39102
+ id: "muzzle_brake",
39103
+ mount: "muzzle"
39104
+ },
39105
+ {
39106
+ id: "scope_3_9x",
39107
+ mount: "rail_top"
39108
+ },
39109
+ {
39110
+ id: "bipod",
39111
+ mount: "rail_bottom"
39112
+ }
39113
+ ]
39114
+ },
39115
+ {
39116
+ id: "mossberg_590",
39117
+ name: "Mossberg 590",
39118
+ maker: "Mossberg",
39119
+ category: "shotgun",
39120
+ action: {
39121
+ kind: "firearm",
39122
+ caliber: "12ga_00buck",
39123
+ rpm: 120,
39124
+ modes: [
39125
+ "pump"
39126
+ ],
39127
+ magazine: 8,
39128
+ chamber: 1,
39129
+ reload: 0.55,
39130
+ perRound: true,
39131
+ muzzleSpeed: 400,
39132
+ spreadDeg: 1,
39133
+ bloomDeg: 0.5,
39134
+ recoveryDegPerS: 3,
39135
+ pelletSpreadDeg: 3,
39136
+ recoil: [
39137
+ 3.62,
39138
+ 0.91
39139
+ ],
39140
+ range: 40,
39141
+ family: "pump",
39142
+ timings: {
39143
+ draw: 0.35,
39144
+ reload: 0.55,
39145
+ perRound: 0.55,
39146
+ cycle: 0.5,
39147
+ clear: 1.2
39148
+ }
39149
+ },
39150
+ mass: 3.3,
39151
+ dimensions: {
39152
+ length: 1.003,
39153
+ barrel: 0.508,
39154
+ emptyMass: 3.3
39155
+ },
39156
+ archetype: "shotgun_pump_12",
39157
+ variantOf: "mossberg_500",
39158
+ sources: [
39159
+ "reference",
39160
+ "derived",
39161
+ "sdk"
39162
+ ],
39163
+ mounts: [
39164
+ {
39165
+ kind: "barrel",
39166
+ interfaces: [
39167
+ "barrel_swap"
39168
+ ],
39169
+ sources: [
39170
+ "derived"
39171
+ ]
39172
+ },
39173
+ {
39174
+ kind: "sling",
39175
+ interfaces: [
39176
+ "swivel"
39177
+ ],
39178
+ sources: [
39179
+ "derived"
39180
+ ]
39181
+ }
39182
+ ],
39183
+ fitted: []
39184
+ },
39185
+ {
39186
+ id: "fn_m240b",
39187
+ name: "FN M240B",
39188
+ maker: "FN",
39189
+ category: "machine_gun",
39190
+ action: {
39191
+ kind: "firearm",
39192
+ caliber: "7_62x51",
39193
+ rpm: 750,
39194
+ modes: [
39195
+ "auto"
39196
+ ],
39197
+ magazine: 100,
39198
+ reload: 8.5,
39199
+ muzzleSpeed: 853,
39200
+ spreadDeg: 0.8,
39201
+ bloomDeg: 0.35,
39202
+ recoveryDegPerS: 4.5,
39203
+ recoil: [
39204
+ 0.77,
39205
+ 0.41
39206
+ ],
39207
+ range: 350,
39208
+ family: "belt",
39209
+ timings: {
39210
+ draw: 0.35,
39211
+ reload: 8.5,
39212
+ clear: 1.2
39213
+ }
39214
+ },
39215
+ mass: 12.5,
39216
+ dimensions: {
39217
+ length: 1.245,
39218
+ barrel: 0.63,
39219
+ emptyMass: 12.5
39220
+ },
39221
+ archetype: "rifle_7_62x39",
39222
+ sources: [
39223
+ "reference",
39224
+ "derived",
39225
+ "sdk"
39226
+ ],
39227
+ mounts: [
39228
+ {
39229
+ kind: "rail_top",
39230
+ interfaces: [
39231
+ "picatinny"
39232
+ ],
39233
+ sources: [
39234
+ "gallery"
39235
+ ]
39236
+ },
39237
+ {
39238
+ kind: "rail_bottom",
39239
+ interfaces: [
39240
+ "bipod_stud"
39241
+ ],
39242
+ sources: [
39243
+ "gallery"
39244
+ ]
39245
+ },
39246
+ {
39247
+ kind: "sling",
39248
+ interfaces: [
39249
+ "swivel"
39250
+ ],
39251
+ sources: [
39252
+ "derived"
39253
+ ]
39254
+ },
39255
+ {
39256
+ kind: "feed",
39257
+ interfaces: [
39258
+ "belt"
39259
+ ],
39260
+ sources: [
39261
+ "derived"
39262
+ ]
39263
+ }
39264
+ ],
39265
+ fitted: [
39266
+ {
39267
+ id: "bipod",
39268
+ mount: "rail_bottom"
39269
+ }
39270
+ ]
39271
+ },
39272
+ {
39273
+ id: "sig_sauer_p320",
39274
+ name: "SIG Sauer P320",
39275
+ maker: "SIG Sauer",
39276
+ category: "pistol",
39277
+ action: {
39278
+ kind: "firearm",
39279
+ caliber: "9x19",
39280
+ rpm: 300,
39281
+ modes: [
39282
+ "semi"
39283
+ ],
39284
+ magazine: 17,
39285
+ reload: 1.9,
39286
+ muzzleSpeed: 350,
39287
+ spreadDeg: 1,
39288
+ bloomDeg: 0.6,
39289
+ recoveryDegPerS: 4,
39290
+ recoil: [
39291
+ 1.2,
39292
+ 0.4
39293
+ ],
39294
+ range: 50,
39295
+ family: "pistol",
39296
+ timings: {
39297
+ draw: 0.35,
39298
+ reload: 1.9,
39299
+ clear: 1.2
39300
+ },
39301
+ faultFactor: 0.8
39302
+ },
39303
+ mass: 0.83,
39304
+ dimensions: {
39305
+ length: 0.203,
39306
+ barrel: 0.12,
39307
+ emptyMass: 0.83
39308
+ },
39309
+ archetype: "pistol_9mm",
39310
+ variantOf: "sig_sauer_p226",
39311
+ sources: [
39312
+ "reference",
39313
+ "derived",
39314
+ "sdk"
39315
+ ],
39316
+ mounts: [
39317
+ {
39318
+ kind: "magazine",
39319
+ interfaces: [
39320
+ "mag_pistol"
39321
+ ],
39322
+ sources: [
39323
+ "derived"
39324
+ ]
39325
+ }
39326
+ ],
39327
+ fitted: []
39328
+ },
39329
+ {
39330
+ id: "hk_usp",
39331
+ name: "HK USP",
39332
+ maker: "HK",
39333
+ category: "pistol",
39334
+ action: {
39335
+ kind: "firearm",
39336
+ caliber: "9x19",
39337
+ rpm: 300,
39338
+ modes: [
39339
+ "semi"
39340
+ ],
39341
+ magazine: 15,
39342
+ reload: 1.9,
39343
+ muzzleSpeed: 350,
39344
+ spreadDeg: 1,
39345
+ bloomDeg: 0.6,
39346
+ recoveryDegPerS: 4,
39347
+ recoil: [
39348
+ 1.29,
39349
+ 0.43
39350
+ ],
39351
+ range: 50,
39352
+ family: "pistol",
39353
+ timings: {
39354
+ draw: 0.35,
39355
+ reload: 1.9,
39356
+ clear: 1.2
39357
+ },
39358
+ faultFactor: 0.8
39359
+ },
39360
+ mass: 0.77,
39361
+ dimensions: {
39362
+ length: 0.194,
39363
+ barrel: 0.108,
39364
+ emptyMass: 0.77
39365
+ },
39366
+ archetype: "pistol_9mm",
39367
+ variantOf: "sig_sauer_p226",
39368
+ sources: [
39369
+ "reference",
39370
+ "derived",
39371
+ "sdk"
39372
+ ],
39373
+ mounts: [
39374
+ {
39375
+ kind: "magazine",
39376
+ interfaces: [
39377
+ "mag_pistol"
39378
+ ],
39379
+ sources: [
39380
+ "derived"
39381
+ ]
39382
+ }
39383
+ ],
39384
+ fitted: []
39385
+ },
39386
+ {
39387
+ id: "walther_p99",
39388
+ name: "Walther P99",
39389
+ maker: "Walther",
39390
+ category: "pistol",
39391
+ action: {
39392
+ kind: "firearm",
39393
+ caliber: "9x19",
39394
+ rpm: 300,
39395
+ modes: [
39396
+ "semi"
39397
+ ],
39398
+ magazine: 15,
39399
+ reload: 1.8,
39400
+ muzzleSpeed: 375,
39401
+ spreadDeg: 1,
39402
+ bloomDeg: 0.6,
39403
+ recoveryDegPerS: 4,
39404
+ recoil: [
39405
+ 1.81,
39406
+ 0.6
39407
+ ],
39408
+ range: 50,
39409
+ family: "pistol",
39410
+ timings: {
39411
+ draw: 0.35,
39412
+ reload: 1.8,
39413
+ clear: 1.2
39414
+ },
39415
+ faultFactor: 0.7
39416
+ },
39417
+ mass: 0.63,
39418
+ dimensions: {
39419
+ length: 0.18,
39420
+ barrel: 0.102,
39421
+ emptyMass: 0.63
39422
+ },
39423
+ archetype: "pistol_9mm",
39424
+ variantOf: "glock_17",
39425
+ sources: [
39426
+ "reference",
39427
+ "derived",
39428
+ "sdk"
39429
+ ],
39430
+ mounts: [
39431
+ {
39432
+ kind: "magazine",
39433
+ interfaces: [
39434
+ "mag_pistol"
39435
+ ],
39436
+ sources: [
39437
+ "derived"
39438
+ ]
39439
+ }
39440
+ ],
39441
+ fitted: []
39442
+ },
39443
+ {
39444
+ id: "colt_python",
39445
+ name: "Colt Python",
39446
+ maker: "Colt",
39447
+ category: "revolver",
39448
+ action: {
39449
+ kind: "firearm",
39450
+ caliber: "357mag",
39451
+ rpm: 150,
39452
+ modes: [
39453
+ "single"
39454
+ ],
39455
+ magazine: 6,
39456
+ reload: 2.4,
39457
+ muzzleSpeed: 430,
39458
+ spreadDeg: 0.8,
39459
+ bloomDeg: 1,
39460
+ recoveryDegPerS: 3,
39461
+ recoil: [
39462
+ 3.13,
39463
+ 0.94
39464
+ ],
39465
+ range: 60,
39466
+ family: "revolver",
39467
+ timings: {
39468
+ draw: 0.35,
39469
+ reload: 2.4,
39470
+ perRound: 1,
39471
+ clear: 1.2
39472
+ }
39473
+ },
39474
+ mass: 1.23,
39475
+ dimensions: {
39476
+ length: 0.292,
39477
+ barrel: 0.152,
39478
+ emptyMass: 1.23
39479
+ },
39480
+ archetype: "revolver_357",
39481
+ variantOf: "sw_686",
39482
+ sources: [
39483
+ "reference",
39484
+ "derived",
39485
+ "sdk"
39486
+ ],
39487
+ mounts: [],
39488
+ fitted: []
39489
+ },
39490
+ {
39491
+ id: "fn_ps90",
39492
+ name: "FN PS90",
39493
+ maker: "FN",
39494
+ category: "smg",
39495
+ action: {
39496
+ kind: "firearm",
39497
+ caliber: "5_7x28",
39498
+ rpm: 120,
39499
+ modes: [
39500
+ "semi"
39501
+ ],
39502
+ magazine: 50,
39503
+ reload: 3,
39504
+ muzzleSpeed: 715,
39505
+ spreadDeg: 1.5,
39506
+ bloomDeg: 0.35,
39507
+ recoveryDegPerS: 6,
39508
+ recoil: [
39509
+ 0.69,
39510
+ 0.38
39511
+ ],
39512
+ range: 80,
39513
+ family: "top_magazine",
39514
+ timings: {
39515
+ draw: 0.35,
39516
+ reload: 3,
39517
+ clear: 1.2
39518
+ }
39519
+ },
39520
+ mass: 2.86,
39521
+ dimensions: {
39522
+ length: 0.668,
39523
+ barrel: 0.407,
39524
+ emptyMass: 2.86
39525
+ },
39526
+ archetype: "smg_9mm",
39527
+ variantOf: "fn_p90",
39528
+ sources: [
39529
+ "reference",
39530
+ "derived",
39531
+ "sdk"
39532
+ ],
39533
+ mounts: [
39534
+ {
39535
+ kind: "sling",
39536
+ interfaces: [
39537
+ "swivel"
39538
+ ],
39539
+ sources: [
39540
+ "derived"
39541
+ ]
39542
+ }
39543
+ ],
39544
+ fitted: []
39545
+ },
39546
+ {
39547
+ id: "bt_mp9",
39548
+ name: "B&T MP9",
39549
+ maker: "B&T",
39550
+ category: "smg",
39551
+ action: {
39552
+ kind: "firearm",
39553
+ caliber: "9x19",
39554
+ rpm: 1100,
39555
+ modes: [
39556
+ "auto",
39557
+ "semi"
39558
+ ],
39559
+ magazine: 30,
39560
+ reload: 2.4,
39561
+ muzzleSpeed: 400,
39562
+ spreadDeg: 1.5,
39563
+ bloomDeg: 0.35,
39564
+ recoveryDegPerS: 6,
39565
+ recoil: [
39566
+ 1.66,
39567
+ 0.92
39568
+ ],
39569
+ range: 80,
39570
+ family: "magazine",
39571
+ timings: {
39572
+ draw: 0.35,
39573
+ reload: 2.4,
39574
+ clear: 1.2
39575
+ }
39576
+ },
39577
+ mass: 1.4,
39578
+ dimensions: {
39579
+ length: 0.523,
39580
+ barrel: 0.13,
39581
+ emptyMass: 1.4,
39582
+ lengthCollapsed: 0.303
39583
+ },
39584
+ archetype: "smg_9mm",
39585
+ variantOf: "uzi",
39586
+ sources: [
39587
+ "reference",
39588
+ "derived",
39589
+ "sdk"
39590
+ ],
39591
+ mounts: [
39592
+ {
39593
+ kind: "rail_top",
39594
+ interfaces: [
39595
+ "picatinny"
39596
+ ],
39597
+ sources: [
39598
+ "gallery"
39599
+ ]
39600
+ },
39601
+ {
39602
+ kind: "magazine",
39603
+ interfaces: [
39604
+ "mag_box"
39605
+ ],
39606
+ sources: [
39607
+ "gallery"
39608
+ ]
39609
+ },
39610
+ {
39611
+ kind: "sling",
39612
+ interfaces: [
39613
+ "swivel"
39614
+ ],
39615
+ sources: [
39616
+ "derived"
39617
+ ]
39618
+ }
39619
+ ],
39620
+ fitted: [],
39621
+ stock: {
39622
+ kind: "fold",
39623
+ foldAxis: "right"
39624
+ }
39625
+ },
39626
+ {
39627
+ id: "mp40",
39628
+ name: "MP40",
39629
+ category: "smg",
39630
+ action: {
39631
+ kind: "firearm",
39632
+ caliber: "9x19",
39633
+ rpm: 550,
39634
+ modes: [
39635
+ "auto",
39636
+ "semi"
39637
+ ],
39638
+ magazine: 32,
39639
+ reload: 2.9,
39640
+ spreadDeg: 1.5,
39641
+ bloomDeg: 0.35,
39642
+ recoveryDegPerS: 6,
39643
+ recoil: [
39644
+ 0.48,
39645
+ 0.27
39646
+ ],
39647
+ range: 80,
39648
+ family: "magazine",
39649
+ timings: {
39650
+ draw: 0.35,
39651
+ reload: 2.9,
39652
+ clear: 1.2
39653
+ }
39654
+ },
39655
+ mass: 4,
39656
+ dimensions: {
39657
+ length: 0.833,
39658
+ barrel: 0.251,
39659
+ emptyMass: 4,
39660
+ lengthCollapsed: 0.63
39661
+ },
39662
+ archetype: "smg_9mm",
39663
+ variantOf: "thompson_m1a1",
39664
+ sources: [
39665
+ "reference",
39666
+ "derived",
39667
+ "sdk"
39668
+ ],
39669
+ mounts: [
39670
+ {
39671
+ kind: "magazine",
39672
+ interfaces: [
39673
+ "mag_box"
39674
+ ],
39675
+ sources: [
39676
+ "gallery"
39677
+ ]
39678
+ },
39679
+ {
39680
+ kind: "sling",
39681
+ interfaces: [
39682
+ "swivel"
39683
+ ],
39684
+ sources: [
39685
+ "derived"
39686
+ ]
39687
+ }
39688
+ ],
39689
+ fitted: [],
39690
+ stock: {
39691
+ kind: "fold",
39692
+ foldAxis: "under"
39693
+ }
39694
+ },
39695
+ {
39696
+ id: "famas",
39697
+ name: "FAMAS",
39698
+ category: "rifle",
39699
+ action: {
39700
+ kind: "firearm",
39701
+ caliber: "5_56x45",
39702
+ rpm: 1e3,
39703
+ modes: [
39704
+ "auto",
39705
+ "semi"
39706
+ ],
39707
+ magazine: 25,
39708
+ reload: 2.8,
39709
+ muzzleSpeed: 960,
39710
+ spreadDeg: 0.6,
39711
+ bloomDeg: 0.25,
39712
+ recoveryDegPerS: 5,
39713
+ recoil: [
39714
+ 0.99,
39715
+ 0.6
39716
+ ],
39717
+ range: 400,
39718
+ family: "magazine",
39719
+ timings: {
39720
+ draw: 0.35,
39721
+ reload: 2.8,
39722
+ reloadEmpty: 3.4,
39723
+ clear: 1.2
39724
+ }
39725
+ },
39726
+ mass: 3.61,
39727
+ dimensions: {
39728
+ length: 0.757,
39729
+ barrel: 0.488,
39730
+ emptyMass: 3.61
39731
+ },
39732
+ archetype: "rifle_5_56",
39733
+ variantOf: "steyr_aug_a1",
39734
+ sources: [
39735
+ "reference",
39736
+ "derived",
39737
+ "sdk"
39738
+ ],
39739
+ mounts: [
39740
+ {
39741
+ kind: "magazine",
39742
+ interfaces: [
39743
+ "mag_box"
39744
+ ],
39745
+ sources: [
39746
+ "derived"
39747
+ ]
39748
+ },
39749
+ {
39750
+ kind: "sling",
39751
+ interfaces: [
39752
+ "swivel"
39753
+ ],
39754
+ sources: [
39755
+ "derived"
39756
+ ]
39757
+ }
39758
+ ],
39759
+ fitted: []
39760
+ },
39761
+ {
39762
+ id: "iwi_tavor",
39763
+ name: "IWI Tavor",
39764
+ maker: "IWI",
39765
+ category: "rifle",
39766
+ action: {
39767
+ kind: "firearm",
39768
+ caliber: "5_56x45",
39769
+ rpm: 850,
39770
+ modes: [
39771
+ "auto",
39772
+ "semi"
39773
+ ],
39774
+ magazine: 30,
39775
+ reload: 2.8,
39776
+ muzzleSpeed: 910,
39777
+ spreadDeg: 0.6,
39778
+ bloomDeg: 0.25,
39779
+ recoveryDegPerS: 5,
39780
+ recoil: [
39781
+ 0.98,
39782
+ 0.59
39783
+ ],
39784
+ range: 400,
39785
+ family: "magazine",
39786
+ timings: {
39787
+ draw: 0.35,
39788
+ reload: 2.8,
39789
+ reloadEmpty: 3.4,
39790
+ clear: 1.2
39791
+ }
39792
+ },
39793
+ mass: 3.27,
39794
+ dimensions: {
39795
+ length: 0.72,
39796
+ barrel: 0.46,
39797
+ emptyMass: 3.27
39798
+ },
39799
+ archetype: "rifle_5_56",
39800
+ variantOf: "steyr_aug_a1",
39801
+ sources: [
39802
+ "reference",
39803
+ "derived",
39804
+ "sdk"
39805
+ ],
39806
+ mounts: [
39807
+ {
39808
+ kind: "rail_top",
39809
+ interfaces: [
39810
+ "picatinny"
39811
+ ],
39812
+ sources: [
39813
+ "gallery"
39814
+ ]
39815
+ },
39816
+ {
39817
+ kind: "magazine",
39818
+ interfaces: [
39819
+ "mag_box"
39820
+ ],
39821
+ sources: [
39822
+ "derived"
39823
+ ]
39824
+ },
39825
+ {
39826
+ kind: "sling",
39827
+ interfaces: [
39828
+ "swivel"
39829
+ ],
39830
+ sources: [
39831
+ "derived"
39832
+ ]
39833
+ }
39834
+ ],
39835
+ fitted: []
39836
+ },
39837
+ {
39838
+ id: "l85a2",
39839
+ name: "L85A2",
39840
+ category: "rifle",
39841
+ action: {
39842
+ kind: "firearm",
39843
+ caliber: "5_56x45",
39844
+ rpm: 700,
39845
+ modes: [
39846
+ "auto",
39847
+ "semi"
39848
+ ],
39849
+ magazine: 30,
39850
+ reload: 2.8,
39851
+ muzzleSpeed: 940,
39852
+ spreadDeg: 0.6,
39853
+ bloomDeg: 0.25,
39854
+ recoveryDegPerS: 5,
39855
+ recoil: [
39856
+ 0.9,
39857
+ 0.54
39858
+ ],
39859
+ range: 400,
39860
+ family: "magazine",
39861
+ timings: {
39862
+ draw: 0.35,
39863
+ reload: 2.8,
39864
+ reloadEmpty: 3.4,
39865
+ clear: 1.2
39866
+ }
39867
+ },
39868
+ mass: 3.82,
39869
+ dimensions: {
39870
+ length: 0.785,
39871
+ barrel: 0.518,
39872
+ emptyMass: 3.82
39873
+ },
39874
+ archetype: "rifle_5_56",
39875
+ variantOf: "steyr_aug_a1",
39876
+ sources: [
39877
+ "reference",
39878
+ "derived",
39879
+ "sdk"
39880
+ ],
39881
+ mounts: [
39882
+ {
39883
+ kind: "rail_top",
39884
+ interfaces: [
39885
+ "picatinny"
39886
+ ],
39887
+ sources: [
39888
+ "gallery"
39889
+ ]
39890
+ },
39891
+ {
39892
+ kind: "magazine",
39893
+ interfaces: [
39894
+ "mag_box"
39895
+ ],
39896
+ sources: [
39897
+ "derived"
39898
+ ]
39899
+ },
39900
+ {
39901
+ kind: "sling",
39902
+ interfaces: [
39903
+ "swivel"
39904
+ ],
39905
+ sources: [
39906
+ "derived"
39907
+ ]
39908
+ }
39909
+ ],
39910
+ fitted: []
39911
+ },
39912
+ {
39913
+ id: "winchester_1887",
39914
+ name: "Winchester 1887",
39915
+ maker: "Winchester",
39916
+ category: "hunting",
39917
+ action: {
39918
+ kind: "firearm",
39919
+ caliber: "12ga_00buck",
39920
+ rpm: 109.09,
39921
+ modes: [
39922
+ "lever"
39923
+ ],
39924
+ magazine: 5,
39925
+ reload: 0.6,
39926
+ perRound: true,
39927
+ spreadDeg: 0.25,
39928
+ bloomDeg: 0.3,
39929
+ recoveryDegPerS: 3,
39930
+ recoil: [
39931
+ 2.33,
39932
+ 0.39
39933
+ ],
39934
+ range: 800,
39935
+ family: "lever",
39936
+ timings: {
39937
+ draw: 0.35,
39938
+ reload: 0.6,
39939
+ perRound: 0.6,
39940
+ cycle: 0.55,
39941
+ clear: 1.2
39942
+ }
39943
+ },
39944
+ mass: 3.6,
39945
+ dimensions: {
39946
+ length: 1.016,
39947
+ barrel: 0.508,
39948
+ emptyMass: 3.6
39949
+ },
39950
+ archetype: "rifle_bolt_308",
39951
+ variantOf: "winchester_1894",
39952
+ sources: [
39953
+ "reference",
39954
+ "derived",
39955
+ "sdk"
39956
+ ],
39957
+ mounts: [
39958
+ {
39959
+ kind: "sling",
39960
+ interfaces: [
39961
+ "swivel"
39962
+ ],
39963
+ sources: [
39964
+ "derived"
39965
+ ]
39966
+ }
39967
+ ],
39968
+ fitted: []
39969
+ },
39970
+ {
39971
+ id: "ppsh_41",
39972
+ name: "PPSh-41",
39973
+ category: "smg",
39974
+ action: {
39975
+ kind: "firearm",
39976
+ caliber: "7_62x25",
39977
+ rpm: 1250,
39978
+ modes: [
39979
+ "auto",
39980
+ "semi"
39981
+ ],
39982
+ magazine: 71,
39983
+ magazineOptions: [
39984
+ 35
39985
+ ],
39986
+ reload: 2.9,
39987
+ muzzleSpeed: 488,
39988
+ spreadDeg: 1.5,
39989
+ bloomDeg: 0.35,
39990
+ recoveryDegPerS: 6,
39991
+ recoil: [
39992
+ 0.7,
39993
+ 0.39
39994
+ ],
39995
+ range: 80,
39996
+ family: "magazine",
39997
+ timings: {
39998
+ draw: 0.35,
39999
+ reload: 2.9,
40000
+ clear: 1.2
40001
+ }
40002
+ },
40003
+ mass: 3.63,
40004
+ dimensions: {
40005
+ length: 0.843,
40006
+ barrel: 0.269,
40007
+ emptyMass: 3.63
40008
+ },
40009
+ archetype: "smg_9mm",
40010
+ variantOf: "thompson_m1a1",
40011
+ sources: [
40012
+ "reference",
40013
+ "derived",
40014
+ "sdk"
40015
+ ],
40016
+ mounts: [
40017
+ {
40018
+ kind: "magazine",
40019
+ interfaces: [
40020
+ "mag_drum"
40021
+ ],
40022
+ sources: [
40023
+ "gallery"
40024
+ ]
40025
+ },
40026
+ {
40027
+ kind: "sling",
40028
+ interfaces: [
40029
+ "swivel"
40030
+ ],
40031
+ sources: [
40032
+ "derived"
40033
+ ]
40034
+ }
40035
+ ],
40036
+ fitted: []
40037
+ },
40038
+ {
40039
+ id: "mauser_kar98k",
40040
+ name: "Mauser Kar98k",
40041
+ maker: "Mauser",
40042
+ category: "sniper",
40043
+ action: {
40044
+ kind: "firearm",
40045
+ caliber: "7_92x57",
40046
+ rpm: 73.17,
40047
+ modes: [
40048
+ "bolt"
40049
+ ],
40050
+ magazine: 5,
40051
+ reload: 2.4,
40052
+ muzzleSpeed: 760,
40053
+ spreadDeg: 0.25,
40054
+ bloomDeg: 0.3,
40055
+ recoveryDegPerS: 3,
40056
+ recoil: [
40057
+ 2.84,
40058
+ 0.47
40059
+ ],
40060
+ range: 800,
40061
+ family: "bolt",
40062
+ timings: {
40063
+ draw: 0.35,
40064
+ reload: 2.4,
40065
+ cycle: 0.82,
40066
+ clear: 1.2
40067
+ }
40068
+ },
40069
+ mass: 3.7,
40070
+ dimensions: {
40071
+ length: 1.11,
40072
+ barrel: 0.6,
40073
+ emptyMass: 3.7
40074
+ },
40075
+ archetype: "rifle_bolt_308",
40076
+ variantOf: "mosin_nagant_91_30",
40077
+ sources: [
40078
+ "reference",
40079
+ "derived",
40080
+ "sdk"
40081
+ ],
40082
+ mounts: [
40083
+ {
40084
+ kind: "sling",
40085
+ interfaces: [
40086
+ "swivel"
40087
+ ],
40088
+ sources: [
40089
+ "derived"
40090
+ ]
40091
+ }
40092
+ ],
40093
+ fitted: []
40094
+ },
40095
+ {
40096
+ id: "mg42",
40097
+ name: "MG42",
40098
+ category: "machine_gun",
40099
+ action: {
40100
+ kind: "firearm",
40101
+ caliber: "7_92x57",
40102
+ rpm: 1200,
40103
+ modes: [
40104
+ "auto"
40105
+ ],
40106
+ magazine: 50,
40107
+ reload: 8.5,
40108
+ muzzleSpeed: 740,
40109
+ spreadDeg: 0.8,
40110
+ bloomDeg: 0.35,
40111
+ recoveryDegPerS: 4.5,
40112
+ recoil: [
40113
+ 0.75,
40114
+ 0.4
40115
+ ],
40116
+ range: 350,
40117
+ family: "belt",
40118
+ timings: {
40119
+ draw: 0.35,
40120
+ reload: 8.5,
40121
+ clear: 1.2
40122
+ },
40123
+ faultFactor: 1.3
40124
+ },
40125
+ mass: 11.6,
40126
+ dimensions: {
40127
+ length: 1.22,
40128
+ barrel: 0.53,
40129
+ emptyMass: 11.6
40130
+ },
40131
+ archetype: "rifle_7_62x39",
40132
+ variantOf: "m60",
40133
+ sources: [
40134
+ "reference",
40135
+ "derived",
40136
+ "sdk"
40137
+ ],
40138
+ mounts: [
40139
+ {
40140
+ kind: "sling",
40141
+ interfaces: [
40142
+ "swivel"
40143
+ ],
40144
+ sources: [
40145
+ "derived"
40146
+ ]
40147
+ }
40148
+ ],
40149
+ fitted: []
40150
+ },
40151
+ {
40152
+ id: "vss_vintorez",
40153
+ name: "VSS Vintorez",
40154
+ category: "dmr",
40155
+ action: {
40156
+ kind: "firearm",
40157
+ caliber: "9x39",
40158
+ rpm: 850,
40159
+ modes: [
40160
+ "auto",
40161
+ "semi"
40162
+ ],
40163
+ magazine: 10,
40164
+ magazineOptions: [
40165
+ 20,
40166
+ 30
40167
+ ],
40168
+ reload: 2.6,
40169
+ muzzleSpeed: 295,
40170
+ noise: 75,
40171
+ spreadDeg: 0.8,
40172
+ bloomDeg: 0.35,
40173
+ recoveryDegPerS: 4.5,
40174
+ recoil: [
40175
+ 0.73,
40176
+ 0.39
40177
+ ],
40178
+ range: 350,
40179
+ family: "magazine",
40180
+ timings: {
40181
+ draw: 0.35,
40182
+ reload: 2.6,
40183
+ reloadEmpty: 3.4,
40184
+ clear: 1.2
40185
+ }
40186
+ },
40187
+ mass: 2.6,
40188
+ dimensions: {
40189
+ length: 0.894,
40190
+ barrel: 0.2,
40191
+ emptyMass: 2.6
40192
+ },
40193
+ archetype: "rifle_7_62x39",
40194
+ variantOf: "svd_dragunov",
40195
+ sources: [
40196
+ "reference",
40197
+ "derived",
40198
+ "sdk"
40199
+ ],
40200
+ mounts: [
40201
+ {
40202
+ kind: "muzzle",
40203
+ interfaces: [
40204
+ "integral"
40205
+ ],
40206
+ sources: [
40207
+ "reference"
40208
+ ]
40209
+ },
40210
+ {
40211
+ kind: "sling",
40212
+ interfaces: [
40213
+ "swivel"
40214
+ ],
40215
+ sources: [
40216
+ "derived"
40217
+ ]
40218
+ }
40219
+ ],
40220
+ fitted: [
40221
+ {
40222
+ id: "integral_suppressor",
40223
+ mount: "muzzle",
40224
+ integral: true
40225
+ }
40226
+ ]
38219
40227
  }
38220
40228
  ];
38221
40229
  var launchers_default = [
@@ -38552,6 +40560,46 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
38552
40560
  "reference",
38553
40561
  "derived"
38554
40562
  ]
40563
+ },
40564
+ {
40565
+ id: "m32_mgl",
40566
+ name: "M32 multiple grenade launcher (Milkor MGL)",
40567
+ category: "launcher",
40568
+ action: {
40569
+ kind: "launcher",
40570
+ projectile: "m79_40mm",
40571
+ magazine: 6,
40572
+ reload: 9.2,
40573
+ timings: {
40574
+ draw: 0.35,
40575
+ reload: 9.2,
40576
+ clear: 1.2
40577
+ },
40578
+ family: "revolver",
40579
+ speed: 75,
40580
+ minArmDistance: 10,
40581
+ backblast: 0,
40582
+ recoil: [
40583
+ 2.25,
40584
+ 0.6
40585
+ ]
40586
+ },
40587
+ mass: 6.68,
40588
+ dimensions: {
40589
+ length: 0.778,
40590
+ lengthCollapsed: 0.565,
40591
+ barrel: 0.3,
40592
+ emptyMass: 5.3
40593
+ },
40594
+ archetype: "shotgun_double_12",
40595
+ stock: {
40596
+ kind: "fold",
40597
+ foldAxis: "over"
40598
+ },
40599
+ sources: [
40600
+ "reference",
40601
+ "derived"
40602
+ ]
38555
40603
  }
38556
40604
  ];
38557
40605
  var INSTALL_RANK = { field: 0, tool: 1, bench: 2 };
@@ -38787,6 +40835,69 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
38787
40835
  tags: [
38788
40836
  "silent"
38789
40837
  ]
40838
+ },
40839
+ {
40840
+ id: "kabar",
40841
+ name: "KA-BAR combat knife",
40842
+ category: "melee_blade",
40843
+ action: {
40844
+ kind: "melee",
40845
+ damage: "slash",
40846
+ energy: 48,
40847
+ radius: 0.03,
40848
+ reach: 0.55,
40849
+ rpm: 100,
40850
+ noise: 1
40851
+ },
40852
+ mass: 0.32,
40853
+ sources: [
40854
+ "reference",
40855
+ "derived"
40856
+ ]
40857
+ },
40858
+ {
40859
+ id: "katana",
40860
+ name: "Katana",
40861
+ category: "melee_blade",
40862
+ action: {
40863
+ kind: "melee",
40864
+ damage: "slash",
40865
+ energy: 230,
40866
+ radius: 0.04,
40867
+ reach: 1.1,
40868
+ rpm: 70,
40869
+ noise: 5
40870
+ },
40871
+ mass: 1.1,
40872
+ tags: [
40873
+ "two_handed"
40874
+ ],
40875
+ sources: [
40876
+ "reference",
40877
+ "derived"
40878
+ ]
40879
+ },
40880
+ {
40881
+ id: "brass_knuckles",
40882
+ name: "Brass knuckles",
40883
+ category: "melee_blunt",
40884
+ action: {
40885
+ kind: "melee",
40886
+ damage: "blunt",
40887
+ energy: 32,
40888
+ radius: 0.04,
40889
+ reach: 0.6,
40890
+ rpm: 120,
40891
+ noise: 5
40892
+ },
40893
+ mass: 0.15,
40894
+ tags: [
40895
+ "fists"
40896
+ ],
40897
+ sources: [
40898
+ "reference",
40899
+ "derived"
40900
+ ]
38790
40901
  }
38791
40902
  ];
38792
40903
  var findDuplicateIds = (kind, entries2) => {
@@ -38987,7 +41098,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
38987
41098
  improvised_spear: "Improvised spear",
38988
41099
  chainsaw: "Chainsaw",
38989
41100
  recurve_bow: "Recurve bow",
38990
- m134: "M134 Minigun"
41101
+ m134: "M134 Minigun",
41102
+ semtex: "Semtex charge",
41103
+ smoke_grenade: "M18 smoke grenade",
41104
+ kabar: "KA-BAR combat knife",
41105
+ katana: "Katana",
41106
+ brass_knuckles: "Brass knuckles"
38991
41107
  },
38992
41108
  modes: modesEn,
38993
41109
  categories: categoriesEn,
@@ -39000,6 +41116,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39000
41116
  c4: "C4 charge",
39001
41117
  claymore: "Claymore mine",
39002
41118
  flashbang: "Flashbang",
41119
+ semtex: "Semtex charge",
41120
+ smoke_grenade: "M18 colored smoke grenade",
39003
41121
  m79_40mm: "40 mm grenade",
39004
41122
  m72_66mm: "66 mm HEAT rocket",
39005
41123
  at4_84mm: "84 mm HEAT rocket",
@@ -39081,7 +41199,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39081
41199
  improvised_spear: "lan\xE7a improvisada",
39082
41200
  chainsaw: "motosserra",
39083
41201
  recurve_bow: "arco recurvo",
39084
- m134: "Minigun M134"
41202
+ m134: "Minigun M134",
41203
+ semtex: "Semtex com detonador",
41204
+ smoke_grenade: "granada de fuma\xE7a M18",
41205
+ kabar: "faca KA-BAR",
41206
+ katana: "espada katana",
41207
+ brass_knuckles: "soco ingl\xEAs"
39085
41208
  },
39086
41209
  modes: { single: "tiro \xFAnico", semi: "semiautom\xE1tico", burst: "rajada", auto: "autom\xE1tico", pump: "repeti\xE7\xE3o", bolt: "ferrolho", lever: "alavanca", break: "basculante", volley: "salva" },
39087
41210
  categories: {
@@ -39117,6 +41240,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39117
41240
  c4: "C4 com detonador",
39118
41241
  claymore: "Mina Claymore",
39119
41242
  flashbang: "Granada de luz",
41243
+ semtex: "Semtex com detonador",
41244
+ smoke_grenade: "Granada de fuma\xE7a M18",
39120
41245
  m79_40mm: "Granada de 40 mm",
39121
41246
  m72_66mm: "Foguete HEAT de 66 mm",
39122
41247
  at4_84mm: "Foguete HEAT de 84 mm",
@@ -39228,6 +41353,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39228
41353
  // the iron sights close the view in a little: more for a long gun, whose sights sit farther from the eye, than for a handgun
39229
41354
  ironSightZoom: { pistol: 1.1, revolver: 1.1, smg: 1.2, carbine: 1.25, shotgun: 1.15, rifle: 1.3, hunting: 1.35, dmr: 1.4, sniper: 1.4, machine_gun: 1.2, launcher: 1.15, bow: 1.15, crossbow: 1.25 }
39230
41355
  },
41356
+ // a weapon in each hand: the handguns, the machine pistols short and light enough for one hand, and the one-hand blades; one hand and no sight picture open the cone and lift the muzzle more
41357
+ dualWield: {
41358
+ categories: ["pistol", "revolver"],
41359
+ weapons: ["mini_uzi", "uzi", "mac_10", "vz61", "hk_mp7a1", "knife", "tactical_knife", "machete", "hatchet"],
41360
+ exclude: [],
41361
+ spread: 1.8,
41362
+ recoil: 1.35,
41363
+ // tucking a pistol in the belt and drawing it back is a short motion of one hand
41364
+ stowSeconds: 0.45
41365
+ },
39231
41366
  // Arsenal §08/§11: coronha retrátil saca mais rápido, mais recuo; os números são padrões aprovados pelo dono (2026-10-24)
39232
41367
  stock: {
39233
41368
  seconds: 0.6,
@@ -39390,7 +41525,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39390
41525
  destructionLevel: def.destructionLevel,
39391
41526
  ...def.cone ? { cone: def.cone } : {},
39392
41527
  ...def.stunSeconds !== void 0 ? { stunSeconds: def.stunSeconds } : {},
39393
- ...def.fire ? { fire: def.fire } : {}
41528
+ ...def.fire ? { fire: def.fire } : {},
41529
+ ...def.smoke ? { smoke: def.smoke } : {}
39394
41530
  };
39395
41531
  return { type: "detonate", at: at3, detonation };
39396
41532
  }
@@ -39650,6 +41786,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39650
41786
  return roll.ammoQuality.faultChance > 0 && roll.random() < roll.ammoQuality.faultChance ? "misfire" : null;
39651
41787
  };
39652
41788
  var isDudWithoutJam = (action, fault) => fault === "misfire" && action.kind === "firearm" && MISFIRE_ONLY.has(action.family);
41789
+ var ONE_HAND = { spread: 1, recoil: 1 };
39653
41790
  var activeParts = (fitted, catalogue) => fitted.flatMap((part) => {
39654
41791
  const def = catalogue.get(part.id);
39655
41792
  const isOn = def?.kind !== "light" && def?.kind !== "laser" ? true : part.on === true;
@@ -39716,12 +41853,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39716
41853
  const read = (name, base) => stat(name, base, sources, active);
39717
41854
  const mass = read("mass", def.mass + parts.reduce((total, part) => total + part.mass, 0) - factory.reduce((total, part) => total + part.mass, 0));
39718
41855
  const hipFactor = context.aiming ? 1 : 1 + (context.rules.aim.hipSpread - 1) * read("hipSpreadExcess", 1);
41856
+ const dual = context.dualWield && context.rules.dualWield ? context.rules.dualWield : ONE_HAND;
39719
41857
  let action = def.action;
39720
41858
  if (firearm) {
39721
41859
  const baseNoise = firearm.noise ?? context.caliber?.noise;
39722
41860
  const effects = {
39723
- spread: read("spread", firearm.spreadDeg) * hipFactor,
39724
- recoil: [read("recoilVertical", firearm.recoil[0]), read("recoilHorizontal", firearm.recoil[1])],
41861
+ spread: read("spread", firearm.spreadDeg) * hipFactor * dual.spread,
41862
+ recoil: [read("recoilVertical", firearm.recoil[0]) * dual.recoil, read("recoilHorizontal", firearm.recoil[1]) * dual.recoil],
39725
41863
  range: read("range", firearm.range),
39726
41864
  noise: baseNoise === void 0 ? void 0 : read("noise", baseNoise),
39727
41865
  speed,
@@ -40090,6 +42228,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
40090
42228
  __publicField4(this, "stockState", "extended");
40091
42229
  __publicField4(this, "aiming", false);
40092
42230
  __publicField4(this, "supportedStance", false);
42231
+ __publicField4(this, "dualStance", false);
40093
42232
  __publicField4(this, "reloadCount", 0);
40094
42233
  __publicField4(this, "catalogue");
40095
42234
  __publicField4(this, "isCompatible");
@@ -40137,7 +42276,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
40137
42276
  }
40138
42277
  resolve() {
40139
42278
  const supported = this.supportedStance;
40140
- return resolveLoadout(this.def, this.fitted, this.catalogue, { aiming: this.aiming, supported, stock: this.stockState, caliber: this.caliber, rules: this.rules });
42279
+ return resolveLoadout(this.def, this.fitted, this.catalogue, { aiming: this.aiming, supported, stock: this.stockState, caliber: this.caliber, rules: this.rules, dualWield: this.dualStance });
40141
42280
  }
40142
42281
  /** Takes the numbers of the accessories on the weapon: the action, the capacity (what no longer fits goes back to the reserve) and the draw time. */
40143
42282
  applyLoad() {
@@ -40310,9 +42449,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
40310
42449
  setStance(stance) {
40311
42450
  const aiming = stance.aiming ?? this.aiming;
40312
42451
  const supported = stance.supported ?? this.supportedStance;
40313
- if (aiming === this.aiming && supported === this.supportedStance) return;
40314
- this.aiming = aiming;
42452
+ const dual = stance.dualWield ?? this.dualStance;
42453
+ const aimed = dual ? false : aiming;
42454
+ if (aimed === this.aiming && supported === this.supportedStance && dual === this.dualStance) return;
42455
+ this.aiming = aimed;
40315
42456
  this.supportedStance = supported;
42457
+ this.dualStance = dual;
40316
42458
  this.applyLoad();
40317
42459
  }
40318
42460
  snapshot() {
@@ -40840,6 +42982,15 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
40840
42982
  const from = [use.origin[0], use.origin[1] + wire.height, use.origin[2]];
40841
42983
  return { from, to: [from[0] + dx * wire.length, from[1], from[2] + dz * wire.length] };
40842
42984
  };
42985
+ function canDualWield(arsenal, weaponId) {
42986
+ const def = arsenal.weapons.get(weaponId);
42987
+ if (!def) return false;
42988
+ return isDualWieldable(arsenal.rules.dualWield, weaponId, def.category);
42989
+ }
42990
+ var isDualWieldable = (rule, weaponId, category) => {
42991
+ if (rule.exclude.includes(weaponId)) return false;
42992
+ return rule.weapons.includes(weaponId) || rule.categories.includes(category);
42993
+ };
40843
42994
  var isGenericKind = (kind, category) => kind === "firearm" && category !== "bow" && category !== "crossbow" || kind === "flame";
40844
42995
  function genericWeapons(arsenal) {
40845
42996
  const archetypes = new Set(ARCHETYPE_IDS);
@@ -47094,6 +49245,13 @@ void main(){
47094
49245
  };
47095
49246
 
47096
49247
  // src/explosives.ts
49248
+ var SMOKE_PUFFS_PER_SECOND = 36;
49249
+ var SMOKE_PUFF_LIFE = 3.2;
49250
+ var SMOKE_PUFF_SPEED = 1.7;
49251
+ var SMOKE_PUFF_SIZE = 5;
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";
47097
49255
  var CONE_BLAST_RADIUS = 0.5;
47098
49256
  var summarizeBlast = (results) => {
47099
49257
  const reports = results.flatMap((r) => r.blast ? [r.blast] : []);
@@ -47109,13 +49267,35 @@ void main(){
47109
49267
  function createExplosives(context) {
47110
49268
  const { THREE, scene: scene2, camera: camera2, world, renderer, arsenal, translate, format, action } = context;
47111
49269
  const projectiles = [];
49270
+ const smokeSources = [];
49271
+ const random = context.random ?? Math.random;
47112
49272
  const meshes = [];
47113
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
+ };
47114
49291
  const drop = (objects) => {
47115
49292
  for (const object of objects) object.removeFromParent();
47116
49293
  releaseObject3D(objects, { keep: scene2 });
47117
49294
  };
47118
49295
  const clear = () => {
49296
+ smokeSources.length = 0;
49297
+ smokeHiss?.stop();
49298
+ smokeHiss = null;
47119
49299
  projectiles.length = 0;
47120
49300
  drop(meshes);
47121
49301
  meshes.length = 0;
@@ -47169,6 +49349,7 @@ void main(){
47169
49349
  age: 0,
47170
49350
  ...use.explosive !== void 0 ? { body: createExplosiveBody(use, arsenal.explosives, arsenal.rules.explosives), aligned: isRocket } : {}
47171
49351
  });
49352
+ if (!isRocket && !isMolotov && use.explosive !== void 0) context.audio?.play(GRENADE_PIN_SOUND, { position: use.origin });
47172
49353
  const key2 = isRocket ? "act.launch" : isMolotov ? "act.throw.molotov" : "act.throw.grenade";
47173
49354
  action(isRocket ? "launch_projectile" : "throw_projectile", translate(key2, { weapon: context.toolName(), fuse: format(use.fuse, 1), arm: format(use.armDistance ?? 0, 0) }), {
47174
49355
  data: { weapon: context.toolId(), fuse: use.fuse, explosive: use.explosive }
@@ -47282,6 +49463,12 @@ void main(){
47282
49463
  const detonate = (projectile, detonation) => {
47283
49464
  const weaponName = context.toolName(projectile.weapon);
47284
49465
  const reach = Math.max(detonation.injuryRadius, detonation.lethalRadius);
49466
+ if (detonation.smoke) {
49467
+ const [shortest, longest] = detonation.smoke.seconds;
49468
+ smokeSources.push({ point: detonation.point, left: shortest + (longest - shortest) * random(), owed: 0, color: detonation.smoke.color });
49469
+ action("smoke", translate("act.smoke", { weapon: weaponName, seconds: format(smokeSources[smokeSources.length - 1].left, 0) }), { data: { weapon: projectile.weapon, explosive: detonation.explosive, point: detonation.point } });
49470
+ return;
49471
+ }
47285
49472
  const results = detonation.energy <= 0 || detonation.cone ? [] : world.explode(detonation.point, detonation.energy, reach, "player", context.blast());
47286
49473
  const reached = detonation.energy <= 0 ? 0 : detonation.cone ? coneBlast(detonation) : results.length;
47287
49474
  if (!detonation.cone) told(results);
@@ -47403,7 +49590,23 @@ void main(){
47403
49590
  action("tripwire", translate("act.tripwire", { weapon: context.toolName(projectile.weapon), who: crosser }), { data: { weapon: projectile.weapon, body: crosser } });
47404
49591
  detonate(projectile, event.detonation);
47405
49592
  };
49593
+ const burnSmoke = (dt) => {
49594
+ for (let index = smokeSources.length - 1; index >= 0; index--) {
49595
+ const source = smokeSources[index];
49596
+ source.left -= dt;
49597
+ source.owed += SMOKE_PUFFS_PER_SECOND * dt;
49598
+ for (; source.owed >= 1; source.owed -= 1) {
49599
+ const angle = random() * Math.PI * 2;
49600
+ const speed = SMOKE_PUFF_SPEED * (0.4 + random());
49601
+ const velocity = [Math.cos(angle) * speed, 0.5 + random() * 0.8, Math.sin(angle) * speed];
49602
+ context.effects.spawn("smoke", [source.point[0], source.point[1] + 0.08, source.point[2]], velocity, { color: source.color, size: SMOKE_PUFF_SIZE, life: SMOKE_PUFF_LIFE, alpha: SMOKE_PUFF_ALPHA });
49603
+ }
49604
+ if (source.left <= 0) smokeSources.splice(index, 1);
49605
+ }
49606
+ hissSmoke();
49607
+ };
47406
49608
  const update = (dt) => {
49609
+ burnSmoke(dt);
47407
49610
  for (let index = projectiles.length - 1; index >= 0; index--) {
47408
49611
  const projectile = projectiles[index];
47409
49612
  if (!projectile) continue;
@@ -47424,7 +49627,7 @@ void main(){
47424
49627
  }
47425
49628
  }
47426
49629
  };
47427
- return { throwUse, placeUse, backblastUse, detonateCharges, blastAt, update, clear };
49630
+ return { throwUse, placeUse, backblastUse, detonateCharges, blastAt, update, clear, smokeLeft: () => smokeSources.map((source) => source.left) };
47428
49631
  }
47429
49632
 
47430
49633
  // src/test-table.ts
@@ -47638,6 +49841,65 @@ void main(){
47638
49841
  }
47639
49842
  };
47640
49843
 
49844
+ // src/dual-reload.ts
49845
+ var otherHand = (hand) => hand === "R" ? "L" : "R";
49846
+ function createDualReload(host, stowSeconds) {
49847
+ let queue = [];
49848
+ let current = null;
49849
+ const next = () => {
49850
+ let hand = queue.shift();
49851
+ while (hand && !host.needs(hand)) hand = queue.shift();
49852
+ if (!hand) {
49853
+ current = null;
49854
+ return;
49855
+ }
49856
+ const seconds = stowSeconds();
49857
+ current = { hand, phase: "stow", left: seconds };
49858
+ host.stow(otherHand(hand), true, seconds);
49859
+ };
49860
+ const draw2 = (hand) => {
49861
+ const seconds = stowSeconds();
49862
+ current = { hand, phase: "draw", left: seconds };
49863
+ host.stow(otherHand(hand), false, seconds);
49864
+ };
49865
+ return {
49866
+ start(order) {
49867
+ if (current) return;
49868
+ queue = [...order];
49869
+ next();
49870
+ },
49871
+ step(dt) {
49872
+ if (!current) return;
49873
+ if (current.phase === "reload") {
49874
+ if (!host.reloading(current.hand)) draw2(current.hand);
49875
+ return;
49876
+ }
49877
+ current.left -= dt;
49878
+ if (current.left > 0) return;
49879
+ if (current.phase === "draw") {
49880
+ next();
49881
+ return;
49882
+ }
49883
+ current.phase = "reload";
49884
+ if (!host.startReload(current.hand)) draw2(current.hand);
49885
+ },
49886
+ get busy() {
49887
+ return current !== null;
49888
+ },
49889
+ get current() {
49890
+ return current ? { hand: current.hand, phase: current.phase } : null;
49891
+ },
49892
+ stowed(hand) {
49893
+ return current !== null && current.phase !== "draw" && otherHand(current.hand) === hand;
49894
+ },
49895
+ cancel() {
49896
+ if (current && current.phase !== "draw") host.stow(otherHand(current.hand), false, 0);
49897
+ current = null;
49898
+ queue = [];
49899
+ }
49900
+ };
49901
+ }
49902
+
47641
49903
  // src/loading-view.ts
47642
49904
  var LOADING_CLASS = "tr-loading";
47643
49905
  var LOADING_CSS = [
@@ -49644,6 +51906,10 @@ void main(){
49644
51906
  o.hooks.primaryDown();
49645
51907
  };
49646
51908
  const onMouseUp = (e) => {
51909
+ if (e.button === 2) {
51910
+ o.hooks.secondaryUp?.();
51911
+ return;
51912
+ }
49647
51913
  if (e.button !== 0) return;
49648
51914
  if (holdTimer) clearTimeout(holdTimer);
49649
51915
  if (dragging) {
@@ -53517,7 +55783,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
53517
55783
  }
53518
55784
 
53519
55785
  // src/avatar.ts
53520
- var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
55786
+ var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", brass_knuckles: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
53521
55787
  var DEFAULT_HAND_STANCES = { fists: "fists" };
53522
55788
  var THIRD_PERSON_DEFAULTS = {
53523
55789
  back: 3.8,
@@ -53690,6 +55956,12 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
53690
55956
  "act.clear": "Cleared the room",
53691
55957
  "act.reset": "Repaired every object",
53692
55958
  "act.tool": "Equipped {weapon}",
55959
+ "act.tool_left": "{weapon} in the left hand: left button fires the right one, right button the left one",
55960
+ "act.tool_left_off": "Left hand free: one weapon again",
55961
+ "act.stow": "{weapon} tucked against the body: the {hand} hand is free for the other magazine",
55962
+ "act.unstow": "{weapon} back in the {hand} hand",
55963
+ "hud.no_dual": "{weapon} does not go one in each hand",
55964
+ "hud.left": "L",
53693
55965
  "act.shoot.hit": "{weapon}: shot, {energy} J, hit {where} at {dist} m",
53694
55966
  "act.shoot.miss": "{weapon}: shot, {energy} J, hit nothing",
53695
55967
  "act.shoot.pellets": "{weapon}: shot, {n} projectiles of up to {energy} J, {hits} hit {where} at {dist} m",
@@ -53719,6 +55991,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
53719
55991
  "hud.no_charges": "No charge set down waits for the remote detonator",
53720
55992
  "blast.zone.lethal": "lethal",
53721
55993
  "blast.zone.injury": "injury",
55994
+ "act.smoke": "{weapon}: smoke, burning for {seconds} s",
53722
55995
  "act.explode": "{weapon}: exploded, {energy} J within {radius} m, {n} objects in range",
53723
55996
  "act.molotov_fire": "{weapon}: fire on the ground, radius {radius} m for {seconds} s",
53724
55997
  "act.tool.none": "{weapon}: nothing in reach",
@@ -53749,6 +56022,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
53749
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)",
53750
56023
  "act.player_stunned": "The blast stunned the player for {stun} s, {distance} m from it: dizzy, ears ringing",
53751
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",
53752
56026
  "act.shoved": "{name} stands free: the shots moved it",
53753
56027
  "act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
53754
56028
  "act.use_item": "{name}: {use}",
@@ -53857,6 +56131,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
53857
56131
  "ui.clear_log": "Clear",
53858
56132
  "ui.catalog": "Catalogue",
53859
56133
  "ui.tool_menu": "Weapons",
56134
+ "ui.tool_menu.dual": "Shift+click: one in each hand",
53860
56135
  "ui.tool_menu.search": "Search weapons and tools",
53861
56136
  "ui.tool_menu.close": "Close",
53862
56137
  "ui.tool_menu.results": "{n} found",
@@ -54060,6 +56335,12 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54060
56335
  "act.clear": "Limpou a sala",
54061
56336
  "act.reset": "Consertou todos os objetos",
54062
56337
  "act.tool": "Equipou {weapon}",
56338
+ "act.tool_left": "{weapon} na m\xE3o esquerda: bot\xE3o esquerdo dispara a da direita, bot\xE3o direito a da esquerda",
56339
+ "act.tool_left_off": "M\xE3o esquerda livre: uma arma de novo",
56340
+ "act.stow": "{weapon} encostada no corpo: a m\xE3o {hand} fica livre para o outro carregador",
56341
+ "act.unstow": "{weapon} de volta na m\xE3o {hand}",
56342
+ "hud.no_dual": "{weapon} n\xE3o vai uma em cada m\xE3o",
56343
+ "hud.left": "E",
54063
56344
  "act.shoot.hit": "{weapon}: disparo, {energy} J, acertou {where} a {dist} m",
54064
56345
  "act.shoot.miss": "{weapon}: disparo, {energy} J, n\xE3o acertou nada",
54065
56346
  "act.shoot.pellets": "{weapon}: disparo, {n} proj\xE9teis de at\xE9 {energy} J, {hits} acertaram {where} a {dist} m",
@@ -54089,6 +56370,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54089
56370
  "hud.no_charges": "Nenhuma carga posta espera o detonador",
54090
56371
  "blast.zone.lethal": "letal",
54091
56372
  "blast.zone.injury": "de ferimento",
56373
+ "act.smoke": "{weapon}: fuma\xE7a, queima por {seconds} s",
54092
56374
  "act.explode": "{weapon}: explodiu, {energy} J em {radius} m, {n} objetos ao alcance",
54093
56375
  "act.molotov_fire": "{weapon}: fogo no ch\xE3o, raio de {radius} m por {seconds} s",
54094
56376
  "act.tool.none": "{weapon}: nada ao alcance",
@@ -54119,6 +56401,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54119
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)",
54120
56402
  "act.player_stunned": "A explos\xE3o atordoou o jogador por {stun} s, a {distance} m dela: tonto, com os ouvidos zumbindo",
54121
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",
54122
56405
  "act.shoved": "{name} estava solto no ch\xE3o: os tiros o moveram",
54123
56406
  "act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
54124
56407
  "act.use_item": "{name}: {use}",
@@ -54227,6 +56510,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54227
56510
  "ui.clear_log": "Limpar",
54228
56511
  "ui.catalog": "Cat\xE1logo",
54229
56512
  "ui.tool_menu": "Armas",
56513
+ "ui.tool_menu.dual": "Shift+clique: uma em cada m\xE3o",
54230
56514
  "ui.tool_menu.search": "Buscar armas e ferramentas",
54231
56515
  "ui.tool_menu.close": "Fechar",
54232
56516
  "ui.tool_menu.results": "{n} encontradas",
@@ -54435,6 +56719,27 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54435
56719
  return wrap;
54436
56720
  };
54437
56721
  let playhead = null;
56722
+ const sheetImage = (image) => {
56723
+ const figure = mk("figure", "tr-sheet-image");
56724
+ const picture = mk("img");
56725
+ picture.src = image.url;
56726
+ picture.alt = image.alt;
56727
+ picture.loading = "lazy";
56728
+ picture.referrerPolicy = "no-referrer";
56729
+ figure.appendChild(picture);
56730
+ if (image.credit || image.page) {
56731
+ const caption = mk("figcaption", "sub", image.credit ?? "");
56732
+ if (image.page) {
56733
+ const link = mk("a", void 0, image.credit ? " \u2197" : image.page);
56734
+ link.href = image.page;
56735
+ link.target = "_blank";
56736
+ link.rel = "noopener noreferrer";
56737
+ caption.appendChild(link);
56738
+ }
56739
+ figure.appendChild(caption);
56740
+ }
56741
+ return figure;
56742
+ };
54438
56743
  const sheetCard = (sheet) => {
54439
56744
  const card = mk("div", "tr-sheet");
54440
56745
  if (sheet.kicker) card.appendChild(mk("div", "sub", sheet.kicker));
@@ -54486,6 +56791,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54486
56791
  playhead = { node: head, metric, end: end3 };
54487
56792
  card.appendChild(strip);
54488
56793
  }
56794
+ if (sheet.image) card.appendChild(sheetImage(sheet.image));
54489
56795
  if (sheet.note) card.appendChild(mk("p", "sub", sheet.note));
54490
56796
  return card;
54491
56797
  };
@@ -54719,7 +57025,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54719
57025
  const close = mk3("button", "tr-btn", "\xD7");
54720
57026
  close.type = "button";
54721
57027
  close.title = o.t("ui.tool_menu.close");
54722
- header.append(mk3("h3", void 0, o.t("ui.tool_menu")), search, close);
57028
+ header.append(mk3("h3", void 0, o.t("ui.tool_menu")), mk3("small", "tr-hint", o.t("ui.tool_menu.dual")), search, close);
54723
57029
  const tabs = mk3("div", "tr-tabs");
54724
57030
  const body = mk3("div", "tr-wmenu-body");
54725
57031
  element.append(header, tabs, body);
@@ -54727,15 +57033,17 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54727
57033
  let activeGroup = "";
54728
57034
  let groups = [];
54729
57035
  const toolButton = (tool, group) => {
54730
- const button = mk3("button", tool.id === o.current() ? "tr-btn on" : "tr-btn");
57036
+ const left = o.currentLeft?.() === tool.id;
57037
+ const button = mk3("button", tool.id === o.current() || left ? "tr-btn on" : "tr-btn");
54731
57038
  button.type = "button";
54732
57039
  button.title = tool.name;
57040
+ if (left) button.dataset.left = "";
54733
57041
  button.dataset.tool = tool.id;
54734
57042
  if (tool.thumbnail) button.appendChild(o.thumb(tool.thumbnail, tool.thumbnailCredit));
54735
57043
  button.appendChild(mk3("span", void 0, tool.name));
54736
57044
  if (group) button.appendChild(mk3("small", void 0, group));
54737
- button.onclick = () => {
54738
- o.onPick(tool.id);
57045
+ button.onclick = (event) => {
57046
+ o.onPick(tool.id, { second: event.shiftKey });
54739
57047
  toggle(false);
54740
57048
  };
54741
57049
  return button;
@@ -54831,6 +57139,8 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54831
57139
  .tr-prog b{display:block;font-weight:500;margin-bottom:3px}
54832
57140
  .tr-sheet{margin-top:8px;padding:8px;border:1px solid #2a2e36;border-radius:6px;font-size:12px;line-height:1.4}
54833
57141
  .tr-sheet h4{margin:2px 0 4px;font-size:13px}.tr-sheet p{margin:4px 0}
57142
+ /* the picture of a sheet floats over the scene, top centre, next to what it shows: the bench panel is too short to hold it */
57143
+ .tr-sheet-image{position:fixed;top:52px;left:50%;transform:translateX(-50%);width:min(400px,42vw);z-index:5;margin:0;padding:6px;background:rgba(18,20,24,.92);border:1px solid #2a2e36;border-radius:6px;pointer-events:auto}.tr-sheet-image img{display:block;width:100%;max-height:200px;object-fit:contain;background:#f4f4f2;border-radius:4px}.tr-sheet-image figcaption{margin-top:3px;font-size:11px}.tr-sheet-image a{color:#8ab4f8}
54834
57144
  .tr-facts{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;margin:4px 0}.tr-facts dt{opacity:.7}.tr-facts dd{margin:0}
54835
57145
  .tr-steps{margin:4px 0;padding-left:16px}.tr-swatches{display:flex;flex-wrap:wrap;gap:6px}.tr-swatch i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:4px}
54836
57146
  .tr-timeline{position:relative;height:22px;margin:8px 0;background:#1b1e24;border-radius:4px;overflow:hidden}
@@ -54958,7 +57268,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
54958
57268
  bottom.append(held, hotbar);
54959
57269
  let currentTool = "";
54960
57270
  const menuOptions = o.toolMenu;
54961
- const toolMenu = menuOptions ? createToolMenu({ t, groups: menuOptions.groups, current: () => currentTool, onPick: menuOptions.onPick, ...menuOptions.onOpen ? { onOpen: menuOptions.onOpen } : {}, thumb }) : null;
57271
+ const toolMenu = menuOptions ? createToolMenu({ t, groups: menuOptions.groups, current: () => currentTool, onPick: menuOptions.onPick, ...menuOptions.onOpen ? { onOpen: menuOptions.onOpen } : {}, ...menuOptions.currentLeft ? { currentLeft: menuOptions.currentLeft } : {}, thumb }) : null;
54962
57272
  if (toolMenu && menuOptions) {
54963
57273
  const menuButton = mk4("button", "tr-slot tr-menu-btn");
54964
57274
  menuButton.type = "button";
@@ -55606,6 +57916,8 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
55606
57916
 
55607
57917
  // src/room.ts
55608
57918
  var FIXED_DT = 1 / 60;
57919
+ var LEFT_HAND_KEY = "#L";
57920
+ var OFF_HAND_SEED = 11;
55609
57921
  var TOOL_MENU_KEY_LABELS = { Backquote: "'" };
55610
57922
  var EXPLOSIVE_EFFECT_IDS = ["m67", "molotov", "pipe_bomb", "m79_40mm", "m72_66mm", "at4_84mm", "dynamite", "c4", "claymore"];
55611
57923
  var DEFAULT_PARTICLE_LIMIT = 900;
@@ -55820,6 +58132,14 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
55820
58132
  rendererPieces.set(objectId, piece);
55821
58133
  action("loosened", t(cause === "blast" ? "act.loosened" : "act.came_apart", { name: e.name }), { objectId, itemId: e.so.itemId });
55822
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
+ },
55823
58143
  onPiece: (piece) => {
55824
58144
  rendererPieces.set(piece.objectId, piece);
55825
58145
  loose.push(piece);
@@ -55901,6 +58221,11 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
55901
58221
  return s;
55902
58222
  };
55903
58223
  let tool = toolState(toolId);
58224
+ let offToolId = null;
58225
+ let offTool = null;
58226
+ let offHeld = false;
58227
+ let offFirst = false;
58228
+ let firedHand = "R";
55904
58229
  const catalogMap = /* @__PURE__ */ new Map();
55905
58230
  for (const i of options.catalog) catalogMap.set(i.id, i);
55906
58231
  const catalog = [...options.catalog];
@@ -56110,9 +58435,19 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
56110
58435
  releaseTrigger();
56111
58436
  },
56112
58437
  secondary: () => {
58438
+ if (offTool && !heldEntry) {
58439
+ offHeld = true;
58440
+ offFirst = true;
58441
+ return;
58442
+ }
56113
58443
  if (heldEntry) throwHeld();
56114
58444
  else grab();
56115
58445
  },
58446
+ secondaryUp: () => {
58447
+ if (!offHeld) return;
58448
+ offHeld = false;
58449
+ releaseTrigger("L");
58450
+ },
56116
58451
  key: (code, down, e) => onKey(code, down, e),
56117
58452
  wheel: (dir) => cycleTool(dir)
56118
58453
  }
@@ -56468,9 +58803,14 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
56468
58803
  bodyBoxes.delete(id);
56469
58804
  return null;
56470
58805
  }
56471
- bodyBounds.setFromObject(e.so.root);
56472
- if (bodyBounds.isEmpty()) return null;
56473
- 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 };
56474
58814
  bodyBoxes.set(id, box2);
56475
58815
  return box2;
56476
58816
  };
@@ -56487,7 +58827,15 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
56487
58827
  const reach = c.radius + r;
56488
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;
56489
58829
  }
56490
- 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;
56491
58839
  };
56492
58840
  blockers.add((x, z, r) => {
56493
58841
  for (const e of entries2.values()) {
@@ -56651,6 +58999,10 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
56651
58999
  for (const listener of [...resetListeners]) listener();
56652
59000
  tools.clear();
56653
59001
  tool = toolState(toolId);
59002
+ dualReload.cancel();
59003
+ offTool = null;
59004
+ offToolId = null;
59005
+ firedHand = "R";
56654
59006
  if (pristineWorld) {
56655
59007
  for (const id of world.ids()) world.remove(id);
56656
59008
  world.load(pristineWorld);
@@ -56916,12 +59268,13 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
56916
59268
  const pocketAt = new THREE.Vector3();
56917
59269
  const aimTurn = new THREE.Quaternion();
56918
59270
  const aimEuler = new THREE.Euler(0, 0, 0, "YXZ");
56919
- const bodyHoldFrame = () => {
56920
- const shoulder = avatar?.shoulder?.("R");
59271
+ const bodyHoldFrame = (hand = "R") => {
59272
+ const shoulder = avatar?.shoulder?.(hand);
56921
59273
  if (!shoulder) return null;
56922
59274
  shoulder.getWorldPosition(pocketAt);
56923
- pocketAt.x -= Math.cos(player.yaw) * SHOULDER_POCKET_IN;
56924
- pocketAt.z += Math.sin(player.yaw) * SHOULDER_POCKET_IN;
59275
+ const inward = hand === "L" ? -SHOULDER_POCKET_IN : SHOULDER_POCKET_IN;
59276
+ pocketAt.x -= Math.cos(player.yaw) * inward;
59277
+ pocketAt.z += Math.sin(player.yaw) * inward;
56925
59278
  aimTurn.setFromEuler(aimEuler.set(player.pitch, player.yaw, 0, "YXZ"));
56926
59279
  return { position: [pocketAt.x, pocketAt.y, pocketAt.z], quaternion: [aimTurn.x, aimTurn.y, aimTurn.z, aimTurn.w] };
56927
59280
  };
@@ -57031,17 +59384,18 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57031
59384
  rng = rng * 1664525 + 1013904223 >>> 0;
57032
59385
  return rng / 4294967296;
57033
59386
  };
57034
- const muzzlePose = () => {
57035
- const lent = muzzleSource?.();
57036
- return lent ? { position: [...lent.position], drawsFlash: lent.drawsFlash } : { position: muzzle2(), drawsFlash: false };
59387
+ const muzzlePose = (hand = "R") => {
59388
+ const lent = muzzleSource?.(hand);
59389
+ return lent ? { position: [...lent.position], drawsFlash: lent.drawsFlash } : { position: muzzle2(hand), drawsFlash: false };
57037
59390
  };
57038
- const muzzle2 = () => {
59391
+ const muzzle2 = (hand = "R") => {
57039
59392
  aim();
57040
59393
  const e = camera2.matrixWorld.elements;
59394
+ const side = hand === "L" ? -0.16 : 0.16;
57041
59395
  return [
57042
- aimO[0] + aimD[0] * 0.45 + e[0] * 0.16 - e[4] * 0.1,
57043
- aimO[1] + aimD[1] * 0.45 + e[1] * 0.16 - e[5] * 0.1,
57044
- aimO[2] + aimD[2] * 0.45 + e[2] * 0.16 - e[6] * 0.1
59396
+ aimO[0] + aimD[0] * 0.45 + e[0] * side - e[4] * 0.1,
59397
+ aimO[1] + aimD[1] * 0.45 + e[1] * side - e[5] * 0.1,
59398
+ aimO[2] + aimD[2] * 0.45 + e[2] * side - e[6] * 0.1
57045
59399
  ];
57046
59400
  };
57047
59401
  const tracers = [];
@@ -57221,10 +59575,10 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57221
59575
  renderer.impulse(objectId, push2.impulse, push2.point);
57222
59576
  }
57223
59577
  };
57224
- const fireShots = (shots, recoil) => {
59578
+ const fireShots = (shots, recoil, id = toolId, hand = "R") => {
57225
59579
  if (shots.length === 0) return;
57226
- const w = toolName();
57227
- const { position: m, drawsFlash } = muzzlePose();
59580
+ const w = toolName(id);
59581
+ const { position: m, drawsFlash } = muzzlePose(hand);
57228
59582
  if (!drawsFlash) {
57229
59583
  renderer.emit("flash", m, [aimD[0], aimD[1], aimD[2]], 1, 0);
57230
59584
  renderer.emit("sparks", m, [aimD[0], aimD[1], aimD[2]], 4, 6);
@@ -57263,7 +59617,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57263
59617
  objectId: f?.object,
57264
59618
  itemId: f ? entryOf(f.object)?.so.itemId : void 0,
57265
59619
  part: f?.part,
57266
- data: { weapon: toolId, shots: shots.length, hits, energyJ: Math.round(energy), caliberMm: shots[0].caliber, distanceM: f ? Math.round(f.distance * 100) / 100 : null }
59620
+ data: { weapon: id, shots: shots.length, hits, energyJ: Math.round(energy), caliberMm: shots[0].caliber, distanceM: f ? Math.round(f.distance * 100) / 100 : null, ...hand === "L" ? { hand } : {} }
57267
59621
  });
57268
59622
  };
57269
59623
  const fireRays = (u) => {
@@ -57367,7 +59721,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57367
59721
  const kickLeg = options.kickLeg ?? "alternate";
57368
59722
  let nextLeg = "right";
57369
59723
  const punchHand = options.punchHand ?? "alternate";
57370
- const punchTools = new Set(options.punchTools ?? ["hand", "fists"]);
59724
+ const punchTools = new Set(options.punchTools ?? ["hand", "fists", "brass_knuckles"]);
57371
59725
  let nextHand = "right";
57372
59726
  const combo = options.strikeCombo;
57373
59727
  let comboOn = !!combo && options.strikeComboOn === true;
@@ -57381,16 +59735,16 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57381
59735
  action("combo", t(on ? "act.combo.on" : "act.combo.off", { name }), { data: { combo: combo.id, on } });
57382
59736
  say(t(on ? "act.combo.on" : "act.combo.off", { name }));
57383
59737
  };
57384
- const strikeUse = (u, id = toolId) => {
59738
+ const strikeUse = (u, id = toolId, side) => {
57385
59739
  const stepIndex = comboOn && combo?.tools.includes(id) && combo.steps.length > 0 ? simTime - comboAt > combo.reset ? 0 : comboNext : null;
57386
59740
  const step2 = stepIndex === null ? void 0 : combo.steps[stepIndex];
57387
59741
  const clip = step2 ? step2.clip : strikeClips[id];
57388
59742
  const leg = id === "kick" ? kickLeg === "alternate" ? nextLeg : kickLeg : void 0;
57389
- const hand = step2 ? step2.hand : punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
57390
- const side = leg ? { leg } : hand ? { hand } : void 0;
57391
- const seconds = clip === void 0 ? null : avatar?.strike?.(clip, side) ?? null;
59743
+ const hand = side ? side.hand : step2 ? step2.hand : punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
59744
+ const limb = leg ? { leg } : hand ? { hand } : void 0;
59745
+ const seconds = clip === void 0 ? null : avatar?.strike?.(clip, limb) ?? null;
57392
59746
  if (leg && seconds !== null && kickLeg === "alternate") nextLeg = leg === "right" ? "left" : "right";
57393
- if (!step2 && hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
59747
+ if (!step2 && !side && hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
57394
59748
  if (step2 && stepIndex !== null) {
57395
59749
  comboNext = (stepIndex + 1) % combo.steps.length;
57396
59750
  comboAt = simTime;
@@ -57411,7 +59765,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57411
59765
  data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {}, ...hand ? { hand } : {}, ...step2 ? { combo: combo.id, step: stepIndex } : {} }
57412
59766
  });
57413
59767
  strikes.push({
57414
- key: id,
59768
+ key: side?.key ?? id,
57415
59769
  remaining: seconds,
57416
59770
  ...swing && seconds > lead ? { swing, lead } : {},
57417
59771
  land: () => {
@@ -57539,22 +59893,53 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57539
59893
  emitHost,
57540
59894
  startFire,
57541
59895
  say,
57542
- projectileModel: options.projectileModel
59896
+ projectileModel: options.projectileModel,
59897
+ audio
57543
59898
  });
57544
- const useTool = (dt, held) => {
59899
+ const toolOf = (hand) => hand === "L" ? offTool && offToolId ? { id: offToolId, state: offTool } : null : { id: toolId, state: tool };
59900
+ const dualReload = createDualReload(
59901
+ {
59902
+ needs: (hand) => {
59903
+ const state = toolOf(hand)?.state;
59904
+ return state !== void 0 && (state.def.action.kind === "firearm" || state.def.action.kind === "launcher") && state.ammo < state.capacity && state.reserve > 0;
59905
+ },
59906
+ startReload: (hand) => startReload(hand),
59907
+ reloading: (hand) => toolOf(hand)?.state.reloading ?? false,
59908
+ stow: (hand, on, seconds) => {
59909
+ const slot2 = toolOf(hand);
59910
+ if (!slot2) return;
59911
+ if (on) slot2.state.release();
59912
+ emitHost({ type: "stow", weapon: slot2.id, on, duration: seconds, ...hand === "L" ? { hand } : {} });
59913
+ action("stow", t(on ? "act.stow" : "act.unstow", { weapon: toolName(slot2.id), hand: t(hand === "L" ? "hand.left" : "hand.right") }), { data: { weapon: slot2.id, hand, on, seconds } });
59914
+ }
59915
+ },
59916
+ () => arsenal.rules.dualWield.stowSeconds
59917
+ );
59918
+ const strikeKeyOf = (id, hand) => hand === "L" ? `${id}${LEFT_HAND_KEY}` : id;
59919
+ const useTool = (dt, held, hand = "R") => {
57545
59920
  aim();
57546
- const cat = tool.def.category;
59921
+ const slot2 = toolOf(hand);
59922
+ if (!slot2) return "cooldown";
59923
+ const { id, state } = slot2;
59924
+ if (offTool && dualReload.busy) {
59925
+ if (!held) say(t("hud.reloading"));
59926
+ return "reloading";
59927
+ }
59928
+ const cat = state.def.category;
57547
59929
  if (cat === "hand") {
57548
59930
  if (strikes.some((strike) => strike.key === "hand" || strike.key === "fists")) return "cooldown";
57549
59931
  strikeUse({ kind: "melee", damage: "blunt", energy: 25, radius: 0.1, reach: 0.8, origin: [aimO[0], aimO[1], aimO[2]], direction: [aimD[0], aimD[1], aimD[2]] }, "hand");
57550
59932
  return "ok";
57551
59933
  }
57552
- if (strikes.some((strike) => strike.key === toolId)) return "cooldown";
57553
- const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
57554
- flushEvents(toolId, tool, res.recoil);
59934
+ if (strikes.some((strike) => strike.key === strikeKeyOf(id, hand))) return "cooldown";
59935
+ const res = state.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
59936
+ flushEvents(id, state, res.recoil, hand);
57555
59937
  if (res.status === "empty") {
57556
- if (!tool.reloading && tool.reserve > 0) startReload();
57557
- say(t(tool.reloading ? "hud.reloading" : "hud.empty"));
59938
+ if (!state.reloading && state.reserve > 0) {
59939
+ if (offTool) dualReload.start([hand, hand === "R" ? "L" : "R"]);
59940
+ else startReload(hand);
59941
+ }
59942
+ say(t(state.reloading ? "hud.reloading" : "hud.empty"));
57558
59943
  return res.status;
57559
59944
  }
57560
59945
  if (res.status !== "ok") {
@@ -57564,15 +59949,18 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57564
59949
  else if (res.status === "safe") say(t("hud.safe"));
57565
59950
  return res.status;
57566
59951
  }
57567
- applyUses(res, dt);
59952
+ firedHand = hand;
59953
+ applyUses(res, dt, hand);
57568
59954
  return res.status;
57569
59955
  };
57570
- const applyUses = (res, dt) => {
57571
- if (res.shots.length) fireShots(res.shots, res.recoil);
59956
+ const applyUses = (res, dt, hand = "R") => {
59957
+ const id = toolOf(hand)?.id ?? toolId;
59958
+ if (res.shots.length) fireShots(res.shots, res.recoil, id, hand);
57572
59959
  for (const u of res.uses) {
57573
59960
  switch (u.kind) {
57574
59961
  case "melee":
57575
- strikeUse(u);
59962
+ if (hand === "L") strikeUse(u, id, { hand: "left", key: strikeKeyOf(id, hand) });
59963
+ else strikeUse(u);
57576
59964
  break;
57577
59965
  case "flame":
57578
59966
  heatRays(u, "fire");
@@ -57600,15 +59988,17 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57600
59988
  }
57601
59989
  }
57602
59990
  };
57603
- const releaseTrigger = () => {
57604
- if (!tool.cooking) {
57605
- tool.release();
59991
+ const releaseTrigger = (hand = "R") => {
59992
+ const slot2 = toolOf(hand);
59993
+ if (!slot2) return;
59994
+ if (!slot2.state.cooking) {
59995
+ slot2.state.release();
57606
59996
  return;
57607
59997
  }
57608
59998
  aim();
57609
- const res = tool.release([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]]);
57610
- flushEvents(toolId, tool);
57611
- if (res) applyUses(res, 0);
59999
+ const res = slot2.state.release([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]]);
60000
+ flushEvents(slot2.id, slot2.state, void 0, hand);
60001
+ if (res) applyUses(res, 0, hand);
57612
60002
  };
57613
60003
  const kick = () => {
57614
60004
  if (!arsenal.weapons.has("kick") || strikes.some((strike) => strike.key === "kick")) return false;
@@ -57637,8 +60027,43 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
57637
60027
  }));
57638
60028
  };
57639
60029
  const toggleToolMenu = (open) => ui?.toolMenu(open) ?? false;
60030
+ const dropOffTool = () => {
60031
+ if (!offTool) return;
60032
+ releaseTrigger("L");
60033
+ offTool = null;
60034
+ offToolId = null;
60035
+ offHeld = false;
60036
+ offFirst = false;
60037
+ dualReload.cancel();
60038
+ firedHand = "R";
60039
+ tool.setStance({ dualWield: false });
60040
+ action("tool_left", t("act.tool_left_off"), { data: { hand: "L" } });
60041
+ emitHost({ type: "tool", weapon: "", hand: "L" });
60042
+ };
60043
+ const setOffTool = (id) => {
60044
+ if (id === null) {
60045
+ dropOffTool();
60046
+ return true;
60047
+ }
60048
+ if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
60049
+ if (!canDualWield(arsenal, id) || !canDualWield(arsenal, toolId)) return false;
60050
+ if (offTool) releaseTrigger("L");
60051
+ const state = arsenal.create(id, { seed: OFF_HAND_SEED, reserve: 999, ...toolSettings });
60052
+ state.setFuseLength(fuseLength);
60053
+ state.refill();
60054
+ state.setStance({ dualWield: true });
60055
+ tool.setStance({ dualWield: true });
60056
+ offTool = state;
60057
+ offToolId = id;
60058
+ dualReload.cancel();
60059
+ action("tool_left", t("act.tool_left", { weapon: toolName(id) }), { data: { weapon: id, hand: "L" } });
60060
+ state.draw();
60061
+ flushEvents(id, state, void 0, "L");
60062
+ return true;
60063
+ };
57640
60064
  const setTool = (id) => {
57641
60065
  if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
60066
+ dropOffTool();
57642
60067
  releaseTrigger();
57643
60068
  toolId = id;
57644
60069
  const i = loadout.indexOf(id);
@@ -58131,15 +60556,13 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58131
60556
  case "KeyR":
58132
60557
  if (heldPiece) heldYaw += Math.PI / 8;
58133
60558
  else {
58134
- startReload();
60559
+ reloadHands();
58135
60560
  say(t("hud.reloading"));
58136
60561
  }
58137
60562
  break;
58138
- case "KeyQ": {
58139
- tool.cycleMode();
58140
- flushEvents(toolId, tool);
60563
+ case "KeyQ":
60564
+ cycleToolMode();
58141
60565
  break;
58142
- }
58143
60566
  case "KeyX":
58144
60567
  clearToolFault();
58145
60568
  break;
@@ -58196,6 +60619,20 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58196
60619
  if (!loadout.includes(weaponId)) loadout.push(weaponId);
58197
60620
  setTool(weaponId);
58198
60621
  };
60622
+ const pickTool = (weaponId, pick3 = { second: false }) => {
60623
+ if (!arsenal.weapons.has(weaponId)) return;
60624
+ if (!pick3.second) {
60625
+ equip(weaponId);
60626
+ return;
60627
+ }
60628
+ if (!canDualWield(arsenal, weaponId)) {
60629
+ equip(weaponId);
60630
+ say(t("hud.no_dual", { weapon: toolName(weaponId) }));
60631
+ return;
60632
+ }
60633
+ if (!offTool || !canDualWield(arsenal, toolId)) equip(weaponId);
60634
+ setOffTool(weaponId);
60635
+ };
58199
60636
  let hudAimHit;
58200
60637
  const inspectTarget = () => {
58201
60638
  if (hudAimHit !== void 0) return hudAimHit;
@@ -58399,6 +60836,11 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58399
60836
  s.update(dt);
58400
60837
  flushEvents(id, s);
58401
60838
  }
60839
+ if (offTool && offToolId) {
60840
+ offTool.update(dt);
60841
+ flushEvents(offToolId, offTool, void 0, "L");
60842
+ dualReload.step(dt);
60843
+ }
58402
60844
  if (audio) {
58403
60845
  const look = camera2.getWorldDirection(listenerDirection);
58404
60846
  audio.setListener({ position: [camera2.position.x, camera2.position.y, camera2.position.z], yaw: Math.atan2(-look.x, -look.z) });
@@ -58444,6 +60886,10 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58444
60886
  } else if (useHeld) useTool(dt, !useFirst);
58445
60887
  useFirst = false;
58446
60888
  }
60889
+ if ((offHeld || offFirst) && offTool && !isPaused && !push.active) {
60890
+ useTool(dt, !offFirst, "L");
60891
+ offFirst = false;
60892
+ }
58447
60893
  if (!isPaused) followHeld(dt);
58448
60894
  };
58449
60895
  const updatePresent = (dt) => {
@@ -58470,12 +60916,18 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58470
60916
  hudAimHit = void 0;
58471
60917
  }
58472
60918
  };
60919
+ const ammoText = (state) => {
60920
+ const kind = state.def.action.kind;
60921
+ if (kind === "firearm") return `${state.ammo}/${state.capacity}${state.reserve ? ` \xB7 ${state.reserve}` : ""}`;
60922
+ return kind === "hand" || kind === "melee" || kind === "tool" ? "" : `${num(state.ammo, 0)}`;
60923
+ };
58473
60924
  const readHudState = () => {
58474
60925
  const page = Math.floor(toolIndex / 10);
58475
60926
  const slots = loadout.slice(page * 10, page * 10 + 10).map((id) => arsenal.name(id));
58476
- const ammo = tool.def.action.kind === "firearm" ? `${tool.ammo}/${tool.capacity}${tool.reserve ? ` \xB7 ${tool.reserve}` : ""}` : tool.def.action.kind === "hand" ? "" : tool.def.action.kind === "melee" || tool.def.action.kind === "tool" ? "" : `${num(tool.ammo, 0)}`;
60927
+ const left = offTool ? ammoText(offTool) : "";
60928
+ const ammo = `${ammoText(tool)}${left ? ` | ${t("hud.left")} ${left}` : ""}`;
58477
60929
  return {
58478
- toolName: toolName(),
60930
+ toolName: offToolId ? `${toolName()} + ${toolName(offToolId)}` : toolName(),
58479
60931
  toolId,
58480
60932
  ammo,
58481
60933
  slots,
@@ -58665,26 +61117,53 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58665
61117
  if (event.type === "explosion") knockPlayer(event);
58666
61118
  for (const listener of hostListeners) listener(event);
58667
61119
  }
58668
- function flushEvents(id, state, recoil) {
61120
+ function flushEvents(id, state, recoil, hand = "R") {
58669
61121
  const phase = state.phase;
58670
61122
  for (const event of state.drain()) {
58671
61123
  const hostEvent = toHostEvent(id, event, { ...recoil ? { recoil } : {}, phase });
58672
- if (hostEvent) emitHost(hostEvent);
61124
+ if (hostEvent) emitHost(hand === "L" ? { ...hostEvent, hand } : hostEvent);
58673
61125
  }
58674
61126
  }
58675
- function startReload() {
58676
- tool.reload();
58677
- flushEvents(toolId, tool);
61127
+ function startReload(hand = "R") {
61128
+ const slot2 = toolOf(hand);
61129
+ if (!slot2) return false;
61130
+ const started = slot2.state.reload();
61131
+ flushEvents(slot2.id, slot2.state, void 0, hand);
61132
+ return started;
61133
+ }
61134
+ function reloadHands() {
61135
+ if (!offTool) {
61136
+ startReload();
61137
+ return;
61138
+ }
61139
+ dualReload.start(["R", "L"]);
58678
61140
  }
61141
+ const commandHand = () => offTool ? firedHand : "R";
58679
61142
  function clearToolFault() {
58680
- if (!tool.clearFault()) return;
58681
- flushEvents(toolId, tool);
58682
- say(t("hud.clearing"));
61143
+ const order = offTool ? [firedHand, firedHand === "R" ? "L" : "R"] : ["R"];
61144
+ for (const hand of order) {
61145
+ const slot2 = toolOf(hand);
61146
+ if (!slot2?.state.fault || !slot2.state.clearFault()) continue;
61147
+ flushEvents(slot2.id, slot2.state, void 0, hand);
61148
+ say(t("hud.clearing"));
61149
+ return;
61150
+ }
58683
61151
  }
58684
61152
  function toggleSafety() {
58685
- tool.setSafety(!tool.safety);
58686
- flushEvents(toolId, tool);
58687
- say(t(tool.safety ? "hud.safety_on" : "hud.safety_off"));
61153
+ const hand = commandHand();
61154
+ const slot2 = toolOf(hand);
61155
+ if (!slot2) return;
61156
+ slot2.state.setSafety(!slot2.state.safety);
61157
+ flushEvents(slot2.id, slot2.state, void 0, hand);
61158
+ say(`${t(slot2.state.safety ? "hud.safety_on" : "hud.safety_off")}${offTool ? ` (${toolName(slot2.id)}, ${t(hand === "L" ? "hand.left" : "hand.right")})` : ""}`);
61159
+ }
61160
+ function cycleToolMode() {
61161
+ const hand = commandHand();
61162
+ const slot2 = toolOf(hand);
61163
+ if (!slot2) return;
61164
+ slot2.state.cycleMode();
61165
+ flushEvents(slot2.id, slot2.state, void 0, hand);
61166
+ if (offTool) say(`${toolName(slot2.id)} (${t(hand === "L" ? "hand.left" : "hand.right")}): ${slot2.state.mode}`);
58688
61167
  }
58689
61168
  function runToolCommand(request) {
58690
61169
  const work = (done) => done.ok ? { ok: true, seconds: done.seconds } : { ok: false, reason: done.reason };
@@ -58706,6 +61185,24 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58706
61185
  return { ok: false, reason: "unknown" };
58707
61186
  }
58708
61187
  }
61188
+ const hostToolOf = (id, state) => ({
61189
+ weapon: id,
61190
+ category: state.def.category,
61191
+ ammo: state.ammo,
61192
+ capacity: state.capacity,
61193
+ reserve: state.reserve,
61194
+ reloading: state.reloading,
61195
+ mode: state.mode,
61196
+ ...state.fault ? { fault: state.fault } : {},
61197
+ safety: state.safety,
61198
+ heat: state.heatLevel,
61199
+ attachments: state.attachments.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } })),
61200
+ stock: state.stock,
61201
+ suppressed: state.suppressed,
61202
+ underbarrelActive: state.underbarrelActive,
61203
+ mass: state.mass,
61204
+ aimSeconds: state.aimSeconds
61205
+ });
58709
61206
  const host = {
58710
61207
  THREE,
58711
61208
  scene: scene2,
@@ -58732,22 +61229,9 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58732
61229
  ...options.blastShove ? { blastShove: options.blastShove } : {},
58733
61230
  blastReach: (point, target, ignore) => world.blastReach(point, target, ignore ? { ignore } : {}),
58734
61231
  tool: () => ({
58735
- weapon: toolId,
58736
- category: tool.def.category,
58737
- ammo: tool.ammo,
58738
- capacity: tool.capacity,
58739
- reserve: tool.reserve,
58740
- reloading: tool.reloading,
58741
- mode: tool.mode,
58742
- ...tool.fault ? { fault: tool.fault } : {},
58743
- safety: tool.safety,
58744
- heat: tool.heatLevel,
58745
- attachments: tool.attachments.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } })),
58746
- stock: tool.stock,
58747
- suppressed: tool.suppressed,
58748
- underbarrelActive: tool.underbarrelActive,
58749
- mass: tool.mass,
58750
- aimSeconds: tool.aimSeconds
61232
+ ...hostToolOf(toolId, tool),
61233
+ ...dualReload.stowed("R") ? { stowed: true } : {},
61234
+ ...offTool && offToolId ? { offHand: { ...hostToolOf(offToolId, offTool), ...dualReload.stowed("L") ? { stowed: true } : {} } } : {}
58751
61235
  }),
58752
61236
  command(request) {
58753
61237
  const result2 = runToolCommand(request);
@@ -58784,7 +61268,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58784
61268
  onFrame(update, frameOptions) {
58785
61269
  return scheduler.add((ctx) => update(ctx.dt), { phase: frameOptions?.phase ?? "lateSimulate", order: frameOptions?.order ?? 0, id: `module.frame#${moduleFrameCount++}` });
58786
61270
  },
58787
- holdFrame: () => bodyHoldFrame(),
61271
+ holdFrame: (hand) => bodyHoldFrame(hand),
58788
61272
  reachFor(reach) {
58789
61273
  if (!avatar) {
58790
61274
  reach.onGrip?.();
@@ -59160,6 +61644,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
59160
61644
  fluids,
59161
61645
  film: filmBridge,
59162
61646
  vfx: vfx.world,
61647
+ effects: vfx,
59163
61648
  fluidRenderer: fluidView,
59164
61649
  audio,
59165
61650
  environment,
@@ -59175,6 +61660,29 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
59175
61660
  clear,
59176
61661
  resetAll,
59177
61662
  setTool,
61663
+ setOffTool,
61664
+ get offTool() {
61665
+ return offTool;
61666
+ },
61667
+ useOff(hold = 0) {
61668
+ if (!offTool) return "cooldown";
61669
+ const status = useTool(FIXED_DT, false, "L");
61670
+ flush();
61671
+ let t0 = 0;
61672
+ while (t0 < hold) {
61673
+ useTool(FIXED_DT, true, "L");
61674
+ directStep = true;
61675
+ try {
61676
+ runHeadless(() => scheduler.advance(FIXED_DT));
61677
+ } finally {
61678
+ directStep = false;
61679
+ }
61680
+ t0 += FIXED_DT;
61681
+ }
61682
+ releaseTrigger("L");
61683
+ return status;
61684
+ },
61685
+ reload: () => reloadHands(),
59178
61686
  useItem,
59179
61687
  use(hold = 0) {
59180
61688
  if (tool.def.category === "hand") {
@@ -59472,7 +61980,8 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
59472
61980
  ...options.toolMenu !== false ? {
59473
61981
  toolMenu: {
59474
61982
  groups: toolMenuGroups,
59475
- onPick: equip,
61983
+ onPick: pickTool,
61984
+ currentLeft: () => offToolId,
59476
61985
  // the list takes clicks: the mouse leaves the view while it is open
59477
61986
  onOpen: (open) => {
59478
61987
  if (open && document.pointerLockElement) document.exitPointerLock?.();