wip
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 +=
|
||||||
|
|||||||
Reference in New Issue
Block a user