diff --git a/src/components/ui/intro/IntroDialogueOverlay.tsx b/src/components/ui/intro/IntroDialogueOverlay.tsx
new file mode 100644
index 0000000..029619e
--- /dev/null
+++ b/src/components/ui/intro/IntroDialogueOverlay.tsx
@@ -0,0 +1,50 @@
+import { useEffect, useRef } from "react";
+import { AudioManager } from "@/managers/AudioManager";
+import { useGameStore } from "@/managers/stores/useGameStore";
+
+const INTRO_DIALOGUE_PATH = "/sounds/dialogue/narrateur_ordreebike.mp3";
+
+/**
+ * Black screen overlay with dialogue audio
+ * - Plays narrateur_ordreebike.mp3
+ * - Transitions to reveal step when dialogue ends
+ */
+export function IntroDialogueOverlay(): React.JSX.Element {
+ const setIntroStep = useGameStore((state) => state.setIntroStep);
+ const dialogueStarted = useRef(false);
+
+ useEffect(() => {
+ if (dialogueStarted.current) return;
+ dialogueStarted.current = true;
+
+ // Play dialogue then transition to reveal
+ const audio = AudioManager.getInstance();
+ audio.playSoundWithCallback(INTRO_DIALOGUE_PATH, 0.8, () => {
+ setIntroStep("reveal");
+ });
+ }, [setIntroStep]);
+
+ return (
+
+
+ ...
+
+
+ );
+}
diff --git a/src/components/ui/intro/IntroRevealOverlay.tsx b/src/components/ui/intro/IntroRevealOverlay.tsx
new file mode 100644
index 0000000..9565e2b
--- /dev/null
+++ b/src/components/ui/intro/IntroRevealOverlay.tsx
@@ -0,0 +1,48 @@
+import { useEffect, useState } from "react";
+import { useGameStore } from "@/managers/stores/useGameStore";
+
+const REVEAL_DURATION_MS = 2000;
+
+/**
+ * Fade-out overlay for reveal transition
+ * - Starts fully black
+ * - Fades out to reveal the game world
+ * - Transitions to playing step when done
+ */
+export function IntroRevealOverlay(): React.JSX.Element {
+ const setIntroStep = useGameStore((state) => state.setIntroStep);
+ const completeIntro = useGameStore((state) => state.completeIntro);
+ const [opacity, setOpacity] = useState(1);
+
+ useEffect(() => {
+ // Start fade out
+ const fadeTimeout = window.setTimeout(() => {
+ setOpacity(0);
+ }, 100);
+
+ // Complete intro after fade
+ const completeTimeout = window.setTimeout(() => {
+ setIntroStep("playing");
+ completeIntro();
+ }, REVEAL_DURATION_MS);
+
+ return () => {
+ window.clearTimeout(fadeTimeout);
+ window.clearTimeout(completeTimeout);
+ };
+ }, [setIntroStep, completeIntro]);
+
+ return (
+
+ );
+}
diff --git a/src/components/ui/intro/IntroVideoPlayer.tsx b/src/components/ui/intro/IntroVideoPlayer.tsx
new file mode 100644
index 0000000..d595330
--- /dev/null
+++ b/src/components/ui/intro/IntroVideoPlayer.tsx
@@ -0,0 +1,80 @@
+import { useCallback, useRef, useEffect } from "react";
+import { useGameStore } from "@/managers/stores/useGameStore";
+
+const INTRO_VIDEO_PATH = "/cinematics/intro.mp4";
+
+/**
+ * Full-screen video player for intro cinematic
+ * - Plays intro.mp4 in fullscreen
+ * - Automatically advances to dialogue-intro step when video ends
+ * - Allows skipping with Enter/Space/Click
+ */
+export function IntroVideoPlayer(): React.JSX.Element {
+ const videoRef = useRef(null);
+ const setIntroStep = useGameStore((state) => state.setIntroStep);
+
+ const handleVideoEnd = useCallback(() => {
+ setIntroStep("dialogue-intro");
+ }, [setIntroStep]);
+
+ const handleSkip = useCallback(() => {
+ if (videoRef.current) {
+ videoRef.current.pause();
+ }
+ setIntroStep("dialogue-intro");
+ }, [setIntroStep]);
+
+ // Handle keyboard skip (Enter/Space)
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ handleSkip();
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [handleSkip]);
+
+ return (
+
+
+
+ Appuyez pour passer
+
+
+ );
+}
diff --git a/src/components/ui/intro/index.ts b/src/components/ui/intro/index.ts
new file mode 100644
index 0000000..6031a8e
--- /dev/null
+++ b/src/components/ui/intro/index.ts
@@ -0,0 +1,3 @@
+export { IntroVideoPlayer } from "./IntroVideoPlayer";
+export { IntroDialogueOverlay } from "./IntroDialogueOverlay";
+export { IntroRevealOverlay } from "./IntroRevealOverlay";
diff --git a/src/pages/page.tsx b/src/pages/page.tsx
index a148d3c..45611d7 100644
--- a/src/pages/page.tsx
+++ b/src/pages/page.tsx
@@ -1,18 +1,35 @@
import { Suspense, useCallback, useEffect, useState } from "react";
+import { useNavigate } from "@tanstack/react-router";
import { Canvas } from "@react-three/fiber";
import * as THREE from "three";
import { DebugPerf } from "@/components/debug/DebugPerf";
import { DialogMessage } from "@/components/ui/DialogMessage";
import { GameUI } from "@/components/ui/GameUI";
+import {
+ IntroDialogueOverlay,
+ IntroRevealOverlay,
+ IntroVideoPlayer,
+} from "@/components/ui/intro";
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
import { INITIAL_SCENE_LOADING_STATE } from "@/data/world/sceneLoadingConfig";
import { useGameStore } from "@/managers/stores/useGameStore";
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
import type { SceneLoadingState } from "@/types/world/sceneLoading";
+import { hasSiteBeenVisitedToday } from "@/utils/cookies/siteVisitCookie";
import { logger } from "@/utils/core/Logger";
import { World } from "@/world/World";
export function HomePage(): React.JSX.Element {
+ const navigate = useNavigate();
+ const introStep = useGameStore((state) => state.intro.currentStep);
+ const setIntroStep = useGameStore((state) => state.setIntroStep);
+
+ useEffect(() => {
+ if (!hasSiteBeenVisitedToday()) {
+ navigate({ to: "/site", replace: true });
+ }
+ }, [navigate]);
+
const dialogMessage = useGameStore(
(state) => state.missionFlow.dialogMessage,
);
@@ -49,6 +66,12 @@ export function HomePage(): React.JSX.Element {
[],
);
+ useEffect(() => {
+ if (introStep === "loading-map" && sceneLoadingState.status === "ready") {
+ setIntroStep("video");
+ }
+ }, [introStep, sceneLoadingState.status, setIntroStep]);
+
const handleCanvasCreated = useCallback(
({ gl }: { gl: THREE.WebGLRenderer }) => {
const canvas = gl.domElement;
@@ -75,6 +98,19 @@ export function HomePage(): React.JSX.Element {
[],
);
+ const renderIntroOverlay = () => {
+ switch (introStep) {
+ case "video":
+ return ;
+ case "dialogue-intro":
+ return ;
+ case "reveal":
+ return ;
+ default:
+ return null;
+ }
+ };
+
return (
) : null}
-
+ {introStep === "loading-map" && (
+
+ )}
+ {renderIntroOverlay()}
);
}