import { useEffect, useRef, useState } from "react"; import type { AudioCategory } from "@/data/audioConfig"; import { AudioManager } from "@/managers/AudioManager"; const OUTRO_VIDEO_SRC = "/cinematics/outro.mp4"; const TRANSITION_FADE_MS = 600; const TRANSITION_HOLD_MS = 2000; const TRANSITION_TEXT_FADE_MS = 500; // Delay between "Next step :" appearing and "La ferme" fading in. const TRANSITION_LAFERME_DELAY_MS = 500; const MUTED_CATEGORIES: readonly AudioCategory[] = ["music", "sfx", "dialogue"]; type Stage = | "hidden" | "fading-in" | "showing-text" | "fading-text-out" | "video"; /** * End-of-demo overlay. Triggered by the "outro-cinematic-complete" window * event dispatched from GameCinematics.tsx. * * Sequence: * 1. Fade to black (TRANSITION_FADE_MS) * 2. Reveal "Next step: La ferme" text + hold (TRANSITION_HOLD_MS) * 3. Fade text out (TRANSITION_TEXT_FADE_MS) * 4. Play `outro.mp4` full-screen with all game audio muted */ export function OutroVideoOverlay(): React.JSX.Element | null { const [stage, setStage] = useState("hidden"); const [lafermeVisible, setLafermeVisible] = useState(false); const videoRef = useRef(null); const savedVolumesRef = useRef>>({}); useEffect(() => { function handleCinematicComplete(): void { setStage("fading-in"); } window.addEventListener( "outro-cinematic-complete", handleCinematicComplete, ); return () => { window.removeEventListener( "outro-cinematic-complete", handleCinematicComplete, ); }; }, []); // Drive the transition timeline. useEffect(() => { if (stage === "fading-in") { const timer = window.setTimeout( () => setStage("showing-text"), TRANSITION_FADE_MS, ); return () => window.clearTimeout(timer); } if (stage === "showing-text") { const timer = window.setTimeout( () => setStage("fading-text-out"), TRANSITION_HOLD_MS, ); return () => window.clearTimeout(timer); } if (stage === "fading-text-out") { const timer = window.setTimeout( () => setStage("video"), TRANSITION_TEXT_FADE_MS, ); return () => window.clearTimeout(timer); } return undefined; }, [stage]); // Stagger the second word ("La ferme") so it fades in after "Next step :" // is already visible. useEffect(() => { if (stage === "showing-text") { const timer = window.setTimeout( () => setLafermeVisible(true), TRANSITION_LAFERME_DELAY_MS, ); return () => window.clearTimeout(timer); } if (stage === "hidden" || stage === "fading-in") { // Reset the staged reveal so a re-triggered outro replays correctly. // eslint-disable-next-line react-hooks/set-state-in-effect setLafermeVisible(false); } return undefined; }, [stage]); // Mute all game audio while the video is showing; restore on cleanup so // a re-mounted page doesn't stay silent. useEffect(() => { if (stage !== "video") return; const audioManager = AudioManager.getInstance(); const saved: Partial> = {}; for (const category of MUTED_CATEGORIES) { saved[category] = audioManager.getCategoryVolume(category); audioManager.setCategoryVolume(category, 0); } savedVolumesRef.current = saved; void videoRef.current?.play(); return () => { for (const category of MUTED_CATEGORIES) { const previous = savedVolumesRef.current[category]; if (previous !== undefined) { audioManager.setCategoryVolume(category, previous); } } savedVolumesRef.current = {}; }; }, [stage]); if (stage === "hidden") return null; const showText = stage === "showing-text" || stage === "fading-text-out"; const textOpacity = stage === "showing-text" ? 1 : 0; return (
{showText ? (
Next step :{" "} La ferme
) : null} {stage === "video" ? (
); }