refactor(ebike): drop redundant 'locked' substate, single entry trigger
The ebike mission previously had two redundant entry-point sub-states
('locked' and 'waiting') that were behaviorally identical from the
player's perspective:
- both showed the same 'Lancer le Repair Game' prompt
- both allowed the press-E handler in Ebike.tsx to jump to 'inspected'
In addition, the locked state caused two latent bugs:
- the static-map ebike node (GameMap) and the live <Ebike> component
were rendered simultaneously at the same world position
- a generic RepairMissionTrigger anchor sphere was rendered in
parallel to Ebike's own InteractableObject (two triggers, same area)
Changes:
- useGameStore: ebike's initial currentStep + completeIntroState target
is now 'waiting' (pylon/farm still init at 'locked' — they need it).
- Ebike.tsx: drop dead === 'locked' branches in repairGameOwnsEbikeModel
and the press-E handler.
- EbikeRepairNarrator: only reset the played-set on 'waiting'.
- RepairGame: drop 'locked' from the ebike livePosition guard.
- REPAIR_MISSION_TRIGGERS: empty array (the duplicate ebike anchor
sphere is gone). Keep the array + RepairMissionTrigger component for
future re-use.
- GameMap: hide the static-map ebike node as soon as
mainState === 'ebike' (was: only when ebikeStep !== 'locked').
- repairMissionState.getPreviousMissionStep: ebike rewinds from
'waiting' to 'waiting' (cap), pylon to 'npc-return', farm to 'locked'.
The 'locked' value is intentionally kept in the MissionStep type union
because the farm mission still uses it as a meaningful kickoff state
driving FarmNarrativeFlow's auto-transition to electricienne_history.
This commit is contained in:
@@ -73,7 +73,6 @@ export function Ebike({
|
|||||||
const updateEbikeSounds = useEbikeSounds();
|
const updateEbikeSounds = useEbikeSounds();
|
||||||
const repairGameOwnsEbikeModel =
|
const repairGameOwnsEbikeModel =
|
||||||
mainState === "ebike" &&
|
mainState === "ebike" &&
|
||||||
ebikeStep !== "locked" &&
|
|
||||||
ebikeStep !== "waiting" &&
|
ebikeStep !== "waiting" &&
|
||||||
ebikeStep !== "inspected";
|
ebikeStep !== "inspected";
|
||||||
|
|
||||||
@@ -362,10 +361,7 @@ export function Ebike({
|
|||||||
if (window.ebikeBreakdownActive === true) return;
|
if (window.ebikeBreakdownActive === true) return;
|
||||||
|
|
||||||
if (movementMode === "walk") {
|
if (movementMode === "walk") {
|
||||||
if (
|
if (mainState === "ebike" && ebikeStep === "waiting") {
|
||||||
mainState === "ebike" &&
|
|
||||||
(ebikeStep === "locked" || ebikeStep === "waiting")
|
|
||||||
) {
|
|
||||||
setMissionStep("ebike", "inspected");
|
setMissionStep("ebike", "inspected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { playDialogueById } from "@/utils/dialogues/playDialogue";
|
|||||||
* - `done` -> "Eeeet voilà! Il fonctionne comme une horloge!..."
|
* - `done` -> "Eeeet voilà! Il fonctionne comme une horloge!..."
|
||||||
*
|
*
|
||||||
* 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 `waiting` so debug-panel replays still
|
||||||
* still trigger the narration.
|
* trigger the narration.
|
||||||
*
|
*
|
||||||
* Audio AND subtitles are strictly scoped to `mainState === "ebike"`. If
|
* Audio AND subtitles are strictly scoped to `mainState === "ebike"`. If
|
||||||
* the player leaves the ebike main state mid-line (debug panel jump,
|
* the player leaves the ebike main state mid-line (debug panel jump,
|
||||||
@@ -46,7 +46,7 @@ export function EbikeRepairNarrator(): null {
|
|||||||
const activeAudioRef = useRef<HTMLAudioElement | null>(null);
|
const activeAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ebikeStep === "locked" || ebikeStep === "waiting") {
|
if (ebikeStep === "waiting") {
|
||||||
playedRef.current.clear();
|
playedRef.current.clear();
|
||||||
}
|
}
|
||||||
}, [ebikeStep]);
|
}, [ebikeStep]);
|
||||||
|
|||||||
@@ -74,13 +74,13 @@ export function RepairGame({
|
|||||||
readonly RepairScannedBrokenPart[]
|
readonly RepairScannedBrokenPart[]
|
||||||
>([]);
|
>([]);
|
||||||
// For the ebike mission, use the bike's live parked world position once
|
// For the ebike mission, use the bike's live parked world position once
|
||||||
// the repair flow leaves the waiting/locked phase so the repair happens
|
// the repair flow leaves the waiting phase so the repair happens
|
||||||
// wherever the player parked the bike, not at the static zone anchor.
|
// wherever the player parked the bike, not at the static zone anchor.
|
||||||
// window.ebikeParkedPosition is set by Ebike when the player drops the
|
// window.ebikeParkedPosition is set by Ebike when the player drops the
|
||||||
// bike and stays stable through the rest of the repair flow.
|
// bike and stays stable through the rest of the repair flow.
|
||||||
const livePosition = useMemo<Vector3Tuple>(() => {
|
const livePosition = useMemo<Vector3Tuple>(() => {
|
||||||
if (mission !== "ebike" || mainState !== mission) return position;
|
if (mission !== "ebike" || mainState !== mission) return position;
|
||||||
if (step === "locked" || step === "waiting") return position;
|
if (step === "waiting") return position;
|
||||||
const parked = window.ebikeParkedPosition;
|
const parked = window.ebikeParkedPosition;
|
||||||
if (!parked) return position;
|
if (!parked) return position;
|
||||||
return [parked[0], parked[1], parked[2]];
|
return [parked[0], parked[1], parked[2]];
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ const REPAIR_MISSION_POSITIONS = {
|
|||||||
farm: [-24, 0, 42],
|
farm: [-24, 0, 42],
|
||||||
} as const satisfies Record<RepairMissionId, Vector3Tuple>;
|
} as const satisfies Record<RepairMissionId, Vector3Tuple>;
|
||||||
|
|
||||||
export const REPAIR_MISSION_TRIGGERS = [
|
// Currently empty: the ebike mission entry point is handled directly by
|
||||||
{
|
// `Ebike.tsx`'s own InteractableObject ("Lancer le Repair Game"), and the
|
||||||
mission: "ebike",
|
// pylon/farm missions transition through their narrative flows
|
||||||
label: "Réparer l'e-bike",
|
// (PylonNarrativeFlow / FarmNarrativeFlow). Keep the array typed so we
|
||||||
radius: 4,
|
// can re-introduce a generic anchor trigger in the future without
|
||||||
},
|
// touching the consumer in `GameStageContent.tsx`.
|
||||||
] as const satisfies readonly RepairMissionTriggerConfig[];
|
export const REPAIR_MISSION_TRIGGERS: readonly RepairMissionTriggerConfig[] =
|
||||||
|
[];
|
||||||
|
|
||||||
export const REPAIR_MISSION_POSITION_ENTRIES = Object.entries(
|
export const REPAIR_MISSION_POSITION_ENTRIES = Object.entries(
|
||||||
REPAIR_MISSION_POSITIONS,
|
REPAIR_MISSION_POSITIONS,
|
||||||
|
|||||||
@@ -105,7 +105,12 @@ export function getPreviousMissionStep(
|
|||||||
case "npc-return":
|
case "npc-return":
|
||||||
return "arrived";
|
return "arrived";
|
||||||
case "waiting":
|
case "waiting":
|
||||||
return mission === "pylon" ? "npc-return" : "locked";
|
// Ebike no longer has a "locked" entry state — its mission starts
|
||||||
|
// directly at "waiting". Pylon rewinds to its NPC return loop, farm
|
||||||
|
// rewinds to its narrative-driven locked kickoff.
|
||||||
|
if (mission === "pylon") return "npc-return";
|
||||||
|
if (mission === "farm") return "locked";
|
||||||
|
return "waiting";
|
||||||
case "inspected":
|
case "inspected":
|
||||||
return "waiting";
|
return "waiting";
|
||||||
case "fragmented":
|
case "fragmented":
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ function completeIntroState(state: GameState): GameStateUpdate {
|
|||||||
},
|
},
|
||||||
ebike: {
|
ebike: {
|
||||||
...state.ebike,
|
...state.ebike,
|
||||||
currentStep: "locked",
|
currentStep: "waiting",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -265,7 +265,7 @@ function createInitialGameState(): GameState {
|
|||||||
isEbikeUnlocked: false,
|
isEbikeUnlocked: false,
|
||||||
},
|
},
|
||||||
ebike: {
|
ebike: {
|
||||||
currentStep: "locked",
|
currentStep: "waiting",
|
||||||
dialogueAudio: null,
|
dialogueAudio: null,
|
||||||
isRepaired: false,
|
isRepaired: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -323,9 +323,11 @@ function MapNodeInstance({
|
|||||||
}): React.JSX.Element | null {
|
}): React.JSX.Element | null {
|
||||||
const isGeneratedModel = isGeneratedMapModelName(node.name);
|
const isGeneratedModel = isGeneratedMapModelName(node.name);
|
||||||
const mainState = useGameStore((state) => state.mainState);
|
const mainState = useGameStore((state) => state.mainState);
|
||||||
const ebikeStep = useGameStore((state) => state.ebike.currentStep);
|
// The static-map ebike node is replaced by the live `Ebike` component
|
||||||
const hideEbikeMapModel =
|
// (rendered from GameStageContent) as soon as the ebike mission begins,
|
||||||
node.name === "ebike" && mainState === "ebike" && ebikeStep !== "locked";
|
// so hide the static one to avoid a dual-render at the same world
|
||||||
|
// position.
|
||||||
|
const hideEbikeMapModel = node.name === "ebike" && mainState === "ebike";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modelUrl !== null || isGeneratedModel) return;
|
if (modelUrl !== null || isGeneratedModel) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user