Merge e_bike + gps into develop #7

Merged
math-pixel merged 23 commits from feat/gps into develop 2026-05-28 05:55:19 +00:00
6 changed files with 174 additions and 3 deletions
Showing only changes of commit cff7744ad9 - Show all commits
+80
View File
@@ -0,0 +1,80 @@
import { useRef } from "react";
import * as THREE from "three";
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
import { useClonedObject } from "@/hooks/three/useClonedObject";
import { animateCameraTransition } from "@/world/GameCinematics";
import { useGameStore } from "@/managers/stores/useGameStore";
import type { Vector3Tuple } from "@/types/three/three";
const EBIKE_MODEL_PATH = "/models/ebike/model.gltf";
const EBIKE_CAMERA_POSITION: Vector3Tuple = [0, 1.5, -2];
const EBIKE_DROP_PLAYER_POSITION: Vector3Tuple = [2, 0, 0];
interface EbikeProps {
position: Vector3Tuple;
}
export function Ebike({ position }: EbikeProps): React.JSX.Element {
const groupRef = useRef<THREE.Group>(null);
const { scene } = useLoggedGLTF(EBIKE_MODEL_PATH, {
scope: "Ebike",
position,
});
const model = useClonedObject(scene);
const movementMode = useGameStore((state) => state.player.movementMode);
const handleInteract = (): void => {
if (movementMode === "walk") {
const targetCamPos: Vector3Tuple = [
position[0] + EBIKE_CAMERA_POSITION[0],
position[1] + EBIKE_CAMERA_POSITION[1],
position[2] + EBIKE_CAMERA_POSITION[2],
];
const targetLookAt: Vector3Tuple = [
position[0],
position[1] + 1,
position[2],
];
animateCameraTransition(targetCamPos, targetLookAt, 1, () => {
useGameStore.getState().setPlayerMovementMode("ebike");
});
} else {
const targetCamPos: Vector3Tuple = [
position[0] + EBIKE_DROP_PLAYER_POSITION[0],
position[1] + EBIKE_DROP_PLAYER_POSITION[1],
position[2] + EBIKE_DROP_PLAYER_POSITION[2],
];
const targetLookAt: Vector3Tuple = [
position[0],
position[1] + 1,
position[2],
];
animateCameraTransition(targetCamPos, targetLookAt, 1, () => {
useGameStore.getState().setPlayerMovementMode("walk");
});
}
};
return (
<group ref={groupRef}>
<primitive object={model} />
<InteractableObject
kind="trigger"
label={
movementMode === "walk" ? "Monter sur le bike" : "Descendre du bike"
}
position={position}
radius={10}
onPress={handleInteract}
>
<mesh>
<boxGeometry args={[1.5, 1.5, 1.5]} />
<meshStandardMaterial color="red" opacity={0.5} transparent />
</mesh>
</InteractableObject>
</group>
);
}
+1
View File
@@ -4,6 +4,7 @@ export const PLAYER_EYE_HEIGHT = 1.75;
export const PLAYER_CAPSULE_RADIUS = 0.35; export const PLAYER_CAPSULE_RADIUS = 0.35;
export const PLAYER_WALK_SPEED = 11; export const PLAYER_WALK_SPEED = 11;
export const PLAYER_EBIKE_SPEED = 25;
export const PLAYER_AIR_CONTROL_FACTOR = 0.35; export const PLAYER_AIR_CONTROL_FACTOR = 0.35;
export const PLAYER_JUMP_SPEED = 9; export const PLAYER_JUMP_SPEED = 9;
export const PLAYER_GRAVITY = 30; export const PLAYER_GRAVITY = 30;
+24
View File
@@ -7,8 +7,13 @@ import {
type MissionStep, type MissionStep,
type RepairMissionId, type RepairMissionId,
} from "@/types/gameplay/repairMission"; } from "@/types/gameplay/repairMission";
import {
PLAYER_WALK_SPEED,
PLAYER_EBIKE_SPEED,
} from "@/data/player/playerConfig";
export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro"; export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro";
export type PlayerMovementMode = "walk" | "ebike";
export type { MissionStep, RepairMissionId }; export type { MissionStep, RepairMissionId };
interface IntroState { interface IntroState {
@@ -30,10 +35,16 @@ interface MissionFlowState {
playerName: string; playerName: string;
} }
interface PlayerState {
movementMode: PlayerMovementMode;
currentSpeed: number;
}
interface GameState { interface GameState {
mainState: MainGameState; mainState: MainGameState;
isCinematicPlaying: boolean; isCinematicPlaying: boolean;
missionFlow: MissionFlowState; missionFlow: MissionFlowState;
player: PlayerState;
intro: IntroState; intro: IntroState;
bike: MissionState & { bike: MissionState & {
isRepaired: boolean; isRepaired: boolean;
@@ -56,6 +67,7 @@ interface GameActions {
hideDialog: () => void; hideDialog: () => void;
setActivityCity: (activityCity: boolean) => void; setActivityCity: (activityCity: boolean) => void;
setCanMove: (canMove: boolean) => void; setCanMove: (canMove: boolean) => void;
setPlayerMovementMode: (mode: PlayerMovementMode) => void;
setIntroStep: (step: GameStep) => void; setIntroStep: (step: GameStep) => void;
setIntroState: (intro: Partial<IntroState>) => void; setIntroState: (intro: Partial<IntroState>) => void;
setPlayerName: (playerName: string) => void; setPlayerName: (playerName: string) => void;
@@ -209,6 +221,10 @@ function createInitialGameState(): GameState {
dialogMessage: null, dialogMessage: null,
playerName: "", playerName: "",
}, },
player: {
movementMode: "walk",
currentSpeed: PLAYER_WALK_SPEED,
},
intro: { intro: {
currentStep: "intro", currentStep: "intro",
dialogueAudio: null, dialogueAudio: null,
@@ -249,6 +265,14 @@ export const useGameStore = create<GameStore>()((set) => ({
set((state) => ({ set((state) => ({
missionFlow: { ...state.missionFlow, activityCity }, missionFlow: { ...state.missionFlow, activityCity },
})), })),
setPlayerMovementMode: (mode) =>
set((state) => ({
player: {
...state.player,
movementMode: mode,
currentSpeed: mode === "ebike" ? PLAYER_EBIKE_SPEED : PLAYER_WALK_SPEED,
},
})),
setCanMove: (canMove) => setCanMove: (canMove) =>
set((state) => ({ set((state) => ({
missionFlow: { ...state.missionFlow, canMove }, missionFlow: { ...state.missionFlow, canMove },
+62
View File
@@ -9,6 +9,7 @@ import type {
CinematicManifest, CinematicManifest,
} from "@/types/cinematics/cinematics"; } from "@/types/cinematics/cinematics";
import type { DialogueManifest } from "@/types/dialogues/dialogues"; import type { DialogueManifest } from "@/types/dialogues/dialogues";
import type { Vector3Tuple } from "@/types/three/three";
import { logger } from "@/utils/core/Logger"; import { logger } from "@/utils/core/Logger";
import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest"; import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest";
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest"; import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
@@ -16,6 +17,11 @@ import { queueDialogueById } from "@/utils/dialogues/playDialogue";
export function GameCinematics(): null { export function GameCinematics(): null {
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
useEffect(() => {
setGlobalCamera(camera);
}, [camera]);
const [manifest, setManifest] = useState<CinematicManifest | null>(null); const [manifest, setManifest] = useState<CinematicManifest | null>(null);
const [dialogueManifest, setDialogueManifest] = const [dialogueManifest, setDialogueManifest] =
useState<DialogueManifest | null>(null); useState<DialogueManifest | null>(null);
@@ -171,3 +177,59 @@ function playCinematic(
timelineRef.current = timeline; timelineRef.current = timeline;
} }
let cameraTransitionTimeline: gsap.core.Timeline | null = null;
let globalCamera: THREE.Camera | null = null;
export function setGlobalCamera(camera: THREE.Camera): void {
globalCamera = camera;
}
export function animateCameraTransition(
targetPosition: Vector3Tuple,
targetLookAt: Vector3Tuple,
duration: number = 1,
onComplete?: () => void,
): void {
if (!globalCamera) {
logger.warn("GameCinematics", "Camera not found for transition");
onComplete?.();
return;
}
const camera = globalCamera;
cameraTransitionTimeline?.kill();
useGameStore.getState().setCinematicPlaying(true);
const target = new THREE.Vector3(...targetLookAt);
cameraTransitionTimeline = gsap.timeline({
onUpdate: () => camera.lookAt(target),
onComplete: () => {
cameraTransitionTimeline = null;
useGameStore.getState().setCinematicPlaying(false);
onComplete?.();
},
});
cameraTransitionTimeline.to(camera.position, {
x: targetPosition[0],
y: targetPosition[1],
z: targetPosition[2],
duration,
ease: "power2.inOut",
});
cameraTransitionTimeline.to(
target,
{
x: targetLookAt[0],
y: targetLookAt[1],
z: targetLookAt[2],
duration,
ease: "power2.inOut",
},
0,
);
}
+4
View File
@@ -1,4 +1,5 @@
import { RepairGame } from "@/components/three/gameplay/RepairGame"; import { RepairGame } from "@/components/three/gameplay/RepairGame";
import { Ebike } from "@/components/ebike/Ebike";
import { useGameStore } from "@/managers/stores/useGameStore"; import { useGameStore } from "@/managers/stores/useGameStore";
import type { RepairMissionId } from "@/types/gameplay/repairMission"; import type { RepairMissionId } from "@/types/gameplay/repairMission";
import type { Vector3Tuple } from "@/types/three/three"; import type { Vector3Tuple } from "@/types/three/three";
@@ -50,12 +51,15 @@ function StageAnchor({
export function GameStageContent(): React.JSX.Element { export function GameStageContent(): React.JSX.Element {
const mainState = useGameStore((state) => state.mainState); const mainState = useGameStore((state) => state.mainState);
const isBikeUnlocked = useGameStore((state) => state.intro.isBikeUnlocked);
return ( return (
<> <>
{mainState === "intro" ? ( {mainState === "intro" ? (
<StageAnchor color="#7dd3fc" position={[0, 4, 0]} /> <StageAnchor color="#7dd3fc" position={[0, 4, 0]} />
) : null} ) : null}
{/* {isBikeUnlocked ? <Ebike position={[0, 15, 0]} /> : null} */}
<Ebike position={[0, 5, 0]} />
{GAME_REPAIR_ZONES.map((zone) => ( {GAME_REPAIR_ZONES.map((zone) => (
<RepairGame <RepairGame
key={zone.mission} key={zone.mission}
+3 -3
View File
@@ -20,7 +20,6 @@ import {
PLAYER_GRAVITY, PLAYER_GRAVITY,
PLAYER_JUMP_SPEED, PLAYER_JUMP_SPEED,
PLAYER_MAX_DELTA, PLAYER_MAX_DELTA,
PLAYER_WALK_SPEED,
PLAYER_XZ_DAMPING_FACTOR, PLAYER_XZ_DAMPING_FACTOR,
} from "@/data/player/playerConfig"; } from "@/data/player/playerConfig";
import { useRepairMovementLocked } from "@/hooks/gameplay/useRepairMovementLocked"; import { useRepairMovementLocked } from "@/hooks/gameplay/useRepairMovementLocked";
@@ -108,6 +107,7 @@ export function PlayerController({
const wantsJump = useRef(false); const wantsJump = useRef(false);
const initializedRef = useRef(false); const initializedRef = useRef(false);
const canMove = useGameStore((state) => state.missionFlow.canMove); const canMove = useGameStore((state) => state.missionFlow.canMove);
const currentSpeed = useGameStore((state) => state.player.currentSpeed);
const capsule = useRef(createSpawnCapsule(spawnPosition)); const capsule = useRef(createSpawnCapsule(spawnPosition));
@@ -237,8 +237,8 @@ export function PlayerController({
if (_wishDir.lengthSq() > 0) _wishDir.normalize(); if (_wishDir.lengthSq() > 0) _wishDir.normalize();
const accel = onFloor.current const accel = onFloor.current
? PLAYER_WALK_SPEED ? currentSpeed
: PLAYER_WALK_SPEED * PLAYER_AIR_CONTROL_FACTOR; : currentSpeed * PLAYER_AIR_CONTROL_FACTOR;
velocity.current.x += velocity.current.x +=
_wishDir.x * accel * dt * PLAYER_ACCELERATION_MULTIPLIER; _wishDir.x * accel * dt * PLAYER_ACCELERATION_MULTIPLIER;
velocity.current.z += velocity.current.z +=