feat mission-2

This commit is contained in:
math-pixel
2026-06-01 14:40:17 +02:00
parent 813c10f3f7
commit cd0afcda8c
13 changed files with 191 additions and 113 deletions
+9 -3
View File
@@ -78,19 +78,19 @@
{ {
"id": "narrateur_coupureelec", "id": "narrateur_coupureelec",
"voice": "narrateur", "voice": "narrateur",
"audio": "/sounds/dialogue/narrateur_coupureélec.mp3", "audio": "/sounds/dialogue/narrateur_coupure_elec.mp3",
"subtitleCueIndex": 9 "subtitleCueIndex": 9
}, },
{ {
"id": "narrateur_poteaueleccasse", "id": "narrateur_poteaueleccasse",
"voice": "narrateur", "voice": "narrateur",
"audio": "/sounds/dialogue/narrateur_poteauéleccassé.mp3", "audio": "/sounds/dialogue/narrateur_poteau_elec_casse.mp3",
"subtitleCueIndex": 10 "subtitleCueIndex": 10
}, },
{ {
"id": "narrateur_courantrepare", "id": "narrateur_courantrepare",
"voice": "narrateur", "voice": "narrateur",
"audio": "/sounds/dialogue/narrateur_courantparé.mp3", "audio": "/sounds/dialogue/narrateur_courant_repare.mp3",
"subtitleCueIndex": 11 "subtitleCueIndex": 11
}, },
{ {
@@ -165,6 +165,12 @@
"audio": "/sounds/dialogue/narrateur_histoireelectricienne.mp3", "audio": "/sounds/dialogue/narrateur_histoireelectricienne.mp3",
"subtitleCueIndex": 23 "subtitleCueIndex": 23
}, },
{
"id": "narrateur_demande_aide",
"voice": "narrateur",
"audio": "/sounds/dialogue/narrateur_demande_aide.mp3",
"subtitleCueIndex": 24
},
{ {
"id": "fermier_coupdemain", "id": "fermier_coupdemain",
"voice": "fermier", "voice": "fermier",
@@ -12,8 +12,10 @@ import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
import { playDialogueById } from "@/utils/dialogues/playDialogue"; import { playDialogueById } from "@/utils/dialogues/playDialogue";
export function EbikeIntroSequence(): React.JSX.Element | null { export function EbikeIntroSequence(): React.JSX.Element | null {
const mainState = useGameStore((state) => state.mainState);
const introStep = useGameStore((state) => state.intro.currentStep); const introStep = useGameStore((state) => state.intro.currentStep);
const movementMode = useGameStore((state) => state.player.movementMode); const movementMode = useGameStore((state) => state.player.movementMode);
const pylonStep = useGameStore((state) => state.pylon.currentStep);
const setIntroStep = useGameStore((state) => state.setIntroStep); const setIntroStep = useGameStore((state) => state.setIntroStep);
const completeIntro = useGameStore((state) => state.completeIntro); const completeIntro = useGameStore((state) => state.completeIntro);
const [breakdownDialogueDone, setBreakdownDialogueDone] = useState(false); const [breakdownDialogueDone, setBreakdownDialogueDone] = useState(false);
@@ -100,6 +102,16 @@ export function EbikeIntroSequence(): React.JSX.Element | null {
} }
}, [introStep]); }, [introStep]);
if (mainState === "pylon") {
if (pylonStep === "approaching") {
return <MissionNotification mission="pylon" visible />;
}
if (pylonStep === "narrator-outro") {
return <MissionNotification mission="farm" visible />;
}
return null;
}
if (introStep !== "await-ebike-mount" && introStep !== "ebike-intro-ride") { if (introStep !== "await-ebike-mount" && introStep !== "ebike-intro-ride") {
return null; return null;
} }
@@ -1,4 +1,4 @@
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useFrame } from "@react-three/fiber"; import { useFrame } from "@react-three/fiber";
import { useGLTF } from "@react-three/drei"; import { useGLTF } from "@react-three/drei";
import * as THREE from "three"; import * as THREE from "three";
@@ -14,6 +14,7 @@ import {
PYLON_UPRIGHT_ROTATION, PYLON_UPRIGHT_ROTATION,
PYLON_WORLD_POSITION, PYLON_WORLD_POSITION,
} from "@/data/gameplay/pylonConfig"; } from "@/data/gameplay/pylonConfig";
import { pylonStraighteningSignal } from "@/components/gameplay/pylon/pylonSignals";
const PYLON_MODEL_PATH = "/models/pylone/model.gltf"; const PYLON_MODEL_PATH = "/models/pylone/model.gltf";
@@ -25,6 +26,11 @@ export function PylonDownedPylon(): React.JSX.Element | null {
const [isStraightening, setIsStraightening] = useState(false); const [isStraightening, setIsStraightening] = useState(false);
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
const straightenStartRef = useRef<number | null>(null); const straightenStartRef = useRef<number | null>(null);
const hasPlayedFirstAudioRef = useRef(false);
useEffect(() => {
if (step === "arrived") hasPlayedFirstAudioRef.current = false;
}, [step]);
const { scene } = useGLTF(PYLON_MODEL_PATH); const { scene } = useGLTF(PYLON_MODEL_PATH);
@@ -33,11 +39,7 @@ export function PylonDownedPylon(): React.JSX.Element | null {
if (!group) return; if (!group) return;
if (!isStraightening || straightenStartRef.current === null) { if (!isStraightening || straightenStartRef.current === null) {
const targetRotation = group.rotation.set(...(showUpright ? PYLON_UPRIGHT_ROTATION : PYLON_DOWNED_ROTATION));
step === "narrator-outro"
? PYLON_UPRIGHT_ROTATION
: PYLON_DOWNED_ROTATION;
group.rotation.set(...targetRotation);
return; return;
} }
@@ -53,25 +55,22 @@ export function PylonDownedPylon(): React.JSX.Element | null {
); );
}); });
if (mainState !== "pylon") return null; const showUpright =
mainState !== "pylon" ||
if (
step === "approaching" ||
step === "waiting" || step === "waiting" ||
step === "inspected" || step === "inspected" ||
step === "fragmented" || step === "fragmented" ||
step === "scanning" || step === "scanning" ||
step === "repairing" || step === "repairing" ||
step === "reassembling" || step === "reassembling" ||
step === "done" step === "done" ||
) { step === "narrator-outro";
return null;
}
const isPylonInteractive = step === "arrived" || step === "npc-return"; const isPylonInteractive = step === "arrived" || step === "npc-return";
const beginStraighten = (): void => { const beginStraighten = (): void => {
setIsStraightening(true); setIsStraightening(true);
pylonStraighteningSignal.started = true;
straightenStartRef.current = performance.now(); straightenStartRef.current = performance.now();
setCanMove(false); setCanMove(false);
if (groupRef.current) { if (groupRef.current) {
@@ -79,8 +78,9 @@ export function PylonDownedPylon(): React.JSX.Element | null {
} }
window.setTimeout(() => { window.setTimeout(() => {
setIsStraightening(false); setIsStraightening(false);
pylonStraighteningSignal.started = false;
setCanMove(true); setCanMove(true);
setMissionStep("pylon", "waiting"); setMissionStep("pylon", "inspected");
}, PYLON_STRAIGHTEN_ANIMATION_DURATION_MS); }, PYLON_STRAIGHTEN_ANIMATION_DURATION_MS);
}; };
@@ -101,14 +101,35 @@ export function PylonDownedPylon(): React.JSX.Element | null {
radius={PYLON_NARRATIVE_INTERACT_RADIUS} radius={PYLON_NARRATIVE_INTERACT_RADIUS}
onPress={() => { onPress={() => {
if (step === "arrived") { if (step === "arrived") {
void (async () => { if (!hasPlayedFirstAudioRef.current) {
const manifest = await loadDialogueManifest(); hasPlayedFirstAudioRef.current = true;
if (!manifest) return; void (async () => {
await playDialogueById( const manifest = await loadDialogueManifest();
manifest, if (!manifest) return;
PYLON_NARRATIVE_DIALOGUES.brokenPylon, const audio = await playDialogueById(
); manifest,
})(); PYLON_NARRATIVE_DIALOGUES.brokenPylon,
);
if (!audio) return;
audio.addEventListener(
"ended",
() => {
void (async () => {
const m = await loadDialogueManifest();
if (!m) return;
await playDialogueById(m, PYLON_NARRATIVE_DIALOGUES.demandeAide);
})();
},
{ once: true },
);
})();
} else {
void (async () => {
const manifest = await loadDialogueManifest();
if (!manifest) return;
await playDialogueById(manifest, PYLON_NARRATIVE_DIALOGUES.demandeAide);
})();
}
} else if (step === "npc-return" && !isStraightening) { } else if (step === "npc-return" && !isStraightening) {
beginStraighten(); beginStraighten();
} }
@@ -7,51 +7,58 @@ import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
import { playDialogueById } from "@/utils/dialogues/playDialogue"; import { playDialogueById } from "@/utils/dialogues/playDialogue";
import { import {
PYLON_FARMER_NPC_AFTER_POSITION, PYLON_FARMER_NPC_AFTER_POSITION,
PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight,
PYLON_FARMER_NPC_AFTER_ROTATION,
PYLON_FARMER_NPC_AFTER_SCALE,
PYLON_FARMER_NPC_POSITION, PYLON_FARMER_NPC_POSITION,
PYLON_FARMER_NPC_WALK_SPEED,
PYLON_NARRATIVE_DIALOGUES, PYLON_NARRATIVE_DIALOGUES,
PYLON_NARRATIVE_INTERACT_RADIUS, PYLON_NARRATIVE_INTERACT_RADIUS,
} from "@/data/gameplay/pylonConfig"; } from "@/data/gameplay/pylonConfig";
import { pylonStraighteningSignal } from "@/components/gameplay/pylon/pylonSignals";
const _target = new THREE.Vector3();
export function PylonFarmerNPC(): React.JSX.Element | null { export function PylonFarmerNPC(): React.JSX.Element | null {
const mainState = useGameStore((state) => state.mainState); const mainState = useGameStore((state) => state.mainState);
const step = useGameStore((state) => state.pylon.currentStep); const step = useGameStore((state) => state.pylon.currentStep);
const setMissionStep = useGameStore((state) => state.setMissionStep); const setMissionStep = useGameStore((state) => state.setMissionStep);
const setCanMove = useGameStore((state) => state.setCanMove);
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
const currentPosRef = useRef(
new THREE.Vector3(...PYLON_FARMER_NPC_POSITION),
);
// Reset position when entering arrived, set target when entering npc-return
useEffect(() => { useEffect(() => {
if (mainState !== "pylon" || step !== "arrived") return; if (step === "arrived") {
currentPosRef.current.set(...PYLON_FARMER_NPC_POSITION);
}
}, [step]);
if (!groupRef.current) return; useFrame((_, delta) => {
(groupRef.current.userData as Record<string, unknown>).startTime =
undefined;
}, [mainState, step]);
useFrame(() => {
const group = groupRef.current; const group = groupRef.current;
if (!group) return; if (!group) return;
if ( if (step === "npc-return") {
step === "npc-return" || const targetPos = pylonStraighteningSignal.started
step === "waiting" || ? PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight
step === "narrator-outro" : PYLON_FARMER_NPC_AFTER_POSITION;
) { _target.set(...targetPos);
const startTime = (group.userData as Record<string, unknown>) currentPosRef.current.lerp(_target, Math.min(PYLON_FARMER_NPC_WALK_SPEED * delta, 1));
.startTime as number | undefined; group.position.copy(currentPosRef.current);
if (startTime === undefined) { group.rotation.set(...PYLON_FARMER_NPC_AFTER_ROTATION);
(group.userData as Record<string, unknown>).startTime = group.scale.setScalar(PYLON_FARMER_NPC_AFTER_SCALE);
performance.now(); } else if (step === "inspected") {
group.position.set(...PYLON_FARMER_NPC_AFTER_POSITION); group.position.set(...PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight);
return; group.rotation.set(...PYLON_FARMER_NPC_AFTER_ROTATION);
} group.scale.setScalar(PYLON_FARMER_NPC_AFTER_SCALE);
group.position.set(...PYLON_FARMER_NPC_AFTER_POSITION);
} else { } else {
group.position.set(...PYLON_FARMER_NPC_POSITION); group.position.set(...PYLON_FARMER_NPC_POSITION);
} }
}); });
if (mainState !== "pylon") return null; if (mainState !== "pylon") return null;
if (step !== "arrived") return null; if (step !== "arrived" && step !== "npc-return" && step !== "inspected") return null;
return ( return (
<group ref={groupRef} position={PYLON_FARMER_NPC_POSITION}> <group ref={groupRef} position={PYLON_FARMER_NPC_POSITION}>
@@ -63,45 +70,42 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
<sphereGeometry args={[0.28, 12, 12]} /> <sphereGeometry args={[0.28, 12, 12]} />
<meshStandardMaterial color="#fde68a" /> <meshStandardMaterial color="#fde68a" />
</mesh> </mesh>
<InteractableObject
kind="trigger" {step === "arrived" ? (
label="Parler au fermier" <InteractableObject
position={PYLON_FARMER_NPC_POSITION} kind="trigger"
radius={PYLON_NARRATIVE_INTERACT_RADIUS} label="Parler au fermier"
onPress={() => { position={PYLON_FARMER_NPC_POSITION}
setCanMove(false); radius={PYLON_NARRATIVE_INTERACT_RADIUS}
void (async () => { onPress={() => {
const manifest = await loadDialogueManifest(); void (async () => {
if (!manifest) { const manifest = await loadDialogueManifest();
setCanMove(true); if (!manifest) {
setMissionStep("pylon", "npc-return");
return;
}
const audio = await playDialogueById(
manifest,
PYLON_NARRATIVE_DIALOGUES.farmerHelp,
);
if (!audio) {
setCanMove(true);
setMissionStep("pylon", "npc-return");
return;
}
audio.addEventListener(
"ended",
() => {
setCanMove(true);
setMissionStep("pylon", "npc-return"); setMissionStep("pylon", "npc-return");
}, return;
{ once: true }, }
); const audio = await playDialogueById(
})(); manifest,
}} PYLON_NARRATIVE_DIALOGUES.farmerHelp,
> );
<mesh> if (!audio) {
<sphereGeometry args={[1, 8, 8]} /> setMissionStep("pylon", "npc-return");
<meshBasicMaterial transparent opacity={0} depthWrite={false} /> return;
</mesh> }
</InteractableObject> audio.addEventListener(
"ended",
() => setMissionStep("pylon", "npc-return"),
{ once: true },
);
})();
}}
>
<mesh>
<sphereGeometry args={[1, 8, 8]} />
<meshBasicMaterial transparent opacity={0} depthWrite={false} />
</mesh>
</InteractableObject>
) : null}
</group> </group>
); );
} }
@@ -1,10 +1,9 @@
import { useGameStore } from "@/managers/stores/useGameStore"; import { useGameStore } from "@/managers/stores/useGameStore";
import { useDialoguePlayback } from "@/hooks/gameplay/useDialoguePlayback"; import { useDialoguePlayback } from "@/hooks/gameplay/useDialoguePlayback";
import { ZoneDetection } from "@/components/zone/ZoneDetection"; import { ZoneDetection } from "@/components/zone/ZoneDetection";
import { PylonDownedPylon } from "@/components/gameplay/pylon/PylonDownedPylon";
import { PylonFarmerNPC } from "@/components/gameplay/pylon/PylonFarmerNPC"; import { PylonFarmerNPC } from "@/components/gameplay/pylon/PylonFarmerNPC";
import { PylonNarratorOutro } from "@/components/gameplay/pylon/PylonNarratorOutro"; import { PylonNarratorOutro } from "@/components/gameplay/pylon/PylonNarratorOutro";
import { PYLON_ARRIVED_ZONE } from "@/data/gameplay/zones"; import { PYLON_APPROACH_ZONE, PYLON_ARRIVED_ZONE } from "@/data/gameplay/zones";
import { PYLON_NARRATIVE_DIALOGUES } from "@/data/gameplay/pylonConfig"; import { PYLON_NARRATIVE_DIALOGUES } from "@/data/gameplay/pylonConfig";
export function PylonNarrativeFlow(): React.JSX.Element | null { export function PylonNarrativeFlow(): React.JSX.Element | null {
@@ -31,26 +30,28 @@ export function PylonNarrativeFlow(): React.JSX.Element | null {
if (mainState !== "pylon") return null; if (mainState !== "pylon") return null;
if (step === "locked") {
return (
<ZoneDetection
key="pylon-approach"
zone={PYLON_APPROACH_ZONE}
onEnter={() => setMissionStep("pylon", "approaching")}
/>
);
}
if (step === "approaching") { if (step === "approaching") {
return ( return (
<ZoneDetection <ZoneDetection
key="pylon-arrived"
zone={PYLON_ARRIVED_ZONE} zone={PYLON_ARRIVED_ZONE}
onEnter={() => setMissionStep("pylon", "arrived")} onEnter={() => setMissionStep("pylon", "arrived")}
/> />
); );
} }
if (step === "arrived") { if (step === "arrived" || step === "npc-return" || step === "inspected") {
return ( return <PylonFarmerNPC />;
<>
<PylonDownedPylon />
<PylonFarmerNPC />
</>
);
}
if (step === "npc-return") {
return <PylonDownedPylon />;
} }
if (step === "narrator-outro") { if (step === "narrator-outro") {
@@ -0,0 +1,5 @@
/**
* Shared runtime signal set by PylonDownedPylon when the straighten
* animation starts, so PylonFarmerNPC can switch its lerp target.
*/
export const pylonStraighteningSignal = { started: false };
+1 -1
View File
@@ -12,7 +12,7 @@ interface ZoneDetectionProps {
const _cameraPos = new THREE.Vector3(); const _cameraPos = new THREE.Vector3();
function ZoneDebugVisual({ export function ZoneDebugVisual({
zone, zone,
active, active,
}: { }: {
+19 -2
View File
@@ -2,7 +2,7 @@ import type { Vector3Tuple } from "@/types/three/three";
export const PYLON_WORLD_POSITION: Vector3Tuple = [43, 5, 45]; export const PYLON_WORLD_POSITION: Vector3Tuple = [43, 5, 45];
export const PYLON_DOWNED_ROTATION: Vector3Tuple = [0, 0, -1.4]; export const PYLON_DOWNED_ROTATION: Vector3Tuple = [0, 0, -0.9];
export const PYLON_UPRIGHT_ROTATION: Vector3Tuple = [0, 0, 0]; export const PYLON_UPRIGHT_ROTATION: Vector3Tuple = [0, 0, 0];
@@ -13,11 +13,27 @@ export const PYLON_FARMER_NPC_POSITION: Vector3Tuple = [
]; ];
export const PYLON_FARMER_NPC_AFTER_POSITION: Vector3Tuple = [ export const PYLON_FARMER_NPC_AFTER_POSITION: Vector3Tuple = [
PYLON_WORLD_POSITION[0] + 3,
PYLON_WORLD_POSITION[1],
PYLON_WORLD_POSITION[2],
];
/** Position finale du PNJ quand le pylône se redresse */
export const PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight: Vector3Tuple = [
PYLON_WORLD_POSITION[0] + 1, PYLON_WORLD_POSITION[0] + 1,
PYLON_WORLD_POSITION[1], PYLON_WORLD_POSITION[1],
PYLON_WORLD_POSITION[2] - 2, PYLON_WORLD_POSITION[2],
]; ];
/** Rotation (X Y Z radians) du PNJ une fois arrivé sous le pylône */
export const PYLON_FARMER_NPC_AFTER_ROTATION: Vector3Tuple = [0, 0, 0];
/** Scale uniforme du PNJ une fois arrivé sous le pylône */
export const PYLON_FARMER_NPC_AFTER_SCALE = 1;
/** Vitesse du lerp de déplacement du PNJ (unités/s) */
export const PYLON_FARMER_NPC_WALK_SPEED = 2;
export const PYLON_NARRATIVE_INTERACT_RADIUS = 3.5; export const PYLON_NARRATIVE_INTERACT_RADIUS = 3.5;
export const PYLON_STRAIGHTEN_ANIMATION_DURATION_MS = 2200; export const PYLON_STRAIGHTEN_ANIMATION_DURATION_MS = 2200;
@@ -26,6 +42,7 @@ export const PYLON_NARRATIVE_DIALOGUES = {
electricOutage: "narrateur_coupureelec", electricOutage: "narrateur_coupureelec",
searchCentral: "narrateur_fouillelecentre", searchCentral: "narrateur_fouillelecentre",
brokenPylon: "narrateur_poteaueleccasse", brokenPylon: "narrateur_poteaueleccasse",
demandeAide: "narrateur_demande_aide",
farmerHelp: "fermier_coupdemain", farmerHelp: "fermier_coupdemain",
powerRestored: "narrateur_courantrepare", powerRestored: "narrateur_courantrepare",
} as const; } as const;
+2 -1
View File
@@ -4,6 +4,7 @@ import type {
RepairMissionTriggerConfig, RepairMissionTriggerConfig,
} from "@/types/gameplay/repairMission"; } from "@/types/gameplay/repairMission";
import { EBIKE_WORLD_POSITION } from "@/data/ebike/ebikeConfig"; import { EBIKE_WORLD_POSITION } from "@/data/ebike/ebikeConfig";
import { PYLON_WORLD_POSITION } from "@/data/gameplay/pylonConfig";
export const REPAIR_MISSION_ANCHOR_IDS: Partial< export const REPAIR_MISSION_ANCHOR_IDS: Partial<
Record<RepairMissionId, string> Record<RepairMissionId, string>
@@ -15,7 +16,7 @@ const EBIKE_REPAIR_POSITION = EBIKE_WORLD_POSITION satisfies Vector3Tuple;
const REPAIR_MISSION_POSITIONS = { const REPAIR_MISSION_POSITIONS = {
ebike: EBIKE_REPAIR_POSITION, ebike: EBIKE_REPAIR_POSITION,
pylon: [64, 0, -66], pylon: PYLON_WORLD_POSITION,
farm: [-24, 0, 42], farm: [-24, 0, 42],
} as const satisfies Record<RepairMissionId, Vector3Tuple>; } as const satisfies Record<RepairMissionId, Vector3Tuple>;
+1 -1
View File
@@ -76,7 +76,7 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
{ {
id: "pylon-damaged-panel", id: "pylon-damaged-panel",
label: "Damaged solar panel", label: "Damaged solar panel",
nodeName: "panneau2", nodeName: "pylone",
caseSlotName: "placeholder_2", caseSlotName: "placeholder_2",
}, },
], ],
+8 -8
View File
@@ -4,11 +4,11 @@ import { PYLON_WORLD_POSITION } from "@/data/gameplay/pylonConfig";
export const PYLON_APPROACH_ZONE: ZoneConfig = { export const PYLON_APPROACH_ZONE: ZoneConfig = {
id: "pylon-approach", id: "pylon-approach",
position: [ position: [
PYLON_WORLD_POSITION[0] - 20, PYLON_WORLD_POSITION[0],
PYLON_WORLD_POSITION[1], PYLON_WORLD_POSITION[1]- 5,
PYLON_WORLD_POSITION[2] - 5, PYLON_WORLD_POSITION[2],
], ],
radius: 12, radius: 5,
height: 18, height: 18,
oneShot: true, oneShot: true,
}; };
@@ -16,11 +16,11 @@ export const PYLON_APPROACH_ZONE: ZoneConfig = {
export const PYLON_ARRIVED_ZONE: ZoneConfig = { export const PYLON_ARRIVED_ZONE: ZoneConfig = {
id: "pylon-arrived", id: "pylon-arrived",
position: [ position: [
PYLON_WORLD_POSITION[0] - 3, PYLON_WORLD_POSITION[0] + 5,
PYLON_WORLD_POSITION[1], PYLON_WORLD_POSITION[1] - 5,
PYLON_WORLD_POSITION[2] + 2, PYLON_WORLD_POSITION[2] + 5,
], ],
radius: 8, radius: 5,
height: 15, height: 15,
oneShot: true, oneShot: true,
}; };
+11
View File
@@ -1,7 +1,11 @@
import { Ebike } from "@/components/ebike/Ebike"; import { Ebike } from "@/components/ebike/Ebike";
import { InteractableObject } from "@/components/three/interaction/InteractableObject"; import { InteractableObject } from "@/components/three/interaction/InteractableObject";
import { RepairGame } from "@/components/three/gameplay/RepairGame"; import { RepairGame } from "@/components/three/gameplay/RepairGame";
import { PylonDownedPylon } from "@/components/gameplay/pylon/PylonDownedPylon";
import { PylonNarrativeFlow } from "@/components/gameplay/pylon/PylonNarrativeFlow"; import { PylonNarrativeFlow } from "@/components/gameplay/pylon/PylonNarrativeFlow";
import { ZoneDebugVisual } from "@/components/zone/ZoneDetection";
import { PYLON_APPROACH_ZONE, PYLON_ARRIVED_ZONE } from "@/data/gameplay/zones";
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
import { import {
REPAIR_MISSION_POSITION_ENTRIES, REPAIR_MISSION_POSITION_ENTRIES,
REPAIR_MISSION_TRIGGERS, REPAIR_MISSION_TRIGGERS,
@@ -89,6 +93,13 @@ export function GameStageContent(): React.JSX.Element {
<> <>
{mainState === "intro" ? <StageAnchor {...INTRO_STAGE_ANCHOR} /> : null} {mainState === "intro" ? <StageAnchor {...INTRO_STAGE_ANCHOR} /> : null}
<Ebike position={EBIKE_WORLD_POSITION} /> <Ebike position={EBIKE_WORLD_POSITION} />
<PylonDownedPylon />
{isDebugEnabled() ? (
<>
<ZoneDebugVisual zone={PYLON_APPROACH_ZONE} active={false} />
<ZoneDebugVisual zone={PYLON_ARRIVED_ZONE} active={false} />
</>
) : null}
{mainState === "pylon" ? <PylonNarrativeFlow /> : null} {mainState === "pylon" ? <PylonNarrativeFlow /> : null}
{REPAIR_MISSION_POSITION_ENTRIES.map(({ mission }) => { {REPAIR_MISSION_POSITION_ENTRIES.map(({ mission }) => {
const position = getRepairMissionPosition(mission, anchors); const position = getRepairMissionPosition(mission, anchors);