diff --git a/src/components/game/EbikeRepairNarrator.tsx b/src/components/game/EbikeRepairNarrator.tsx new file mode 100644 index 0000000..6d39fb1 --- /dev/null +++ b/src/components/game/EbikeRepairNarrator.tsx @@ -0,0 +1,61 @@ +import { useEffect, useRef } from "react"; +import { + EBIKE_DIAGNOSTIC_DIALOGUE_ID, + EBIKE_REPAIRED_DIALOGUE_ID, + EBIKE_SCAN_HINT_DIALOGUE_ID, +} from "@/data/ebike/ebikeConfig"; +import { useGameStore } from "@/managers/stores/useGameStore"; +import type { MissionStep } from "@/types/gameplay/repairMission"; +import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest"; +import { playDialogueById } from "@/utils/dialogues/playDialogue"; + +/** + * Plays narrator cues during the ebike repair game: + * - `fragmented` -> "Alors? Pas magnifique ça?... ces galets vont scanner..." + * - `repairing` -> "Parfait! C'est le refroidisseur qui a lâché..." + * - `done` -> "Eeeet voilà! Il fonctionne comme une horloge!..." + * + * Each cue is one-shot per mission run; the played-set resets when the + * mission state rolls back to `locked`/`waiting` so debug-panel replays + * still trigger the narration. + */ +const STEP_TO_DIALOGUE_ID: Partial> = { + fragmented: EBIKE_SCAN_HINT_DIALOGUE_ID, + repairing: EBIKE_DIAGNOSTIC_DIALOGUE_ID, + done: EBIKE_REPAIRED_DIALOGUE_ID, +}; + +export function EbikeRepairNarrator(): null { + const mainState = useGameStore((state) => state.mainState); + const ebikeStep = useGameStore((state) => state.ebike.currentStep); + const playedRef = useRef>(new Set()); + + useEffect(() => { + if (ebikeStep === "locked" || ebikeStep === "waiting") { + playedRef.current.clear(); + } + }, [ebikeStep]); + + useEffect(() => { + if (mainState !== "ebike") return; + + const dialogueId = STEP_TO_DIALOGUE_ID[ebikeStep]; + if (!dialogueId) return; + if (playedRef.current.has(ebikeStep)) return; + + playedRef.current.add(ebikeStep); + + let cancelled = false; + void (async () => { + const manifest = await loadDialogueManifest(); + if (cancelled || !manifest) return; + await playDialogueById(manifest, dialogueId); + })(); + + return () => { + cancelled = true; + }; + }, [mainState, ebikeStep]); + + return null; +} diff --git a/src/data/ebike/ebikeConfig.ts b/src/data/ebike/ebikeConfig.ts index deb5f1f..2c354f2 100644 --- a/src/data/ebike/ebikeConfig.ts +++ b/src/data/ebike/ebikeConfig.ts @@ -33,3 +33,7 @@ export const EBIKE_SOUNDS = { } as const; export const EBIKE_BREAKDOWN_DIALOGUE_ID = "narrateur_ebikecasse"; +export const EBIKE_SCAN_HINT_DIALOGUE_ID = "narrateur_galetscan"; +export const EBIKE_DIAGNOSTIC_DIALOGUE_ID = + "narrateur_refroidisseur_diagnostic"; +export const EBIKE_REPAIRED_DIALOGUE_ID = "narrateur_ebikerepare"; diff --git a/src/pages/page.tsx b/src/pages/page.tsx index d6b42a5..ae45dcc 100644 --- a/src/pages/page.tsx +++ b/src/pages/page.tsx @@ -4,6 +4,7 @@ import { Canvas } from "@react-three/fiber"; import * as THREE from "three"; import { DebugPerf } from "@/components/debug/DebugPerf"; import { EbikeIntroSequence } from "@/components/game/EbikeIntroSequence"; +import { EbikeRepairNarrator } from "@/components/game/EbikeRepairNarrator"; import { AppLoadingIndicator } from "@/components/ui/AppLoadingIndicator"; import { DialogMessage } from "@/components/ui/DialogMessage"; import { GameUI } from "@/components/ui/GameUI"; @@ -259,6 +260,7 @@ export function HomePage(): React.JSX.Element | null { ) : null} {renderIntroOverlay()} + ); }