From 396e7e4ff064f39e7357d44511c3c9b3642fd5c3 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Sun, 31 May 2026 11:36:19 +0200 Subject: [PATCH] feat(ebike): add speedometer --- public/assets/world/gps/cadran.png | 3 + public/assets/world/gps/fleche.png | 3 + src/components/ebike/Ebike.tsx | 101 +++++++++++++--------- src/components/ebike/EbikeGPSMap.tsx | 6 +- src/components/ebike/EbikeSpeedometer.tsx | 90 +++++++++++++++++++ 5 files changed, 161 insertions(+), 42 deletions(-) create mode 100644 public/assets/world/gps/cadran.png create mode 100644 public/assets/world/gps/fleche.png create mode 100644 src/components/ebike/EbikeSpeedometer.tsx diff --git a/public/assets/world/gps/cadran.png b/public/assets/world/gps/cadran.png new file mode 100644 index 0000000..652420c --- /dev/null +++ b/public/assets/world/gps/cadran.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65883c1760293f3a415268a59cae60d0b35de8760de752c8dedb4ab3e19c0e96 +size 531191 diff --git a/public/assets/world/gps/fleche.png b/public/assets/world/gps/fleche.png new file mode 100644 index 0000000..e146cc7 --- /dev/null +++ b/public/assets/world/gps/fleche.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:286164bc5aeb147abb145857cb56832f04a2d17afd50a3d9000ae81059b8201e +size 121079 diff --git a/src/components/ebike/Ebike.tsx b/src/components/ebike/Ebike.tsx index b49e3c4..addef6d 100644 --- a/src/components/ebike/Ebike.tsx +++ b/src/components/ebike/Ebike.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState, useMemo, useCallback } from "react"; import * as THREE from "three"; import { useFrame, useThree } from "@react-three/fiber"; import { EbikeGPSMap } from "@/components/ebike/EbikeGPSMap"; +import { EbikeSpeedometer } from "@/components/ebike/EbikeSpeedometer"; import { InteractableObject } from "@/components/three/interaction/InteractableObject"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import { useClonedObject } from "@/hooks/three/useClonedObject"; @@ -37,6 +38,11 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { const setMissionStep = useGameStore((state) => state.setMissionStep); const camera = useThree((state) => state.camera); const updateEbikeSounds = useEbikeSounds(); + const repairGameOwnsEbikeModel = + mainState === "ebike" && + ebikeStep !== "locked" && + ebikeStep !== "waiting" && + ebikeStep !== "inspected"; // Map active mainState to target repair zone coordinate const destPos = useMemo(() => { @@ -169,16 +175,30 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { debugRestingPosition[1] + EBIKE_DROP_PLAYER_TRANSFORM.position[1], debugRestingPosition[2] + EBIKE_DROP_PLAYER_TRANSFORM.position[2], ]; + const interactionLabel = + mainState === "ebike" + ? "Réparer l'e-bike" + : movementMode === "walk" + ? "Monter sur le bike" + : "Descendre du bike"; const handleInteract = useCallback((): void => { if (window.ebikeBreakdownActive === true) return; if (movementMode === "walk") { - if (mainState === "ebike" && ebikeStep === "waiting") { + if ( + mainState === "ebike" && + (ebikeStep === "locked" || ebikeStep === "waiting") + ) { setMissionStep("ebike", "inspected"); return; } + if (mainState === "ebike" && ebikeStep === "inspected") { + setMissionStep("ebike", "fragmented"); + return; + } + const cameraOffset = new THREE.Vector3( ...EBIKE_CAMERA_TRANSFORM.position, ); @@ -258,51 +278,50 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element { return ( <> - - - - - - - - + + + + + + + - {/* Dynamic 3D GPS Dashboard Screen */} - - + {/* Dynamic 3D GPS Dashboard Screen */} + + + + + + - + ) : null} - {showCameraPoints && ( + {showCameraPoints && !repairGameOwnsEbikeModel && ( <> diff --git a/src/components/ebike/EbikeGPSMap.tsx b/src/components/ebike/EbikeGPSMap.tsx index e51108c..08fab3e 100644 --- a/src/components/ebike/EbikeGPSMap.tsx +++ b/src/components/ebike/EbikeGPSMap.tsx @@ -89,6 +89,8 @@ export interface EbikeGPSMapProps { * Default: 1 */ zoom?: number; + + renderOrder?: number; } /** @@ -107,6 +109,7 @@ export const EbikeGPSMap: React.FC = ({ position = [0, 0, 0], canvasSize = 1024, zoom = 1, + renderOrder = 10_000, }) => { const [waypoints, setWaypoints] = useState([]); const [mapImage, setMapImage] = useState< @@ -506,12 +509,13 @@ export const EbikeGPSMap: React.FC = ({ }, [draw]); return ( - + diff --git a/src/components/ebike/EbikeSpeedometer.tsx b/src/components/ebike/EbikeSpeedometer.tsx new file mode 100644 index 0000000..82d7040 --- /dev/null +++ b/src/components/ebike/EbikeSpeedometer.tsx @@ -0,0 +1,90 @@ +import { useEffect, useRef } from "react"; +import { useFrame } from "@react-three/fiber"; +import { useTexture } from "@react-three/drei"; +import * as THREE from "three"; + +const SPEEDOMETER_DIAL_TEXTURE = "/assets/world/gps/cadran.png"; +const SPEEDOMETER_NEEDLE_TEXTURE = "/assets/world/gps/fleche.png"; +const SPEEDOMETER_MIN_ANGLE = Math.PI / 2; +const SPEEDOMETER_MAX_ANGLE = -Math.PI / 2; +const SPEEDOMETER_RENDER_ORDER = 10_000; + +interface EbikeSpeedometerProps { + width?: number; + height?: number; +} + +export function EbikeSpeedometer({ + width = 0.9, + height = 0.5, +}: EbikeSpeedometerProps): React.JSX.Element { + const needleGroupRef = useRef(null); + const speedFactorRef = useRef(0); + const [dialTexture, needleTexture] = useTexture([ + SPEEDOMETER_DIAL_TEXTURE, + SPEEDOMETER_NEEDLE_TEXTURE, + ]) as [THREE.Texture, THREE.Texture]; + const needleWidth = width * 0.68; + const needleHeight = needleWidth / 2; + + useEffect(() => { + [dialTexture, needleTexture].forEach((texture) => { + texture.colorSpace = THREE.SRGBColorSpace; + texture.needsUpdate = true; + }); + }, [dialTexture, needleTexture]); + + useFrame((_, delta) => { + const targetSpeedFactor = THREE.MathUtils.clamp( + window.ebikeSpeedFactor ?? 0, + 0, + 1, + ); + speedFactorRef.current = THREE.MathUtils.lerp( + speedFactorRef.current, + targetSpeedFactor, + Math.min(1, delta * 10), + ); + + if (needleGroupRef.current) { + needleGroupRef.current.rotation.z = THREE.MathUtils.lerp( + SPEEDOMETER_MIN_ANGLE, + SPEEDOMETER_MAX_ANGLE, + speedFactorRef.current, + ); + } + }); + + return ( + + + + + + + + + + + + + + ); +}