forked from phenom/healer-man
143 lines
5.6 KiB
TypeScript
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,
|
|
});
|
|
}
|