import { useEffect, useRef, useState, useMemo } from "react"; import * as THREE from "three"; import { useFrame, useThree } from "@react-three/fiber"; import { EbikeGPSMap } from "@/components/ebike/EbikeGPSMap"; 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 { animateCameraTransformTransition } 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"; export interface CameraTransform { position: Vector3Tuple; rotation: Vector3Tuple; } export const EBIKE_CAMERA_TRANSFORM: CameraTransform = { position: [-3.5, 6, 0], rotation: [-10, -90, 0], }; const EBIKE_DROP_PLAYER_TRANSFORM: CameraTransform = { position: [0, 1.5, -3], rotation: [0, 0, 0], }; interface EbikeProps { position: Vector3Tuple; } export function Ebike({ position }: EbikeProps): React.JSX.Element { const groupRef = useRef(null); const { scene } = useLoggedGLTF(EBIKE_MODEL_PATH, { scope: "Ebike", position: position, }); const model = useClonedObject(scene); const movementMode = useGameStore((state) => state.player.movementMode); const mainState = useGameStore((state) => state.mainState); const camera = useThree((state) => state.camera); // Map active mainState to target repair zone coordinate const destPos = useMemo(() => { switch (mainState) { case "bike": return { x: 8, y: 0, z: -6 }; case "pylone": return { x: 64, y: 0, z: -66 }; case "ferme": return { x: -24, y: 0, z: 42 }; default: return undefined; } }, [mainState]); // Throttled GPS start position to optimize pathfinding A* algorithm execution const [gpsStartPos, setGpsStartPos] = useState<{ x: number; y: number; z: number }>({ x: position[0], y: position[1], z: position[2], }); const lastGpsUpdatePos = useRef(new THREE.Vector3(...position)); const restingPosition = useRef([ position[0], position[1] - PLAYER_EYE_HEIGHT, position[2], ]); const restingRotation = useRef(0); const forkRef = useRef(null); useEffect(() => { if (model) { const fork = model.getObjectByName("fourche"); if (fork) { forkRef.current = fork; } } }, [model]); useEffect(() => { (window as any).ebikeVisualGroup = groupRef; (window as any).ebikeParkedPosition = restingPosition.current; (window as any).ebikeParkedRotation = restingRotation.current; return () => { (window as any).ebikeVisualGroup = null; (window as any).ebikeParkedPosition = null; (window as any).ebikeParkedRotation = null; }; }, []); useFrame((_, delta) => { if (groupRef.current) { if (movementMode === "ebike") { restingPosition.current = [ groupRef.current.position.x, groupRef.current.position.y, groupRef.current.position.z, ]; restingRotation.current = groupRef.current.rotation.y; // Smoothly rotate the front fork ("fourche") up to 15 degrees in its own Z axis const steerFactor = (window as any).ebikeSteerFactor || 0; if (forkRef.current) { // 15 degrees is 0.26 radians const targetForkRotation = steerFactor * 0.26; forkRef.current.rotation.z = THREE.MathUtils.lerp( forkRef.current.rotation.z, targetForkRotation, 12 * delta ); } // Throttled GPS start position update to prevent performance loss const currentPos = groupRef.current.position; if (currentPos.distanceTo(lastGpsUpdatePos.current) > 2.0) { lastGpsUpdatePos.current.copy(currentPos); setGpsStartPos({ x: currentPos.x, y: currentPos.y, z: currentPos.z }); } } else { groupRef.current.position.set(...restingPosition.current); groupRef.current.rotation.set(0, restingRotation.current, 0); // Reset fork rotation when parked if (forkRef.current) { forkRef.current.rotation.z = 0; } } (window as any).ebikeParkedPosition = restingPosition.current; (window as any).ebikeParkedRotation = restingRotation.current; } }); const camPointPos: Vector3Tuple = [ restingPosition.current[0] + EBIKE_CAMERA_TRANSFORM.position[0], restingPosition.current[1] + EBIKE_CAMERA_TRANSFORM.position[1], restingPosition.current[2] + EBIKE_CAMERA_TRANSFORM.position[2], ]; const dropPointPos: Vector3Tuple = [ restingPosition.current[0] + EBIKE_DROP_PLAYER_TRANSFORM.position[0], restingPosition.current[1] + EBIKE_DROP_PLAYER_TRANSFORM.position[1], restingPosition.current[2] + EBIKE_DROP_PLAYER_TRANSFORM.position[2], ]; const handleInteract = (): void => { if (movementMode === "walk") { const cameraOffset = new THREE.Vector3(...EBIKE_CAMERA_TRANSFORM.position); cameraOffset.applyAxisAngle(new THREE.Vector3(0, 1, 0), restingRotation.current); const targetCamPos: Vector3Tuple = [ restingPosition.current[0] + cameraOffset.x, restingPosition.current[1] + cameraOffset.y, restingPosition.current[2] + cameraOffset.z, ]; const targetRotation: Vector3Tuple = [ EBIKE_CAMERA_TRANSFORM.rotation[0], EBIKE_CAMERA_TRANSFORM.rotation[1] + THREE.MathUtils.radToDeg(restingRotation.current), EBIKE_CAMERA_TRANSFORM.rotation[2], ]; animateCameraTransformTransition(targetCamPos, targetRotation, 1, () => { useGameStore.getState().setPlayerMovementMode("ebike"); }); } else { const currentPos = new THREE.Vector3(); if (groupRef.current) { groupRef.current.getWorldPosition(currentPos); } else { currentPos.set(...position); } const targetCamPos: Vector3Tuple = [ currentPos.x + EBIKE_DROP_PLAYER_TRANSFORM.position[0], currentPos.y + EBIKE_DROP_PLAYER_TRANSFORM.position[1], currentPos.z + EBIKE_DROP_PLAYER_TRANSFORM.position[2], ]; // Get camera's current rotation in degrees so we keep the exact orientation during dismount const currentEuler = new THREE.Euler().setFromQuaternion(camera.quaternion, "YXZ"); const targetRotation: Vector3Tuple = [ THREE.MathUtils.radToDeg(currentEuler.x), THREE.MathUtils.radToDeg(currentEuler.y), THREE.MathUtils.radToDeg(currentEuler.z), ]; animateCameraTransformTransition(targetCamPos, targetRotation, 1, () => { useGameStore.getState().setPlayerMovementMode("walk"); }); } }; const handleInteractRef = useRef(handleInteract); handleInteractRef.current = handleInteract; const debugRef = useRef({ showCameraPoints: true }); const debugActions = useRef({ toggleRide: () => { handleInteractRef.current(); } }); useDebugFolder("Ebike", (folder) => { folder .add(debugRef.current, "showCameraPoints") .name("Show Camera Points") .onChange((value: boolean) => { debugRef.current.showCameraPoints = value; }); folder .add(debugActions.current, "toggleRide") .name("Monter / Descendre"); }); return ( <> {/* Dynamic 3D GPS Dashboard Screen */} {debugRef.current.showCameraPoints && ( <> )} ); }