@adatechnology/game-test-room 0.12.0 → 0.13.0

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.
package/dist/index.d.ts CHANGED
@@ -156,6 +156,11 @@ interface HudState {
156
156
  * that fits it, such as `createPlayerAvatar` of `@adatechnology/game-characters-three`), and no character code is imported here.
157
157
  */
158
158
 
159
+ interface RoomAvatarItemUse {
160
+ readonly id: string;
161
+ readonly position: readonly [number, number, number];
162
+ readonly rotation: readonly [number, number, number];
163
+ }
159
164
  /** What the room tells the avatar every frame. */
160
165
  interface RoomAvatarState {
161
166
  /** Ground speed of the player, m/s (from the movement of the feet, jumps ignored). */
@@ -168,6 +173,10 @@ interface RoomAvatarState {
168
173
  armed: boolean;
169
174
  /** Camera pitch, radians, positive looking up (the room's own convention). */
170
175
  pitch: number;
176
+ /** Whether the player is carrying an item in hand. */
177
+ holding?: boolean;
178
+ /** Active scripted item use (`drink`, `sip`, `chug`, `pour`, `open`, `close`, `tilt`). */
179
+ itemUse?: RoomAvatarItemUse | null;
171
180
  }
172
181
  /** What the room tells the first-person arms every frame, after the modules have placed the tool in the hands. */
173
182
  interface RoomFirstPersonState {
@@ -397,6 +406,10 @@ interface TestRoomOptions {
397
406
  * supplies the real model; where it answers nothing the room draws a plain primitive (a sphere, a cylinder, a box).
398
407
  */
399
408
  projectileModel?: (query: ProjectileQuery) => ProjectileModel | null | undefined;
409
+ /** Keys (`KeyboardEvent.code`) that crouch while held; default `['KeyC']`. Ctrl is left out on purpose: Ctrl+W closes the tab. */
410
+ crouch?: {
411
+ keys?: string[];
412
+ };
400
413
  /** Spawn these catalogue ids on start, in a row in front of the player. */
401
414
  initial?: string[];
402
415
  /** Open on one catalogue group (`furniture`, `structure`, `weapons`…): see `TestRoom.openArea`. */
@@ -4276,13 +4276,14 @@ var GameTestRoom = (() => {
4276
4276
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
4277
4277
  let exitPoint;
4278
4278
  if (penetrated) {
4279
- exitPoint = vec.madd(entry, d, Math.min(pathLen, effM + layersThickness(p.layers, face) / slant + layersThickness(p.layers, -face) / slant));
4279
+ const exitDist = p.spec.contains || p.spec.fill !== void 0 && p.spec.fill < 0.5 || p.thickness < pathLen * 0.5 ? pathLen : Math.min(pathLen, effM + layersThickness(p.layers, face) / slant + layersThickness(p.layers, -face) / slant);
4280
+ exitPoint = vec.madd(entry, d, exitDist);
4280
4281
  const hole = addHole(o, p, entry, n, caliber / 1e3 * (mat.ductility >= 0.3 ? 1 : 1.5), true, exitPoint, src);
4281
4282
  if (p.layers.length > 0) spallOuter(o, p, exitPoint, -face, vec.scale(n, -1), Math.max(0.03, caliber / 1e3 * 4), src);
4282
4283
  addBreach(o, p, entry, Math.PI * (hole.diameter / 2) * (hole.diameter / 2), E, d, src);
4283
4284
  if (mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_DISHING) addDent(o, p, entry, n, Math.max(caliber * 3 / 1e3, 0.012), clamp2(E * 2e-5, 1e-3, 0.03), src);
4284
4285
  if (brittle && mat.fracture === "shatter") addCrack(o, p, entry, n, crackLen, src);
4285
- if (p.spec.contains && (p.st.liters ?? 0) > 0) {
4286
+ if (p.spec.contains && (p.st.liters ?? 0) > 1e-4) {
4286
4287
  emit2({
4287
4288
  ...base(o, src),
4288
4289
  type: "leak",
@@ -16728,7 +16729,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16728
16729
  depthWrite: false,
16729
16730
  polygonOffset: true,
16730
16731
  polygonOffsetFactor: -4,
16731
- polygonOffsetUnits: -4
16732
+ polygonOffsetUnits: -4,
16733
+ side: this.THREE.DoubleSide
16732
16734
  }));
16733
16735
  }
16734
16736
  /** Opaque patch in the colour of what shows under a spalled coat. */
@@ -17120,6 +17122,27 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17120
17122
  }
17121
17123
  addHole(mv, worldPoint, worldNormal, diameter) {
17122
17124
  const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
17125
+ const work = mv.host ? mv.host.work : mv.work;
17126
+ if (work && mv.touched) {
17127
+ const ln = worldNormal.clone().transformDirection(this.m4.copy(mv.mesh.matrixWorld).invert()).normalize();
17128
+ let minDistSq = Infinity;
17129
+ let nearestDisp = 0;
17130
+ const from = mv.range ? mv.range[0] * 3 : 0;
17131
+ const to = mv.range ? mv.range[1] * 3 : work.length;
17132
+ for (let i = from; i < to; i += 3) {
17133
+ const dx = work[i] - pos.x;
17134
+ const dy = work[i + 1] - pos.y;
17135
+ const dz = work[i + 2] - pos.z;
17136
+ const d2 = dx * dx + dy * dy + dz * dz;
17137
+ if (d2 < minDistSq) {
17138
+ minDistSq = d2;
17139
+ nearestDisp = dx * ln.x + dy * ln.y + dz * ln.z;
17140
+ }
17141
+ }
17142
+ if (minDistSq < (0.06 / s) ** 2 && nearestDisp < 0) {
17143
+ pos.addScaledVector(ln, nearestDisp);
17144
+ }
17145
+ }
17123
17146
  const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.discMaterial());
17124
17147
  disc.position.copy(pos);
17125
17148
  disc.quaternion.copy(quat2);
@@ -17589,7 +17612,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17589
17612
  gas: 15921906,
17590
17613
  sap: 12098362,
17591
17614
  acid: 10211900,
17592
- blood: 9046538
17615
+ blood: 9046538,
17616
+ soda: 3808786,
17617
+ coffee: 3875860,
17618
+ beer: 14259516,
17619
+ milk: 16052714,
17620
+ wine: 7016479,
17621
+ juice: 15238923
17593
17622
  };
17594
17623
  var SMOKE_TONES = { white: 15132390, grey: 9079434, black: 2763306 };
17595
17624
  var DEFAULT_SPEED = {
@@ -18114,13 +18143,39 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18114
18143
  }
18115
18144
  case "leak": {
18116
18145
  const key = `${ev.object}|${ev.part}`;
18146
+ const b = this.bounds.get(ev.object);
18147
+ let localPoint;
18148
+ let localDirection;
18149
+ if (b) {
18150
+ b.root.updateWorldMatrix(true, false);
18151
+ const inv = new this.THREE.Matrix4().copy(b.root.matrixWorld).invert();
18152
+ const lp = this.v1.set(ev.point[0], ev.point[1], ev.point[2]).applyMatrix4(inv);
18153
+ const ld = this.v2.set(ev.direction[0], ev.direction[1], ev.direction[2]).transformDirection(inv).normalize();
18154
+ localPoint = [lp.x, lp.y, lp.z];
18155
+ localDirection = [ld.x, ld.y, ld.z];
18156
+ }
18117
18157
  const same = this.leaks.find((l) => l.object === ev.object && l.part === ev.part && Math.hypot(l.point[0] - ev.point[0], l.point[1] - ev.point[1], l.point[2] - ev.point[2]) < 0.03);
18118
18158
  if (same) {
18119
18159
  same.rate = ev.rate;
18120
18160
  same.direction = ev.direction;
18161
+ if (localPoint) same.localPoint = localPoint;
18162
+ if (localDirection) same.localDirection = localDirection;
18121
18163
  } else {
18122
18164
  if (this.leaks.length >= 24) this.leaks.shift();
18123
- this.leaks.push({ object: ev.object, part: ev.part, fluid: ev.fluid, point: ev.point, direction: ev.direction, rate: ev.rate, pressurized: ev.pressurized, acc: 0, opened: this.world.part(ev.object, ev.part)?.holes.some((h) => h.through) ?? false });
18165
+ this.leaks.push({
18166
+ object: ev.object,
18167
+ part: ev.part,
18168
+ fluid: ev.fluid,
18169
+ point: ev.point,
18170
+ direction: ev.direction,
18171
+ rate: ev.rate,
18172
+ pressurized: ev.pressurized,
18173
+ acc: 0,
18174
+ opened: this.world.part(ev.object, ev.part)?.holes.some((h) => h.through) ?? false,
18175
+ localPoint,
18176
+ localDirection,
18177
+ life: 0
18178
+ });
18124
18179
  }
18125
18180
  void key;
18126
18181
  break;
@@ -18556,21 +18611,44 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18556
18611
  updateLeaks(dt) {
18557
18612
  for (let i = this.leaks.length - 1; i >= 0; i--) {
18558
18613
  const l = this.leaks[i];
18614
+ const b = this.bounds.get(l.object);
18559
18615
  const ps = this.world.part(l.object, l.part);
18560
- if (!ps || ps.liters === void 0 || ps.liters <= 0 || l.opened && !ps.holes.some((h) => h.through)) {
18616
+ if (!b || !ps || ps.liters === void 0 || ps.liters <= 1e-4 || l.opened && !ps.holes.some((h) => h.through)) {
18561
18617
  this.leaks.splice(i, 1);
18562
18618
  continue;
18563
18619
  }
18620
+ l.life = (l.life ?? 0) + dt;
18621
+ if (l.pressurized && l.life > 0.8) l.pressurized = false;
18622
+ if (l.lastLiters !== void 0 && Math.abs(ps.liters - l.lastLiters) < 1e-5) {
18623
+ l.idleSeconds = (l.idleSeconds ?? 0) + dt;
18624
+ if (l.idleSeconds > 0.4) {
18625
+ this.leaks.splice(i, 1);
18626
+ continue;
18627
+ }
18628
+ } else {
18629
+ l.idleSeconds = 0;
18630
+ l.lastLiters = ps.liters;
18631
+ }
18632
+ let pt2 = l.point;
18633
+ let dir = l.direction;
18634
+ if (l.localPoint && l.localDirection) {
18635
+ b.root.updateWorldMatrix(true, false);
18636
+ const wp = this.v1.set(l.localPoint[0], l.localPoint[1], l.localPoint[2]).applyMatrix4(b.root.matrixWorld);
18637
+ const wd = this.v2.set(l.localDirection[0], l.localDirection[1], l.localDirection[2]).transformDirection(b.root.matrixWorld).normalize();
18638
+ pt2 = [wp.x, wp.y, wp.z];
18639
+ dir = [wd.x, wd.y, wd.z];
18640
+ l.point = pt2;
18641
+ l.direction = dir;
18642
+ }
18564
18643
  l.acc += clamp6(20 + l.rate * 400, 15, 120) * dt;
18565
18644
  const color = FLUID_COLORS[l.fluid] ?? 11184810;
18566
- const d = l.direction;
18567
- const dl = Math.hypot(d[0], d[1], d[2]) || 1;
18645
+ const dl = Math.hypot(dir[0], dir[1], dir[2]) || 1;
18568
18646
  const sp = l.pressurized ? 2.5 + this.rng() * 1.5 : 0.3 + this.rng() * 0.3;
18569
18647
  while (l.acc >= 1) {
18570
18648
  l.acc -= 1;
18571
- const vel = [d[0] / dl * sp + (this.rng() - 0.5) * 0.25, d[1] / dl * sp + (this.rng() - 0.5) * 0.25, d[2] / dl * sp + (this.rng() - 0.5) * 0.25];
18572
- if (l.fluid === "gas") this.vfx.spawn("steam", l.point, vel, { color, size: 0.5, alpha: 0.5 });
18573
- else this.vfx.spawn("drips", l.point, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0, owner: this.owner });
18649
+ const vel = [dir[0] / dl * sp + (this.rng() - 0.5) * 0.25, dir[1] / dl * sp + (this.rng() - 0.5) * 0.25, dir[2] / dl * sp + (this.rng() - 0.5) * 0.25];
18650
+ if (l.fluid === "gas") this.vfx.spawn("steam", pt2, vel, { color, size: 0.5, alpha: 0.5 });
18651
+ else this.vfx.spawn("drips", pt2, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0, owner: this.owner });
18574
18652
  }
18575
18653
  }
18576
18654
  }
@@ -39658,6 +39736,9 @@ void main(){
39658
39736
  var EYE = 1.7;
39659
39737
  var PLAYER_RADIUS = 0.3;
39660
39738
  var WALK = 3.4;
39739
+ var CROUCH_DROP = 0.55;
39740
+ var CROUCH_WALK = 1.7;
39741
+ var CROUCH_RATE = 9;
39661
39742
  var RUN = 6.2;
39662
39743
  var JUMP = 4.6;
39663
39744
  var GRAVITY4 = 13;
@@ -39668,17 +39749,20 @@ void main(){
39668
39749
  let pitch = 0;
39669
39750
  let y = 0;
39670
39751
  let eye = EYE;
39752
+ let crouch = 0;
39671
39753
  let vy2 = 0;
39672
39754
  let walked = 0;
39673
39755
  let running = false;
39674
39756
  const px = { x: o.start[0], z: o.start[2] };
39675
39757
  let locked = false;
39676
39758
  const keys = /* @__PURE__ */ new Set();
39759
+ const crouchKeys = o.crouchKeys ?? ["KeyC"];
39760
+ const isCrouchHeld = () => crouchKeys.some((code) => keys.has(code));
39677
39761
  const euler = new THREE.Euler(0, 0, 0, "YXZ");
39678
39762
  const apply = () => {
39679
39763
  euler.set(pitch, yaw, 0, "YXZ");
39680
39764
  cam.quaternion.setFromEuler(euler);
39681
- cam.position.set(px.x, eye + y, px.z);
39765
+ cam.position.set(px.x, eye + y - CROUCH_DROP * crouch, px.z);
39682
39766
  cam.updateMatrixWorld(true);
39683
39767
  };
39684
39768
  apply();
@@ -39705,6 +39789,20 @@ void main(){
39705
39789
  };
39706
39790
  const onLockChange = () => {
39707
39791
  locked = document.pointerLockElement === o.dom;
39792
+ if (locked) {
39793
+ try {
39794
+ const nav = typeof navigator !== "undefined" ? navigator : null;
39795
+ nav?.keyboard?.lock?.().catch(() => {
39796
+ });
39797
+ } catch {
39798
+ }
39799
+ } else {
39800
+ try {
39801
+ const nav = typeof navigator !== "undefined" ? navigator : null;
39802
+ nav?.keyboard?.unlock?.();
39803
+ } catch {
39804
+ }
39805
+ }
39708
39806
  };
39709
39807
  const onMouseDown = (e) => {
39710
39808
  o.dom.focus?.();
@@ -39774,6 +39872,10 @@ void main(){
39774
39872
  const onKeyDown = (e) => {
39775
39873
  if (typing(e)) return;
39776
39874
  if (e.code === "Space" || e.code === "Tab") e.preventDefault();
39875
+ const isControlHeld = e.ctrlKey || e.metaKey || keys.has("ControlLeft") || keys.has("ControlRight");
39876
+ if (isControlHeld || e.altKey && locked) {
39877
+ if (!/^F\d+$/.test(e.code)) e.preventDefault();
39878
+ }
39777
39879
  if (!e.repeat) keys.add(e.code);
39778
39880
  o.hooks.key(e.code, true, e);
39779
39881
  };
@@ -39784,6 +39886,11 @@ void main(){
39784
39886
  };
39785
39887
  const onContext = (e) => e.preventDefault();
39786
39888
  const onBlur = () => keys.clear();
39889
+ const onBeforeUnload = (e) => {
39890
+ if (!locked && !keys.has("ControlLeft") && !keys.has("ControlRight")) return;
39891
+ e.preventDefault();
39892
+ e.returnValue = "";
39893
+ };
39787
39894
  o.dom.addEventListener("mousedown", onMouseDown);
39788
39895
  window.addEventListener("mouseup", onMouseUp);
39789
39896
  window.addEventListener("mousemove", onMouseMove);
@@ -39793,6 +39900,7 @@ void main(){
39793
39900
  window.addEventListener("keydown", onKeyDown);
39794
39901
  window.addEventListener("keyup", onKeyUp);
39795
39902
  window.addEventListener("blur", onBlur);
39903
+ window.addEventListener("beforeunload", onBeforeUnload);
39796
39904
  return {
39797
39905
  get yaw() {
39798
39906
  return yaw;
@@ -39821,12 +39929,18 @@ void main(){
39821
39929
  get running() {
39822
39930
  return running;
39823
39931
  },
39932
+ get crouching() {
39933
+ return isCrouchHeld();
39934
+ },
39824
39935
  keys,
39825
39936
  update(dt) {
39826
- const run = keys.has("ShiftLeft") || keys.has("ShiftRight");
39937
+ const crouched = isCrouchHeld();
39938
+ crouch += ((crouched ? 1 : 0) - crouch) * Math.min(1, dt * CROUCH_RATE);
39939
+ if (!crouched && crouch < 1e-3) crouch = 0;
39940
+ const run = !crouched && (keys.has("ShiftLeft") || keys.has("ShiftRight"));
39827
39941
  const fromX = px.x;
39828
39942
  const fromZ = px.z;
39829
- const speed = run ? RUN : WALK;
39943
+ const speed = crouched ? CROUCH_WALK : run ? RUN : WALK;
39830
39944
  let f = 0;
39831
39945
  let r = 0;
39832
39946
  if (keys.has("KeyW") || keys.has("ArrowUp")) f += 1;
@@ -39902,13 +40016,14 @@ void main(){
39902
40016
  window.removeEventListener("keydown", onKeyDown);
39903
40017
  window.removeEventListener("keyup", onKeyUp);
39904
40018
  window.removeEventListener("blur", onBlur);
40019
+ window.removeEventListener("beforeunload", onBeforeUnload);
39905
40020
  if (document.pointerLockElement === o.dom) document.exitPointerLock?.();
39906
40021
  }
39907
40022
  };
39908
40023
  }
39909
40024
 
39910
40025
  // src/avatar.ts
39911
- var DEFAULT_STRIKE_CLIPS = { fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
40026
+ var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
39912
40027
  var DEFAULT_HAND_STANCES = { fists: "fists" };
39913
40028
  var THIRD_PERSON_DEFAULTS = {
39914
40029
  back: 3.8,
@@ -40149,7 +40264,7 @@ void main(){
40149
40264
  "hud.throw": "throw",
40150
40265
  "hud.tilt": "tilt (hold)",
40151
40266
  "hud.equip": "equip {weapon}",
40152
- "ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use \xB7 E: grab/drop \xB7 G or RMB: throw \xB7 R: reload \xB7 Q: fire mode \xB7 X: clear a jam \xB7 B: safety \xB7 Y: detonate charges set down \xB7 Tab: panels \xB7 Esc: free the mouse",
40267
+ "ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 C: crouch \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use \xB7 E: grab/drop \xB7 G or RMB: throw \xB7 R: reload \xB7 Q: fire mode \xB7 X: clear a jam \xB7 B: safety \xB7 Y: detonate charges set down \xB7 Tab: panels \xB7 Esc: free the mouse",
40153
40268
  "ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
40154
40269
  "camera.first": "1st person",
40155
40270
  "camera.third": "3rd person",
@@ -40170,6 +40285,8 @@ void main(){
40170
40285
  "ui.slow": "Slow motion 0.25\xD7",
40171
40286
  "ui.fast": "Time \xD710",
40172
40287
  "ui.tap": "Tap",
40288
+ "ui.fullscreen": "Full screen",
40289
+ "ui.fullscreen.hint": "Full screen opened from here keeps the whole keyboard in the game, Ctrl+W included (F11 does not)",
40173
40290
  "ui.blast.shelter": "Shelter",
40174
40291
  "ui.blast.fragmentHits": "Fragments",
40175
40292
  "ui.blast.confinement": "Confinement",
@@ -40428,7 +40545,7 @@ void main(){
40428
40545
  "hud.throw": "arremessar",
40429
40546
  "hud.tilt": "inclinar (segurar)",
40430
40547
  "hud.equip": "equipar {weapon}",
40431
- "ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar \xB7 E: pegar/soltar \xB7 G ou bot\xE3o direito: arremessar \xB7 R: recarregar \xB7 Q: modo de tiro \xB7 X: destravar \xB7 B: trava \xB7 Y: detonar cargas postas \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
40548
+ "ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 C: agachar \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar \xB7 E: pegar/soltar \xB7 G ou bot\xE3o direito: arremessar \xB7 R: recarregar \xB7 Q: modo de tiro \xB7 X: destravar \xB7 B: trava \xB7 Y: detonar cargas postas \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
40432
40549
  "ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
40433
40550
  "camera.first": "1\xAA pessoa",
40434
40551
  "camera.third": "3\xAA pessoa",
@@ -40449,6 +40566,8 @@ void main(){
40449
40566
  "ui.slow": "C\xE2mera lenta 0,25\xD7",
40450
40567
  "ui.fast": "Tempo \xD710",
40451
40568
  "ui.tap": "Torneira",
40569
+ "ui.fullscreen": "Tela cheia",
40570
+ "ui.fullscreen.hint": "A tela cheia aberta por aqui segura o teclado todo no jogo, inclusive Ctrl+W (o F11 n\xE3o)",
40452
40571
  "ui.blast.shelter": "Abrigo",
40453
40572
  "ui.blast.fragmentHits": "Estilha\xE7os",
40454
40573
  "ui.blast.confinement": "Confinamento",
@@ -40876,6 +40995,19 @@ void main(){
40876
40995
  e.style.setProperty("--tr-thumb", `url("${thumbSource(thumbnail)}")`);
40877
40996
  return e;
40878
40997
  };
40998
+ async function toggleFullscreen() {
40999
+ const keyboard = navigator.keyboard;
41000
+ try {
41001
+ if (document.fullscreenElement) {
41002
+ keyboard?.unlock?.();
41003
+ await document.exitFullscreen();
41004
+ return;
41005
+ }
41006
+ await document.documentElement.requestFullscreen();
41007
+ await keyboard?.lock?.();
41008
+ } catch {
41009
+ }
41010
+ }
40879
41011
  function createUi(o) {
40880
41012
  const { t } = o;
40881
41013
  const style = mk2("style");
@@ -41087,6 +41219,8 @@ void main(){
41087
41219
  o.onFast(fast);
41088
41220
  });
41089
41221
  const tapBtn = ctl(t("ui.tap"), () => o.onTap());
41222
+ const fullscreenBtn = ctl(t("ui.fullscreen"), () => void toggleFullscreen());
41223
+ fullscreenBtn.title = t("ui.fullscreen.hint");
41090
41224
  const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
41091
41225
  for (const { key, b } of blastBtns) b.title = t(`ui.blast.${key}.hint`);
41092
41226
  const qSel = mk2("select");
@@ -41640,6 +41774,7 @@ void main(){
41640
41774
  blockers,
41641
41775
  limit: arenaSize,
41642
41776
  start: arena.playerStart,
41777
+ crouchKeys: options.crouch?.keys,
41643
41778
  hooks: {
41644
41779
  primaryDown: () => {
41645
41780
  useHeld = true;
@@ -41892,6 +42027,7 @@ void main(){
41892
42027
  const third = { ...THIRD_PERSON_DEFAULTS, ...options.thirdPerson };
41893
42028
  let avatar = null;
41894
42029
  let cameraMode = options.cameraMode ?? "first";
42030
+ let crouchForced = false;
41895
42031
  const avatarState = { speed: 0, running: false, crouch: false, down: false, armed: false, pitch: 0 };
41896
42032
  let feetPrev = null;
41897
42033
  let armedForced = null;
@@ -41945,9 +42081,12 @@ void main(){
41945
42081
  if (avatarState.speed < 0.02) avatarState.speed = 0;
41946
42082
  }
41947
42083
  feetPrev = [x, z];
41948
- avatarState.running = player.keys.has("ShiftLeft") || player.keys.has("ShiftRight");
42084
+ avatarState.running = !player.crouching && (player.keys.has("ShiftLeft") || player.keys.has("ShiftRight"));
42085
+ avatarState.crouch = crouchForced || player.crouching;
41949
42086
  avatarState.armed = armedForced ?? tool.def.action.kind === "firearm";
41950
42087
  avatarState.pitch = player.pitch;
42088
+ avatarState.holding = heldPiece !== null;
42089
+ avatarState.itemUse = useItemPlayer ? { id: useItemName, ...useItemPlayer.pose() } : heldPiece && heldTilt > 0.01 ? { id: "tilt", position: [0.2, -0.1, -0.38], rotation: [-heldTilt, 0, 0] } : null;
41951
42090
  avatar.root.position.set(x, player.feet, z);
41952
42091
  avatar.root.rotation.set(0, player.yaw + Math.PI, 0);
41953
42092
  avatar.update(dt, avatarState);
@@ -42403,7 +42542,11 @@ void main(){
42403
42542
  const useTool = (dt, held) => {
42404
42543
  aim();
42405
42544
  const cat = tool.def.category;
42406
- if (cat === "hand") return;
42545
+ if (cat === "hand") {
42546
+ if (strikes.some((strike) => strike.key === "hand" || strike.key === "fists")) return;
42547
+ 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");
42548
+ return;
42549
+ }
42407
42550
  if (strikes.some((strike) => strike.key === toolId)) return;
42408
42551
  const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
42409
42552
  flushEvents(toolId, tool, res.recoil);
@@ -42496,6 +42639,7 @@ void main(){
42496
42639
  let heldTilt = 0;
42497
42640
  let tiltKey = 0;
42498
42641
  const heldLocal = new THREE.Vector3();
42642
+ const heldSize = new THREE.Vector3(0.2, 0.2, 0.2);
42499
42643
  let heldRadius = 0.2;
42500
42644
  const heldVel = [0, 0, 0];
42501
42645
  let useItemPlayer = null;
@@ -42541,12 +42685,15 @@ void main(){
42541
42685
  heldName = nameOf(r.object);
42542
42686
  piece.held = true;
42543
42687
  piece.sleeping = false;
42688
+ const savedQ = piece.root.quaternion.clone();
42689
+ piece.root.quaternion.identity();
42690
+ piece.root.updateMatrixWorld(true);
42544
42691
  const box2 = new THREE.Box3().setFromObject(piece.root);
42545
- const c = box2.getCenter(new THREE.Vector3());
42692
+ box2.getCenter(heldLocal).sub(piece.root.position);
42693
+ box2.getSize(heldSize);
42694
+ piece.root.quaternion.copy(savedQ);
42546
42695
  piece.root.updateMatrixWorld(true);
42547
- heldLocal.copy(c).sub(piece.root.position);
42548
- heldLocal.applyQuaternion(piece.root.quaternion.clone().invert());
42549
- heldRadius = box2.getSize(new THREE.Vector3()).length() / 3;
42696
+ heldRadius = heldSize.length() / 3;
42550
42697
  heldYaw = 0;
42551
42698
  heldTilt = 0;
42552
42699
  heldVel[0] = heldVel[1] = heldVel[2] = 0;
@@ -42567,18 +42714,43 @@ void main(){
42567
42714
  fire("throw", { objectId: id, speed });
42568
42715
  return true;
42569
42716
  };
42570
- const carriedTargets = (root) => {
42717
+ const carriedTargets = (root, gripQ) => {
42718
+ const width2 = Math.max(heldSize.x, heldSize.z);
42719
+ const sideGrip = heldSize.y > width2 * 1.04 && width2 <= 0.22;
42720
+ if (sideGrip) {
42721
+ const center2 = heldLocal.clone().applyQuaternion(root.quaternion).add(root.position);
42722
+ const radius = Math.min(heldSize.x, heldSize.z) * 0.5;
42723
+ const handX = new THREE.Vector3(-0.35, 0, -0.94).normalize().applyQuaternion(gripQ);
42724
+ const handY = new THREE.Vector3(0.94, 0, -0.35).normalize().applyQuaternion(gripQ);
42725
+ const handZ = new THREE.Vector3().crossVectors(handX, handY).normalize();
42726
+ const q2 = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(handX, handY, handZ));
42727
+ const up = new THREE.Vector3(0, 1, 0).applyQuaternion(gripQ);
42728
+ const palm = center2.clone().addScaledVector(handY, radius + CARRY_PALM).addScaledVector(handX, -0.022).addScaledVector(up, heldSize.y > 0.16 ? -heldSize.y * 0.08 : -5e-3);
42729
+ if (useItemName === "open" || useItemName === "close") {
42730
+ const leftX = new THREE.Vector3(0.35, 0, -0.94).normalize().applyQuaternion(gripQ);
42731
+ const leftY = new THREE.Vector3(-0.94, 0, -0.35).normalize().applyQuaternion(gripQ);
42732
+ const leftZ = new THREE.Vector3().crossVectors(leftX, leftY).normalize();
42733
+ const lq = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(leftX, leftY, leftZ));
42734
+ const leftPalm = center2.clone().addScaledVector(up, heldSize.y * 0.45).addScaledVector(leftY, radius * 0.65 + CARRY_PALM).addScaledVector(leftX, -0.018);
42735
+ return {
42736
+ R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] },
42737
+ L: { position: [leftPalm.x, leftPalm.y, leftPalm.z], quaternion: [lq.x, lq.y, lq.z, lq.w] },
42738
+ grip: 0.85,
42739
+ gripL: 0.8
42740
+ };
42741
+ }
42742
+ return { R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] }, grip: 0.85 };
42743
+ }
42571
42744
  const box2 = new THREE.Box3().setFromObject(root);
42572
42745
  const center = box2.getCenter(new THREE.Vector3());
42573
- const forward = camera2.getWorldDirection(new THREE.Vector3());
42574
- forward.y = 0;
42575
- if (forward.lengthSq() < 1e-6) forward.set(0, 0, -1);
42576
- forward.normalize();
42577
- const back = new THREE.Vector3(0, -1, 0);
42578
- const z = forward.clone().cross(back).normalize();
42579
- const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(forward, back, z));
42746
+ const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(gripQ).normalize();
42747
+ const back = new THREE.Vector3(0, -1, 0).applyQuaternion(gripQ).normalize();
42748
+ const z = new THREE.Vector3().crossVectors(forward, back).normalize();
42749
+ const y = new THREE.Vector3().crossVectors(z, forward).normalize();
42750
+ const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(forward, y, z));
42580
42751
  return { R: { position: [center.x, box2.min.y - CARRY_PALM, center.z], quaternion: [q.x, q.y, q.z, q.w] }, grip: CARRY_GRIP };
42581
42752
  };
42753
+ const gripQuat = new THREE.Quaternion();
42582
42754
  const followHeld = (dt) => {
42583
42755
  const piece = heldPiece;
42584
42756
  carriedHand = null;
@@ -42588,21 +42760,47 @@ void main(){
42588
42760
  let pos;
42589
42761
  const targetQ = tmpQ;
42590
42762
  const hand = thirdActive() ? avatar?.hand?.("R") : void 0;
42763
+ const isDrink = useItemName === "drink" || useItemName === "sip" || useItemName === "chug";
42591
42764
  if (hand) {
42592
- pos = hand.getWorldPosition(tmpHand);
42593
- targetQ.setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw + heldYaw);
42594
- targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
42765
+ const handWorld = hand.getWorldPosition(tmpHand);
42766
+ const yawQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
42767
+ const pose = useItemPlayer?.pose();
42768
+ const rx = pose ? isDrink ? Math.abs(pose.rotation[0]) * 0.9 : pose.rotation[0] : -heldTilt;
42769
+ const ry = pose ? pose.rotation[1] : 0;
42770
+ const rz = pose ? pose.rotation[2] : 0;
42771
+ gripQuat.copy(yawQ).multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.max(-1.35, Math.min(1.25, rx)), ry, rz, "XYZ")));
42772
+ targetQ.copy(yawQ).multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(rx, ry, rz, "XYZ")));
42773
+ targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
42774
+ const gripOffset = new THREE.Vector3(-0.02, 0.015, -0.04).applyQuaternion(gripQuat);
42775
+ pos = tmpV2.copy(handWorld).add(gripOffset);
42595
42776
  } else if (useItemPlayer) {
42596
42777
  const pose = useItemPlayer.pose();
42597
- const handQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
42598
- pos = tmpV2.set(pose.position[0], pose.position[1], pose.position[2]).applyQuaternion(handQ).add(camera2.position);
42599
- const eq2 = new THREE.Quaternion().setFromEuler(new THREE.Euler(pose.rotation[0], pose.rotation[1], pose.rotation[2], "XYZ"));
42600
- targetQ.copy(handQ).multiply(eq2);
42778
+ const frameQ = avatar?.firstPerson ? camera2.quaternion : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
42779
+ const raise = isDrink ? Math.max(0, Math.min(1, (pose.position[1] + 0.2) / 0.22)) : 0;
42780
+ const px2 = pose.position[0] + (isDrink ? 0.02 * raise : 0);
42781
+ const py2 = pose.position[1] - (isDrink ? 0.08 * raise : 0);
42782
+ const pz2 = pose.position[2] - (isDrink ? 0.05 * raise : 0);
42783
+ pos = tmpV2.set(px2, py2, pz2).applyQuaternion(frameQ).add(camera2.position);
42784
+ const rx = isDrink ? Math.abs(pose.rotation[0]) * 0.85 : pose.rotation[0];
42785
+ const eq2 = new THREE.Quaternion().setFromEuler(new THREE.Euler(rx, pose.rotation[1], pose.rotation[2], "XYZ"));
42786
+ const wristEq = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.max(-1.35, Math.min(1.25, rx)), pose.rotation[1], pose.rotation[2], "XYZ"));
42787
+ gripQuat.copy(frameQ).multiply(wristEq);
42788
+ targetQ.copy(frameQ).multiply(eq2);
42601
42789
  targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
42602
42790
  } else {
42603
- pos = tmpV2.set(0.05, -0.28, avatar?.firstPerson ? -Math.min(1.15, CARRY_NEAR + heldRadius) : -1.15).applyMatrix4(camera2.matrixWorld);
42791
+ const small = heldRadius < 0.22;
42792
+ const holdX = avatar?.firstPerson ? small ? 0.22 : 0.08 : 0.05;
42793
+ const holdY = avatar?.firstPerson ? small ? -0.2 : -0.26 : -0.28;
42794
+ const holdZ = avatar?.firstPerson ? small ? -0.42 : -Math.min(1.15, CARRY_NEAR + heldRadius) : -1.15;
42795
+ pos = tmpV2.set(holdX, holdY, holdZ).applyMatrix4(camera2.matrixWorld);
42796
+ const baseQ = avatar?.firstPerson && small ? camera2.quaternion : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
42797
+ gripQuat.copy(baseQ).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.max(-1.35, -heldTilt)));
42604
42798
  targetQ.setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw + heldYaw);
42605
- targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
42799
+ if (avatar?.firstPerson && small && heldTilt <= 1e-4) {
42800
+ targetQ.copy(camera2.quaternion).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
42801
+ } else {
42802
+ targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
42803
+ }
42606
42804
  }
42607
42805
  const k = Math.min(1, (useItemPlayer ? 30 : 16) * dt);
42608
42806
  const px = root.position.x;
@@ -42617,7 +42815,7 @@ void main(){
42617
42815
  heldVel[2] = (root.position.z - pz) / dt;
42618
42816
  }
42619
42817
  root.updateMatrixWorld(true);
42620
- if (avatar?.firstPerson && cameraMode === "first") carriedHand = carriedTargets(root);
42818
+ if (avatar?.firstPerson && cameraMode === "first") carriedHand = carriedTargets(root, gripQuat);
42621
42819
  };
42622
42820
  const useItem = (useId) => {
42623
42821
  const e = heldEntry;
@@ -42627,11 +42825,13 @@ void main(){
42627
42825
  const c = fluids.container(e.containerId);
42628
42826
  if (!c) return false;
42629
42827
  if (def.liquids && def.liquids.length > 0 && (!c.liquid || !def.liquids.includes(c.liquid))) return false;
42630
- if ((useId === "drink" || useId === "sip" || useId === "chug" || useId === "pour" || useId === "pour_out") && c.liters <= 1e-6) {
42828
+ const consuming = useId === "drink" || useId === "sip" || useId === "chug" || useId === "pour" || useId === "pour_out";
42829
+ if (consuming && c.liters <= 1e-6) {
42631
42830
  say(t("hud.empty_container", { name: e.name }));
42632
42831
  return false;
42633
42832
  }
42634
42833
  if (useItemPlayer && !useItemPlayer.done) return false;
42834
+ if (consuming && c.closed) fluids.open(e.containerId);
42635
42835
  useItemPlayer = createItemUse(fluids, e.containerId, def);
42636
42836
  useItemName = def.id;
42637
42837
  action("use_item", t("act.use_item", { name: e.name, use: S.text[`use.${def.id}`] ?? def.id }), { objectId: e.so.objectId, itemId: e.so.itemId, data: { use: def.id } });
@@ -42753,7 +42953,7 @@ void main(){
42753
42953
  if (heldEntry?.uses[0]) useItem(heldEntry.uses[0]);
42754
42954
  break;
42755
42955
  case "KeyC":
42756
- if (heldEntry?.uses[1]) useItem(heldEntry.uses[1]);
42956
+ if (heldEntry?.uses[1] && !(options.crouch?.keys ?? ["KeyC"]).includes("KeyC")) useItem(heldEntry.uses[1]);
42757
42957
  break;
42758
42958
  case "KeyV":
42759
42959
  if (heldEntry?.uses[2]) useItem(heldEntry.uses[2]);
@@ -42845,6 +43045,7 @@ void main(){
42845
43045
  const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, effects: vfx.world, ids: roomMetricIds, blast: () => lastBlast });
42846
43046
  let qualityName = quality;
42847
43047
  const syncContainers = (sdt) => {
43048
+ const drinkingHeld = useItemPlayer !== null && (useItemName === "drink" || useItemName === "sip" || useItemName === "chug");
42848
43049
  for (const e of entries.values()) {
42849
43050
  if (!e.containerId) continue;
42850
43051
  const host2 = hostOf(e);
@@ -42853,7 +43054,8 @@ void main(){
42853
43054
  const p = tmpV2.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(host2.root.matrixWorld);
42854
43055
  const vel = sdt > 0 ? [(p.x - e.last[0]) / sdt, (p.y - e.last[1]) / sdt, (p.z - e.last[2]) / sdt] : void 0;
42855
43056
  e.last = [p.x, p.y, p.z];
42856
- fluids.setTransform(e.containerId, [p.x, p.y, p.z], [q.x, q.y, q.z, q.w], vel);
43057
+ const rot = drinkingHeld && e === heldEntry ? [0, 0, 0, 1] : [q.x, q.y, q.z, q.w];
43058
+ fluids.setTransform(e.containerId, [p.x, p.y, p.z], rot, vel);
42857
43059
  }
42858
43060
  };
42859
43061
  const syncLiters = () => {
@@ -43604,7 +43806,10 @@ void main(){
43604
43806
  return avatar;
43605
43807
  },
43606
43808
  setAvatarState(state) {
43607
- if (state.crouch !== void 0) avatarState.crouch = state.crouch;
43809
+ if (state.crouch !== void 0) {
43810
+ crouchForced = state.crouch;
43811
+ avatarState.crouch = state.crouch || player.crouching;
43812
+ }
43608
43813
  if (state.down !== void 0) avatarState.down = state.down;
43609
43814
  if (state.armed !== void 0) armedForced = state.armed;
43610
43815
  },