@adatechnology/game-test-room 0.12.0 → 0.14.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
@@ -9,6 +9,9 @@ import { createGroundField, createVfx } from "@adatechnology/game-vfx";
9
9
  import { createGroundPatch, createVfxRenderer } from "@adatechnology/game-vfx-three";
10
10
  import { checkAnimation, checkBench, checkDestructible, checkSpec, createEnvironmentBus, shotExtras, useToHit } from "@adatechnology/game-contracts";
11
11
 
12
+ // src/arena.ts
13
+ import { markKitOrigin } from "@adatechnology/game-contracts";
14
+
12
15
  // src/items.ts
13
16
  var box = (id, material, size, position, extra = {}) => ({ id, material, shape: { kind: "box", size }, position, ...extra });
14
17
  var joint = (a, b, kind, extra = {}) => ({ a, b, kind, ...extra });
@@ -110,7 +113,7 @@ function resolveScenery(scenery) {
110
113
  return [...merged.entries()].map(([id, o]) => ({ id, spec: o.spec, position: o.position, rotationY: o.rotationY ?? 0, colors: o.colors, build: o.build, label: o.label, top: o.top, spout: o.spout }));
111
114
  }
112
115
  function createArena(THREE, size, scene, scenery, tapOption) {
113
- const group = new THREE.Group();
116
+ const group = markKitOrigin(new THREE.Group(), { owner: "game-test-room", kind: "room" });
114
117
  group.name = "test-room-arena";
115
118
  scene.background = new THREE.Color(10466504);
116
119
  group.add(new THREE.HemisphereLight(13624319, 7037527, 0.75));
@@ -1706,6 +1709,9 @@ function createNarrator(c) {
1706
1709
  var EYE = 1.7;
1707
1710
  var PLAYER_RADIUS = 0.3;
1708
1711
  var WALK = 3.4;
1712
+ var CROUCH_DROP = 0.55;
1713
+ var CROUCH_WALK = 1.7;
1714
+ var CROUCH_RATE = 9;
1709
1715
  var RUN = 6.2;
1710
1716
  var JUMP = 4.6;
1711
1717
  var GRAVITY = 13;
@@ -1716,17 +1722,20 @@ function createPlayer(THREE, o) {
1716
1722
  let pitch = 0;
1717
1723
  let y = 0;
1718
1724
  let eye = EYE;
1725
+ let crouch = 0;
1719
1726
  let vy = 0;
1720
1727
  let walked = 0;
1721
1728
  let running = false;
1722
1729
  const px = { x: o.start[0], z: o.start[2] };
1723
1730
  let locked = false;
1724
1731
  const keys = /* @__PURE__ */ new Set();
1732
+ const crouchKeys = o.crouchKeys ?? ["KeyC"];
1733
+ const isCrouchHeld = () => crouchKeys.some((code) => keys.has(code));
1725
1734
  const euler = new THREE.Euler(0, 0, 0, "YXZ");
1726
1735
  const apply = () => {
1727
1736
  euler.set(pitch, yaw, 0, "YXZ");
1728
1737
  cam.quaternion.setFromEuler(euler);
1729
- cam.position.set(px.x, eye + y, px.z);
1738
+ cam.position.set(px.x, eye + y - CROUCH_DROP * crouch, px.z);
1730
1739
  cam.updateMatrixWorld(true);
1731
1740
  };
1732
1741
  apply();
@@ -1753,6 +1762,20 @@ function createPlayer(THREE, o) {
1753
1762
  };
1754
1763
  const onLockChange = () => {
1755
1764
  locked = document.pointerLockElement === o.dom;
1765
+ if (locked) {
1766
+ try {
1767
+ const nav = typeof navigator !== "undefined" ? navigator : null;
1768
+ nav?.keyboard?.lock?.().catch(() => {
1769
+ });
1770
+ } catch {
1771
+ }
1772
+ } else {
1773
+ try {
1774
+ const nav = typeof navigator !== "undefined" ? navigator : null;
1775
+ nav?.keyboard?.unlock?.();
1776
+ } catch {
1777
+ }
1778
+ }
1756
1779
  };
1757
1780
  const onMouseDown = (e) => {
1758
1781
  o.dom.focus?.();
@@ -1822,6 +1845,10 @@ function createPlayer(THREE, o) {
1822
1845
  const onKeyDown = (e) => {
1823
1846
  if (typing(e)) return;
1824
1847
  if (e.code === "Space" || e.code === "Tab") e.preventDefault();
1848
+ const isControlHeld = e.ctrlKey || e.metaKey || keys.has("ControlLeft") || keys.has("ControlRight");
1849
+ if (isControlHeld || e.altKey && locked) {
1850
+ if (!/^F\d+$/.test(e.code)) e.preventDefault();
1851
+ }
1825
1852
  if (!e.repeat) keys.add(e.code);
1826
1853
  o.hooks.key(e.code, true, e);
1827
1854
  };
@@ -1832,6 +1859,11 @@ function createPlayer(THREE, o) {
1832
1859
  };
1833
1860
  const onContext = (e) => e.preventDefault();
1834
1861
  const onBlur = () => keys.clear();
1862
+ const onBeforeUnload = (e) => {
1863
+ if (!locked && !keys.has("ControlLeft") && !keys.has("ControlRight")) return;
1864
+ e.preventDefault();
1865
+ e.returnValue = "";
1866
+ };
1835
1867
  o.dom.addEventListener("mousedown", onMouseDown);
1836
1868
  window.addEventListener("mouseup", onMouseUp);
1837
1869
  window.addEventListener("mousemove", onMouseMove);
@@ -1841,6 +1873,7 @@ function createPlayer(THREE, o) {
1841
1873
  window.addEventListener("keydown", onKeyDown);
1842
1874
  window.addEventListener("keyup", onKeyUp);
1843
1875
  window.addEventListener("blur", onBlur);
1876
+ window.addEventListener("beforeunload", onBeforeUnload);
1844
1877
  return {
1845
1878
  get yaw() {
1846
1879
  return yaw;
@@ -1869,12 +1902,18 @@ function createPlayer(THREE, o) {
1869
1902
  get running() {
1870
1903
  return running;
1871
1904
  },
1905
+ get crouching() {
1906
+ return isCrouchHeld();
1907
+ },
1872
1908
  keys,
1873
1909
  update(dt) {
1874
- const run = keys.has("ShiftLeft") || keys.has("ShiftRight");
1910
+ const crouched = isCrouchHeld();
1911
+ crouch += ((crouched ? 1 : 0) - crouch) * Math.min(1, dt * CROUCH_RATE);
1912
+ if (!crouched && crouch < 1e-3) crouch = 0;
1913
+ const run = !crouched && (keys.has("ShiftLeft") || keys.has("ShiftRight"));
1875
1914
  const fromX = px.x;
1876
1915
  const fromZ = px.z;
1877
- const speed = run ? RUN : WALK;
1916
+ const speed = crouched ? CROUCH_WALK : run ? RUN : WALK;
1878
1917
  let f = 0;
1879
1918
  let r = 0;
1880
1919
  if (keys.has("KeyW") || keys.has("ArrowUp")) f += 1;
@@ -1950,13 +1989,14 @@ function createPlayer(THREE, o) {
1950
1989
  window.removeEventListener("keydown", onKeyDown);
1951
1990
  window.removeEventListener("keyup", onKeyUp);
1952
1991
  window.removeEventListener("blur", onBlur);
1992
+ window.removeEventListener("beforeunload", onBeforeUnload);
1953
1993
  if (document.pointerLockElement === o.dom) document.exitPointerLock?.();
1954
1994
  }
1955
1995
  };
1956
1996
  }
1957
1997
 
1958
1998
  // src/avatar.ts
1959
- var DEFAULT_STRIKE_CLIPS = { fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
1999
+ var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
1960
2000
  var DEFAULT_HAND_STANCES = { fists: "fists" };
1961
2001
  var THIRD_PERSON_DEFAULTS = {
1962
2002
  back: 3.8,
@@ -2197,7 +2237,7 @@ var en = {
2197
2237
  "hud.throw": "throw",
2198
2238
  "hud.tilt": "tilt (hold)",
2199
2239
  "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",
2240
+ "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
2241
  "ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
2202
2242
  "camera.first": "1st person",
2203
2243
  "camera.third": "3rd person",
@@ -2218,6 +2258,8 @@ var en = {
2218
2258
  "ui.slow": "Slow motion 0.25\xD7",
2219
2259
  "ui.fast": "Time \xD710",
2220
2260
  "ui.tap": "Tap",
2261
+ "ui.fullscreen": "Full screen",
2262
+ "ui.fullscreen.hint": "Full screen opened from here keeps the whole keyboard in the game, Ctrl+W included (F11 does not)",
2221
2263
  "ui.blast.shelter": "Shelter",
2222
2264
  "ui.blast.fragmentHits": "Fragments",
2223
2265
  "ui.blast.confinement": "Confinement",
@@ -2476,7 +2518,7 @@ var ptBR = {
2476
2518
  "hud.throw": "arremessar",
2477
2519
  "hud.tilt": "inclinar (segurar)",
2478
2520
  "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",
2521
+ "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
2522
  "ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
2481
2523
  "camera.first": "1\xAA pessoa",
2482
2524
  "camera.third": "3\xAA pessoa",
@@ -2497,6 +2539,8 @@ var ptBR = {
2497
2539
  "ui.slow": "C\xE2mera lenta 0,25\xD7",
2498
2540
  "ui.fast": "Tempo \xD710",
2499
2541
  "ui.tap": "Torneira",
2542
+ "ui.fullscreen": "Tela cheia",
2543
+ "ui.fullscreen.hint": "A tela cheia aberta por aqui segura o teclado todo no jogo, inclusive Ctrl+W (o F11 n\xE3o)",
2500
2544
  "ui.blast.shelter": "Abrigo",
2501
2545
  "ui.blast.fragmentHits": "Estilha\xE7os",
2502
2546
  "ui.blast.confinement": "Confinamento",
@@ -2924,6 +2968,19 @@ var thumb = (thumbnail, credit) => {
2924
2968
  e.style.setProperty("--tr-thumb", `url("${thumbSource(thumbnail)}")`);
2925
2969
  return e;
2926
2970
  };
2971
+ async function toggleFullscreen() {
2972
+ const keyboard = navigator.keyboard;
2973
+ try {
2974
+ if (document.fullscreenElement) {
2975
+ keyboard?.unlock?.();
2976
+ await document.exitFullscreen();
2977
+ return;
2978
+ }
2979
+ await document.documentElement.requestFullscreen();
2980
+ await keyboard?.lock?.();
2981
+ } catch {
2982
+ }
2983
+ }
2927
2984
  function createUi(o) {
2928
2985
  const { t } = o;
2929
2986
  const style = mk2("style");
@@ -3135,6 +3192,8 @@ function createUi(o) {
3135
3192
  o.onFast(fast);
3136
3193
  });
3137
3194
  const tapBtn = ctl(t("ui.tap"), () => o.onTap());
3195
+ const fullscreenBtn = ctl(t("ui.fullscreen"), () => void toggleFullscreen());
3196
+ fullscreenBtn.title = t("ui.fullscreen.hint");
3138
3197
  const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
3139
3198
  for (const { key, b } of blastBtns) b.title = t(`ui.blast.${key}.hint`);
3140
3199
  const qSel = mk2("select");
@@ -3688,6 +3747,7 @@ function createTestRoom(options) {
3688
3747
  blockers,
3689
3748
  limit: arenaSize,
3690
3749
  start: arena.playerStart,
3750
+ crouchKeys: options.crouch?.keys,
3691
3751
  hooks: {
3692
3752
  primaryDown: () => {
3693
3753
  useHeld = true;
@@ -3940,6 +4000,7 @@ function createTestRoom(options) {
3940
4000
  const third = { ...THIRD_PERSON_DEFAULTS, ...options.thirdPerson };
3941
4001
  let avatar = null;
3942
4002
  let cameraMode = options.cameraMode ?? "first";
4003
+ let crouchForced = false;
3943
4004
  const avatarState = { speed: 0, running: false, crouch: false, down: false, armed: false, pitch: 0 };
3944
4005
  let feetPrev = null;
3945
4006
  let armedForced = null;
@@ -3993,9 +4054,12 @@ function createTestRoom(options) {
3993
4054
  if (avatarState.speed < 0.02) avatarState.speed = 0;
3994
4055
  }
3995
4056
  feetPrev = [x, z];
3996
- avatarState.running = player.keys.has("ShiftLeft") || player.keys.has("ShiftRight");
4057
+ avatarState.running = !player.crouching && (player.keys.has("ShiftLeft") || player.keys.has("ShiftRight"));
4058
+ avatarState.crouch = crouchForced || player.crouching;
3997
4059
  avatarState.armed = armedForced ?? tool.def.action.kind === "firearm";
3998
4060
  avatarState.pitch = player.pitch;
4061
+ avatarState.holding = heldPiece !== null;
4062
+ 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
4063
  avatar.root.position.set(x, player.feet, z);
4000
4064
  avatar.root.rotation.set(0, player.yaw + Math.PI, 0);
4001
4065
  avatar.update(dt, avatarState);
@@ -4451,7 +4515,11 @@ function createTestRoom(options) {
4451
4515
  const useTool = (dt, held) => {
4452
4516
  aim();
4453
4517
  const cat = tool.def.category;
4454
- if (cat === "hand") return;
4518
+ if (cat === "hand") {
4519
+ if (strikes.some((strike) => strike.key === "hand" || strike.key === "fists")) return;
4520
+ 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");
4521
+ return;
4522
+ }
4455
4523
  if (strikes.some((strike) => strike.key === toolId)) return;
4456
4524
  const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
4457
4525
  flushEvents(toolId, tool, res.recoil);
@@ -4544,6 +4612,7 @@ function createTestRoom(options) {
4544
4612
  let heldTilt = 0;
4545
4613
  let tiltKey = 0;
4546
4614
  const heldLocal = new THREE.Vector3();
4615
+ const heldSize = new THREE.Vector3(0.2, 0.2, 0.2);
4547
4616
  let heldRadius = 0.2;
4548
4617
  const heldVel = [0, 0, 0];
4549
4618
  let useItemPlayer = null;
@@ -4589,12 +4658,15 @@ function createTestRoom(options) {
4589
4658
  heldName = nameOf(r.object);
4590
4659
  piece.held = true;
4591
4660
  piece.sleeping = false;
4661
+ const savedQ = piece.root.quaternion.clone();
4662
+ piece.root.quaternion.identity();
4663
+ piece.root.updateMatrixWorld(true);
4592
4664
  const box2 = new THREE.Box3().setFromObject(piece.root);
4593
- const c = box2.getCenter(new THREE.Vector3());
4665
+ box2.getCenter(heldLocal).sub(piece.root.position);
4666
+ box2.getSize(heldSize);
4667
+ piece.root.quaternion.copy(savedQ);
4594
4668
  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;
4669
+ heldRadius = heldSize.length() / 3;
4598
4670
  heldYaw = 0;
4599
4671
  heldTilt = 0;
4600
4672
  heldVel[0] = heldVel[1] = heldVel[2] = 0;
@@ -4615,18 +4687,43 @@ function createTestRoom(options) {
4615
4687
  fire("throw", { objectId: id, speed });
4616
4688
  return true;
4617
4689
  };
4618
- const carriedTargets = (root) => {
4690
+ const carriedTargets = (root, gripQ) => {
4691
+ const width2 = Math.max(heldSize.x, heldSize.z);
4692
+ const sideGrip = heldSize.y > width2 * 1.04 && width2 <= 0.22;
4693
+ if (sideGrip) {
4694
+ const center2 = heldLocal.clone().applyQuaternion(root.quaternion).add(root.position);
4695
+ const radius = Math.min(heldSize.x, heldSize.z) * 0.5;
4696
+ const handX = new THREE.Vector3(-0.35, 0, -0.94).normalize().applyQuaternion(gripQ);
4697
+ const handY = new THREE.Vector3(0.94, 0, -0.35).normalize().applyQuaternion(gripQ);
4698
+ const handZ = new THREE.Vector3().crossVectors(handX, handY).normalize();
4699
+ const q2 = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(handX, handY, handZ));
4700
+ const up = new THREE.Vector3(0, 1, 0).applyQuaternion(gripQ);
4701
+ 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);
4702
+ if (useItemName === "open" || useItemName === "close") {
4703
+ const leftX = new THREE.Vector3(0.35, 0, -0.94).normalize().applyQuaternion(gripQ);
4704
+ const leftY = new THREE.Vector3(-0.94, 0, -0.35).normalize().applyQuaternion(gripQ);
4705
+ const leftZ = new THREE.Vector3().crossVectors(leftX, leftY).normalize();
4706
+ const lq = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(leftX, leftY, leftZ));
4707
+ const leftPalm = center2.clone().addScaledVector(up, heldSize.y * 0.45).addScaledVector(leftY, radius * 0.65 + CARRY_PALM).addScaledVector(leftX, -0.018);
4708
+ return {
4709
+ R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] },
4710
+ L: { position: [leftPalm.x, leftPalm.y, leftPalm.z], quaternion: [lq.x, lq.y, lq.z, lq.w] },
4711
+ grip: 0.85,
4712
+ gripL: 0.8
4713
+ };
4714
+ }
4715
+ return { R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] }, grip: 0.85 };
4716
+ }
4619
4717
  const box2 = new THREE.Box3().setFromObject(root);
4620
4718
  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));
4719
+ const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(gripQ).normalize();
4720
+ const back = new THREE.Vector3(0, -1, 0).applyQuaternion(gripQ).normalize();
4721
+ const z = new THREE.Vector3().crossVectors(forward, back).normalize();
4722
+ const y = new THREE.Vector3().crossVectors(z, forward).normalize();
4723
+ const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(forward, y, z));
4628
4724
  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
4725
  };
4726
+ const gripQuat = new THREE.Quaternion();
4630
4727
  const followHeld = (dt) => {
4631
4728
  const piece = heldPiece;
4632
4729
  carriedHand = null;
@@ -4636,21 +4733,47 @@ function createTestRoom(options) {
4636
4733
  let pos;
4637
4734
  const targetQ = tmpQ;
4638
4735
  const hand = thirdActive() ? avatar?.hand?.("R") : void 0;
4736
+ const isDrink = useItemName === "drink" || useItemName === "sip" || useItemName === "chug";
4639
4737
  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));
4738
+ const handWorld = hand.getWorldPosition(tmpHand);
4739
+ const yawQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
4740
+ const pose = useItemPlayer?.pose();
4741
+ const rx = pose ? isDrink ? Math.abs(pose.rotation[0]) * 0.9 : pose.rotation[0] : -heldTilt;
4742
+ const ry = pose ? pose.rotation[1] : 0;
4743
+ const rz = pose ? pose.rotation[2] : 0;
4744
+ gripQuat.copy(yawQ).multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.max(-1.35, Math.min(1.25, rx)), ry, rz, "XYZ")));
4745
+ targetQ.copy(yawQ).multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(rx, ry, rz, "XYZ")));
4746
+ targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
4747
+ const gripOffset = new THREE.Vector3(-0.02, 0.015, -0.04).applyQuaternion(gripQuat);
4748
+ pos = tmpV.copy(handWorld).add(gripOffset);
4643
4749
  } else if (useItemPlayer) {
4644
4750
  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);
4751
+ const frameQ = avatar?.firstPerson ? camera.quaternion : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
4752
+ const raise = isDrink ? Math.max(0, Math.min(1, (pose.position[1] + 0.2) / 0.22)) : 0;
4753
+ const px2 = pose.position[0] + (isDrink ? 0.02 * raise : 0);
4754
+ const py2 = pose.position[1] - (isDrink ? 0.08 * raise : 0);
4755
+ const pz2 = pose.position[2] - (isDrink ? 0.05 * raise : 0);
4756
+ pos = tmpV.set(px2, py2, pz2).applyQuaternion(frameQ).add(camera.position);
4757
+ const rx = isDrink ? Math.abs(pose.rotation[0]) * 0.85 : pose.rotation[0];
4758
+ const eq = new THREE.Quaternion().setFromEuler(new THREE.Euler(rx, pose.rotation[1], pose.rotation[2], "XYZ"));
4759
+ 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"));
4760
+ gripQuat.copy(frameQ).multiply(wristEq);
4761
+ targetQ.copy(frameQ).multiply(eq);
4649
4762
  targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
4650
4763
  } else {
4651
- pos = tmpV.set(0.05, -0.28, avatar?.firstPerson ? -Math.min(1.15, CARRY_NEAR + heldRadius) : -1.15).applyMatrix4(camera.matrixWorld);
4764
+ const small = heldRadius < 0.22;
4765
+ const holdX = avatar?.firstPerson ? small ? 0.22 : 0.08 : 0.05;
4766
+ const holdY = avatar?.firstPerson ? small ? -0.2 : -0.26 : -0.28;
4767
+ const holdZ = avatar?.firstPerson ? small ? -0.42 : -Math.min(1.15, CARRY_NEAR + heldRadius) : -1.15;
4768
+ pos = tmpV.set(holdX, holdY, holdZ).applyMatrix4(camera.matrixWorld);
4769
+ const baseQ = avatar?.firstPerson && small ? camera.quaternion : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
4770
+ gripQuat.copy(baseQ).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.max(-1.35, -heldTilt)));
4652
4771
  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));
4772
+ if (avatar?.firstPerson && small && heldTilt <= 1e-4) {
4773
+ targetQ.copy(camera.quaternion).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
4774
+ } else {
4775
+ targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
4776
+ }
4654
4777
  }
4655
4778
  const k = Math.min(1, (useItemPlayer ? 30 : 16) * dt);
4656
4779
  const px = root.position.x;
@@ -4665,7 +4788,7 @@ function createTestRoom(options) {
4665
4788
  heldVel[2] = (root.position.z - pz) / dt;
4666
4789
  }
4667
4790
  root.updateMatrixWorld(true);
4668
- if (avatar?.firstPerson && cameraMode === "first") carriedHand = carriedTargets(root);
4791
+ if (avatar?.firstPerson && cameraMode === "first") carriedHand = carriedTargets(root, gripQuat);
4669
4792
  };
4670
4793
  const useItem = (useId) => {
4671
4794
  const e = heldEntry;
@@ -4675,11 +4798,13 @@ function createTestRoom(options) {
4675
4798
  const c = fluids.container(e.containerId);
4676
4799
  if (!c) return false;
4677
4800
  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) {
4801
+ const consuming = useId === "drink" || useId === "sip" || useId === "chug" || useId === "pour" || useId === "pour_out";
4802
+ if (consuming && c.liters <= 1e-6) {
4679
4803
  say(t("hud.empty_container", { name: e.name }));
4680
4804
  return false;
4681
4805
  }
4682
4806
  if (useItemPlayer && !useItemPlayer.done) return false;
4807
+ if (consuming && c.closed) fluids.open(e.containerId);
4683
4808
  useItemPlayer = createItemUse(fluids, e.containerId, def);
4684
4809
  useItemName = def.id;
4685
4810
  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 +4926,7 @@ function createTestRoom(options) {
4801
4926
  if (heldEntry?.uses[0]) useItem(heldEntry.uses[0]);
4802
4927
  break;
4803
4928
  case "KeyC":
4804
- if (heldEntry?.uses[1]) useItem(heldEntry.uses[1]);
4929
+ if (heldEntry?.uses[1] && !(options.crouch?.keys ?? ["KeyC"]).includes("KeyC")) useItem(heldEntry.uses[1]);
4805
4930
  break;
4806
4931
  case "KeyV":
4807
4932
  if (heldEntry?.uses[2]) useItem(heldEntry.uses[2]);
@@ -4893,6 +5018,7 @@ function createTestRoom(options) {
4893
5018
  const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, effects: vfx.world, ids: roomMetricIds, blast: () => lastBlast });
4894
5019
  let qualityName = quality;
4895
5020
  const syncContainers = (sdt) => {
5021
+ const drinkingHeld = useItemPlayer !== null && (useItemName === "drink" || useItemName === "sip" || useItemName === "chug");
4896
5022
  for (const e of entries.values()) {
4897
5023
  if (!e.containerId) continue;
4898
5024
  const host2 = hostOf(e);
@@ -4901,7 +5027,8 @@ function createTestRoom(options) {
4901
5027
  const p = tmpV.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(host2.root.matrixWorld);
4902
5028
  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
5029
  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);
5030
+ const rot = drinkingHeld && e === heldEntry ? [0, 0, 0, 1] : [q.x, q.y, q.z, q.w];
5031
+ fluids.setTransform(e.containerId, [p.x, p.y, p.z], rot, vel);
4905
5032
  }
4906
5033
  };
4907
5034
  const syncLiters = () => {
@@ -5625,6 +5752,7 @@ function createTestRoom(options) {
5625
5752
  },
5626
5753
  equip,
5627
5754
  openArea,
5755
+ modules: () => [...modules.values()].map((m) => ({ id: m.id, name: m.name, group: m.group, hasArea: typeof m.area === "function" })),
5628
5756
  openAreaAsync: (area) => loading.busy(t("loading.open_area", { area: areaLabel(area) }), () => openArea(area)),
5629
5757
  loading,
5630
5758
  busy: (label, work, busyOptions) => loading.busy(label, work, busyOptions),
@@ -5652,7 +5780,10 @@ function createTestRoom(options) {
5652
5780
  return avatar;
5653
5781
  },
5654
5782
  setAvatarState(state) {
5655
- if (state.crouch !== void 0) avatarState.crouch = state.crouch;
5783
+ if (state.crouch !== void 0) {
5784
+ crouchForced = state.crouch;
5785
+ avatarState.crouch = state.crouch || player.crouching;
5786
+ }
5656
5787
  if (state.down !== void 0) avatarState.down = state.down;
5657
5788
  if (state.armed !== void 0) armedForced = state.armed;
5658
5789
  },