feat(ebike): add speedometer
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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 (
|
||||
<>
|
||||
<group
|
||||
ref={groupRef}
|
||||
position={position}
|
||||
rotation={[0, EBIKE_WORLD_ROTATION_Y, 0]}
|
||||
>
|
||||
<primitive object={model} />
|
||||
<InteractableObject
|
||||
kind="trigger"
|
||||
label={
|
||||
mainState === "ebike" && ebikeStep === "waiting"
|
||||
? "Inspecter l'e-bike"
|
||||
: movementMode === "walk"
|
||||
? "Monter sur le bike"
|
||||
: "Descendre du bike"
|
||||
}
|
||||
{!repairGameOwnsEbikeModel ? (
|
||||
<group
|
||||
ref={groupRef}
|
||||
position={position}
|
||||
radius={15}
|
||||
onPress={handleInteract}
|
||||
rotation={[0, EBIKE_WORLD_ROTATION_Y, 0]}
|
||||
>
|
||||
<mesh>
|
||||
<boxGeometry args={[10, 13, 2]} />
|
||||
<meshBasicMaterial colorWrite={false} depthWrite={false} />
|
||||
</mesh>
|
||||
</InteractableObject>
|
||||
<primitive object={model} />
|
||||
<InteractableObject
|
||||
kind="trigger"
|
||||
label={interactionLabel}
|
||||
position={position}
|
||||
radius={15}
|
||||
onPress={handleInteract}
|
||||
>
|
||||
<mesh>
|
||||
<boxGeometry args={[10, 13, 2]} />
|
||||
<meshBasicMaterial colorWrite={false} depthWrite={false} />
|
||||
</mesh>
|
||||
</InteractableObject>
|
||||
|
||||
{/* Dynamic 3D GPS Dashboard Screen */}
|
||||
<group position={[0, 7, 0]} rotation={[0, 90, 0]}>
|
||||
<EbikeGPSMap
|
||||
width={0.8}
|
||||
height={0.8}
|
||||
startPos={gpsStartPos}
|
||||
destPos={destPos}
|
||||
mapImageUrl="/assets/world/gps/map_background.png"
|
||||
worldBounds={{
|
||||
minX: -166,
|
||||
maxX: 163,
|
||||
minZ: -142,
|
||||
maxZ: 138,
|
||||
}}
|
||||
zoom={4}
|
||||
/>
|
||||
{/* Dynamic 3D GPS Dashboard Screen */}
|
||||
<group position={[0, 7, 0]} rotation={[0, 90, 0]}>
|
||||
<EbikeGPSMap
|
||||
width={0.8}
|
||||
height={0.8}
|
||||
startPos={gpsStartPos}
|
||||
destPos={destPos}
|
||||
mapImageUrl="/assets/world/gps/map_background.png"
|
||||
worldBounds={{
|
||||
minX: -166,
|
||||
maxX: 163,
|
||||
minZ: -142,
|
||||
maxZ: 138,
|
||||
}}
|
||||
zoom={4}
|
||||
/>
|
||||
</group>
|
||||
<group position={[0, 6.35, 0]} rotation={[0, 90, 0]}>
|
||||
<EbikeSpeedometer />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
) : null}
|
||||
|
||||
{showCameraPoints && (
|
||||
{showCameraPoints && !repairGameOwnsEbikeModel && (
|
||||
<>
|
||||
<mesh position={camPointPos}>
|
||||
<sphereGeometry args={[0.3, 16, 16]} />
|
||||
|
||||
@@ -89,6 +89,8 @@ export interface EbikeGPSMapProps {
|
||||
* Default: 1
|
||||
*/
|
||||
zoom?: number;
|
||||
|
||||
renderOrder?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,6 +109,7 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
position = [0, 0, 0],
|
||||
canvasSize = 1024,
|
||||
zoom = 1,
|
||||
renderOrder = 10_000,
|
||||
}) => {
|
||||
const [waypoints, setWaypoints] = useState<Waypoint[]>([]);
|
||||
const [mapImage, setMapImage] = useState<
|
||||
@@ -506,12 +509,13 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
||||
}, [draw]);
|
||||
|
||||
return (
|
||||
<mesh castShadow receiveShadow position={position}>
|
||||
<mesh position={position} renderOrder={renderOrder}>
|
||||
<planeGeometry args={[width, height]} />
|
||||
<meshBasicMaterial
|
||||
toneMapped={false}
|
||||
transparent={true}
|
||||
opacity={1}
|
||||
depthTest={false}
|
||||
depthWrite={false}
|
||||
side={THREE.DoubleSide}
|
||||
>
|
||||
|
||||
@@ -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<THREE.Group>(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 (
|
||||
<group renderOrder={SPEEDOMETER_RENDER_ORDER}>
|
||||
<mesh renderOrder={SPEEDOMETER_RENDER_ORDER}>
|
||||
<planeGeometry args={[width, height]} />
|
||||
<meshBasicMaterial
|
||||
map={dialTexture}
|
||||
transparent
|
||||
depthTest={false}
|
||||
depthWrite={false}
|
||||
toneMapped={false}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
<group ref={needleGroupRef} position={[0, -height * 0.38, 0.002]}>
|
||||
<mesh
|
||||
position={[0, needleHeight / 2, 0]}
|
||||
renderOrder={SPEEDOMETER_RENDER_ORDER + 1}
|
||||
>
|
||||
<planeGeometry args={[needleWidth, needleHeight]} />
|
||||
<meshBasicMaterial
|
||||
map={needleTexture}
|
||||
transparent
|
||||
depthTest={false}
|
||||
depthWrite={false}
|
||||
toneMapped={false}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user