@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.js CHANGED
@@ -1706,6 +1706,9 @@ function createNarrator(c) {
1706
1706
  var EYE = 1.7;
1707
1707
  var PLAYER_RADIUS = 0.3;
1708
1708
  var WALK = 3.4;
1709
+ var CROUCH_DROP = 0.55;
1710
+ var CROUCH_WALK = 1.7;
1711
+ var CROUCH_RATE = 9;
1709
1712
  var RUN = 6.2;
1710
1713
  var JUMP = 4.6;
1711
1714
  var GRAVITY = 13;
@@ -1716,17 +1719,20 @@ function createPlayer(THREE, o) {
1716
1719
  let pitch = 0;
1717
1720
  let y = 0;
1718
1721
  let eye = EYE;
1722
+ let crouch = 0;
1719
1723
  let vy = 0;
1720
1724
  let walked = 0;
1721
1725
  let running = false;
1722
1726
  const px = { x: o.start[0], z: o.start[2] };
1723
1727
  let locked = false;
1724
1728
  const keys = /* @__PURE__ */ new Set();
1729
+ const crouchKeys = o.crouchKeys ?? ["KeyC"];
1730
+ const isCrouchHeld = () => crouchKeys.some((code) => keys.has(code));
1725
1731
  const euler = new THREE.Euler(0, 0, 0, "YXZ");
1726
1732
  const apply = () => {
1727
1733
  euler.set(pitch, yaw, 0, "YXZ");
1728
1734
  cam.quaternion.setFromEuler(euler);
1729
- cam.position.set(px.x, eye + y, px.z);
1735
+ cam.position.set(px.x, eye + y - CROUCH_DROP * crouch, px.z);
1730
1736
  cam.updateMatrixWorld(true);
1731
1737
  };
1732
1738
  apply();
@@ -1753,6 +1759,20 @@ function createPlayer(THREE, o) {
1753
1759
  };
1754
1760
  const onLockChange = () => {
1755
1761
  locked = document.pointerLockElement === o.dom;
1762
+ if (locked) {
1763
+ try {
1764
+ const nav = typeof navigator !== "undefined" ? navigator : null;
1765
+ nav?.keyboard?.lock?.().catch(() => {
1766
+ });
1767
+ } catch {
1768
+ }
1769
+ } else {
1770
+ try {
1771
+ const nav = typeof navigator !== "undefined" ? navigator : null;
1772
+ nav?.keyboard?.unlock?.();
1773
+ } catch {
1774
+ }
1775
+ }
1756
1776
  };
1757
1777
  const onMouseDown = (e) => {
1758
1778
  o.dom.focus?.();
@@ -1822,6 +1842,10 @@ function createPlayer(THREE, o) {
1822
1842
  const onKeyDown = (e) => {
1823
1843
  if (typing(e)) return;
1824
1844
  if (e.code === "Space" || e.code === "Tab") e.preventDefault();
1845
+ const isControlHeld = e.ctrlKey || e.metaKey || keys.has("ControlLeft") || keys.has("ControlRight");
1846
+ if (isControlHeld || e.altKey && locked) {
1847
+ if (!/^F\d+$/.test(e.code)) e.preventDefault();
1848
+ }
1825
1849
  if (!e.repeat) keys.add(e.code);
1826
1850
  o.hooks.key(e.code, true, e);
1827
1851
  };
@@ -1832,6 +1856,11 @@ function createPlayer(THREE, o) {
1832
1856
  };
1833
1857
  const onContext = (e) => e.preventDefault();
1834
1858
  const onBlur = () => keys.clear();
1859
+ const onBeforeUnload = (e) => {
1860
+ if (!locked && !keys.has("ControlLeft") && !keys.has("ControlRight")) return;
1861
+ e.preventDefault();
1862
+ e.returnValue = "";
1863
+ };
1835
1864
  o.dom.addEventListener("mousedown", onMouseDown);
1836
1865
  window.addEventListener("mouseup", onMouseUp);
1837
1866
  window.addEventListener("mousemove", onMouseMove);
@@ -1841,6 +1870,7 @@ function createPlayer(THREE, o) {
1841
1870
  window.addEventListener("keydown", onKeyDown);
1842
1871
  window.addEventListener("keyup", onKeyUp);
1843
1872
  window.addEventListener("blur", onBlur);
1873
+ window.addEventListener("beforeunload", onBeforeUnload);
1844
1874
  return {
1845
1875
  get yaw() {
1846
1876
  return yaw;
@@ -1869,12 +1899,18 @@ function createPlayer(THREE, o) {
1869
1899
  get running() {
1870
1900
  return running;
1871
1901
  },
1902
+ get crouching() {
1903
+ return isCrouchHeld();
1904
+ },
1872
1905
  keys,
1873
1906
  update(dt) {
1874
- const run = keys.has("ShiftLeft") || keys.has("ShiftRight");
1907
+ const crouched = isCrouchHeld();
1908
+ crouch += ((crouched ? 1 : 0) - crouch) * Math.min(1, dt * CROUCH_RATE);
1909
+ if (!crouched && crouch < 1e-3) crouch = 0;
1910
+ const run = !crouched && (keys.has("ShiftLeft") || keys.has("ShiftRight"));
1875
1911
  const fromX = px.x;
1876
1912
  const fromZ = px.z;
1877
- const speed = run ? RUN : WALK;
1913
+ const speed = crouched ? CROUCH_WALK : run ? RUN : WALK;
1878
1914
  let f = 0;
1879
1915
  let r = 0;
1880
1916
  if (keys.has("KeyW") || keys.has("ArrowUp")) f += 1;
@@ -1950,13 +1986,14 @@ function createPlayer(THREE, o) {
1950
1986
  window.removeEventListener("keydown", onKeyDown);
1951
1987
  window.removeEventListener("keyup", onKeyUp);
1952
1988
  window.removeEventListener("blur", onBlur);
1989
+ window.removeEventListener("beforeunload", onBeforeUnload);
1953
1990
  if (document.pointerLockElement === o.dom) document.exitPointerLock?.();
1954
1991
  }
1955
1992
  };
1956
1993
  }
1957
1994
 
1958
1995
  // src/avatar.ts
1959
- var DEFAULT_STRIKE_CLIPS = { fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
1996
+ var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
1960
1997
  var DEFAULT_HAND_STANCES = { fists: "fists" };
1961
1998
  var THIRD_PERSON_DEFAULTS = {
1962
1999
  back: 3.8,
@@ -2197,7 +2234,7 @@ var en = {
2197
2234
  "hud.throw": "throw",
2198
2235
  "hud.tilt": "tilt (hold)",
2199
2236
  "hud.equip": "equip {weapon}",
2200
- "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",
2237
+ "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",
2201
2238
  "ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
2202
2239
  "camera.first": "1st person",
2203
2240
  "camera.third": "3rd person",
@@ -2218,6 +2255,8 @@ var en = {
2218
2255
  "ui.slow": "Slow motion 0.25\xD7",
2219
2256
  "ui.fast": "Time \xD710",
2220
2257
  "ui.tap": "Tap",
2258
+ "ui.fullscreen": "Full screen",
2259
+ "ui.fullscreen.hint": "Full screen opened from here keeps the whole keyboard in the game, Ctrl+W included (F11 does not)",
2221
2260
  "ui.blast.shelter": "Shelter",
2222
2261
  "ui.blast.fragmentHits": "Fragments",
2223
2262
  "ui.blast.confinement": "Confinement",
@@ -2476,7 +2515,7 @@ var ptBR = {
2476
2515
  "hud.throw": "arremessar",
2477
2516
  "hud.tilt": "inclinar (segurar)",
2478
2517
  "hud.equip": "equipar {weapon}",
2479
- "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",
2518
+ "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",
2480
2519
  "ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
2481
2520
  "camera.first": "1\xAA pessoa",
2482
2521
  "camera.third": "3\xAA pessoa",
@@ -2497,6 +2536,8 @@ var ptBR = {
2497
2536
  "ui.slow": "C\xE2mera lenta 0,25\xD7",
2498
2537
  "ui.fast": "Tempo \xD710",
2499
2538
  "ui.tap": "Torneira",
2539
+ "ui.fullscreen": "Tela cheia",
2540
+ "ui.fullscreen.hint": "A tela cheia aberta por aqui segura o teclado todo no jogo, inclusive Ctrl+W (o F11 n\xE3o)",
2500
2541
  "ui.blast.shelter": "Abrigo",
2501
2542
  "ui.blast.fragmentHits": "Estilha\xE7os",
2502
2543
  "ui.blast.confinement": "Confinamento",
@@ -2924,6 +2965,19 @@ var thumb = (thumbnail, credit) => {
2924
2965
  e.style.setProperty("--tr-thumb", `url("${thumbSource(thumbnail)}")`);
2925
2966
  return e;
2926
2967
  };
2968
+ async function toggleFullscreen() {
2969
+ const keyboard = navigator.keyboard;
2970
+ try {
2971
+ if (document.fullscreenElement) {
2972
+ keyboard?.unlock?.();
2973
+ await document.exitFullscreen();
2974
+ return;
2975
+ }
2976
+ await document.documentElement.requestFullscreen();
2977
+ await keyboard?.lock?.();
2978
+ } catch {
2979
+ }
2980
+ }
2927
2981
  function createUi(o) {
2928
2982
  const { t } = o;
2929
2983
  const style = mk2("style");
@@ -3135,6 +3189,8 @@ function createUi(o) {
3135
3189
  o.onFast(fast);
3136
3190
  });
3137
3191
  const tapBtn = ctl(t("ui.tap"), () => o.onTap());
3192
+ const fullscreenBtn = ctl(t("ui.fullscreen"), () => void toggleFullscreen());
3193
+ fullscreenBtn.title = t("ui.fullscreen.hint");
3138
3194
  const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
3139
3195
  for (const { key, b } of blastBtns) b.title = t(`ui.blast.${key}.hint`);
3140
3196
  const qSel = mk2("select");
@@ -3688,6 +3744,7 @@ function createTestRoom(options) {
3688
3744
  blockers,
3689
3745
  limit: arenaSize,
3690
3746
  start: arena.playerStart,
3747
+ crouchKeys: options.crouch?.keys,
3691
3748
  hooks: {
3692
3749
  primaryDown: () => {
3693
3750
  useHeld = true;
@@ -3940,6 +3997,7 @@ function createTestRoom(options) {
3940
3997
  const third = { ...THIRD_PERSON_DEFAULTS, ...options.thirdPerson };
3941
3998
  let avatar = null;
3942
3999
  let cameraMode = options.cameraMode ?? "first";
4000
+ let crouchForced = false;
3943
4001
  const avatarState = { speed: 0, running: false, crouch: false, down: false, armed: false, pitch: 0 };
3944
4002
  let feetPrev = null;
3945
4003
  let armedForced = null;
@@ -3993,9 +4051,12 @@ function createTestRoom(options) {
3993
4051
  if (avatarState.speed < 0.02) avatarState.speed = 0;
3994
4052
  }
3995
4053
  feetPrev = [x, z];
3996
- avatarState.running = player.keys.has("ShiftLeft") || player.keys.has("ShiftRight");
4054
+ avatarState.running = !player.crouching && (player.keys.has("ShiftLeft") || player.keys.has("ShiftRight"));
4055
+ avatarState.crouch = crouchForced || player.crouching;
3997
4056
  avatarState.armed = armedForced ?? tool.def.action.kind === "firearm";
3998
4057
  avatarState.pitch = player.pitch;
4058
+ avatarState.holding = heldPiece !== null;
4059
+ 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;
3999
4060
  avatar.root.position.set(x, player.feet, z);
4000
4061
  avatar.root.rotation.set(0, player.yaw + Math.PI, 0);
4001
4062
  avatar.update(dt, avatarState);
@@ -4451,7 +4512,11 @@ function createTestRoom(options) {
4451
4512
  const useTool = (dt, held) => {
4452
4513
  aim();
4453
4514
  const cat = tool.def.category;
4454
- if (cat === "hand") return;
4515
+ if (cat === "hand") {
4516
+ if (strikes.some((strike) => strike.key === "hand" || strike.key === "fists")) return;
4517
+ 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");
4518
+ return;
4519
+ }
4455
4520
  if (strikes.some((strike) => strike.key === toolId)) return;
4456
4521
  const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
4457
4522
  flushEvents(toolId, tool, res.recoil);
@@ -4544,6 +4609,7 @@ function createTestRoom(options) {
4544
4609
  let heldTilt = 0;
4545
4610
  let tiltKey = 0;
4546
4611
  const heldLocal = new THREE.Vector3();
4612
+ const heldSize = new THREE.Vector3(0.2, 0.2, 0.2);
4547
4613
  let heldRadius = 0.2;
4548
4614
  const heldVel = [0, 0, 0];
4549
4615
  let useItemPlayer = null;
@@ -4589,12 +4655,15 @@ function createTestRoom(options) {
4589
4655
  heldName = nameOf(r.object);
4590
4656
  piece.held = true;
4591
4657
  piece.sleeping = false;
4658
+ const savedQ = piece.root.quaternion.clone();
4659
+ piece.root.quaternion.identity();
4660
+ piece.root.updateMatrixWorld(true);
4592
4661
  const box2 = new THREE.Box3().setFromObject(piece.root);
4593
- const c = box2.getCenter(new THREE.Vector3());
4662
+ box2.getCenter(heldLocal).sub(piece.root.position);
4663
+ box2.getSize(heldSize);
4664
+ piece.root.quaternion.copy(savedQ);
4594
4665
  piece.root.updateMatrixWorld(true);
4595
- heldLocal.copy(c).sub(piece.root.position);
4596
- heldLocal.applyQuaternion(piece.root.quaternion.clone().invert());
4597
- heldRadius = box2.getSize(new THREE.Vector3()).length() / 3;
4666
+ heldRadius = heldSize.length() / 3;
4598
4667
  heldYaw = 0;
4599
4668
  heldTilt = 0;
4600
4669
  heldVel[0] = heldVel[1] = heldVel[2] = 0;
@@ -4615,18 +4684,43 @@ function createTestRoom(options) {
4615
4684
  fire("throw", { objectId: id, speed });
4616
4685
  return true;
4617
4686
  };
4618
- const carriedTargets = (root) => {
4687
+ const carriedTargets = (root, gripQ) => {
4688
+ const width2 = Math.max(heldSize.x, heldSize.z);
4689
+ const sideGrip = heldSize.y > width2 * 1.04 && width2 <= 0.22;
4690
+ if (sideGrip) {
4691
+ const center2 = heldLocal.clone().applyQuaternion(root.quaternion).add(root.position);
4692
+ const radius = Math.min(heldSize.x, heldSize.z) * 0.5;
4693
+ const handX = new THREE.Vector3(-0.35, 0, -0.94).normalize().applyQuaternion(gripQ);
4694
+ const handY = new THREE.Vector3(0.94, 0, -0.35).normalize().applyQuaternion(gripQ);
4695
+ const handZ = new THREE.Vector3().crossVectors(handX, handY).normalize();
4696
+ const q2 = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(handX, handY, handZ));
4697
+ const up = new THREE.Vector3(0, 1, 0).applyQuaternion(gripQ);
4698
+ 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);
4699
+ if (useItemName === "open" || useItemName === "close") {
4700
+ const leftX = new THREE.Vector3(0.35, 0, -0.94).normalize().applyQuaternion(gripQ);
4701
+ const leftY = new THREE.Vector3(-0.94, 0, -0.35).normalize().applyQuaternion(gripQ);
4702
+ const leftZ = new THREE.Vector3().crossVectors(leftX, leftY).normalize();
4703
+ const lq = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(leftX, leftY, leftZ));
4704
+ const leftPalm = center2.clone().addScaledVector(up, heldSize.y * 0.45).addScaledVector(leftY, radius * 0.65 + CARRY_PALM).addScaledVector(leftX, -0.018);
4705
+ return {
4706
+ R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] },
4707
+ L: { position: [leftPalm.x, leftPalm.y, leftPalm.z], quaternion: [lq.x, lq.y, lq.z, lq.w] },
4708
+ grip: 0.85,
4709
+ gripL: 0.8
4710
+ };
4711
+ }
4712
+ return { R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] }, grip: 0.85 };
4713
+ }
4619
4714
  const box2 = new THREE.Box3().setFromObject(root);
4620
4715
  const center = box2.getCenter(new THREE.Vector3());
4621
- const forward = camera.getWorldDirection(new THREE.Vector3());
4622
- forward.y = 0;
4623
- if (forward.lengthSq() < 1e-6) forward.set(0, 0, -1);
4624
- forward.normalize();
4625
- const back = new THREE.Vector3(0, -1, 0);
4626
- const z = forward.clone().cross(back).normalize();
4627
- const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(forward, back, z));
4716
+ const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(gripQ).normalize();
4717
+ const back = new THREE.Vector3(0, -1, 0).applyQuaternion(gripQ).normalize();
4718
+ const z = new THREE.Vector3().crossVectors(forward, back).normalize();
4719
+ const y = new THREE.Vector3().crossVectors(z, forward).normalize();
4720
+ const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(forward, y, z));
4628
4721
  return { R: { position: [center.x, box2.min.y - CARRY_PALM, center.z], quaternion: [q.x, q.y, q.z, q.w] }, grip: CARRY_GRIP };
4629
4722
  };
4723
+ const gripQuat = new THREE.Quaternion();
4630
4724
  const followHeld = (dt) => {
4631
4725
  const piece = heldPiece;
4632
4726
  carriedHand = null;
@@ -4636,21 +4730,47 @@ function createTestRoom(options) {
4636
4730
  let pos;
4637
4731
  const targetQ = tmpQ;
4638
4732
  const hand = thirdActive() ? avatar?.hand?.("R") : void 0;
4733
+ const isDrink = useItemName === "drink" || useItemName === "sip" || useItemName === "chug";
4639
4734
  if (hand) {
4640
- pos = hand.getWorldPosition(tmpHand);
4641
- targetQ.setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw + heldYaw);
4642
- targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
4735
+ const handWorld = hand.getWorldPosition(tmpHand);
4736
+ const yawQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
4737
+ const pose = useItemPlayer?.pose();
4738
+ const rx = pose ? isDrink ? Math.abs(pose.rotation[0]) * 0.9 : pose.rotation[0] : -heldTilt;
4739
+ const ry = pose ? pose.rotation[1] : 0;
4740
+ const rz = pose ? pose.rotation[2] : 0;
4741
+ gripQuat.copy(yawQ).multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.max(-1.35, Math.min(1.25, rx)), ry, rz, "XYZ")));
4742
+ targetQ.copy(yawQ).multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(rx, ry, rz, "XYZ")));
4743
+ targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
4744
+ const gripOffset = new THREE.Vector3(-0.02, 0.015, -0.04).applyQuaternion(gripQuat);
4745
+ pos = tmpV.copy(handWorld).add(gripOffset);
4643
4746
  } else if (useItemPlayer) {
4644
4747
  const pose = useItemPlayer.pose();
4645
- const handQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
4646
- pos = tmpV.set(pose.position[0], pose.position[1], pose.position[2]).applyQuaternion(handQ).add(camera.position);
4647
- const eq = new THREE.Quaternion().setFromEuler(new THREE.Euler(pose.rotation[0], pose.rotation[1], pose.rotation[2], "XYZ"));
4648
- targetQ.copy(handQ).multiply(eq);
4748
+ const frameQ = avatar?.firstPerson ? camera.quaternion : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
4749
+ const raise = isDrink ? Math.max(0, Math.min(1, (pose.position[1] + 0.2) / 0.22)) : 0;
4750
+ const px2 = pose.position[0] + (isDrink ? 0.02 * raise : 0);
4751
+ const py2 = pose.position[1] - (isDrink ? 0.08 * raise : 0);
4752
+ const pz2 = pose.position[2] - (isDrink ? 0.05 * raise : 0);
4753
+ pos = tmpV.set(px2, py2, pz2).applyQuaternion(frameQ).add(camera.position);
4754
+ const rx = isDrink ? Math.abs(pose.rotation[0]) * 0.85 : pose.rotation[0];
4755
+ const eq = new THREE.Quaternion().setFromEuler(new THREE.Euler(rx, pose.rotation[1], pose.rotation[2], "XYZ"));
4756
+ 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"));
4757
+ gripQuat.copy(frameQ).multiply(wristEq);
4758
+ targetQ.copy(frameQ).multiply(eq);
4649
4759
  targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
4650
4760
  } else {
4651
- pos = tmpV.set(0.05, -0.28, avatar?.firstPerson ? -Math.min(1.15, CARRY_NEAR + heldRadius) : -1.15).applyMatrix4(camera.matrixWorld);
4761
+ const small = heldRadius < 0.22;
4762
+ const holdX = avatar?.firstPerson ? small ? 0.22 : 0.08 : 0.05;
4763
+ const holdY = avatar?.firstPerson ? small ? -0.2 : -0.26 : -0.28;
4764
+ const holdZ = avatar?.firstPerson ? small ? -0.42 : -Math.min(1.15, CARRY_NEAR + heldRadius) : -1.15;
4765
+ pos = tmpV.set(holdX, holdY, holdZ).applyMatrix4(camera.matrixWorld);
4766
+ const baseQ = avatar?.firstPerson && small ? camera.quaternion : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
4767
+ gripQuat.copy(baseQ).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.max(-1.35, -heldTilt)));
4652
4768
  targetQ.setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw + heldYaw);
4653
- targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
4769
+ if (avatar?.firstPerson && small && heldTilt <= 1e-4) {
4770
+ targetQ.copy(camera.quaternion).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
4771
+ } else {
4772
+ targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
4773
+ }
4654
4774
  }
4655
4775
  const k = Math.min(1, (useItemPlayer ? 30 : 16) * dt);
4656
4776
  const px = root.position.x;
@@ -4665,7 +4785,7 @@ function createTestRoom(options) {
4665
4785
  heldVel[2] = (root.position.z - pz) / dt;
4666
4786
  }
4667
4787
  root.updateMatrixWorld(true);
4668
- if (avatar?.firstPerson && cameraMode === "first") carriedHand = carriedTargets(root);
4788
+ if (avatar?.firstPerson && cameraMode === "first") carriedHand = carriedTargets(root, gripQuat);
4669
4789
  };
4670
4790
  const useItem = (useId) => {
4671
4791
  const e = heldEntry;
@@ -4675,11 +4795,13 @@ function createTestRoom(options) {
4675
4795
  const c = fluids.container(e.containerId);
4676
4796
  if (!c) return false;
4677
4797
  if (def.liquids && def.liquids.length > 0 && (!c.liquid || !def.liquids.includes(c.liquid))) return false;
4678
- if ((useId === "drink" || useId === "sip" || useId === "chug" || useId === "pour" || useId === "pour_out") && c.liters <= 1e-6) {
4798
+ const consuming = useId === "drink" || useId === "sip" || useId === "chug" || useId === "pour" || useId === "pour_out";
4799
+ if (consuming && c.liters <= 1e-6) {
4679
4800
  say(t("hud.empty_container", { name: e.name }));
4680
4801
  return false;
4681
4802
  }
4682
4803
  if (useItemPlayer && !useItemPlayer.done) return false;
4804
+ if (consuming && c.closed) fluids.open(e.containerId);
4683
4805
  useItemPlayer = createItemUse(fluids, e.containerId, def);
4684
4806
  useItemName = def.id;
4685
4807
  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 } });
@@ -4801,7 +4923,7 @@ function createTestRoom(options) {
4801
4923
  if (heldEntry?.uses[0]) useItem(heldEntry.uses[0]);
4802
4924
  break;
4803
4925
  case "KeyC":
4804
- if (heldEntry?.uses[1]) useItem(heldEntry.uses[1]);
4926
+ if (heldEntry?.uses[1] && !(options.crouch?.keys ?? ["KeyC"]).includes("KeyC")) useItem(heldEntry.uses[1]);
4805
4927
  break;
4806
4928
  case "KeyV":
4807
4929
  if (heldEntry?.uses[2]) useItem(heldEntry.uses[2]);
@@ -4893,6 +5015,7 @@ function createTestRoom(options) {
4893
5015
  const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, effects: vfx.world, ids: roomMetricIds, blast: () => lastBlast });
4894
5016
  let qualityName = quality;
4895
5017
  const syncContainers = (sdt) => {
5018
+ const drinkingHeld = useItemPlayer !== null && (useItemName === "drink" || useItemName === "sip" || useItemName === "chug");
4896
5019
  for (const e of entries.values()) {
4897
5020
  if (!e.containerId) continue;
4898
5021
  const host2 = hostOf(e);
@@ -4901,7 +5024,8 @@ function createTestRoom(options) {
4901
5024
  const p = tmpV.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(host2.root.matrixWorld);
4902
5025
  const vel = sdt > 0 ? [(p.x - e.last[0]) / sdt, (p.y - e.last[1]) / sdt, (p.z - e.last[2]) / sdt] : void 0;
4903
5026
  e.last = [p.x, p.y, p.z];
4904
- fluids.setTransform(e.containerId, [p.x, p.y, p.z], [q.x, q.y, q.z, q.w], vel);
5027
+ const rot = drinkingHeld && e === heldEntry ? [0, 0, 0, 1] : [q.x, q.y, q.z, q.w];
5028
+ fluids.setTransform(e.containerId, [p.x, p.y, p.z], rot, vel);
4905
5029
  }
4906
5030
  };
4907
5031
  const syncLiters = () => {
@@ -5652,7 +5776,10 @@ function createTestRoom(options) {
5652
5776
  return avatar;
5653
5777
  },
5654
5778
  setAvatarState(state) {
5655
- if (state.crouch !== void 0) avatarState.crouch = state.crouch;
5779
+ if (state.crouch !== void 0) {
5780
+ crouchForced = state.crouch;
5781
+ avatarState.crouch = state.crouch || player.crouching;
5782
+ }
5656
5783
  if (state.down !== void 0) avatarState.down = state.down;
5657
5784
  if (state.armed !== void 0) armedForced = state.armed;
5658
5785
  },