refactor(repair): unify exploded model across phases, simplify ebike flow

- RepairGame: lift a single ExplodableModel mounted across fragmented
  -> done so the model loads once, animates from its real original
  positions, and never re-instantiates between phases. Eliminates the
  position/rotation jumps and re-explosion that occurred when each
  step instantiated its own model.
- ExplodableModel: expose splitSpeed prop so the explode/reassemble
  lerp can be slowed down (REPAIR_FRAGMENT_SPLIT_SPEED = 1.8) for a
  more deliberate visual where each node is seen leaving its origin.
- RepairScanSequence: drop its own ExplodableModel, receive parts
  from the upstream shared instance. Logs the available part names
  when broken-part nodes can't be matched so config drift is visible.
- RepairReassemblyStep: reduced to the completion particles + a
  delayed onSettled callback. The collapse animation is now driven by
  the shared ExplodableModel switching split=false at the reassembling
  phase. After REPAIR_REASSEMBLY_HOLD_MS (1500ms) the upstream flow
  auto-advances to done.
- RepairEbikeRepairTrigger: new minimal interactable for the ebike
  repairing step. Replaces the heavier grabbable-parts UX (cercles,
  ranger pieces) with a single 'Changez le refroidisseur' prompt that
  advances directly to reassembling. Pylon/farm keep RepairRepairingStep.
- RepairCompletionStep: drop the duplicated RepairObjectModel; the
  shared ExplodableModel renders the repaired model at done.
- RepairGame ebike-done: play narrateur_ebikerepare and call
  completeMission on the audio's ended event (with REPAIR_DONE_DIALOGUE_FALLBACK_MS
  fallback). Hands off to pylon without a Validate button.
- EbikeRepairNarrator: drop the done entry; RepairGame owns it now so
  the audio's end event can drive the mission completion handoff.
- RepairGame: drop the window.ebikeParkedPosition livePosition logic.
  Ebike movement is disabled during the repair flow so the static zone
  position is the source of truth, fixing the floating-bike issue
  observed in TestMap.
This commit is contained in:
Tom Boullay
2026-06-03 06:21:29 +02:00
parent 9841b14388
commit 5a6596b755
8 changed files with 279 additions and 110 deletions
+7 -12
View File
@@ -1,8 +1,5 @@
import { useEffect, useRef } from "react";
import {
EBIKE_REPAIRED_DIALOGUE_ID,
EBIKE_SCAN_HINT_DIALOGUE_ID,
} from "@/data/ebike/ebikeConfig";
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";
@@ -12,14 +9,13 @@ import { playDialogueById } from "@/utils/dialogues/playDialogue";
/**
* Plays narrator cues during the ebike repair game:
* - `fragmented` -> "Alors? Pas magnifique ça?... ces galets vont scanner..."
* - `done` -> "Eeeet voilà! Il fonctionne comme une horloge!..."
*
* The `narrateur_refroidisseur_diagnostic` line is no longer played
* here on the `repairing` step. It is now triggered by the scan
* sequence itself when it lands on the refroidisseur node (configured
* via `RepairMissionPartConfig.voiceLineId` on the broken part). That
* keeps the audio synchronized with the red broken-part highlight and
* gates the `scanning -> repairing` transition on the audio's `ended`.
* The `narrateur_refroidisseur_diagnostic` line is triggered by the
* scan sequence itself when it lands on the refroidisseur node
* (configured via `RepairMissionPartConfig.voiceLineId` on the broken
* part). The `narrateur_ebikerepare` line is triggered by RepairGame
* directly at the `done` step so its `ended` event can drive the
* mission completion handoff.
*
* Each cue is one-shot per mission run; the played-set resets when the
* mission state rolls back to `waiting` so debug-panel replays still
@@ -32,7 +28,6 @@ import { playDialogueById } from "@/utils/dialogues/playDialogue";
*/
const STEP_TO_DIALOGUE_ID: Partial<Record<MissionStep, string>> = {
fragmented: EBIKE_SCAN_HINT_DIALOGUE_ID,
done: EBIKE_REPAIRED_DIALOGUE_ID,
};
function stopAudio(audio: HTMLAudioElement | null): void {