feat mission-2
This commit is contained in:
@@ -12,8 +12,10 @@ import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
||||
import { playDialogueById } from "@/utils/dialogues/playDialogue";
|
||||
|
||||
export function EbikeIntroSequence(): React.JSX.Element | null {
|
||||
const mainState = useGameStore((state) => state.mainState);
|
||||
const introStep = useGameStore((state) => state.intro.currentStep);
|
||||
const movementMode = useGameStore((state) => state.player.movementMode);
|
||||
const pylonStep = useGameStore((state) => state.pylon.currentStep);
|
||||
const setIntroStep = useGameStore((state) => state.setIntroStep);
|
||||
const completeIntro = useGameStore((state) => state.completeIntro);
|
||||
const [breakdownDialogueDone, setBreakdownDialogueDone] = useState(false);
|
||||
@@ -100,6 +102,16 @@ export function EbikeIntroSequence(): React.JSX.Element | null {
|
||||
}
|
||||
}, [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") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
PYLON_UPRIGHT_ROTATION,
|
||||
PYLON_WORLD_POSITION,
|
||||
} from "@/data/gameplay/pylonConfig";
|
||||
import { pylonStraighteningSignal } from "@/components/gameplay/pylon/pylonSignals";
|
||||
|
||||
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 groupRef = useRef<THREE.Group>(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);
|
||||
|
||||
@@ -33,11 +39,7 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
if (!group) return;
|
||||
|
||||
if (!isStraightening || straightenStartRef.current === null) {
|
||||
const targetRotation =
|
||||
step === "narrator-outro"
|
||||
? PYLON_UPRIGHT_ROTATION
|
||||
: PYLON_DOWNED_ROTATION;
|
||||
group.rotation.set(...targetRotation);
|
||||
group.rotation.set(...(showUpright ? PYLON_UPRIGHT_ROTATION : PYLON_DOWNED_ROTATION));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,25 +55,22 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
);
|
||||
});
|
||||
|
||||
if (mainState !== "pylon") return null;
|
||||
|
||||
if (
|
||||
step === "approaching" ||
|
||||
const showUpright =
|
||||
mainState !== "pylon" ||
|
||||
step === "waiting" ||
|
||||
step === "inspected" ||
|
||||
step === "fragmented" ||
|
||||
step === "scanning" ||
|
||||
step === "repairing" ||
|
||||
step === "reassembling" ||
|
||||
step === "done"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
step === "done" ||
|
||||
step === "narrator-outro";
|
||||
|
||||
const isPylonInteractive = step === "arrived" || step === "npc-return";
|
||||
|
||||
const beginStraighten = (): void => {
|
||||
setIsStraightening(true);
|
||||
pylonStraighteningSignal.started = true;
|
||||
straightenStartRef.current = performance.now();
|
||||
setCanMove(false);
|
||||
if (groupRef.current) {
|
||||
@@ -79,8 +78,9 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
setIsStraightening(false);
|
||||
pylonStraighteningSignal.started = false;
|
||||
setCanMove(true);
|
||||
setMissionStep("pylon", "waiting");
|
||||
setMissionStep("pylon", "inspected");
|
||||
}, PYLON_STRAIGHTEN_ANIMATION_DURATION_MS);
|
||||
};
|
||||
|
||||
@@ -101,14 +101,35 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
radius={PYLON_NARRATIVE_INTERACT_RADIUS}
|
||||
onPress={() => {
|
||||
if (step === "arrived") {
|
||||
void (async () => {
|
||||
const manifest = await loadDialogueManifest();
|
||||
if (!manifest) return;
|
||||
await playDialogueById(
|
||||
manifest,
|
||||
PYLON_NARRATIVE_DIALOGUES.brokenPylon,
|
||||
);
|
||||
})();
|
||||
if (!hasPlayedFirstAudioRef.current) {
|
||||
hasPlayedFirstAudioRef.current = true;
|
||||
void (async () => {
|
||||
const manifest = await loadDialogueManifest();
|
||||
if (!manifest) return;
|
||||
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) {
|
||||
beginStraighten();
|
||||
}
|
||||
|
||||
@@ -7,51 +7,58 @@ import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
||||
import { playDialogueById } from "@/utils/dialogues/playDialogue";
|
||||
import {
|
||||
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_WALK_SPEED,
|
||||
PYLON_NARRATIVE_DIALOGUES,
|
||||
PYLON_NARRATIVE_INTERACT_RADIUS,
|
||||
} from "@/data/gameplay/pylonConfig";
|
||||
import { pylonStraighteningSignal } from "@/components/gameplay/pylon/pylonSignals";
|
||||
|
||||
const _target = new THREE.Vector3();
|
||||
|
||||
export function PylonFarmerNPC(): React.JSX.Element | null {
|
||||
const mainState = useGameStore((state) => state.mainState);
|
||||
const step = useGameStore((state) => state.pylon.currentStep);
|
||||
const setMissionStep = useGameStore((state) => state.setMissionStep);
|
||||
const setCanMove = useGameStore((state) => state.setCanMove);
|
||||
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(() => {
|
||||
if (mainState !== "pylon" || step !== "arrived") return;
|
||||
if (step === "arrived") {
|
||||
currentPosRef.current.set(...PYLON_FARMER_NPC_POSITION);
|
||||
}
|
||||
}, [step]);
|
||||
|
||||
if (!groupRef.current) return;
|
||||
(groupRef.current.userData as Record<string, unknown>).startTime =
|
||||
undefined;
|
||||
}, [mainState, step]);
|
||||
|
||||
useFrame(() => {
|
||||
useFrame((_, delta) => {
|
||||
const group = groupRef.current;
|
||||
if (!group) return;
|
||||
|
||||
if (
|
||||
step === "npc-return" ||
|
||||
step === "waiting" ||
|
||||
step === "narrator-outro"
|
||||
) {
|
||||
const startTime = (group.userData as Record<string, unknown>)
|
||||
.startTime as number | undefined;
|
||||
if (startTime === undefined) {
|
||||
(group.userData as Record<string, unknown>).startTime =
|
||||
performance.now();
|
||||
group.position.set(...PYLON_FARMER_NPC_AFTER_POSITION);
|
||||
return;
|
||||
}
|
||||
group.position.set(...PYLON_FARMER_NPC_AFTER_POSITION);
|
||||
if (step === "npc-return") {
|
||||
const targetPos = pylonStraighteningSignal.started
|
||||
? PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight
|
||||
: PYLON_FARMER_NPC_AFTER_POSITION;
|
||||
_target.set(...targetPos);
|
||||
currentPosRef.current.lerp(_target, Math.min(PYLON_FARMER_NPC_WALK_SPEED * delta, 1));
|
||||
group.position.copy(currentPosRef.current);
|
||||
group.rotation.set(...PYLON_FARMER_NPC_AFTER_ROTATION);
|
||||
group.scale.setScalar(PYLON_FARMER_NPC_AFTER_SCALE);
|
||||
} else if (step === "inspected") {
|
||||
group.position.set(...PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight);
|
||||
group.rotation.set(...PYLON_FARMER_NPC_AFTER_ROTATION);
|
||||
group.scale.setScalar(PYLON_FARMER_NPC_AFTER_SCALE);
|
||||
} else {
|
||||
group.position.set(...PYLON_FARMER_NPC_POSITION);
|
||||
}
|
||||
});
|
||||
|
||||
if (mainState !== "pylon") return null;
|
||||
if (step !== "arrived") return null;
|
||||
if (step !== "arrived" && step !== "npc-return" && step !== "inspected") return null;
|
||||
|
||||
return (
|
||||
<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]} />
|
||||
<meshStandardMaterial color="#fde68a" />
|
||||
</mesh>
|
||||
<InteractableObject
|
||||
kind="trigger"
|
||||
label="Parler au fermier"
|
||||
position={PYLON_FARMER_NPC_POSITION}
|
||||
radius={PYLON_NARRATIVE_INTERACT_RADIUS}
|
||||
onPress={() => {
|
||||
setCanMove(false);
|
||||
void (async () => {
|
||||
const manifest = await loadDialogueManifest();
|
||||
if (!manifest) {
|
||||
setCanMove(true);
|
||||
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);
|
||||
|
||||
{step === "arrived" ? (
|
||||
<InteractableObject
|
||||
kind="trigger"
|
||||
label="Parler au fermier"
|
||||
position={PYLON_FARMER_NPC_POSITION}
|
||||
radius={PYLON_NARRATIVE_INTERACT_RADIUS}
|
||||
onPress={() => {
|
||||
void (async () => {
|
||||
const manifest = await loadDialogueManifest();
|
||||
if (!manifest) {
|
||||
setMissionStep("pylon", "npc-return");
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 8, 8]} />
|
||||
<meshBasicMaterial transparent opacity={0} depthWrite={false} />
|
||||
</mesh>
|
||||
</InteractableObject>
|
||||
return;
|
||||
}
|
||||
const audio = await playDialogueById(
|
||||
manifest,
|
||||
PYLON_NARRATIVE_DIALOGUES.farmerHelp,
|
||||
);
|
||||
if (!audio) {
|
||||
setMissionStep("pylon", "npc-return");
|
||||
return;
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import { useDialoguePlayback } from "@/hooks/gameplay/useDialoguePlayback";
|
||||
import { ZoneDetection } from "@/components/zone/ZoneDetection";
|
||||
import { PylonDownedPylon } from "@/components/gameplay/pylon/PylonDownedPylon";
|
||||
import { PylonFarmerNPC } from "@/components/gameplay/pylon/PylonFarmerNPC";
|
||||
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";
|
||||
|
||||
export function PylonNarrativeFlow(): React.JSX.Element | null {
|
||||
@@ -31,26 +30,28 @@ export function PylonNarrativeFlow(): React.JSX.Element | 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") {
|
||||
return (
|
||||
<ZoneDetection
|
||||
key="pylon-arrived"
|
||||
zone={PYLON_ARRIVED_ZONE}
|
||||
onEnter={() => setMissionStep("pylon", "arrived")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === "arrived") {
|
||||
return (
|
||||
<>
|
||||
<PylonDownedPylon />
|
||||
<PylonFarmerNPC />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === "npc-return") {
|
||||
return <PylonDownedPylon />;
|
||||
if (step === "arrived" || step === "npc-return" || step === "inspected") {
|
||||
return <PylonFarmerNPC />;
|
||||
}
|
||||
|
||||
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 };
|
||||
@@ -12,7 +12,7 @@ interface ZoneDetectionProps {
|
||||
|
||||
const _cameraPos = new THREE.Vector3();
|
||||
|
||||
function ZoneDebugVisual({
|
||||
export function ZoneDebugVisual({
|
||||
zone,
|
||||
active,
|
||||
}: {
|
||||
|
||||
Reference in New Issue
Block a user