180 lines
7.9 KiB
TypeScript
180 lines
7.9 KiB
TypeScript
import { useCombatStore } from "./combatStore";
|
|
import type { BiteSizedRunSelection } from "../../shared/bite-sized-layouts.mjs";
|
|
import { preparedBiteSizedRunSelection } from "../app/biteSizedLayoutContent";
|
|
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,
|
|
prepareDungeonSessionDefinition,
|
|
legacyBiteSizedRunSelection,
|
|
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 biteSizedSelection?: BiteSizedRunSelection;
|
|
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;
|
|
/** Shared online activity id, or another stable fresh-run identity used by seeded variants. */
|
|
readonly runId?: string;
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
let 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));
|
|
const suppliedSelection = options.biteSizedSelection ?? options.runProgress?.biteSizedSelection
|
|
?? (options.preserveCharacter ? useDungeonRunStore.getState().progress?.biteSizedSelection : undefined)
|
|
?? (options.runId ? preparedBiteSizedRunSelection(dungeonId, options.runId) : undefined);
|
|
if (!definition || (!testing && !dungeonSimulation && !dungeonCanEnter(dungeonId))
|
|
|| !dungeonContentIsUsable(testing ? testingSource! : suppliedSelection?.spot.sourceDungeonId ?? 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, options.runId);
|
|
}
|
|
const runSeed = useDungeonRunStore.getState().progress?.id ?? options.runId ?? "catalog-template";
|
|
const legacySelection = options.runProgress && !options.runProgress.biteSizedSelection
|
|
? legacyBiteSizedRunSelection(dungeonId, runSeed) : undefined;
|
|
definition = prepareDungeonSessionDefinition(dungeonId, runSeed,
|
|
suppliedSelection ?? legacySelection ?? preparedBiteSizedRunSelection(dungeonId, runSeed),
|
|
difficultyId ?? definition.defaultDifficultyId) ?? definition;
|
|
const progress = useDungeonRunStore.getState().progress;
|
|
if (progress && definition.biteSizedSelection) useDungeonRunStore.setState({ progress: {
|
|
...progress, biteSizedSelection: definition.biteSizedSelection, ...(options.gmTestMode ? { timed: false } : {}),
|
|
} });
|
|
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,
|
|
showDungeonIntro: !options.preserveCharacter && !options.legacyResume
|
|
&& options.runProgress?.startedAt == null && (options.gameMode ?? "dungeon") === "dungeon"
|
|
&& !options.gmTestMode && !options.dungeonSimulation,
|
|
});
|
|
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,
|
|
biteSizedSelection: useDungeonRunStore.getState().progress?.biteSizedSelection,
|
|
});
|
|
}
|