Phase 1: player character depth
- Inventory + currency + encumbrance vs carrying capacity - Class resources with correct short/long/daily rest recovery (applyRest) - Spellcasting: slots (+pact), known/prepared spells, derived DC + attack - Defenses: 5e death saves/inspiration/exhaustion, pf2e dying/wounded/hero points - Derived weapon attacks + passive perception via extended RulesSystem - Dexie v2 migration backfills new fields; debounced save flushes on pagehide - 9 new unit tests, new character-depth e2e; all green Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,11 @@ 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'];
|
||||
|
||||
@@ -28,23 +33,9 @@ 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>(character);
|
||||
const save = useDebouncedCallback((next: Character) => {
|
||||
void charactersRepo.update(next.id, {
|
||||
name: next.name,
|
||||
ancestry: next.ancestry,
|
||||
className: next.className,
|
||||
level: next.level,
|
||||
kind: next.kind,
|
||||
abilities: next.abilities,
|
||||
hp: next.hp,
|
||||
speed: next.speed,
|
||||
armorBonus: next.armorBonus,
|
||||
skillRanks: next.skillRanks,
|
||||
saveRanks: next.saveRanks,
|
||||
perceptionRank: next.perceptionRank,
|
||||
...(next.spellcastingAbility ? { spellcastingAbility: next.spellcastingAbility } : {}),
|
||||
...(next.spellcastingRank ? { spellcastingRank: next.spellcastingRank } : {}),
|
||||
notes: next.notes,
|
||||
});
|
||||
// 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<Character>) => {
|
||||
@@ -192,13 +183,28 @@ export function CharacterSheet({ character }: { character: Character }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Status & defenses (system-specific) */}
|
||||
<DefensesSection c={c} update={update} />
|
||||
|
||||
{/* Attacks + passive perception */}
|
||||
<AttacksSection c={c} update={update} />
|
||||
|
||||
{/* Spellcasting */}
|
||||
<SpellcastingSection c={c} update={update} />
|
||||
|
||||
{/* Class resources + rest */}
|
||||
<ResourcesSection c={c} update={update} />
|
||||
|
||||
{/* Inventory, currency, encumbrance */}
|
||||
<InventorySection c={c} update={update} />
|
||||
|
||||
{/* Notes */}
|
||||
<section className="mb-6">
|
||||
<SectionTitle>Notes</SectionTitle>
|
||||
<textarea
|
||||
value={c.notes}
|
||||
onChange={(e) => update({ notes: e.target.value })}
|
||||
placeholder="Backstory, bonds, inventory, reminders…"
|
||||
placeholder="Backstory, bonds, reminders…"
|
||||
className="min-h-32 w-full resize-y rounded-md border border-line bg-surface px-3 py-2 text-sm text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/60"
|
||||
/>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user