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 * as THREE from "three";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { EbikeGPSMap } from "@/components/ebike/EbikeGPSMap";
|
import { EbikeGPSMap } from "@/components/ebike/EbikeGPSMap";
|
||||||
|
import { EbikeSpeedometer } from "@/components/ebike/EbikeSpeedometer";
|
||||||
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
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 setMissionStep = useGameStore((state) => state.setMissionStep);
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const updateEbikeSounds = useEbikeSounds();
|
const updateEbikeSounds = useEbikeSounds();
|
||||||
|
const repairGameOwnsEbikeModel =
|
||||||
|
mainState === "ebike" &&
|
||||||
|
ebikeStep !== "locked" &&
|
||||||
|
ebikeStep !== "waiting" &&
|
||||||
|
ebikeStep !== "inspected";
|
||||||
|
|
||||||
// Map active mainState to target repair zone coordinate
|
// Map active mainState to target repair zone coordinate
|
||||||
const destPos = useMemo(() => {
|
const destPos = useMemo(() => {
|
||||||
@@ -169,16 +175,30 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
debugRestingPosition[1] + EBIKE_DROP_PLAYER_TRANSFORM.position[1],
|
debugRestingPosition[1] + EBIKE_DROP_PLAYER_TRANSFORM.position[1],
|
||||||
debugRestingPosition[2] + EBIKE_DROP_PLAYER_TRANSFORM.position[2],
|
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 => {
|
const handleInteract = useCallback((): void => {
|
||||||
if (window.ebikeBreakdownActive === true) return;
|
if (window.ebikeBreakdownActive === true) return;
|
||||||
|
|
||||||
if (movementMode === "walk") {
|
if (movementMode === "walk") {
|
||||||
if (mainState === "ebike" && ebikeStep === "waiting") {
|
if (
|
||||||
|
mainState === "ebike" &&
|
||||||
|
(ebikeStep === "locked" || ebikeStep === "waiting")
|
||||||
|
) {
|
||||||
setMissionStep("ebike", "inspected");
|
setMissionStep("ebike", "inspected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mainState === "ebike" && ebikeStep === "inspected") {
|
||||||
|
setMissionStep("ebike", "fragmented");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const cameraOffset = new THREE.Vector3(
|
const cameraOffset = new THREE.Vector3(
|
||||||
...EBIKE_CAMERA_TRANSFORM.position,
|
...EBIKE_CAMERA_TRANSFORM.position,
|
||||||
);
|
);
|
||||||
@@ -258,6 +278,7 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{!repairGameOwnsEbikeModel ? (
|
||||||
<group
|
<group
|
||||||
ref={groupRef}
|
ref={groupRef}
|
||||||
position={position}
|
position={position}
|
||||||
@@ -266,13 +287,7 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
<primitive object={model} />
|
<primitive object={model} />
|
||||||
<InteractableObject
|
<InteractableObject
|
||||||
kind="trigger"
|
kind="trigger"
|
||||||
label={
|
label={interactionLabel}
|
||||||
mainState === "ebike" && ebikeStep === "waiting"
|
|
||||||
? "Inspecter l'e-bike"
|
|
||||||
: movementMode === "walk"
|
|
||||||
? "Monter sur le bike"
|
|
||||||
: "Descendre du bike"
|
|
||||||
}
|
|
||||||
position={position}
|
position={position}
|
||||||
radius={15}
|
radius={15}
|
||||||
onPress={handleInteract}
|
onPress={handleInteract}
|
||||||
@@ -300,9 +315,13 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
|
|||||||
zoom={4}
|
zoom={4}
|
||||||
/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
|
<group position={[0, 6.35, 0]} rotation={[0, 90, 0]}>
|
||||||
|
<EbikeSpeedometer />
|
||||||
</group>
|
</group>
|
||||||
|
</group>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{showCameraPoints && (
|
{showCameraPoints && !repairGameOwnsEbikeModel && (
|
||||||
<>
|
<>
|
||||||
<mesh position={camPointPos}>
|
<mesh position={camPointPos}>
|
||||||
<sphereGeometry args={[0.3, 16, 16]} />
|
<sphereGeometry args={[0.3, 16, 16]} />
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ export interface EbikeGPSMapProps {
|
|||||||
* Default: 1
|
* Default: 1
|
||||||
*/
|
*/
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
|
|
||||||
|
renderOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,6 +109,7 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
|||||||
position = [0, 0, 0],
|
position = [0, 0, 0],
|
||||||
canvasSize = 1024,
|
canvasSize = 1024,
|
||||||
zoom = 1,
|
zoom = 1,
|
||||||
|
renderOrder = 10_000,
|
||||||
}) => {
|
}) => {
|
||||||
const [waypoints, setWaypoints] = useState<Waypoint[]>([]);
|
const [waypoints, setWaypoints] = useState<Waypoint[]>([]);
|
||||||
const [mapImage, setMapImage] = useState<
|
const [mapImage, setMapImage] = useState<
|
||||||
@@ -506,12 +509,13 @@ export const EbikeGPSMap: React.FC<EbikeGPSMapProps> = ({
|
|||||||
}, [draw]);
|
}, [draw]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh castShadow receiveShadow position={position}>
|
<mesh position={position} renderOrder={renderOrder}>
|
||||||
<planeGeometry args={[width, height]} />
|
<planeGeometry args={[width, height]} />
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
toneMapped={false}
|
toneMapped={false}
|
||||||
transparent={true}
|
transparent={true}
|
||||||
opacity={1}
|
opacity={1}
|
||||||
|
depthTest={false}
|
||||||
depthWrite={false}
|
depthWrite={false}
|
||||||
side={THREE.DoubleSide}
|
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