fix(site): repair onboarding audio cleanup, redirect, and manifest fetches
- loadDialogueManifest: cache the resolved manifest at module level and dedupe concurrent fetches so each screen no longer re-downloads it - useGameStore: completeIntroState now also advances intro.currentStep to "playing" so callers do not need a separate setIntroStep call - SiteNamingScreen and SiteTransitionOverlay: replace ref-based guards with an isCancelled flag captured per effect. The previous guards persisted across StrictMode remounts, leaving mount 2 unable to re-run the effect after mount 1's chain was cancelled, which broke the fade animations, the second narrator dialogue and the redirect. Both screens now also call stopCurrentDialogue on unmount so audio cannot bleed across routes, and the transition gets a safety timeout in case the dialogue audio fails to fire its "ended" event - SiteTransitionOverlay: keep the <Subtitles /> mount inside the overlay so it renders inside the z-index 1000 stacking context (above the black screen); the one in SiteLayout sits behind it - IntroDialogueOverlay: route through playDialogueById instead of AudioManager.playSoundWithCallback so the narrator subtitles play in sync, and add the same isCancelled cleanup pattern - IntroRevealOverlay: rely on completeIntro alone now that it advances intro.currentStep, and skip the fade when reduced motion is requested - SiteMobileBlocker: correct logo path from public/... to /...
This commit is contained in:
@@ -1,31 +1,63 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { AudioManager } from "@/managers/AudioManager";
|
||||
import { useEffect } from "react";
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import { SITE_DIALOGUE_IDS } from "@/data/site/dialogueIds";
|
||||
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
||||
import {
|
||||
playDialogueById,
|
||||
stopCurrentDialogue,
|
||||
} from "@/utils/dialogues/playDialogue";
|
||||
|
||||
const INTRO_DIALOGUE_PATH = "/sounds/dialogue/narrateur_ordreebike.mp3";
|
||||
const DIALOGUE_FALLBACK_TIMEOUT_MS = 12000;
|
||||
|
||||
/**
|
||||
* Black screen overlay with dialogue audio
|
||||
* - Plays narrateur_ordreebike.mp3
|
||||
* - Transitions to reveal step when dialogue ends
|
||||
* Black screen overlay that plays the intro dialogue (with synced subtitles)
|
||||
* via the dialogue manifest, then transitions to the reveal step.
|
||||
*/
|
||||
export function IntroDialogueOverlay(): React.JSX.Element {
|
||||
const setIntroStep = useGameStore((state) => state.setIntroStep);
|
||||
const dialogueStarted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogueStarted.current) return;
|
||||
dialogueStarted.current = true;
|
||||
let cancelled = false;
|
||||
let safetyTimeoutId: number | null = null;
|
||||
|
||||
// Play dialogue then transition to reveal
|
||||
const audio = AudioManager.getInstance();
|
||||
audio.playSoundWithCallback(INTRO_DIALOGUE_PATH, 0.8, () => {
|
||||
const advance = (): void => {
|
||||
if (cancelled) return;
|
||||
if (safetyTimeoutId !== null) window.clearTimeout(safetyTimeoutId);
|
||||
setIntroStep("reveal");
|
||||
});
|
||||
};
|
||||
|
||||
void (async () => {
|
||||
const manifest = await loadDialogueManifest();
|
||||
if (cancelled) return;
|
||||
|
||||
const audio = manifest
|
||||
? await playDialogueById(manifest, SITE_DIALOGUE_IDS.introOrder)
|
||||
: null;
|
||||
if (cancelled) return;
|
||||
|
||||
if (!audio) {
|
||||
advance();
|
||||
return;
|
||||
}
|
||||
|
||||
safetyTimeoutId = window.setTimeout(
|
||||
advance,
|
||||
DIALOGUE_FALLBACK_TIMEOUT_MS,
|
||||
);
|
||||
audio.addEventListener("ended", advance, { once: true });
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (safetyTimeoutId !== null) window.clearTimeout(safetyTimeoutId);
|
||||
stopCurrentDialogue();
|
||||
};
|
||||
}, [setIntroStep]);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="region"
|
||||
aria-label="Dialogue d'introduction"
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
@@ -37,6 +69,7 @@ export function IntroDialogueOverlay(): React.JSX.Element {
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
fontSize: 16,
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import { usePrefersReducedMotion } from "@/hooks/ui/usePrefersReducedMotion";
|
||||
|
||||
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
|
||||
* Fade-out overlay revealing the game world.
|
||||
* Calls completeIntro() when the fade is done — completeIntro also flips
|
||||
* intro.currentStep to "playing" so no separate setIntroStep call is needed.
|
||||
*/
|
||||
export function IntroRevealOverlay(): React.JSX.Element {
|
||||
const setIntroStep = useGameStore((state) => state.setIntroStep);
|
||||
const completeIntro = useGameStore((state) => state.completeIntro);
|
||||
const prefersReducedMotion = usePrefersReducedMotion();
|
||||
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);
|
||||
|
||||
@@ -30,16 +27,19 @@ export function IntroRevealOverlay(): React.JSX.Element {
|
||||
window.clearTimeout(fadeTimeout);
|
||||
window.clearTimeout(completeTimeout);
|
||||
};
|
||||
}, [setIntroStep, completeIntro]);
|
||||
}, [completeIntro]);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
background: "#000",
|
||||
opacity,
|
||||
transition: `opacity ${REVEAL_DURATION_MS}ms ease-out`,
|
||||
transition: prefersReducedMotion
|
||||
? "none"
|
||||
: `opacity ${REVEAL_DURATION_MS}ms ease-out`,
|
||||
zIndex: 998,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user