fix(repair-ebike): gate scanning on scan intro dialogue
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled

This commit is contained in:
Tom Boullay
2026-06-03 07:04:44 +02:00
parent a0482aa04b
commit 5968f0f67c
2 changed files with 85 additions and 7 deletions
+6 -6
View File
@@ -1,5 +1,4 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { EBIKE_SCAN_HINT_DIALOGUE_ID } from "@/data/ebike/ebikeConfig";
import { useGameStore } from "@/managers/stores/useGameStore"; import { useGameStore } from "@/managers/stores/useGameStore";
import { useSubtitleStore } from "@/managers/stores/useSubtitleStore"; import { useSubtitleStore } from "@/managers/stores/useSubtitleStore";
import type { MissionStep } from "@/types/gameplay/repairMission"; import type { MissionStep } from "@/types/gameplay/repairMission";
@@ -7,8 +6,11 @@ import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
import { playDialogueById } from "@/utils/dialogues/playDialogue"; import { playDialogueById } from "@/utils/dialogues/playDialogue";
/** /**
* Plays narrator cues during the ebike repair game: * Previously played the ebike repair cues directly. `RepairGame` now
* - `fragmented` -> "Alors? Pas magnifique ça?... ces galets vont scanner..." * owns the repair-game cue timings that gate gameplay transitions
* (`fragmented` waits for `narrateur_galetscan`, `done` waits for
* `narrateur_ebikerepare`). This component remains as the central
* safety cleanup for legacy/queued ebike narrator audio.
* *
* The `narrateur_refroidisseur_diagnostic` line is triggered by the * The `narrateur_refroidisseur_diagnostic` line is triggered by the
* scan sequence itself when it lands on the refroidisseur node * scan sequence itself when it lands on the refroidisseur node
@@ -26,9 +28,7 @@ import { playDialogueById } from "@/utils/dialogues/playDialogue";
* mission transition, etc.), the active audio is paused and the * mission transition, etc.), the active audio is paused and the
* subtitle is force-cleared so nothing bleeds into pylon/farm/outro. * 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,
};
function stopAudio(audio: HTMLAudioElement | null): void { function stopAudio(audio: HTMLAudioElement | null): void {
if (!audio) return; if (!audio) return;
+79 -1
View File
@@ -23,7 +23,10 @@ import {
REPAIR_REASSEMBLY_HOLD_MS, REPAIR_REASSEMBLY_HOLD_MS,
} from "@/data/gameplay/repairGameConfig"; } from "@/data/gameplay/repairGameConfig";
import { REPAIR_MISSIONS } from "@/data/gameplay/repairMissions"; import { REPAIR_MISSIONS } from "@/data/gameplay/repairMissions";
import { EBIKE_REPAIRED_DIALOGUE_ID } from "@/data/ebike/ebikeConfig"; import {
EBIKE_REPAIRED_DIALOGUE_ID,
EBIKE_SCAN_HINT_DIALOGUE_ID,
} from "@/data/ebike/ebikeConfig";
import { useRepairFragmentationInput } from "@/hooks/gameplay/useRepairFragmentationInput"; import { useRepairFragmentationInput } from "@/hooks/gameplay/useRepairFragmentationInput";
import { useRepairMissionStep } from "@/hooks/gameplay/useRepairMissionStep"; import { useRepairMissionStep } from "@/hooks/gameplay/useRepairMissionStep";
import { useTerrainSnappedPosition } from "@/hooks/three/useTerrainHeight"; import { useTerrainSnappedPosition } from "@/hooks/three/useTerrainHeight";
@@ -115,6 +118,8 @@ export function RepairGame({
const [ebikeRepairTransform, setEbikeRepairTransform] = const [ebikeRepairTransform, setEbikeRepairTransform] =
useState<EbikeRepairTransform | null>(null); useState<EbikeRepairTransform | null>(null);
const [ebikeCoolingInstalled, setEbikeCoolingInstalled] = useState(false); const [ebikeCoolingInstalled, setEbikeCoolingInstalled] = useState(false);
const fragmentedSplitSettledRef = useRef(false);
const fragmentedDialogueDoneRef = useRef(false);
const reassemblyDoneTimeoutRef = useRef<number | null>(null); const reassemblyDoneTimeoutRef = useRef<number | null>(null);
// Ebike-specific: once the repair starts, keep the entire repair flow // Ebike-specific: once the repair starts, keep the entire repair flow
// exactly where the bike currently is. `Ebike` owns the live parked // exactly where the bike currently is. `Ebike` owns the live parked
@@ -275,6 +280,7 @@ export function RepairGame({
useEffect(() => { useEffect(() => {
if (mainState !== mission) return undefined; if (mainState !== mission) return undefined;
if (step !== "fragmented") return undefined; if (step !== "fragmented") return undefined;
if (mission === "ebike") return undefined;
const timeoutId = window.setTimeout( const timeoutId = window.setTimeout(
() => { () => {
@@ -288,6 +294,71 @@ export function RepairGame({
}; };
}, [mainState, mission, setMissionStep, step]); }, [mainState, mission, setMissionStep, step]);
useEffect(() => {
if (mainState !== mission) return undefined;
if (mission !== "ebike") return undefined;
if (step !== "fragmented") return undefined;
fragmentedSplitSettledRef.current = false;
fragmentedDialogueDoneRef.current = false;
let cancelled = false;
let activeAudio: HTMLAudioElement | null = null;
let fallbackTimeoutId: number | null = null;
const tryAdvance = (): void => {
if (cancelled) return;
if (!fragmentedSplitSettledRef.current) return;
if (!fragmentedDialogueDoneRef.current) return;
setMissionStep(mission, "scanning");
};
const markDialogueDone = (): void => {
if (cancelled) return;
fragmentedDialogueDoneRef.current = true;
tryAdvance();
};
void (async () => {
const manifest = await loadDialogueManifest();
if (cancelled) return;
const audio = manifest
? await playDialogueById(manifest, EBIKE_SCAN_HINT_DIALOGUE_ID)
: null;
if (cancelled) {
if (audio && !audio.paused) {
audio.pause();
audio.currentTime = 0;
}
useSubtitleStore.getState().clearActiveSubtitle();
return;
}
activeAudio = audio;
if (audio) {
audio.addEventListener("ended", markDialogueDone, { once: true });
fallbackTimeoutId = window.setTimeout(markDialogueDone, 15000);
} else {
fallbackTimeoutId = window.setTimeout(markDialogueDone, 1000);
}
})();
return () => {
cancelled = true;
if (activeAudio) {
activeAudio.removeEventListener("ended", markDialogueDone);
if (!activeAudio.paused) {
activeAudio.pause();
activeAudio.currentTime = 0;
}
}
if (fallbackTimeoutId !== null) {
window.clearTimeout(fallbackTimeoutId);
}
useSubtitleStore.getState().clearActiveSubtitle();
};
}, [mainState, mission, setMissionStep, step]);
useEffect(() => { useEffect(() => {
if (mainState !== mission) return undefined; if (mainState !== mission) return undefined;
if (step !== "reassembling") return undefined; if (step !== "reassembling") return undefined;
@@ -381,6 +452,13 @@ export function RepairGame({
() => (settledAt: 0 | 1) => { () => (settledAt: 0 | 1) => {
const currentStep = stepRef.current; const currentStep = stepRef.current;
if (settledAt === 1 && currentStep === "fragmented") { if (settledAt === 1 && currentStep === "fragmented") {
if (mission === "ebike") {
fragmentedSplitSettledRef.current = true;
if (fragmentedDialogueDoneRef.current) {
setMissionStep(mission, "scanning");
}
return;
}
setMissionStep(mission, "scanning"); setMissionStep(mission, "scanning");
} }
if (settledAt === 0 && currentStep === "reassembling") { if (settledAt === 0 && currentStep === "reassembling") {