import { useState } from 'react'; import { Sparkles, BrainCircuit } from 'lucide-react'; import { newId } from '@/lib/ids'; import { getSystem } from '@/lib/rules'; import type { AbilityKey, CharacterRulesInput, ProficiencyRank } from '@/lib/rules'; import { type SpellEntry, newSpellEntry } from '@/lib/schemas'; import { castSpell, dropConcentration } from '@/lib/mechanics'; import { formatModifier } from '@/lib/format'; import { Button } from '@/components/ui/Button'; import { Input, Select } from '@/components/ui/Input'; import { NumberField } from '@/components/ui/NumberField'; import { SheetSection, type SectionProps } from './common'; const CASTING_ABILITIES: { key: AbilityKey; label: string }[] = [ { key: 'int', label: 'Intelligence' }, { key: 'wis', label: 'Wisdom' }, { key: 'cha', label: 'Charisma' }, ]; export function SpellcastingSection({ c, update }: SectionProps) { const [spellName, setSpellName] = useState(''); const [spellLevel, setSpellLevel] = useState(0); const sys = getSystem(c.system); const sc = c.spellcasting; const ability = c.spellcastingAbility; const rulesInput: CharacterRulesInput = { level: c.level, abilities: c.abilities, ...(c.spellcastingRank ? { spellcastingRank: c.spellcastingRank as ProficiencyRank } : {}), }; const dc = ability ? sys.spellSaveDc?.(rulesInput, ability) : undefined; const atk = ability ? sys.spellAttackBonus(rulesInput, ability) : undefined; const setSlots = (slots: typeof sc.slots) => update({ spellcasting: { ...sc, slots } }); const patchSlot = (level: number, p: Partial<(typeof sc.slots)[number]>) => setSlots(sc.slots.map((s) => (s.level === level ? { ...s, ...p } : s))); const addSlotLevel = () => { const next = (sc.slots.at(-1)?.level ?? 0) + 1; if (next > 9) return; setSlots([...sc.slots, { level: next, max: 1, current: 1 }]); }; const addSpell = () => { if (spellName.trim() === '') return; const entry: SpellEntry = newSpellEntry({ id: newId(), name: spellName.trim(), level: spellLevel }); update({ spellcasting: { ...sc, spells: [...sc.spells, entry] } }); setSpellName(''); }; const patchSpell = (id: string, p: Partial) => update({ spellcasting: { ...sc, spells: sc.spells.map((s) => (s.id === id ? { ...s, ...p } : s)) } }); const removeSpell = (id: string) => update({ spellcasting: { ...sc, spells: sc.spells.filter((s) => s.id !== id) } }); // Casting a spell enforces slot consumption + concentration via the kernel. const [castMsg, setCastMsg] = useState<{ text: string; ok: boolean } | null>(null); const cast = (id: string) => { const res = castSpell(c, id); if (res.ok) { update(res.patch); setCastMsg({ text: res.log.join(' '), ok: true }); } else { setCastMsg({ text: res.reason, ok: false }); } }; const spellsByLevel = [...sc.spells].sort((a, b) => a.level - b.level || a.name.localeCompare(b.name)); return (
{ability && (
Spell DC {dc} Attack {atk !== undefined ? formatModifier(atk) : '—'}
)}
{/* Concentration banner — enforced: one spell at a time. */} {c.concentration && (
Concentrating on {c.concentration.spellName}
)} {castMsg && (

{castMsg.text}

)} {/* Spell slots */}
Spell slots
{sc.slots.length === 0 ? (

No spell slots configured.

) : (
{sc.slots.map((s) => (
Lv {s.level}
{s.current}/ patchSlot(s.level, { max: v, current: Math.min(v, s.current) })} aria-label={`Level ${s.level} max slots`} />
))}
)}
{/* Spell list */}
{spellsByLevel.length === 0 ? (

No spells added.

) : ( )}
); }