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
🔍 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:
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { EBIKE_SCAN_HINT_DIALOGUE_ID } from "@/data/ebike/ebikeConfig";
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import { useSubtitleStore } from "@/managers/stores/useSubtitleStore";
|
||||
import type { MissionStep } from "@/types/gameplay/repairMission";
|
||||
@@ -7,8 +6,11 @@ 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..."
|
||||
* Previously played the ebike repair cues directly. `RepairGame` now
|
||||
* 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
|
||||
* 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
|
||||
* subtitle is force-cleared so nothing bleeds into pylon/farm/outro.
|
||||
*/
|
||||
const STEP_TO_DIALOGUE_ID: Partial<Record<MissionStep, string>> = {
|
||||
fragmented: EBIKE_SCAN_HINT_DIALOGUE_ID,
|
||||
};
|
||||
const STEP_TO_DIALOGUE_ID: Partial<Record<MissionStep, string>> = {};
|
||||
|
||||
function stopAudio(audio: HTMLAudioElement | null): void {
|
||||
if (!audio) return;
|
||||
|
||||
@@ -23,7 +23,10 @@ import {
|
||||
REPAIR_REASSEMBLY_HOLD_MS,
|
||||
} from "@/data/gameplay/repairGameConfig";
|
||||
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 { useRepairMissionStep } from "@/hooks/gameplay/useRepairMissionStep";
|
||||
import { useTerrainSnappedPosition } from "@/hooks/three/useTerrainHeight";
|
||||
@@ -115,6 +118,8 @@ export function RepairGame({
|
||||
const [ebikeRepairTransform, setEbikeRepairTransform] =
|
||||
useState<EbikeRepairTransform | null>(null);
|
||||
const [ebikeCoolingInstalled, setEbikeCoolingInstalled] = useState(false);
|
||||
const fragmentedSplitSettledRef = useRef(false);
|
||||
const fragmentedDialogueDoneRef = useRef(false);
|
||||
const reassemblyDoneTimeoutRef = useRef<number | null>(null);
|
||||
// Ebike-specific: once the repair starts, keep the entire repair flow
|
||||
// exactly where the bike currently is. `Ebike` owns the live parked
|
||||
@@ -275,6 +280,7 @@ export function RepairGame({
|
||||
useEffect(() => {
|
||||
if (mainState !== mission) return undefined;
|
||||
if (step !== "fragmented") return undefined;
|
||||
if (mission === "ebike") return undefined;
|
||||
|
||||
const timeoutId = window.setTimeout(
|
||||
() => {
|
||||
@@ -288,6 +294,71 @@ export function RepairGame({
|
||||
};
|
||||
}, [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(() => {
|
||||
if (mainState !== mission) return undefined;
|
||||
if (step !== "reassembling") return undefined;
|
||||
@@ -381,6 +452,13 @@ export function RepairGame({
|
||||
() => (settledAt: 0 | 1) => {
|
||||
const currentStep = stepRef.current;
|
||||
if (settledAt === 1 && currentStep === "fragmented") {
|
||||
if (mission === "ebike") {
|
||||
fragmentedSplitSettledRef.current = true;
|
||||
if (fragmentedDialogueDoneRef.current) {
|
||||
setMissionStep(mission, "scanning");
|
||||
}
|
||||
return;
|
||||
}
|
||||
setMissionStep(mission, "scanning");
|
||||
}
|
||||
if (settledAt === 0 && currentStep === "reassembling") {
|
||||
|
||||
Reference in New Issue
Block a user