import { useEffect, useRef } from 'react'; import { useFrame, useThree } from '@react-three/fiber'; import { PointerLockControls } from '@react-three/drei'; import * as THREE from 'three'; import { useModelStore } from '@/stores/useModelStore'; export function DesktopFirstPersonControls() { const { camera } = useThree(); const activeMode = useModelStore((s) => s.desktopCameraMode); const xrEnvironment = useModelStore((s) => s.xrEnvironment); // Armazena o estado atual das teclas de locomoção const keys = useRef({ w: false, a: false, s: false, d: false, arrowup: false, arrowleft: false, arrowdown: false, arrowright: false, q: false, // voar para baixo e: false, // voar para cima }); useEffect(() => { if (activeMode !== 'walk') return; const handleKeyDown = (e: KeyboardEvent) => { const k = e.key.toLowerCase(); if (k in keys.current) { keys.current[k as keyof typeof keys.current] = true; } }; const handleKeyUp = (e: KeyboardEvent) => { const k = e.key.toLowerCase(); if (k in keys.current) { keys.current[k as keyof typeof keys.current] = false; } }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); }; }, [activeMode]); // Define posições iniciais amigáveis ao alternar para o modo passeio const lastMode = useRef(null); useEffect(() => { if (activeMode === 'walk' && lastMode.current !== 'walk') { if (xrEnvironment === 'office') { // Posiciona o operador em pé na ponta da mesa camera.position.set(0, 1.4, 1.3); camera.lookAt(0, 0.85, 0); } else { // Posiciona no campo aberto em escala real camera.position.set(0, 1.65, 3); camera.lookAt(0, 1.0, 0); } } lastMode.current = activeMode; }, [activeMode, xrEnvironment, camera]); useFrame((state, delta) => { if (activeMode !== 'walk') return; const speed = 2.0 * delta; // Metros por frame adaptativo const rotSpeed = 1.5 * delta; const direction = new THREE.Vector3(); const side = new THREE.Vector3(); // Obtém vetor de olhar da câmera camera.getWorldDirection(direction); direction.y = 0; // Trava o vetor na horizontal para não voar para cima/baixo ao olhar direction.normalize(); // Obtém vetor lateral side.crossVectors(camera.up, direction).normalize(); // 1. Leituras do teclado let moveFwd = 0; let moveSide = 0; let moveUp = 0; if (keys.current.w || keys.current.arrowup) moveFwd += 1; if (keys.current.s || keys.current.arrowdown) moveFwd -= 1; if (keys.current.d || keys.current.arrowright) moveSide -= 1; if (keys.current.a || keys.current.arrowleft) moveSide += 1; if (keys.current.e) moveUp += 1; if (keys.current.q) moveUp -= 1; camera.position.addScaledVector(direction, moveFwd * speed); camera.position.addScaledVector(side, moveSide * speed); camera.position.y += moveUp * speed; // 2. Leituras da Gamepad API (Xbox / PlayStation) const gamepads = navigator.getGamepads(); const gp = gamepads[0] || gamepads[1]; if (gp) { const deadzone = 0.15; // Analógico Esquerdo (Locomoção) const stickLX = gp.axes[0]; // Eixo lateral const stickLY = gp.axes[1]; // Eixo frente/trás if (Math.abs(stickLY) > deadzone) { camera.position.addScaledVector(direction, -stickLY * speed); } if (Math.abs(stickLX) > deadzone) { camera.position.addScaledVector(side, -stickLX * speed); } // Analógico Direito (Rotação da Câmera) const stickRX = gp.axes[2]; // Rotação Y const stickRY = gp.axes[3]; // Olhar X cima/baixo if (Math.abs(stickRX) > deadzone) { camera.rotation.y -= stickRX * rotSpeed * 1.6; } if (Math.abs(stickRY) > deadzone) { // Rotaciona verticalmente camera.rotation.x -= stickRY * rotSpeed * 1.6; // Limita ângulo vertical para evitar inverter a visão (entre -80° e 80°) camera.rotation.x = Math.max(-1.4, Math.min(1.4, camera.rotation.x)); } // Gatilhos para controle de altura (RT sobe, LT desce) const triggerLT = gp.buttons[6]; const triggerRT = gp.buttons[7]; if (triggerRT && triggerRT.value > 0.1) { camera.position.y += triggerRT.value * speed; } if (triggerLT && triggerLT.value > 0.1) { camera.position.y -= triggerLT.value * speed; } } // Trava de colisão com o chão/mesa para manter realismo const minHeight = xrEnvironment === 'office' ? 0.9 : 0.1; if (camera.position.y < minHeight) { camera.position.y = minHeight; } }); if (activeMode !== 'walk') return null; return ; }