import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent, } from "react"; import { Canvas } from "@react-three/fiber"; import { Physics } from "@react-three/rapier"; import { requireDungeonDefinition } from "../game/dungeonRegistry"; import { spawnMatchesDifficulty } from "../game/mobPopulation"; import { beginMouseLookDrag, endMouseLookDrag, isDesktopCameraLookPointer, releasePointerLock, } from "../game/inputManager"; import { useGameStore } from "../game/store"; import { DungeonEnvironment } from "./DungeonEnvironment"; import { MobPopulation } from "./MobPopulation"; import { PartyPopulation } from "./PartyPopulation"; import { GroundEffectTelegraphs } from "./GroundEffectTelegraphs"; import { PlayerRig } from "./PlayerRig"; import { useManastormStore } from "../game/manastormStore"; import { ManastormPortal } from "./ManastormPortal"; import { encounterWorldIsReady, encounterWorldKey, rendererCanvasKey, sceneFrameLoop, } from "./sceneLifecycle"; import { manastormChaoticLinkRuntimeIds } from "../game/manastormChaoticLink"; import { manastormStagePopulation, manastormStageRoamingPacks } from "../game/manastormStagePopulation"; import { resolveStagePresentation } from "../game/manastormStagePresentation"; import { useWailingEncounterStore, wailingStaticSpawnsForPhase, } from "../game/wailingCavernsEncounter"; import { WailingNaralexEvent } from "./WailingNaralexEvent"; import { GameGltfLoaderLifecycle } from "./useGameGLTF"; import { CombatEffects } from "./CombatEffects"; import { PLAYER_GRAVITY } from "../game/playerMovement"; import { GmPlacementMarkers } from "./GmPlacementMarkers"; import { useOnlineGroupStore } from "../app/onlineGroupStore"; import { useOnlineSessionStore } from "../app/onlineSessionStore"; import { OnlinePlayerPopulation } from "./OnlinePlayerPopulation"; import { DevelopmentPerformanceProbe } from "./DevelopmentPerformanceProbe"; import { AdaptiveGraphicsQuality } from "./AdaptiveGraphicsQuality"; import { useCombatStore } from "../game/combatStore"; import { numericFrameRateLimit } from "../game/frameRateSettings"; import { useDocumentVisibility } from "../platform/useDocumentVisibility"; import { CappedFrameLoop } from "./CappedFrameLoop"; export function GameScene() { const activeDungeonId = useGameStore((state) => state.activeDungeonId); const gameMode = useGameStore((state) => state.gameMode); const sessionRevision = useGameStore((state) => state.sessionRevision); const manastormEncounter = useManastormStore((state) => state.currentEncounter); const manastormPhase = useManastormStore((state) => state.phase); const wailingPhase = useWailingEncounterStore((state) => state.phase); const dungeon = requireDungeonDefinition(activeDungeonId); const cameraFar = Math.max( 950, Math.min( 10_000, Math.hypot( dungeon.bounds.max[0] - dungeon.bounds.min[0], dungeon.bounds.max[2] - dungeon.bounds.min[2], ) * 1.35, ), ); const stagePresentation = resolveStagePresentation(gameMode, manastormEncounter, { title: dungeon.title, mapId: dungeon.mapId, areaName: dungeon.title, }); const activeDifficultyId = useGameStore((state) => state.activeDifficultyId); const difficulty = dungeon.difficultyVariants.find((variant) => variant.id === activeDifficultyId) ?? dungeon.difficultyVariants.find((variant) => variant.id === dungeon.defaultDifficultyId) ?? dungeon.difficultyVariants[0]; const difficultyMask = difficulty?.spawnMask ?? 1; const stagePopulation = useMemo( () => gameMode === "manastorm" ? manastormStagePopulation(dungeon.entities, dungeon.staticSpawns, manastormEncounter) : { entities: dungeon.entities, staticSpawns: dungeon.staticSpawns }, [dungeon, gameMode, manastormEncounter], ); const roamingPacks = manastormStageRoamingPacks( dungeon.roamingPacks.filter((pack) => spawnMatchesDifficulty(pack.spawnMask, difficultyMask)), gameMode === "manastorm" ? manastormEncounter : null, ); const difficultySpawns = stagePopulation.staticSpawns.filter( (spawn) => spawnMatchesDifficulty(spawn.spawnMask, difficultyMask), ); const staticSpawns = activeDungeonId === "wailing-caverns" && gameMode === "dungeon" ? wailingStaticSpawnsForPhase(difficultySpawns, wailingPhase) : difficultySpawns; const paused = useGameStore((state) => state.paused); const mapOpen = useGameStore((state) => state.mapOpen); const companionOpen = useGameStore((state) => state.companionOpen); const gameplayFrameRate = useCombatStore((state) => state.settings.gameplayFrameRate); const sharedOnline = useOnlineGroupStore((state) => state.activeActivity !== null); const onlineAuthority = useOnlineSessionStore((state) => state.snapshot?.authority ?? true); const documentVisible = useDocumentVisibility(); const canvasRef = useRef(null); const [readySessionRevision, setReadySessionRevision] = useState(null); const [safeGraphics, setSafeGraphics] = useState(false); const [adaptiveDpr, setAdaptiveDpr] = useState(1.5); const [canvasRevision, setCanvasRevision] = useState(0); const [graphicsRecovery, setGraphicsRecovery] = useState<"lost" | "recovering" | null>(null); const worldReady = encounterWorldIsReady(readySessionRevision, sessionRevision); const simulationBlocked = graphicsRecovery !== null || !worldReady || (!documentVisible && !sharedOnline) || (!sharedOnline && (paused || mapOpen || companionOpen)); const frameloop = sceneFrameLoop({ worldReady, graphicsRecovering: graphicsRecovery !== null, sharedOnline, paused, mapOpen, companionOpen, documentVisible, frameRateLimit: gameplayFrameRate, }); const targetFps = numericFrameRateLimit(gameplayFrameRate); const allowedRuntimeIds = useMemo( () => gameMode === "manastorm" && manastormEncounter ? manastormChaoticLinkRuntimeIds(manastormEncounter, manastormPhase) : undefined, [gameMode, manastormEncounter, manastormPhase], ); const markWorldReady = useCallback(() => { setReadySessionRevision(sessionRevision); setGraphicsRecovery((state) => state === "recovering" ? null : state); }, [sessionRevision]); const restartRenderer = useCallback(() => { releasePointerLock(); useGameStore.getState().resetAtActiveSpawn(); setSafeGraphics(true); setReadySessionRevision(null); setGraphicsRecovery("recovering"); setCanvasRevision((revision) => revision + 1); }, []); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const handleContextLost = (rawEvent: Event) => { const event = rawEvent as WebGLContextEvent; console.error("Healer Man WebGL context lost", event.statusMessage); releasePointerLock(); setGraphicsRecovery("lost"); }; const handleContextRestored = () => { // Three reinitializes its renderer state and GPU resources on restore. // Keep the existing Rapier world so recovery does not move the player. console.info("Healer Man WebGL context restored"); setGraphicsRecovery(null); }; canvas.addEventListener("webglcontextlost", handleContextLost); canvas.addEventListener("webglcontextrestored", handleContextRestored); return () => { canvas.removeEventListener("webglcontextlost", handleContextLost); canvas.removeEventListener("webglcontextrestored", handleContextRestored); }; }, [canvasRevision]); useEffect(() => { useWailingEncounterStore.getState().reset(); }, [activeDungeonId, gameMode, sessionRevision]); useEffect(() => { setAdaptiveDpr(1.5); }, [activeDungeonId, sessionRevision]); const endMouseLook = useCallback(() => { endMouseLookDrag(); }, []); useEffect(() => { const handleMouseUp = (event: MouseEvent) => { if (isDesktopCameraLookPointer("mouse", event.button)) endMouseLook(); }; window.addEventListener("mouseup", handleMouseUp, true); window.addEventListener("blur", endMouseLook); return () => { window.removeEventListener("mouseup", handleMouseUp, true); window.removeEventListener("blur", endMouseLook); endMouseLook(); }; }, [endMouseLook]); const beginMouseLook = (event: ReactPointerEvent) => { if ( simulationBlocked || !isDesktopCameraLookPointer(event.pointerType, event.button) ) return; event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); beginMouseLookDrag(event.clientX, event.clientY); }; return (
event.preventDefault()} aria-label="Healer Man 3D dungeon view" data-frame-rate-limit={gameplayFrameRate} data-frame-loop={frameloop} > {frameloop === "never" && targetFps !== null && ( )} {!safeGraphics && worldReady && ( )} {import.meta.env.DEV && } {worldReady && ( <> )} {graphicsRecovery && (
event.stopPropagation()} >

Graphics renderer interrupted

{graphicsRecovery === "recovering" ? `Rebuilding ${stagePresentation.title}...` : "The 3D view stopped responding."}

{graphicsRecovery === "recovering" ? "Restarting with reduced graphics settings." : "Your interface is safe. If it does not recover automatically, restart at the entrance with reduced graphics."}

{graphicsRecovery === "lost" && ( )}
)}
); }