Files
healer-man/src/game/dungeonSession.ts
T
2026-09-14 13:20:20 -04:00

143 lines
5.6 KiB
TypeScript

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";
export interface DungeonSessionOptions {
readonly bossDifficulty?: number;
readonly gameMode?: GameMode;
readonly spawn?: WorldSpawn;
readonly gmTestMode?: 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);
if (!definition || (!testing && !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();
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,
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,
});
}