diff --git a/src/components/ebike/Ebike.tsx b/src/components/ebike/Ebike.tsx index dac7b9d..42e6732 100644 --- a/src/components/ebike/Ebike.tsx +++ b/src/components/ebike/Ebike.tsx @@ -1,11 +1,13 @@ import { useRef } from "react"; import * as THREE from "three"; +import { useFrame } from "@react-three/fiber"; import { InteractableObject } from "@/components/three/interaction/InteractableObject"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import { useClonedObject } from "@/hooks/three/useClonedObject"; import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; import { animateCameraTransition } from "@/world/GameCinematics"; import { useGameStore } from "@/managers/stores/useGameStore"; +import { PLAYER_EYE_HEIGHT } from "@/data/player/playerConfig"; import type { Vector3Tuple } from "@/types/three/three"; const EBIKE_MODEL_PATH = "/models/ebike/model.gltf"; @@ -38,6 +40,24 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { const model = useClonedObject(scene); const movementMode = useGameStore((state) => state.player.movementMode); + useFrame((state) => { + if (groupRef.current) { + if (movementMode === "ebike") { + // Follow player physical position (capsule end) + const playerPos = (window as any).playerPos || [0, 10, 0]; + groupRef.current.position.set(playerPos[0], playerPos[1] - PLAYER_EYE_HEIGHT, playerPos[2]); + + // Match the e-bike's actual physical steering heading + const angle = (window as any).ebikeAngle || 0; + groupRef.current.rotation.set(0, angle, 0); + } else { + // Reset to original position + groupRef.current.position.set(...position); + groupRef.current.rotation.set(0, 0, 0); + } + } + }); + const debugRef = useRef({ showCameraPoints: true }); useDebugFolder("Ebike", (folder) => { folder @@ -76,15 +96,22 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { useGameStore.getState().setPlayerMovementMode("ebike"); }); } else { + const currentPos = new THREE.Vector3(); + if (groupRef.current) { + groupRef.current.getWorldPosition(currentPos); + } else { + currentPos.set(...position); + } + const targetCamPos: Vector3Tuple = [ - position[0] + EBIKE_DROP_PLAYER_TRANSFORM.position[0], - position[1] + EBIKE_DROP_PLAYER_TRANSFORM.position[1], - position[2] + EBIKE_DROP_PLAYER_TRANSFORM.position[2], + currentPos.x + EBIKE_DROP_PLAYER_TRANSFORM.position[0], + currentPos.y + EBIKE_DROP_PLAYER_TRANSFORM.position[1], + currentPos.z + EBIKE_DROP_PLAYER_TRANSFORM.position[2], ]; const targetLookAt: Vector3Tuple = [ - position[0], - position[1] + 1, - position[2], + currentPos.x, + currentPos.y + 1, + currentPos.z, ]; animateCameraTransition(targetCamPos, targetLookAt, 1, () => { @@ -103,7 +130,7 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { movementMode === "walk" ? "Monter sur le bike" : "Descendre du bike" } position={position} - radius={10} + radius={15} onPress={handleInteract} > diff --git a/src/world/player/PlayerController.tsx b/src/world/player/PlayerController.tsx index e31f0c9..f7db40b 100644 --- a/src/world/player/PlayerController.tsx +++ b/src/world/player/PlayerController.tsx @@ -110,11 +110,51 @@ export function PlayerController({ const currentSpeed = useGameStore((state) => state.player.currentSpeed); const movementMode = useGameStore((state) => state.player.movementMode); const movementModeRef = useRef(movementMode); + const prevMovementModeRef = useRef(movementMode); + const ebikeAngle = useRef(0); useEffect(() => { movementModeRef.current = movementMode; }, [movementMode]); + useEffect(() => { + if (movementMode === "ebike") { + // Teleport player capsule to the bike's spawning position [0, 10, 0] + const targetPos: Vector3Tuple = [0, 10, 0]; + capsule.current.start.set( + targetPos[0], + targetPos[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS, + targetPos[2], + ); + capsule.current.end.set(...targetPos); + velocity.current.set(0, 0, 0); + onFloor.current = false; + wantsJump.current = false; + + // Initialize ebikeAngle to the bike's visual orientation (0 by default) + ebikeAngle.current = 0; + + // Position the camera exactly at the EBIKE_CAMERA_TRANSFORM offset [-3, 8, 0] + const cameraOffset = new THREE.Vector3(-3, 8, 0); + const camPos = new THREE.Vector3() + .copy(capsule.current.end) + .add(cameraOffset); + camera.position.copy(camPos); + camera.lookAt(capsule.current.end.x, capsule.current.end.y + 1, capsule.current.end.z); + } else if (movementMode === "walk" && prevMovementModeRef.current === "ebike") { + // Dismount! Teleport player capsule 3 units to the right + const rightDir = new THREE.Vector3(); + camera.getWorldDirection(_forward); + _forward.setY(0).normalize(); + rightDir.crossVectors(_forward, _up).normalize(); + + const shift = rightDir.multiplyScalar(3); + capsule.current.translate(shift); + camera.position.copy(capsule.current.end); + } + prevMovementModeRef.current = movementMode; + }, [movementMode, camera]); + const capsule = useRef(createSpawnCapsule(spawnPosition)); useLayoutEffect(() => { @@ -226,19 +266,39 @@ export function PlayerController({ const dt = Math.min(delta, PLAYER_MAX_DELTA); - camera.getWorldDirection(_forward); - _forward.setY(0); - if (_forward.lengthSq() > 0) { - _forward.normalize(); + // Rotate camera on Y-axis for ebike steering + if (movementModeRef.current === "ebike") { + const turnSpeed = 1.8; // radians per second + if (keys.current.left) { + ebikeAngle.current += turnSpeed * dt; + camera.rotateOnWorldAxis(_up, turnSpeed * dt); + } + if (keys.current.right) { + ebikeAngle.current -= turnSpeed * dt; + camera.rotateOnWorldAxis(_up, -turnSpeed * dt); + } + } + + if (movementModeRef.current === "ebike") { + _forward.set(Math.sin(ebikeAngle.current), 0, Math.cos(ebikeAngle.current)).normalize(); _right.crossVectors(_forward, _up).normalize(); + } else { + camera.getWorldDirection(_forward); + _forward.setY(0); + if (_forward.lengthSq() > 0) { + _forward.normalize(); + _right.crossVectors(_forward, _up).normalize(); + } } _wishDir.set(0, 0, 0); if (!movementLocked) { if (keys.current.forward) _wishDir.add(_forward); if (keys.current.backward) _wishDir.sub(_forward); - if (keys.current.left) _wishDir.sub(_right); - if (keys.current.right) _wishDir.add(_right); + if (movementModeRef.current !== "ebike") { + if (keys.current.left) _wishDir.sub(_right); + if (keys.current.right) _wishDir.add(_right); + } } if (_wishDir.lengthSq() > 0) _wishDir.normalize(); @@ -288,9 +348,22 @@ export function PlayerController({ } } - if (movementModeRef.current !== "ebike") { + if (movementModeRef.current === "ebike") { + // Offset of [-3, 8, 0] rotated by e-bike angle + const cameraOffset = new THREE.Vector3(-3, 8, 0); + cameraOffset.applyAxisAngle(_up, ebikeAngle.current); + + const camPos = new THREE.Vector3() + .copy(capsule.current.end) + .add(cameraOffset); + camera.position.copy(camPos); + } else { camera.position.copy(capsule.current.end); } + + // Save player capsule end position and e-bike angle globally so other components (like Ebike) can access it + (window as any).playerPos = [capsule.current.end.x, capsule.current.end.y, capsule.current.end.z]; + (window as any).ebikeAngle = ebikeAngle.current; }); return null;