import type { Combatant, CreatureId, DifficultyTier, PlayerCharacter, } from "@initiative/domain"; import { calculateEncounterDifficulty, crToXp } from "@initiative/domain"; import { useMemo } from "react"; import { useBestiaryContext } from "../contexts/bestiary-context.js"; import { useEncounterContext } from "../contexts/encounter-context.js"; import { usePlayerCharactersContext } from "../contexts/player-characters-context.js"; export interface BreakdownCombatant { readonly combatant: Combatant; readonly cr: string | null; readonly xp: number | null; readonly source: string | null; readonly editable: boolean; } interface DifficultyBreakdown { readonly tier: DifficultyTier; readonly totalMonsterXp: number; readonly partyBudget: { readonly low: number; readonly moderate: number; readonly high: number; }; readonly pcCount: number; readonly combatants: readonly BreakdownCombatant[]; } export function useDifficultyBreakdown(): DifficultyBreakdown | null { const { encounter } = useEncounterContext(); const { characters } = usePlayerCharactersContext(); const { getCreature } = useBestiaryContext(); return useMemo(() => { const partyLevels = derivePartyLevels(encounter.combatants, characters); const { entries, crs } = classifyCombatants( encounter.combatants, getCreature, ); if (partyLevels.length === 0 || crs.length === 0) { return null; } const result = calculateEncounterDifficulty(partyLevels, crs); return { ...result, pcCount: partyLevels.length, combatants: entries, }; }, [encounter.combatants, characters, getCreature]); } function classifyBestiaryCombatant( c: Combatant, getCreature: ( id: CreatureId, ) => { cr: string; source: string; sourceDisplayName: string } | undefined, ): { entry: BreakdownCombatant; cr: string | null } { const creature = c.creatureId ? getCreature(c.creatureId) : undefined; if (creature) { return { entry: { combatant: c, cr: creature.cr, xp: crToXp(creature.cr), source: creature.sourceDisplayName ?? creature.source, editable: false, }, cr: creature.cr, }; } return { entry: { combatant: c, cr: null, xp: null, source: null, editable: false, }, cr: null, }; } function classifyCombatants( combatants: readonly Combatant[], getCreature: ( id: CreatureId, ) => { cr: string; source: string; sourceDisplayName: string } | undefined, ): { entries: BreakdownCombatant[]; crs: string[] } { const entries: BreakdownCombatant[] = []; const crs: string[] = []; for (const c of combatants) { if (c.playerCharacterId) continue; if (c.creatureId) { const { entry, cr } = classifyBestiaryCombatant(c, getCreature); entries.push(entry); if (cr) crs.push(cr); } else if (c.cr) { crs.push(c.cr); entries.push({ combatant: c, cr: c.cr, xp: crToXp(c.cr), source: null, editable: true, }); } else { entries.push({ combatant: c, cr: null, xp: null, source: null, editable: true, }); } } return { entries, crs }; } function derivePartyLevels( combatants: readonly Combatant[], characters: readonly PlayerCharacter[], ): number[] { const levels: number[] = []; for (const c of combatants) { if (!c.playerCharacterId) continue; const pc = characters.find((p) => p.id === c.playerCharacterId); if (pc?.level !== undefined) levels.push(pc.level); } return levels; }