Files
healer-man/src/game/dungeonRunStore.ts
T
2026-09-27 21:48:00 -04:00

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] } : {}) } });
},
}));