diff --git a/src/components/ebike/EbikeGPSMap.tsx b/src/components/ebike/EbikeGPSMap.tsx index 6909930..dfcef7e 100644 --- a/src/components/ebike/EbikeGPSMap.tsx +++ b/src/components/ebike/EbikeGPSMap.tsx @@ -181,9 +181,12 @@ export const EbikeGPSMap: React.FC = ({ // Sync texture into uniform when it changes (canvas resize) useEffect(() => { + const mapUniform = shaderMat.uniforms.map; + if (!mapUniform) return; + // External Three.js material uniform sync — intentional side effect. // eslint-disable-next-line react-hooks/immutability - shaderMat.uniforms.map.value = texture; + mapUniform.value = texture; }, [shaderMat, texture]); // Cleanup on unmount diff --git a/src/components/game/EbikeIntroSequence.tsx b/src/components/game/EbikeIntroSequence.tsx index 3529619..7eda6ba 100644 --- a/src/components/game/EbikeIntroSequence.tsx +++ b/src/components/game/EbikeIntroSequence.tsx @@ -146,16 +146,6 @@ export function EbikeIntroSequence(): React.JSX.Element | null { return null; } - if (mainState == "pylon") { - if (pylonStep === "approaching") { - return ; - } - if (pylonStep === "narrator-outro") { - return ; - } - return null; - } - if ( introStep !== "reveal" && introStep !== "await-ebike-mount" && diff --git a/src/components/gameplay/farm/FarmNarrativeFlow.tsx b/src/components/gameplay/farm/FarmNarrativeFlow.tsx index 19bb1a5..5cc1358 100644 --- a/src/components/gameplay/farm/FarmNarrativeFlow.tsx +++ b/src/components/gameplay/farm/FarmNarrativeFlow.tsx @@ -3,7 +3,8 @@ import { useGameStore } from "@/managers/stores/useGameStore"; import { useSubtitleStore } from "@/managers/stores/useSubtitleStore"; import { AudioManager } from "@/managers/AudioManager"; -const HISTOIRE_AUDIO_PATH = "/sounds/dialogue/narrateur_histoireelectricienne.mp3"; +const HISTOIRE_AUDIO_PATH = + "/sounds/dialogue/narrateur_histoireelectricienne.mp3"; const OUTRO_DELAY_MS = 5_000; // delay after audio ends before transitioning to outro /** @@ -78,9 +79,12 @@ function useHistoireSubtitlePlayback( ({ start, end }) => t >= start && t < end, ); if (idx >= 0) { + const text = HISTOIRE_BLOCKS[idx]; + if (text === undefined) return; + setActiveSubtitle({ speaker: "Narrateur", - text: HISTOIRE_BLOCKS[idx], + text, }); } } @@ -136,7 +140,9 @@ export function FarmNarrativeFlow(): null { // After the audio finishes, wait 5 s then transition to outro. // The timeout ID is kept in a ref so we can cancel on unmount. - const outroTimeoutRef = useRef | null>(null); + const outroTimeoutRef = useRef | null>( + null, + ); useEffect(() => { return () => { diff --git a/src/components/gameplay/pylon/PylonDownedPylon.tsx b/src/components/gameplay/pylon/PylonDownedPylon.tsx index 3347bf1..f31bea0 100644 --- a/src/components/gameplay/pylon/PylonDownedPylon.tsx +++ b/src/components/gameplay/pylon/PylonDownedPylon.tsx @@ -33,7 +33,9 @@ export function PylonDownedPylon(): React.JSX.Element | null { ); // 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 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. @@ -63,7 +65,9 @@ export function PylonDownedPylon(): React.JSX.Element | null { if (!group) return; if (!isStraightening || straightenStartRef.current === null) { - group.rotation.set(...(isRaised ? PYLON_UPRIGHT_ROTATION : PYLON_DOWNED_ROTATION)); + group.rotation.set( + ...(isRaised ? PYLON_UPRIGHT_ROTATION : PYLON_DOWNED_ROTATION), + ); return; } @@ -104,11 +108,7 @@ export function PylonDownedPylon(): React.JSX.Element | null { if (!shouldRender) return null; return ( - + {isPylonInteractive ? ( { // 1. Play the generator powerdown sound effect - const sfx = AudioManager.getInstance().playSound( - PYLON_POWERDOWN_SFX, - 1, - { category: "sfx" }, - ); + const sfx = AudioManager.getInstance().playSound(PYLON_POWERDOWN_SFX, 1, { + category: "sfx", + }); // 2. Wait for it to finish (or skip if it can't load) if (sfx) { diff --git a/src/components/site/SiteNamingScreen.tsx b/src/components/site/SiteNamingScreen.tsx index 2ee9090..148efbb 100644 --- a/src/components/site/SiteNamingScreen.tsx +++ b/src/components/site/SiteNamingScreen.tsx @@ -15,7 +15,6 @@ import { } from "@/utils/dialogues/playDialogue"; const TYPEWRITER_CHAR_DELAY_MS = 150; -const TYPEWRITER_START_DELAY_MS = 12000; // Fallback in case nothing else triggers the typewriter (audio failed to // load, no subtitles, "ended" never fires). Long enough not to fire // before the narration on a slow load. diff --git a/src/components/ui/OutroVideoOverlay.tsx b/src/components/ui/OutroVideoOverlay.tsx index dd7ac62..e41fd24 100644 --- a/src/components/ui/OutroVideoOverlay.tsx +++ b/src/components/ui/OutroVideoOverlay.tsx @@ -16,7 +16,10 @@ export function OutroVideoOverlay(): React.JSX.Element | null { setVisible(true); } - window.addEventListener("outro-cinematic-complete", handleCinematicComplete); + window.addEventListener( + "outro-cinematic-complete", + handleCinematicComplete, + ); return () => { window.removeEventListener( "outro-cinematic-complete", diff --git a/src/components/zone/ZoneDetection.tsx b/src/components/zone/ZoneDetection.tsx index 8c4e067..f4c20b1 100644 --- a/src/components/zone/ZoneDetection.tsx +++ b/src/components/zone/ZoneDetection.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { useFrame, useThree } from "@react-three/fiber"; import * as THREE from "three"; import { isDebugEnabled } from "@/utils/debug/isDebugEnabled"; @@ -50,11 +50,10 @@ export function ZoneDetection({ zone, onEnter, height, -}: ZoneDetectionProps): React.JSX.Element { +}: ZoneDetectionProps): React.JSX.Element | null { const camera = useThree((state) => state.camera); const hasTriggeredRef = useRef(false); const onEnterRef = useRef(onEnter); - const [isActive, setIsActive] = useState(false); useEffect(() => { onEnterRef.current = onEnter; @@ -75,9 +74,8 @@ export function ZoneDetection({ if (_cameraPos.y > zone.position[1] + zoneHeight / 2) return; hasTriggeredRef.current = true; - setIsActive(true); onEnterRef.current(); }); - return ; + return null; } diff --git a/src/data/gameplay/repairMissionState.ts b/src/data/gameplay/repairMissionState.ts index 71a93de..a9caf70 100644 --- a/src/data/gameplay/repairMissionState.ts +++ b/src/data/gameplay/repairMissionState.ts @@ -10,6 +10,7 @@ const REPAIR_MISSION_ID_VALUES: ReadonlySet = new Set( export const MISSION_STEPS = [ "locked", + "electricienne_history", "approaching", "arrived", "npc-return", @@ -30,12 +31,20 @@ const PYLON_ONLY_MISSION_STEPS = new Set([ "npc-return", "narrator-outro", ]); +const FARM_ONLY_MISSION_STEPS = new Set(["electricienne_history"]); export function getMissionStepsFor( mission: RepairMissionId, ): readonly MissionStep[] { - if (mission === "pylon") return MISSION_STEPS; - return MISSION_STEPS.filter((step) => !PYLON_ONLY_MISSION_STEPS.has(step)); + return MISSION_STEPS.filter((step) => { + if (mission !== "pylon" && PYLON_ONLY_MISSION_STEPS.has(step)) { + return false; + } + if (mission !== "farm" && FARM_ONLY_MISSION_STEPS.has(step)) { + return false; + } + return true; + }); } export function isRepairMissionId(value: string): value is RepairMissionId { @@ -53,6 +62,8 @@ export function getNextMissionStep( switch (step) { case "locked": return mission === "pylon" ? "approaching" : "waiting"; + case "electricienne_history": + return "done"; case "approaching": return "arrived"; case "arrived": @@ -85,6 +96,8 @@ export function getPreviousMissionStep( switch (step) { case "locked": return "locked"; + case "electricienne_history": + return "locked"; case "approaching": return "locked"; case "arrived": diff --git a/src/utils/map/mapInstanceTransform.ts b/src/utils/map/mapInstanceTransform.ts index 257a10f..5d0bde4 100644 --- a/src/utils/map/mapInstanceTransform.ts +++ b/src/utils/map/mapInstanceTransform.ts @@ -3,10 +3,15 @@ import type { MapNode, MapNodeInstanceTransform } from "@/types/map/mapScene"; export function mapNodeToInstanceTransform( node: MapNode, ): MapNodeInstanceTransform { - return { - id: node.id, + const transform: MapNodeInstanceTransform = { position: node.position, rotation: node.rotation, scale: node.scale, }; + + if (node.id !== undefined) { + transform.id = node.id; + } + + return transform; } diff --git a/src/world/GameStageContent.tsx b/src/world/GameStageContent.tsx index 158d4a0..6ba0d78 100644 --- a/src/world/GameStageContent.tsx +++ b/src/world/GameStageContent.tsx @@ -6,9 +6,6 @@ import { FarmNarrativeFlow } from "@/components/gameplay/farm/FarmNarrativeFlow" import { PylonDownedPylon } from "@/components/gameplay/pylon/PylonDownedPylon"; import { PylonLightingEffect } from "@/components/gameplay/pylon/PylonLightingEffect"; 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 { REPAIR_MISSION_POSITION_ENTRIES, REPAIR_MISSION_TRIGGERS, @@ -18,7 +15,6 @@ import { OUTRO_STAGE_ANCHOR, } from "@/data/gameplay/gameStageAnchors"; import { useGameStore } from "@/managers/stores/useGameStore"; -import { useRepairFocusStore } from "@/managers/stores/useRepairFocusStore"; import { useRepairMissionAnchorStore } from "@/managers/stores/useRepairMissionAnchorStore"; import { isFarmNarrativeStep, @@ -92,14 +88,12 @@ export function GameStageContent(): React.JSX.Element { const mainState = useGameStore((state) => state.mainState); const pylonStep = useGameStore((state) => state.pylon.currentStep); const anchors = useRepairMissionAnchorStore((state) => state.anchors); - const repairFocusActive = useRepairFocusStore((state) => state.active); const farmStep = useGameStore((state) => state.farm.currentStep); const pylonInNarrative = mainState === "pylon" && isPylonNarrativeStep(pylonStep); - const farmInNarrative = - mainState === "farm" && isFarmNarrativeStep(farmStep); + const farmInNarrative = mainState === "farm" && isFarmNarrativeStep(farmStep); return ( <> @@ -107,12 +101,6 @@ export function GameStageContent(): React.JSX.Element { - {isDebugEnabled() && !repairFocusActive ? ( - <> - - - - ) : null} {mainState === "pylon" ? : null} {mainState === "farm" ? : null} {REPAIR_MISSION_POSITION_ENTRIES.map(({ mission }) => {