import type { DungeonId } from "../game/dungeonRegistry"; import type { Frameloop } from "@react-three/fiber"; import type { GameplayFrameRateLimit } from "../game/frameRateSettings"; /** * Encounter transitions must never remount the Canvas: R3F intentionally loses * the old WebGL context during Canvas disposal. Only explicit graphics recovery * is allowed to replace the renderer. */ export function rendererCanvasKey(canvasRevision: number): string { return `renderer:${Math.max(0, Math.trunc(canvasRevision))}`; } /** The Rapier world and all encounter actors are rebuilt for every session. */ export function encounterWorldKey(dungeonId: DungeonId, sessionRevision: number): string { return `${dungeonId}:encounter:${Math.max(0, Math.trunc(sessionRevision))}`; } /** * Readiness belongs to one encounter world. Comparing revisions avoids a * child collision-ready effect being overwritten by a later parent reset in * the same React effect flush, and prevents stale worlds from unblocking the * current session. */ export function encounterWorldIsReady( readySessionRevision: number | null, sessionRevision: number, ): boolean { return readySessionRevision !== null && readySessionRevision === Math.max(0, Math.trunc(sessionRevision)); } /** * A paused local world only needs a frame when React or R3F invalidates it. * Loading, recovery, and shared-online worlds keep the continuous loop so * asynchronous scene setup and authoritative remote simulation can progress. */ export function sceneFrameLoop(options: { readonly worldReady: boolean; readonly graphicsRecovering: boolean; readonly sharedOnline: boolean; readonly paused: boolean; readonly mapOpen: boolean; readonly companionOpen: boolean; readonly documentVisible: boolean; readonly frameRateLimit: GameplayFrameRateLimit; }): Frameloop { if (!options.documentVisible && !options.sharedOnline) return "demand"; const locallyPaused = options.worldReady && !options.graphicsRecovering && !options.sharedOnline && (options.paused || options.mapOpen || options.companionOpen); if (locallyPaused) return "demand"; return options.frameRateLimit === "display" ? "always" : "never"; }