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 { 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;
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
Reference in New Issue
Block a user