72 lines
4.1 KiB
TypeScript
72 lines
4.1 KiB
TypeScript
import { create } from "zustand";
|
|
import { dungeonDefinitionById } from "./dungeonRegistry";
|
|
import type { DungeonDefinition } from "./dungeonTypes";
|
|
import type { DungeonRunMember, DungeonRunProgress, DungeonRunRecord } from "./dungeonRunTypes";
|
|
|
|
export function dungeonBossesForDifficulty(definition: DungeonDefinition, difficultyId = definition.defaultDifficultyId) {
|
|
const difficulty = definition.difficultyVariants.find((variant) => variant.id === difficultyId);
|
|
return definition.bosses.filter((boss) => {
|
|
const spawn = definition.staticSpawns.find((entry) => entry.id === boss.id);
|
|
return !spawn || (((spawn.spawnMask ?? 1) & (difficulty?.spawnMask ?? 1)) !== 0);
|
|
});
|
|
}
|
|
|
|
export function finalDungeonBoss(definition: DungeonDefinition, difficultyId = definition.defaultDifficultyId) {
|
|
const bosses = dungeonBossesForDifficulty(definition, difficultyId);
|
|
const bossIds = new Set(bosses.map((boss) => boss.id));
|
|
return definition.objectiveOrder.filter((objective) => !objective.optional && bossIds.has(objective.id)).at(-1)
|
|
?? bosses.filter((boss) => !boss.optional).at(-1);
|
|
}
|
|
|
|
export function completedDungeonRun(progress: DungeonRunProgress): DungeonRunRecord | null {
|
|
const definition = dungeonDefinitionById(progress.dungeonId);
|
|
if (!definition || !progress.timed || progress.finishedAt === null || progress.durationMs === null) return null;
|
|
return { id: progress.id, dungeonId: progress.dungeonId, difficultyId: progress.difficultyId,
|
|
...(progress.biteSizedSelection ? { biteSizedSelection: progress.biteSizedSelection } : {}),
|
|
bossDifficulty: progress.bossDifficulty,
|
|
finishedAt: progress.finishedAt, durationMs: progress.durationMs, killedBossIds: progress.killedBossIds,
|
|
party: progress.party, fullClear: dungeonBossesForDifficulty(definition, progress.difficultyId)
|
|
.every((boss) => progress.killedBossIds.includes(boss.id)) };
|
|
}
|
|
|
|
interface DungeonRunState {
|
|
readonly progress: DungeonRunProgress | null;
|
|
begin: (dungeonId: string, difficultyId: string, resume?: DungeonRunProgress, legacyResume?: boolean,
|
|
bossDifficulty?: number, runId?: string) => void;
|
|
start: (now: number, party: readonly DungeonRunMember[]) => void;
|
|
advance: (completedObjectiveIds: readonly string[], deadBossIds: readonly string[], now: number,
|
|
party: readonly DungeonRunMember[]) => void;
|
|
}
|
|
|
|
export const useDungeonRunStore = create<DungeonRunState>((set, get) => ({
|
|
progress: null,
|
|
begin: (dungeonId, difficultyId, resume, legacyResume = false, bossDifficulty = 1, runId) => set({
|
|
progress: resume?.dungeonId === dungeonId && resume.difficultyId === difficultyId
|
|
&& resume.bossDifficulty === bossDifficulty ? resume : {
|
|
id: runId ?? globalThis.crypto.randomUUID(), dungeonId, difficultyId, startedAt: null,
|
|
finishedAt: null, durationMs: null, killedBossIds: [], party: [], timed: !legacyResume, bossDifficulty,
|
|
},
|
|
}),
|
|
start: (now, party) => {
|
|
const progress = get().progress;
|
|
if (!progress || progress.startedAt !== null || party.length === 0) return;
|
|
set({ progress: { ...progress, startedAt: now, party: [...party] } });
|
|
},
|
|
advance: (completedObjectiveIds, deadBossIds, now, party) => {
|
|
const progress = get().progress;
|
|
if (!progress || progress.startedAt === null || progress.finishedAt !== null) return;
|
|
const definition = dungeonDefinitionById(progress.dungeonId);
|
|
if (!definition) return;
|
|
const bossIds = new Set(definition.bosses.map((boss) => boss.id));
|
|
const killedBossIds = [...new Set([...progress.killedBossIds, ...completedObjectiveIds, ...deadBossIds])]
|
|
.filter((id) => bossIds.has(id));
|
|
const finalBoss = finalDungeonBoss(definition, progress.difficultyId);
|
|
// A scripted final boss must award its objective; despawns and raw corpse states cannot complete it.
|
|
const finished = finalBoss !== undefined && completedObjectiveIds.includes(finalBoss.id);
|
|
if (!finished && killedBossIds.length === progress.killedBossIds.length) return;
|
|
set({ progress: { ...progress, killedBossIds,
|
|
...(finished ? { finishedAt: Math.max(now, progress.startedAt), durationMs: Math.max(1, Math.round(now - progress.startedAt)),
|
|
party: [...party] } : {}) } });
|
|
},
|
|
}));
|