import type { Monster } from '@/lib/compendium/types'; import { asDamageType, type DamageDefenses, type DamageType } from '../types'; /** * Parse a free-text defense string like "cold, fire" or * "bludgeoning, piercing, and slashing from nonmagical attacks". * * We deliberately only enforce *clean* lists: a clause that carries a qualifier * ("from nonmagical attacks", "that is silvered", etc.) is dropped into `notes` * for the GM to read, never auto-applied — matching the "don't over-promise * automation" rule. Returns the clean damage types plus any leftover notes. */ function parseDamageList(raw: string | undefined): { types: DamageType[]; notes: string[] } { const types: DamageType[] = []; const notes: string[] = []; const text = (raw ?? '').trim(); if (!text) return { types, notes }; // Split on ';' first (distinct clauses), then commas/"and" within a clause. for (const clause of text.split(/;/)) { const c = clause.trim(); if (!c) continue; // A qualifier ("from", "that aren't", "except") means it's conditional — note it. if (/\b(from|that|except|unless|nonmagical|magical|silvered|adamantine)\b/i.test(c)) { notes.push(c); continue; } for (const word of c.split(/,|\band\b/)) { const t = asDamageType(word); if (t && !types.includes(t)) types.push(t); } } return { types, notes }; } function parseConditionList(raw: string | undefined): string[] { return (raw ?? '') .split(/,|\band\b|;/) .map((s) => s.trim().toLowerCase()) .filter(Boolean); } /** Normalize a pf2e damage/condition word ("non-magical attacks", "cold_iron"). */ function pf2eType(s: string): DamageType | undefined { return asDamageType(s.replace(/_/g, ' ').trim()); } /** pf2e creature flat-defense shape, ready to attach to a combatant. */ export interface Pf2eDefenses { immune: DamageType[]; conditionImmune: string[]; resistFlat: { type: string; amount: number }[]; weakness: { type: string; amount: number }[]; notes: string[]; } /** * pf2e creature → flat resistances/weaknesses + immunities. pf2e uses flat * amounts (resistance 5, weakness 5) rather than 5e's halve/double, and 'all' * applies to every type. Immunities mix damage types and conditions. */ export function normalizeMonsterDefensesPf2e(raw: { immunity?: unknown; resistance?: unknown; weakness?: unknown; }): Pf2eDefenses { const immune: DamageType[] = []; const conditionImmune: string[] = []; for (const i of Array.isArray(raw.immunity) ? (raw.immunity as string[]) : []) { const t = pf2eType(String(i)); if (t) immune.push(t); else conditionImmune.push(String(i).trim().toLowerCase()); } const flat = (obj: unknown): { type: string; amount: number }[] => { if (!obj || typeof obj !== 'object') return []; const out: { type: string; amount: number }[] = []; for (const [k, v] of Object.entries(obj as Record)) { const amount = Number(v); if (!Number.isFinite(amount) || amount <= 0) continue; const key = k.toLowerCase() === 'all' ? 'all' : pf2eType(k); if (key) out.push({ type: key, amount }); } return out; }; return { immune, conditionImmune, resistFlat: flat(raw.resistance), weakness: flat(raw.weakness), notes: [] }; } /** Open5e monster → structured damage/condition defenses. */ export function normalizeMonsterDefenses(raw: Monster): DamageDefenses { const resist = parseDamageList(raw.damage_resistances); const immune = parseDamageList(raw.damage_immunities); const vulnerable = parseDamageList(raw.damage_vulnerabilities); return { resist: resist.types, immune: immune.types, vulnerable: vulnerable.types, conditionImmune: parseConditionList(raw.condition_immunities), notes: [...resist.notes, ...immune.notes, ...vulnerable.notes], }; }