import type { Combatant, CreatureId, DifficultyThreshold, 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"; import { useRulesEditionContext } from "../contexts/rules-edition-context.js"; import { resolveSide } from "./use-difficulty.js"; export interface BreakdownCombatant { readonly combatant: Combatant; readonly cr: string | null; readonly xp: number | null; readonly source: string | null; readonly editable: boolean; readonly side: "party" | "enemy"; readonly level: number | undefined; } interface DifficultyBreakdown { readonly tier: DifficultyTier; readonly totalMonsterXp: number; readonly thresholds: readonly DifficultyThreshold[]; readonly encounterMultiplier: number | undefined; readonly adjustedXp: number | undefined; readonly partySizeAdjusted: boolean | undefined; readonly pcCount: number; readonly partyCombatants: readonly BreakdownCombatant[]; readonly enemyCombatants: readonly BreakdownCombatant[]; } export function useDifficultyBreakdown(): DifficultyBreakdown | null { const { encounter } = useEncounterContext(); const { characters } = usePlayerCharactersContext(); const { getCreature } = useBestiaryContext(); const { edition } = useRulesEditionContext(); return useMemo(() => { const { partyCombatants, enemyCombatants, descriptors, pcCount } = classifyCombatants(encounter.combatants, characters, getCreature); const hasPartyLevel = descriptors.some( (d) => d.side === "party" && d.level !== undefined, ); const hasCr = descriptors.some((d) => d.cr !== undefined); if (!hasPartyLevel || !hasCr) return null; const result = calculateEncounterDifficulty(descriptors, edition); return { ...result, pcCount, partyCombatants, enemyCombatants, }; }, [encounter.combatants, characters, getCreature, edition]); } type CreatureInfo = { cr?: string; source: string; sourceDisplayName: string; }; function buildBreakdownEntry( c: Combatant, side: "party" | "enemy", level: number | undefined, creature: CreatureInfo | undefined, ): BreakdownCombatant { if (c.playerCharacterId) { return { combatant: c, cr: null, xp: null, source: null, editable: false, side, level, }; } if (creature) { const cr = creature.cr ?? null; return { combatant: c, cr, xp: cr ? crToXp(cr) : null, source: creature.sourceDisplayName ?? creature.source, editable: false, side, level: undefined, }; } if (c.cr) { return { combatant: c, cr: c.cr, xp: crToXp(c.cr), source: null, editable: true, side, level: undefined, }; } return { combatant: c, cr: null, xp: null, source: null, editable: !c.creatureId, side, level: undefined, }; } function resolveLevel( c: Combatant, characters: readonly PlayerCharacter[], ): number | undefined { if (!c.playerCharacterId) return undefined; return characters.find((p) => p.id === c.playerCharacterId)?.level; } function resolveCr( c: Combatant, getCreature: (id: CreatureId) => CreatureInfo | undefined, ): { cr: string | null; creature: CreatureInfo | undefined } { const creature = c.creatureId ? getCreature(c.creatureId) : undefined; const cr = creature?.cr ?? c.cr ?? null; return { cr, creature }; } function classifyCombatants( combatants: readonly Combatant[], characters: readonly PlayerCharacter[], getCreature: (id: CreatureId) => CreatureInfo | undefined, ) { const partyCombatants: BreakdownCombatant[] = []; const enemyCombatants: BreakdownCombatant[] = []; const descriptors: { level?: number; cr?: string; side: "party" | "enemy"; }[] = []; let pcCount = 0; for (const c of combatants) { const side = resolveSide(c); const level = resolveLevel(c, characters); if (level !== undefined) pcCount++; const { cr, creature } = resolveCr(c, getCreature); if (level !== undefined || cr != null) { descriptors.push({ level, cr: cr ?? undefined, side }); } const entry = buildBreakdownEntry(c, side, level, creature); const target = side === "party" ? partyCombatants : enemyCombatants; target.push(entry); } return { partyCombatants, enemyCombatants, descriptors, pcCount }; }