edit electricienne + poto

This commit is contained in:
math-pixel
2026-06-03 00:05:07 +02:00
parent 0a3966a339
commit 18fb5e39e9
8 changed files with 96 additions and 31 deletions
@@ -4,6 +4,8 @@ import { useGLTF } from "@react-three/drei";
import * as THREE from "three";
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
import { useGameStore } from "@/managers/stores/useGameStore";
import { useRepairMissionAnchorStore } from "@/managers/stores/useRepairMissionAnchorStore";
import { useTerrainSnappedPosition } from "@/hooks/three/useTerrainHeight";
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
import { playDialogueById } from "@/utils/dialogues/playDialogue";
import {
@@ -14,6 +16,7 @@ import {
PYLON_UPRIGHT_ROTATION,
PYLON_WORLD_POSITION,
} from "@/data/gameplay/pylonConfig";
import { isRepairGameStep } from "@/types/gameplay/repairMission";
import { pylonStraighteningSignal } from "@/components/gameplay/pylon/pylonSignals";
const PYLON_MODEL_PATH = "/models/pylone/model.glb";
@@ -22,6 +25,15 @@ export function PylonDownedPylon(): React.JSX.Element | null {
const mainState = useGameStore((state) => state.mainState);
const step = useGameStore((state) => state.pylon.currentStep);
const setCanMove = useGameStore((state) => state.setCanMove);
// Use the repair:pylon anchor from the store so the downed pylon is always
// co-located with the instanced mesh it replaces. Falls back to the
// hard-coded constant while the map is loading or unavailable.
const pylonAnchor = useRepairMissionAnchorStore(
(state) => state.anchors.pylon,
);
// Snap to terrain so the downed/upright model sits flush on the ground,
// matching the Y adjustment that InstancedMapAsset applies to the same node.
const position = useTerrainSnappedPosition(pylonAnchor ?? PYLON_WORLD_POSITION);
const [isStraightening, setIsStraightening] = useState(false);
// Keeps the pylon upright after the animation completes while
// PylonFarmerNPC plays the post-raise audio sequence.
@@ -30,6 +42,10 @@ export function PylonDownedPylon(): React.JSX.Element | null {
const straightenStartRef = useRef<number | null>(null);
const hasPlayedFirstAudioRef = useRef(false);
// Hidden outside the pylon mission and once the pylon has been raised
// (repair-game steps take over from there).
const shouldRender = mainState === "pylon" && !isRepairGameStep(step);
useEffect(() => {
if (step === "arrived") {
hasPlayedFirstAudioRef.current = false;
@@ -44,7 +60,7 @@ export function PylonDownedPylon(): React.JSX.Element | null {
if (!group) return;
if (!isStraightening || straightenStartRef.current === null) {
group.rotation.set(...(showUpright ? PYLON_UPRIGHT_ROTATION : PYLON_DOWNED_ROTATION));
group.rotation.set(...(isRaised ? PYLON_UPRIGHT_ROTATION : PYLON_DOWNED_ROTATION));
return;
}
@@ -60,18 +76,6 @@ export function PylonDownedPylon(): React.JSX.Element | null {
);
});
const showUpright =
isRaised ||
mainState !== "pylon" ||
step === "waiting" ||
step === "inspected" ||
step === "fragmented" ||
step === "scanning" ||
step === "repairing" ||
step === "reassembling" ||
step === "done" ||
step === "narrator-outro";
const isPylonInteractive = step === "arrived" || step === "npc-return";
const beginStraighten = (): void => {
@@ -94,10 +98,12 @@ export function PylonDownedPylon(): React.JSX.Element | null {
}, PYLON_STRAIGHTEN_ANIMATION_DURATION_MS);
};
if (!shouldRender) return null;
return (
<group
ref={groupRef}
position={PYLON_WORLD_POSITION}
position={position}
rotation={PYLON_DOWNED_ROTATION}
>
<primitive object={scene.clone(true)} />
@@ -107,7 +113,7 @@ export function PylonDownedPylon(): React.JSX.Element | null {
label={
step === "arrived" ? "Inspecter le pylône" : "Redresser le pylône"
}
position={PYLON_WORLD_POSITION}
position={position}
radius={PYLON_NARRATIVE_INTERACT_RADIUS}
onPress={() => {
if (step === "arrived") {
@@ -40,9 +40,32 @@ function faceToward(from: THREE.Vector3, to: readonly [number, number, number]):
return Math.atan2(dx, dz);
}
/**
* Outer shell — only checks visibility conditions.
* Rendering is delegated to PylonFarmerNPCContent so that the heavy hooks
* (useFrame, useAnimations) are only active while the NPC is actually shown.
*/
export function PylonFarmerNPC(): React.JSX.Element | null {
const mainState = useGameStore((state) => state.mainState);
const step = useGameStore((state) => state.pylon.currentStep);
if (mainState !== "pylon") return null;
// Visible during narrative + at repair completion (hides during repair steps)
if (
step !== "arrived" &&
step !== "npc-return" &&
step !== "inspected" &&
step !== "done"
) {
return null;
}
return <PylonFarmerNPCContent />;
}
// ─── Inner component — heavy hooks only run when NPC is mounted ──────────────
function PylonFarmerNPCContent(): React.JSX.Element {
const step = useGameStore((state) => state.pylon.currentStep);
const setMissionStep = useGameStore((state) => state.setMissionStep);
const camera = useThree((state) => state.camera);
@@ -69,8 +92,6 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
const { actions } = useAnimations(animations, model);
// ─── playAnim ─────────────────────────────────────────────────────────────
// NOTE: actions is intentionally in the dep array so this callback is
// recreated when drei's internal state populates the actions map.
const playAnim = useCallback(
(name: NPCAnimation, fade = ANIM_FADE): void => {
if (currentAnimRef.current === name) return;
@@ -94,7 +115,6 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
const playPostRaiseAudioAndAdvance = useCallback(async () => {
const manifest = await loadDialogueManifest();
if (manifest) {
// "N'hésite pas, si tu as besoin d'autre chose !"
const audio = await playDialogueById(
manifest,
PYLON_NARRATIVE_DIALOGUES.electricienneApresMontage,
@@ -111,7 +131,6 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
}, [setMissionStep]);
// ─── Step-driven animation ────────────────────────────────────────────────
// Fires when step changes OR when playAnim changes (i.e. when actions load).
useEffect(() => {
currentAnimRef.current = null;
if (step === "arrived") {
@@ -124,6 +143,15 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
playAnim("walk");
} else if (step === "inspected") {
playAnim("idle");
} else if (step === "done") {
// NPC reappears at repair completion — position at the post-raise spot,
// facing the pylon, playing idle.
currentPosRef.current.set(...PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight);
savedRotationYRef.current = faceToward(
currentPosRef.current,
PYLON_WORLD_POSITION,
);
playAnim("idle");
}
}, [step, playAnim]);
@@ -171,7 +199,7 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
savedRotationYRef.current = faceToward(currentPosRef.current, PYLON_WORLD_POSITION);
}
group.position.copy(currentPosRef.current);
} else if (step === "inspected") {
} else if (step === "inspected" || step === "done") {
group.position.set(...PYLON_FARMER_NPC_AFTER_POSITION_pylone_straight);
} else if (isCompleted) {
group.position.copy(currentPosRef.current);
@@ -190,10 +218,6 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
group.scale.setScalar(PYLON_FARMER_NPC_AFTER_SCALE);
});
if (mainState !== "pylon") return null;
if (step !== "arrived" && step !== "npc-return" && step !== "inspected")
return null;
return (
<group ref={groupRef} position={PYLON_FARMER_NPC_POSITION}>
<primitive object={model} />
@@ -204,6 +228,13 @@ export function PylonFarmerNPC(): React.JSX.Element | null {
position={PYLON_FARMER_NPC_POSITION}
radius={PYLON_NARRATIVE_INTERACT_RADIUS}
onPress={() => {
// Turn to face the player the moment they engage the NPC
savedRotationYRef.current = faceToward(currentPosRef.current, [
camera.position.x,
camera.position.y,
camera.position.z,
]);
void (async () => {
const manifest = await loadDialogueManifest();
if (!manifest) {
@@ -45,7 +45,12 @@ export function PylonNarrativeFlow(): React.JSX.Element | null {
);
}
if (step === "arrived" || step === "npc-return" || step === "inspected") {
if (
step === "arrived" ||
step === "npc-return" ||
step === "inspected" ||
step === "done"
) {
return <PylonFarmerNPC />;
}