feat(ebike): play narrator cues during repair flow (scan hint, diagnostic, completion)

This commit is contained in:
Tom Boullay
2026-06-03 02:11:45 +02:00
parent 8b0dd31014
commit 47b69b01d2
3 changed files with 67 additions and 0 deletions
@@ -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<Record<MissionStep, string>> = {
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<Set<MissionStep>>(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;
}
+4
View File
@@ -33,3 +33,7 @@ export const EBIKE_SOUNDS = {
} as const; } as const;
export const EBIKE_BREAKDOWN_DIALOGUE_ID = "narrateur_ebikecasse"; 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";
+2
View File
@@ -4,6 +4,7 @@ import { Canvas } from "@react-three/fiber";
import * as THREE from "three"; import * as THREE from "three";
import { DebugPerf } from "@/components/debug/DebugPerf"; import { DebugPerf } from "@/components/debug/DebugPerf";
import { EbikeIntroSequence } from "@/components/game/EbikeIntroSequence"; import { EbikeIntroSequence } from "@/components/game/EbikeIntroSequence";
import { EbikeRepairNarrator } from "@/components/game/EbikeRepairNarrator";
import { AppLoadingIndicator } from "@/components/ui/AppLoadingIndicator"; import { AppLoadingIndicator } from "@/components/ui/AppLoadingIndicator";
import { DialogMessage } from "@/components/ui/DialogMessage"; import { DialogMessage } from "@/components/ui/DialogMessage";
import { GameUI } from "@/components/ui/GameUI"; import { GameUI } from "@/components/ui/GameUI";
@@ -259,6 +260,7 @@ export function HomePage(): React.JSX.Element | null {
) : null} ) : null}
{renderIntroOverlay()} {renderIntroOverlay()}
<EbikeIntroSequence /> <EbikeIntroSequence />
<EbikeRepairNarrator />
</HandTrackingProvider> </HandTrackingProvider>
); );
} }