import type { DifficultyTier } from "@initiative/domain"; import { ArrowLeftRight } from "lucide-react"; import { useRef } from "react"; import { useEncounterContext } from "../contexts/encounter-context.js"; import { useClickOutside } from "../hooks/use-click-outside.js"; import { type BreakdownCombatant, useDifficultyBreakdown, } from "../hooks/use-difficulty-breakdown.js"; import { CrPicker } from "./cr-picker.js"; import { Button } from "./ui/button.js"; const TIER_LABELS: Record = { trivial: { label: "Trivial", color: "text-muted-foreground" }, low: { label: "Low", color: "text-green-500" }, moderate: { label: "Moderate", color: "text-yellow-500" }, high: { label: "High", color: "text-red-500" }, }; function formatXp(xp: number): string { return xp.toLocaleString(); } function PcRow({ entry }: { entry: BreakdownCombatant }) { return (
{entry.combatant.name} {entry.level === undefined ? "\u2014" : `Lv ${entry.level}`} {"\u2014"}
); } function NpcRow({ entry, onToggleSide, }: { entry: BreakdownCombatant; onToggleSide: () => void; }) { const { setCr } = useEncounterContext(); const isParty = entry.side === "party"; const targetSide = isParty ? "enemy" : "party"; let xpDisplay: string; if (entry.xp == null) { xpDisplay = "\u2014"; } else if (isParty && entry.cr) { xpDisplay = `\u2212${formatXp(entry.xp)}`; } else { xpDisplay = formatXp(entry.xp); } return (
{entry.combatant.name} {entry.editable ? ( setCr(entry.combatant.id, cr)} /> ) : ( {entry.cr ? `CR ${entry.cr}` : "\u2014"} )} {xpDisplay}
); } export function DifficultyBreakdownPanel({ onClose }: { onClose: () => void }) { const ref = useRef(null); useClickOutside(ref, onClose); const { setSide } = useEncounterContext(); const breakdown = useDifficultyBreakdown(); if (!breakdown) return null; const tierConfig = TIER_LABELS[breakdown.tier]; const handleToggle = (entry: BreakdownCombatant) => { const newSide = entry.side === "party" ? "enemy" : "party"; setSide(entry.combatant.id, newSide); }; const isPC = (entry: BreakdownCombatant) => entry.combatant.playerCharacterId != null; return (
Encounter Difficulty:{" "} {tierConfig.label}
Party Budget ({breakdown.pcCount}{" "} {breakdown.pcCount === 1 ? "PC" : "PCs"})
Low: {formatXp(breakdown.partyBudget.low)} Mod: {formatXp(breakdown.partyBudget.moderate)} High: {formatXp(breakdown.partyBudget.high)}
Allied NPC XP is subtracted from encounter difficulty
Party XP
{breakdown.partyCombatants.map((entry) => isPC(entry) ? ( ) : ( handleToggle(entry)} /> ), )}
Enemy XP
{breakdown.enemyCombatants.map((entry) => isPC(entry) ? ( ) : ( handleToggle(entry)} /> ), )}
Net Monster XP {formatXp(breakdown.totalMonsterXp)}
); }