update: intro flow overlays

This commit is contained in:
Tom Boullay
2026-05-30 04:00:20 +02:00
parent a2cff0567e
commit ce5dc8ada0
5 changed files with 221 additions and 1 deletions
@@ -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 (
<div
style={{
position: "fixed",
inset: 0,
background: "#000",
zIndex: 999,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
color: "rgba(255, 255, 255, 0.5)",
fontSize: 16,
fontFamily: "system-ui, sans-serif",
}}
>
...
</span>
</div>
);
}
@@ -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 (
<div
style={{
position: "fixed",
inset: 0,
background: "#000",
opacity,
transition: `opacity ${REVEAL_DURATION_MS}ms ease-out`,
zIndex: 998,
pointerEvents: "none",
}}
/>
);
}
@@ -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<HTMLVideoElement>(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 (
<div
onClick={handleSkip}
style={{
position: "fixed",
inset: 0,
background: "#000",
zIndex: 1000,
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<video
ref={videoRef}
src={INTRO_VIDEO_PATH}
autoPlay
playsInline
onEnded={handleVideoEnd}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
<span
style={{
position: "absolute",
bottom: 32,
right: 32,
color: "rgba(255, 255, 255, 0.6)",
fontSize: 14,
fontFamily: "system-ui, sans-serif",
}}
>
Appuyez pour passer
</span>
</div>
);
}
+3
View File
@@ -0,0 +1,3 @@
export { IntroVideoPlayer } from "./IntroVideoPlayer";
export { IntroDialogueOverlay } from "./IntroDialogueOverlay";
export { IntroRevealOverlay } from "./IntroRevealOverlay";
+40 -1
View File
@@ -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 <IntroVideoPlayer />;
case "dialogue-intro":
return <IntroDialogueOverlay />;
case "reveal":
return <IntroRevealOverlay />;
default:
return null;
}
};
return (
<HandTrackingProvider>
<Canvas
@@ -100,7 +136,10 @@ export function HomePage(): React.JSX.Element {
onClose={hideDialog}
/>
) : null}
<SceneLoadingOverlay state={sceneLoadingState} />
{introStep === "loading-map" && (
<SceneLoadingOverlay state={sceneLoadingState} />
)}
{renderIntroOverlay()}
</HandTrackingProvider>
);
}