import { useCombatStore } from "./combatStore"; import { releaseDungeonGltfCache } from "./dungeonAssetCache"; import { clearDungeonRuntimeCaches } from "./dungeonRuntimeReset"; import { clearMobRuntimeRegistry } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry"; import { resetPartyRuntimeClock } from "./partyRuntime"; import { usePartyStore } from "./partyStore"; import { useGameStore, type BossTestSession, type GameMode } from "./store"; import { bossTestingSourceDungeonId, dungeonCanEnter, dungeonDefinitionById, type DungeonId } from "./dungeonRegistry"; import type { WorldSpawn } from "./dungeonTypes"; import type { GmReviewTarget } from "./gmContentReview"; import { shelvedDungeonSpawn } from "@shelvedDungeonSession"; import { activateDungeonEncounterDefinitions, exportDungeonEncounterSnapshots, importDungeonEncounterSnapshots, resetDungeonEncounters, } from "./dungeonEncounterRuntime"; import type { DungeonEncounterSnapshot } from "./dungeonEncounterTypes"; import { dungeonContentIsUsable } from "../content/contentManager"; import { useDungeonRunStore } from "./dungeonRunStore"; import type { DungeonRunProgress } from "./dungeonRunTypes"; import { validBossDifficulty } from "./dungeonDifficulty"; import { resetDungeonRouteRuntime } from "./dungeonRouteRuntime"; import { useDungeonSimulationStore } from "./dungeonSimulationStore"; import { dungeonRouteDefinition } from "./dungeonRoutes"; export interface DungeonSessionOptions { readonly bossDifficulty?: number; readonly gameMode?: GameMode; readonly spawn?: WorldSpawn; readonly gmTestMode?: boolean; readonly dungeonSimulation?: boolean; readonly bossTest?: BossTestSession | null; readonly gmReviewTarget?: GmReviewTarget | null; /** Used by in-game encounter transitions so character progression and inventory survive. */ readonly preserveCharacter?: boolean; /** Hydrates save-safe state after definitions have been activated and reset. */ readonly encounterSnapshots?: readonly DungeonEncounterSnapshot[]; readonly runProgress?: DungeonRunProgress; readonly legacyResume?: boolean; } /** * The single dungeon-switch boundary. It deliberately clears renderer-facing * registries before publishing the new active id, preventing stale positions, * targets, routes, or prepared animation templates from crossing instances. */ export function activateDungeonSession( dungeonId: string, difficultyId?: string, options: DungeonSessionOptions = {}, ): boolean { const definition = dungeonDefinitionById(dungeonId); const testingSource = bossTestingSourceDungeonId(dungeonId); const testing = Boolean(testingSource && options.gmTestMode && options.bossTest?.sourceDungeonId === testingSource); const dungeonSimulation = Boolean(options.dungeonSimulation && options.gmTestMode && dungeonRouteDefinition(dungeonId)); if (!definition || (!testing && !dungeonSimulation && !dungeonCanEnter(dungeonId)) || !dungeonContentIsUsable(testing ? testingSource! : dungeonId)) { return false; } if ((difficultyId !== undefined && !definition.difficultyVariants.some((variant) => variant.id === difficultyId)) || (options.bossDifficulty !== undefined && !validBossDifficulty(options.bossDifficulty))) return false; const previousDefinition = dungeonDefinitionById(useGameStore.getState().activeDungeonId); const bossDifficulty = options.bossDifficulty ?? options.runProgress?.bossDifficulty ?? 1; if (previousDefinition) releaseDungeonGltfCache(previousDefinition); if (!options.preserveCharacter) { useDungeonRunStore.getState().begin(dungeonId, difficultyId ?? definition.defaultDifficultyId, options.runProgress, options.legacyResume, bossDifficulty); } activateDungeonEncounterDefinitions( dungeonId, definition.encounters ?? [], difficultyId ?? definition.defaultDifficultyId, ); resetDungeonEncounters("dungeon", dungeonId); clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetDungeonRouteRuntime(); useDungeonSimulationStore.getState().leave(); clearDungeonRuntimeCaches(); resetPartyRuntimeClock(); if (options.preserveCharacter) { useCombatStore.getState().resetEncounter(); const combat = useCombatStore.getState(); useCombatStore.setState({ mobs: {}, selectedTargetId: null, activeCast: null, effects: [], comboPoints: 0, feedback: null, }); usePartyStore.getState().resetCombat(combat.level, combat.maxHealth); } else { useCombatStore.setState({ mobs: {}, selectedTargetId: null, activeCast: null, effects: [], comboPoints: 0, feedback: null, }); usePartyStore.setState({ seed: null, spectatorParty: false, members: [], command: "defend", selectedMemberId: null, selfSelected: false, routeBlocked: false, activeMobId: null, objectiveBossId: null, memberTargetIds: {}, }); } useGameStore.getState().activateDungeon(dungeonId as DungeonId, difficultyId, { gameMode: options.gameMode, spawn: shelvedDungeonSpawn(dungeonId, options.spawn), gmTestMode: options.gmTestMode, bossTest: options.bossTest, gmReviewTarget: options.gmReviewTarget, bossDifficulty, }); if (options.encounterSnapshots?.length) { importDungeonEncounterSnapshots(dungeonId, options.encounterSnapshots, { authoritative: true }); } useCombatStore.getState().setPlayerPosition(useGameStore.getState().playerPosition); return true; } /** * Rebuilds the current encounter after an asset/render failure without * discarding the selected character, party composition, or run progression. */ export function restartActiveDungeonSession(): boolean { const game = useGameStore.getState(); const encounterSnapshots = exportDungeonEncounterSnapshots(game.activeDungeonId); return activateDungeonSession(game.activeDungeonId, game.activeDifficultyId, { gameMode: game.gameMode, spawn: game.activeSpawn, gmTestMode: game.gmTestMode, bossTest: game.bossTest, gmReviewTarget: game.activeGmReviewTarget, preserveCharacter: true, bossDifficulty: game.activeBossDifficulty, encounterSnapshots, }); }