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; 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 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]); // 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" ? (
); }