feat: update ui and intro sequence

This commit is contained in:
Tom Boullay
2026-06-01 00:54:59 +02:00
parent 9ef94af488
commit 061e0dc677
7 changed files with 80 additions and 20 deletions
Binary file not shown.
+4 -2
View File
@@ -14,6 +14,7 @@ import { PLAYER_EYE_HEIGHT } from "@/data/player/playerConfig";
import { import {
EBIKE_CAMERA_TRANSFORM, EBIKE_CAMERA_TRANSFORM,
EBIKE_DROP_PLAYER_TRANSFORM, EBIKE_DROP_PLAYER_TRANSFORM,
EBIKE_WORLD_SCALE,
EBIKE_WORLD_ROTATION_Y, EBIKE_WORLD_ROTATION_Y,
} from "@/data/ebike/ebikeConfig"; } from "@/data/ebike/ebikeConfig";
import type { Vector3Tuple } from "@/types/three/three"; import type { Vector3Tuple } from "@/types/three/three";
@@ -283,17 +284,18 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
ref={groupRef} ref={groupRef}
position={position} position={position}
rotation={[0, EBIKE_WORLD_ROTATION_Y, 0]} rotation={[0, EBIKE_WORLD_ROTATION_Y, 0]}
scale={EBIKE_WORLD_SCALE}
> >
<primitive object={model} /> <primitive object={model} />
<InteractableObject <InteractableObject
kind="trigger" kind="trigger"
label={interactionLabel} label={interactionLabel}
position={position} position={position}
radius={15} radius={5}
onPress={handleInteract} onPress={handleInteract}
> >
<mesh> <mesh>
<boxGeometry args={[10, 13, 2]} /> <boxGeometry args={[8, 9, 2]} />
<meshBasicMaterial colorWrite={false} depthWrite={false} /> <meshBasicMaterial colorWrite={false} depthWrite={false} />
</mesh> </mesh>
</InteractableObject> </InteractableObject>
+57 -10
View File
@@ -1,11 +1,13 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { MissionNotification } from "@/components/ui/MissionNotification"; import { MissionNotification } from "@/components/ui/MissionNotification";
import { import {
EBIKE_BREAKDOWN_DIALOGUE_DELAY_MS, EBIKE_BREAKDOWN_DIALOGUE_DELAY_MS,
EBIKE_BREAKDOWN_DIALOGUE_ID, EBIKE_BREAKDOWN_DIALOGUE_ID,
EBIKE_INTRO_RIDE_DURATION_MS, EBIKE_INTRO_BREAKDOWN_DISTANCE,
EBIKE_SOUNDS, EBIKE_SOUNDS,
} from "@/data/ebike/ebikeConfig"; } from "@/data/ebike/ebikeConfig";
import { INTRO_MISSION_NOTIFICATION_IMAGE_PATH } from "@/data/gameplay/missionNotifications";
import { AudioManager } from "@/managers/AudioManager"; import { AudioManager } from "@/managers/AudioManager";
import { useGameStore } from "@/managers/stores/useGameStore"; import { useGameStore } from "@/managers/stores/useGameStore";
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest"; import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
@@ -18,6 +20,9 @@ export function EbikeIntroSequence(): React.JSX.Element | null {
const completeIntro = useGameStore((state) => state.completeIntro); const completeIntro = useGameStore((state) => state.completeIntro);
const [breakdownDialogueDone, setBreakdownDialogueDone] = useState(false); const [breakdownDialogueDone, setBreakdownDialogueDone] = useState(false);
const hasStartedBreakdown = useRef(false); const hasStartedBreakdown = useRef(false);
const rideDistance = useRef(0);
const lastRidePosition = useRef<THREE.Vector3 | null>(null);
const currentRidePosition = useRef(new THREE.Vector3());
useEffect(() => { useEffect(() => {
if (introStep !== "await-ebike-mount" || movementMode !== "ebike") return; if (introStep !== "await-ebike-mount" || movementMode !== "ebike") return;
@@ -26,16 +31,45 @@ export function EbikeIntroSequence(): React.JSX.Element | null {
}, [introStep, movementMode, setIntroStep]); }, [introStep, movementMode, setIntroStep]);
useEffect(() => { useEffect(() => {
if (introStep !== "ebike-intro-ride") return undefined; if (introStep !== "ebike-intro-ride") return;
const timeoutId = window.setTimeout(() => { rideDistance.current = 0;
lastRidePosition.current = null;
}, [introStep]);
useEffect(() => {
if (introStep !== "ebike-intro-ride" || movementMode !== "ebike") {
return undefined;
}
let animationFrameId = 0;
const tick = () => {
const parkedPosition = window.ebikeParkedPosition;
if (parkedPosition) {
currentRidePosition.current.set(...parkedPosition);
if (!lastRidePosition.current) {
lastRidePosition.current = currentRidePosition.current.clone();
} else {
rideDistance.current += currentRidePosition.current.distanceTo(
lastRidePosition.current,
);
lastRidePosition.current.copy(currentRidePosition.current);
}
if (rideDistance.current >= EBIKE_INTRO_BREAKDOWN_DISTANCE) {
setIntroStep("ebike-breakdown"); setIntroStep("ebike-breakdown");
}, EBIKE_INTRO_RIDE_DURATION_MS); return;
}
}
return () => { animationFrameId = window.requestAnimationFrame(tick);
window.clearTimeout(timeoutId);
}; };
}, [introStep, setIntroStep]);
animationFrameId = window.requestAnimationFrame(tick);
return () => {
window.cancelAnimationFrame(animationFrameId);
};
}, [introStep, movementMode, setIntroStep]);
useEffect(() => { useEffect(() => {
if (introStep !== "ebike-breakdown" || hasStartedBreakdown.current) { if (introStep !== "ebike-breakdown" || hasStartedBreakdown.current) {
@@ -100,14 +134,27 @@ export function EbikeIntroSequence(): React.JSX.Element | null {
} }
}, [introStep]); }, [introStep]);
if (introStep !== "await-ebike-mount" && introStep !== "ebike-intro-ride") { if (
introStep !== "reveal" &&
introStep !== "await-ebike-mount" &&
introStep !== "ebike-intro-ride" &&
introStep !== "ebike-breakdown"
) {
return null; return null;
} }
if (introStep === "ebike-breakdown") {
return <MissionNotification mission="ebike" />;
}
return ( return (
<MissionNotification <MissionNotification
mission="ebike" imagePath={INTRO_MISSION_NOTIFICATION_IMAGE_PATH}
visible={introStep === "await-ebike-mount"} visible={
introStep === "reveal" ||
introStep === "await-ebike-mount" ||
introStep === "ebike-intro-ride"
}
/> />
); );
} }
+7 -2
View File
@@ -2,14 +2,19 @@ import { MISSION_NOTIFICATION_IMAGE_PATHS } from "@/data/gameplay/missionNotific
import type { RepairMissionId } from "@/types/gameplay/repairMission"; import type { RepairMissionId } from "@/types/gameplay/repairMission";
interface MissionNotificationProps { interface MissionNotificationProps {
mission: RepairMissionId; mission?: RepairMissionId;
imagePath?: string;
visible?: boolean; visible?: boolean;
} }
export function MissionNotification({ export function MissionNotification({
mission, mission,
imagePath,
visible = true, visible = true,
}: MissionNotificationProps): React.JSX.Element { }: MissionNotificationProps): React.JSX.Element {
const src =
imagePath ?? (mission ? MISSION_NOTIFICATION_IMAGE_PATHS[mission] : "");
return ( return (
<div <div
className={`mission-notification${visible ? "" : " mission-notification--hidden"}`} className={`mission-notification${visible ? "" : " mission-notification--hidden"}`}
@@ -19,7 +24,7 @@ export function MissionNotification({
<span className="mission-notification__image-wrap"> <span className="mission-notification__image-wrap">
<img <img
className="mission-notification__image" className="mission-notification__image"
src={MISSION_NOTIFICATION_IMAGE_PATHS[mission]} src={src}
alt="Nouvel objectif de mission" alt="Nouvel objectif de mission"
/> />
</span> </span>
+5 -4
View File
@@ -6,19 +6,20 @@ export interface CameraTransform {
} }
export const EBIKE_CAMERA_TRANSFORM: CameraTransform = { export const EBIKE_CAMERA_TRANSFORM: CameraTransform = {
position: [-3.5, 6, 0], position: [-2.6, 4.5, 0],
rotation: [-10, -90, 0], rotation: [-10, -90, 0],
}; };
export const EBIKE_DROP_PLAYER_TRANSFORM: CameraTransform = { export const EBIKE_DROP_PLAYER_TRANSFORM: CameraTransform = {
position: [0, 1.5, -3], position: [0, 1.3, -2.25],
rotation: [0, 0, 0], rotation: [0, 0, 0],
}; };
export const EBIKE_WORLD_POSITION: Vector3Tuple = [61.5, 8.4, 62.4]; export const EBIKE_WORLD_POSITION: Vector3Tuple = [55.8, 1.75, 60.2];
export const EBIKE_WORLD_ROTATION_Y = 2.4107; export const EBIKE_WORLD_ROTATION_Y = 2.4107;
export const EBIKE_WORLD_SCALE = 0.25;
export const EBIKE_INTRO_RIDE_DURATION_MS = 5000; export const EBIKE_INTRO_BREAKDOWN_DISTANCE = 15;
export const EBIKE_BREAKDOWN_DIALOGUE_DELAY_MS = 250; export const EBIKE_BREAKDOWN_DIALOGUE_DELAY_MS = 250;
export const EBIKE_MAX_SPEED = 3; export const EBIKE_MAX_SPEED = 3;
@@ -1,5 +1,8 @@
import type { RepairMissionId } from "@/types/gameplay/repairMission"; import type { RepairMissionId } from "@/types/gameplay/repairMission";
export const INTRO_MISSION_NOTIFICATION_IMAGE_PATH =
"/assets/world/UI/intro-mission-notification.png";
export const MISSION_NOTIFICATION_IMAGE_PATHS: Record<RepairMissionId, string> = export const MISSION_NOTIFICATION_IMAGE_PATHS: Record<RepairMissionId, string> =
{ {
ebike: "/assets/world/UI/ebike-mission-notification.png", ebike: "/assets/world/UI/ebike-mission-notification.png",
@@ -43,7 +43,6 @@ export const CHARACTER_CONFIGS = {
scale: [1.55, 1.55, 1.55], scale: [1.55, 1.55, 1.55],
animations: ["idle", "walk"], animations: ["idle", "walk"],
defaultAnimation: "idle", defaultAnimation: "idle",
snapToTerrain: false,
}, },
fermier: { fermier: {
id: "fermier", id: "fermier",