From cff7744ad9ae162605825eb6174dab0295d42c7d Mon Sep 17 00:00:00 2001 From: math-pixel <59537610+math-pixel@users.noreply.github.com> Date: Sun, 17 May 2026 07:41:29 +0200 Subject: [PATCH] wip --- src/components/ebike/Ebike.tsx | 80 +++++++++++++++++++++++++++ src/data/player/playerConfig.ts | 1 + src/managers/stores/useGameStore.ts | 24 ++++++++ src/world/GameCinematics.tsx | 62 +++++++++++++++++++++ src/world/GameStageContent.tsx | 4 ++ src/world/player/PlayerController.tsx | 6 +- 6 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 src/components/ebike/Ebike.tsx diff --git a/src/components/ebike/Ebike.tsx b/src/components/ebike/Ebike.tsx new file mode 100644 index 0000000..05f13ff --- /dev/null +++ b/src/components/ebike/Ebike.tsx @@ -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(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 ( + + + + + + + + + + ); +} diff --git a/src/data/player/playerConfig.ts b/src/data/player/playerConfig.ts index 360cebe..32db135 100644 --- a/src/data/player/playerConfig.ts +++ b/src/data/player/playerConfig.ts @@ -4,6 +4,7 @@ export const PLAYER_EYE_HEIGHT = 1.75; export const PLAYER_CAPSULE_RADIUS = 0.35; export const PLAYER_WALK_SPEED = 11; +export const PLAYER_EBIKE_SPEED = 25; export const PLAYER_AIR_CONTROL_FACTOR = 0.35; export const PLAYER_JUMP_SPEED = 9; export const PLAYER_GRAVITY = 30; diff --git a/src/managers/stores/useGameStore.ts b/src/managers/stores/useGameStore.ts index 15489dd..20835f4 100644 --- a/src/managers/stores/useGameStore.ts +++ b/src/managers/stores/useGameStore.ts @@ -7,8 +7,13 @@ import { type MissionStep, type RepairMissionId, } 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 PlayerMovementMode = "walk" | "ebike"; export type { MissionStep, RepairMissionId }; interface IntroState { @@ -30,10 +35,16 @@ interface MissionFlowState { playerName: string; } +interface PlayerState { + movementMode: PlayerMovementMode; + currentSpeed: number; +} + interface GameState { mainState: MainGameState; isCinematicPlaying: boolean; missionFlow: MissionFlowState; + player: PlayerState; intro: IntroState; bike: MissionState & { isRepaired: boolean; @@ -56,6 +67,7 @@ interface GameActions { hideDialog: () => void; setActivityCity: (activityCity: boolean) => void; setCanMove: (canMove: boolean) => void; + setPlayerMovementMode: (mode: PlayerMovementMode) => void; setIntroStep: (step: GameStep) => void; setIntroState: (intro: Partial) => void; setPlayerName: (playerName: string) => void; @@ -209,6 +221,10 @@ function createInitialGameState(): GameState { dialogMessage: null, playerName: "", }, + player: { + movementMode: "walk", + currentSpeed: PLAYER_WALK_SPEED, + }, intro: { currentStep: "intro", dialogueAudio: null, @@ -249,6 +265,14 @@ export const useGameStore = create()((set) => ({ set((state) => ({ missionFlow: { ...state.missionFlow, activityCity }, })), + setPlayerMovementMode: (mode) => + set((state) => ({ + player: { + ...state.player, + movementMode: mode, + currentSpeed: mode === "ebike" ? PLAYER_EBIKE_SPEED : PLAYER_WALK_SPEED, + }, + })), setCanMove: (canMove) => set((state) => ({ missionFlow: { ...state.missionFlow, canMove }, diff --git a/src/world/GameCinematics.tsx b/src/world/GameCinematics.tsx index 7f60ca7..c662986 100644 --- a/src/world/GameCinematics.tsx +++ b/src/world/GameCinematics.tsx @@ -9,6 +9,7 @@ import type { CinematicManifest, } from "@/types/cinematics/cinematics"; import type { DialogueManifest } from "@/types/dialogues/dialogues"; +import type { Vector3Tuple } from "@/types/three/three"; import { logger } from "@/utils/core/Logger"; import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest"; import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest"; @@ -16,6 +17,11 @@ import { queueDialogueById } from "@/utils/dialogues/playDialogue"; export function GameCinematics(): null { const camera = useThree((state) => state.camera); + + useEffect(() => { + setGlobalCamera(camera); + }, [camera]); + const [manifest, setManifest] = useState(null); const [dialogueManifest, setDialogueManifest] = useState(null); @@ -171,3 +177,59 @@ function playCinematic( 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, + ); +} diff --git a/src/world/GameStageContent.tsx b/src/world/GameStageContent.tsx index e18be89..1ffe603 100644 --- a/src/world/GameStageContent.tsx +++ b/src/world/GameStageContent.tsx @@ -1,4 +1,5 @@ import { RepairGame } from "@/components/three/gameplay/RepairGame"; +import { Ebike } from "@/components/ebike/Ebike"; import { useGameStore } from "@/managers/stores/useGameStore"; import type { RepairMissionId } from "@/types/gameplay/repairMission"; import type { Vector3Tuple } from "@/types/three/three"; @@ -50,12 +51,15 @@ function StageAnchor({ export function GameStageContent(): React.JSX.Element { const mainState = useGameStore((state) => state.mainState); + const isBikeUnlocked = useGameStore((state) => state.intro.isBikeUnlocked); return ( <> {mainState === "intro" ? ( ) : null} + {/* {isBikeUnlocked ? : null} */} + {GAME_REPAIR_ZONES.map((zone) => ( state.missionFlow.canMove); + const currentSpeed = useGameStore((state) => state.player.currentSpeed); const capsule = useRef(createSpawnCapsule(spawnPosition)); @@ -237,8 +237,8 @@ export function PlayerController({ if (_wishDir.lengthSq() > 0) _wishDir.normalize(); const accel = onFloor.current - ? PLAYER_WALK_SPEED - : PLAYER_WALK_SPEED * PLAYER_AIR_CONTROL_FACTOR; + ? currentSpeed + : currentSpeed * PLAYER_AIR_CONTROL_FACTOR; velocity.current.x += _wishDir.x * accel * dt * PLAYER_ACCELERATION_MULTIPLIER; velocity.current.z +=