feat mission-2
This commit is contained in:
@@ -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_courantréparé.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") {
|
||||||
|
if (!hasPlayedFirstAudioRef.current) {
|
||||||
|
hasPlayedFirstAudioRef.current = true;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const manifest = await loadDialogueManifest();
|
const manifest = await loadDialogueManifest();
|
||||||
if (!manifest) return;
|
if (!manifest) return;
|
||||||
await playDialogueById(
|
const audio = await playDialogueById(
|
||||||
manifest,
|
manifest,
|
||||||
PYLON_NARRATIVE_DIALOGUES.brokenPylon,
|
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,17 +70,17 @@ 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>
|
||||||
|
|
||||||
|
{step === "arrived" ? (
|
||||||
<InteractableObject
|
<InteractableObject
|
||||||
kind="trigger"
|
kind="trigger"
|
||||||
label="Parler au fermier"
|
label="Parler au fermier"
|
||||||
position={PYLON_FARMER_NPC_POSITION}
|
position={PYLON_FARMER_NPC_POSITION}
|
||||||
radius={PYLON_NARRATIVE_INTERACT_RADIUS}
|
radius={PYLON_NARRATIVE_INTERACT_RADIUS}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setCanMove(false);
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const manifest = await loadDialogueManifest();
|
const manifest = await loadDialogueManifest();
|
||||||
if (!manifest) {
|
if (!manifest) {
|
||||||
setCanMove(true);
|
|
||||||
setMissionStep("pylon", "npc-return");
|
setMissionStep("pylon", "npc-return");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -82,16 +89,12 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
|
|||||||
PYLON_NARRATIVE_DIALOGUES.farmerHelp,
|
PYLON_NARRATIVE_DIALOGUES.farmerHelp,
|
||||||
);
|
);
|
||||||
if (!audio) {
|
if (!audio) {
|
||||||
setCanMove(true);
|
|
||||||
setMissionStep("pylon", "npc-return");
|
setMissionStep("pylon", "npc-return");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
audio.addEventListener(
|
audio.addEventListener(
|
||||||
"ended",
|
"ended",
|
||||||
() => {
|
() => setMissionStep("pylon", "npc-return"),
|
||||||
setCanMove(true);
|
|
||||||
setMissionStep("pylon", "npc-return");
|
|
||||||
},
|
|
||||||
{ once: true },
|
{ once: true },
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
@@ -102,6 +105,7 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
|
|||||||
<meshBasicMaterial transparent opacity={0} depthWrite={false} />
|
<meshBasicMaterial transparent opacity={0} depthWrite={false} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</InteractableObject>
|
</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 };
|
||||||
@@ -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,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user