import { useState } from 'react'; import { Link } from '@tanstack/react-router'; import type { Character } from '@/lib/schemas'; import type { AbilityKey, CharacterRulesInput, ProficiencyRank, SystemId } from '@/lib/rules'; import { getSystem, ABILITY_ABBR } from '@/lib/rules'; import { charactersRepo } from '@/lib/db/repositories'; import { PF2E_SAVES } from '@/lib/rules/pf2e/skills'; import { useDebouncedCallback } from '@/lib/useDebouncedCallback'; import { rollCheck } from '@/lib/useRoll'; import { Page } from '@/components/ui/Page'; import { Button } from '@/components/ui/Button'; import { Input, Select } from '@/components/ui/Input'; import { NumberField } from '@/components/ui/NumberField'; import { formatModifier } from '@/lib/format'; import { InventorySection } from './sheet/InventorySection'; import { AttacksSection } from './sheet/AttacksSection'; import { ResourcesSection } from './sheet/ResourcesSection'; import { SpellcastingSection } from './sheet/SpellcastingSection'; import { DefensesSection } from './sheet/DefensesSection'; const ABILITIES: AbilityKey[] = ['str', 'dex', 'con', 'int', 'wis', 'cha']; const RANKS_5E: ProficiencyRank[] = ['untrained', 'trained', 'expert']; const RANKS_PF2E: ProficiencyRank[] = ['untrained', 'trained', 'expert', 'master', 'legendary']; const RANK_LABEL: Record = { untrained: 'Untrained', trained: 'Trained', expert: 'Expert', master: 'Master', legendary: 'Legendary', }; export function CharacterSheet({ character }: { character: Character }) { // Local editable copy; write-through to the DB on change (debounced + flush on unmount). const [c, setC] = useState(character); const save = useDebouncedCallback((next: Character) => { // Persist everything except identity/timestamps (update() stamps updatedAt). const { id, campaignId: _campaignId, createdAt: _createdAt, updatedAt: _updatedAt, ...rest } = next; void charactersRepo.update(id, rest); }, 350); const update = (patch: Partial) => { setC((prev) => { const next = { ...prev, ...patch }; save(next); return next; }); }; const sys = getSystem(c.system); const rulesInput: CharacterRulesInput = { level: c.level, abilities: c.abilities, skillRanks: c.skillRanks as Record, saveRanks: c.saveRanks as Partial>, armorBonus: c.armorBonus, perceptionRank: c.perceptionRank, ...(c.spellcastingRank ? { spellcastingRank: c.spellcastingRank } : {}), }; const ac = sys.baseArmorClass(rulesInput); const initiative = sys.initiativeModifier(rulesInput); const skills = sys.skillModifiers(rulesInput); const saves = sys.saveModifiers(rulesInput); const profLabel = c.system === '5e' ? `+${sys.proficiencyValue(c.level, 'trained')} prof` : `level ${c.level}`; const ranks = c.system === 'pf2e' ? RANKS_PF2E : RANKS_5E; return (
← Back to characters
{/* Header */}
update({ name: e.target.value })} /> {c.kind === 'pc' ? 'PC' : 'NPC'} · {sys.label}
{profLabel}
update({ ancestry: e.target.value })} /> update({ className: e.target.value })} /> update({ level })} /> update({ speed })} />
{/* Vital stats */}
Armor/shield bonus update({ armorBonus })} aria-label="Armor bonus" />
{/* Abilities */}
Ability Scores
{ABILITIES.map((a) => { const mod = sys.abilityModifier(c.abilities[a]); return (
{ABILITY_ABBR[a]}
{formatModifier(mod)}
update({ abilities: { ...c.abilities, [a]: v } })} />
); })}
{/* Saves */}
Saving Throws
{c.system === 'pf2e' ? PF2E_SAVES.map((s) => ( update({ saveRanks: { ...c.saveRanks, [s.ability]: rank } })} system={c.system} /> )) : ABILITIES.map((a) => ( update({ saveRanks: { ...c.saveRanks, [a]: rank } })} system={c.system} /> ))}
{/* Skills */}
Skills
{skills.map((s) => ( update({ skillRanks: { ...c.skillRanks, [s.key]: rank } })} system={c.system} /> ))}
{/* Status & defenses (system-specific) */} {/* Attacks + passive perception */} {/* Spellcasting */} {/* Class resources + rest */} {/* Inventory, currency, encumbrance */} {/* Notes */}
Notes