fix(ebike): force-stop narrator audio + clear subtitle when leaving ebike state
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
EBIKE_SCAN_HINT_DIALOGUE_ID,
|
EBIKE_SCAN_HINT_DIALOGUE_ID,
|
||||||
} from "@/data/ebike/ebikeConfig";
|
} from "@/data/ebike/ebikeConfig";
|
||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
|
import { useSubtitleStore } from "@/managers/stores/useSubtitleStore";
|
||||||
import type { MissionStep } from "@/types/gameplay/repairMission";
|
import type { MissionStep } from "@/types/gameplay/repairMission";
|
||||||
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
||||||
import { playDialogueById } from "@/utils/dialogues/playDialogue";
|
import { playDialogueById } from "@/utils/dialogues/playDialogue";
|
||||||
@@ -18,6 +19,11 @@ import { playDialogueById } from "@/utils/dialogues/playDialogue";
|
|||||||
* Each cue is one-shot per mission run; the played-set resets when the
|
* 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
|
* mission state rolls back to `locked`/`waiting` so debug-panel replays
|
||||||
* still trigger the narration.
|
* still trigger the narration.
|
||||||
|
*
|
||||||
|
* Audio AND subtitles are strictly scoped to `mainState === "ebike"`. If
|
||||||
|
* the player leaves the ebike main state mid-line (debug panel jump,
|
||||||
|
* mission transition, etc.), the active audio is paused and the
|
||||||
|
* subtitle is force-cleared so nothing bleeds into pylon/farm/outro.
|
||||||
*/
|
*/
|
||||||
const STEP_TO_DIALOGUE_ID: Partial<Record<MissionStep, string>> = {
|
const STEP_TO_DIALOGUE_ID: Partial<Record<MissionStep, string>> = {
|
||||||
fragmented: EBIKE_SCAN_HINT_DIALOGUE_ID,
|
fragmented: EBIKE_SCAN_HINT_DIALOGUE_ID,
|
||||||
@@ -25,10 +31,19 @@ const STEP_TO_DIALOGUE_ID: Partial<Record<MissionStep, string>> = {
|
|||||||
done: EBIKE_REPAIRED_DIALOGUE_ID,
|
done: EBIKE_REPAIRED_DIALOGUE_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function stopAudio(audio: HTMLAudioElement | null): void {
|
||||||
|
if (!audio) return;
|
||||||
|
if (!audio.paused) {
|
||||||
|
audio.pause();
|
||||||
|
audio.currentTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function EbikeRepairNarrator(): null {
|
export function EbikeRepairNarrator(): null {
|
||||||
const mainState = useGameStore((state) => state.mainState);
|
const mainState = useGameStore((state) => state.mainState);
|
||||||
const ebikeStep = useGameStore((state) => state.ebike.currentStep);
|
const ebikeStep = useGameStore((state) => state.ebike.currentStep);
|
||||||
const playedRef = useRef<Set<MissionStep>>(new Set());
|
const playedRef = useRef<Set<MissionStep>>(new Set());
|
||||||
|
const activeAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ebikeStep === "locked" || ebikeStep === "waiting") {
|
if (ebikeStep === "locked" || ebikeStep === "waiting") {
|
||||||
@@ -36,6 +51,18 @@ export function EbikeRepairNarrator(): null {
|
|||||||
}
|
}
|
||||||
}, [ebikeStep]);
|
}, [ebikeStep]);
|
||||||
|
|
||||||
|
// Belt-and-suspenders: any time we are NOT in the ebike main state,
|
||||||
|
// make sure no narrator audio or subtitle from this component is
|
||||||
|
// lingering. This catches races where the audio started a tick before
|
||||||
|
// the main state flipped and the per-step cleanup hadn't propagated
|
||||||
|
// yet (subtitle event still queued, etc.).
|
||||||
|
useEffect(() => {
|
||||||
|
if (mainState === "ebike") return;
|
||||||
|
stopAudio(activeAudioRef.current);
|
||||||
|
activeAudioRef.current = null;
|
||||||
|
useSubtitleStore.getState().clearActiveSubtitle();
|
||||||
|
}, [mainState]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mainState !== "ebike") return;
|
if (mainState !== "ebike") return;
|
||||||
|
|
||||||
@@ -46,28 +73,24 @@ export function EbikeRepairNarrator(): null {
|
|||||||
playedRef.current.add(ebikeStep);
|
playedRef.current.add(ebikeStep);
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
let activeAudio: HTMLAudioElement | null = null;
|
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const manifest = await loadDialogueManifest();
|
const manifest = await loadDialogueManifest();
|
||||||
if (cancelled || !manifest) return;
|
if (cancelled || !manifest) return;
|
||||||
const audio = await playDialogueById(manifest, dialogueId);
|
const audio = await playDialogueById(manifest, dialogueId);
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
if (audio && !audio.paused) {
|
stopAudio(audio);
|
||||||
audio.pause();
|
useSubtitleStore.getState().clearActiveSubtitle();
|
||||||
audio.currentTime = 0;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeAudio = audio;
|
activeAudioRef.current = audio;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
if (activeAudio && !activeAudio.paused) {
|
stopAudio(activeAudioRef.current);
|
||||||
activeAudio.pause();
|
activeAudioRef.current = null;
|
||||||
activeAudio.currentTime = 0;
|
useSubtitleStore.getState().clearActiveSubtitle();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [mainState, ebikeStep]);
|
}, [mainState, ebikeStep]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user