import { Suspense, useEffect, useRef } from "react"; import { Physics } from "@react-three/rapier"; import { PLAYER_SPAWN_POSITION_GAME, PLAYER_SPAWN_POSITION_PHYSICS, } from "@/data/player/playerConfig"; import { useRepairTransitionStore } from "@/managers/stores/useRepairTransitionStore"; import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useEnvironmentDebug } from "@/hooks/debug/useEnvironmentDebug"; import { useMapPerformanceDebug } from "@/hooks/debug/useMapPerformanceDebug"; import { useCharacterDebug } from "@/hooks/debug/useCharacterDebug"; import { usePlayerPositionDebug } from "@/hooks/debug/usePlayerPositionDebug"; import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot"; import { useWorldSceneLoading } from "@/hooks/world/useWorldSceneLoading"; import { useGameStore } from "@/managers/stores/useGameStore"; import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls"; import { DebugHelpers } from "@/components/debug/scene/DebugHelpers"; import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove"; import { Environment } from "@/world/Environment"; import { GameCinematics } from "@/world/GameCinematics"; import { GameDialogues } from "@/world/GameDialogues"; import { GameMusic } from "@/world/GameMusic"; import { Lighting } from "@/world/Lighting"; import { GameMap } from "@/world/GameMap"; import { GameStageContent } from "@/world/GameStageContent"; import { CharacterSystem } from "@/world/characters/CharacterSystem"; import { Player } from "@/world/player/Player"; import { TestMap } from "@/world/debug/TestMap"; import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading"; interface WorldProps { onLoadingStateChange?: SceneLoadingChangeHandler | undefined; } export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element { useEnvironmentDebug(); useMapPerformanceDebug(); useCharacterDebug(); usePlayerPositionDebug(); const cameraMode = useCameraMode(); const sceneMode = useSceneMode(); const mainState = useGameStore((state) => state.mainState); const { status, usageStatus } = useHandTrackingSnapshot(); const { octree, gameplayReady, showGameStage, handleGameStageLoaded, handleGameMapLoaded, handleOctreeReady, handleShadowWarmupReady, handleShadowWarmupStarted, shouldWarmUpShadows, } = useWorldSceneLoading({ sceneMode, onLoadingStateChange }); // Capture the spawn position once on mount via a ref so it never changes // mid-session (spawnPosition is reactive in Player and would re-spawn the // character on every prop change). If the player returns from a repair // scene, savedPlayerPosition holds their world position; otherwise fall // back to the default spawn from playerConfig. const savedPlayerPosition = useRepairTransitionStore( (s) => s.savedPlayerPosition, ); const playerSpawnPositionRef = useRef( savedPlayerPosition ?? (sceneMode === "game" ? PLAYER_SPAWN_POSITION_GAME : PLAYER_SPAWN_POSITION_PHYSICS), ); const playerSpawnPosition = playerSpawnPositionRef.current; // Clear the saved position right after capturing it so the next world // mount uses the default spawn instead of the stale repair-exit position. useEffect(() => { if (savedPlayerPosition !== null) { useRepairTransitionStore.getState().setSavedPlayerPosition(null); } // Only on mount — intentionally no deps // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const showHandTrackingGloves = sceneMode === "physics" || (status !== "idle" && usageStatus !== "inactive"); const spawnPlayer = cameraMode !== "debug" && (sceneMode === "game" ? gameplayReady : octree !== null); return ( <> {showHandTrackingGloves ? ( ) : null} {cameraMode === "debug" ? : null} {sceneMode === "game" ? ( <> {showGameStage && mainState !== "ebike" ? : null} {showGameStage ? ( ) : null} {spawnPlayer ? ( <> {mainState === "outro" ? : null} {mainState !== "intro" ? : null} ) : null} ) : ( )} {sceneMode !== "game" && spawnPlayer ? ( ) : null} ); } function GameStageLoaded({ onLoaded }: { onLoaded: () => void }): null { useEffect(() => { onLoaded(); }, [onLoaded]); return null; }