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:
@@ -0,0 +1,86 @@
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { NumberField } from '@/components/ui/NumberField';
|
||||
import { SheetSection, type SectionProps } from './common';
|
||||
|
||||
function Pips({ count, filled, onToggle, label }: { count: number; filled: number; onToggle: (n: number) => void; label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-1" role="group" aria-label={label}>
|
||||
{Array.from({ length: count }, (_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => onToggle(i + 1 === filled ? i : i + 1)}
|
||||
aria-label={`${label} ${i + 1}`}
|
||||
aria-pressed={i < filled}
|
||||
className={
|
||||
'h-5 w-5 rounded-full border ' +
|
||||
(i < filled ? 'border-accent bg-accent' : 'border-line bg-surface')
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DefensesSection({ c, update }: SectionProps) {
|
||||
const d = c.defenses;
|
||||
const setD = (p: Partial<typeof d>) => update({ defenses: { ...d, ...p } });
|
||||
|
||||
return (
|
||||
<SheetSection title="Status & Defenses">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{c.system === '5e' ? (
|
||||
<>
|
||||
<Field label="Death Saves">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="flex items-center gap-1 text-xs text-success">
|
||||
Successes
|
||||
<Pips count={3} filled={d.deathSaves.successes} label="Death save success" onToggle={(n) => setD({ deathSaves: { ...d.deathSaves, successes: n } })} />
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-xs text-danger">
|
||||
Failures
|
||||
<Pips count={3} filled={d.deathSaves.failures} label="Death save failure" onToggle={(n) => setD({ deathSaves: { ...d.deathSaves, failures: n } })} />
|
||||
</span>
|
||||
</div>
|
||||
</Field>
|
||||
<Field label="Inspiration">
|
||||
<Button size="sm" variant={d.inspiration ? 'primary' : 'secondary'} onClick={() => setD({ inspiration: !d.inspiration })}>
|
||||
{d.inspiration ? '★ Inspired' : 'Grant inspiration'}
|
||||
</Button>
|
||||
</Field>
|
||||
<Field label="Exhaustion (0–6)">
|
||||
<NumberField className="w-20" value={d.exhaustion} min={0} max={6} onChange={(v) => setD({ exhaustion: v })} aria-label="Exhaustion level" />
|
||||
</Field>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Field label="Dying (0–4)">
|
||||
<NumberField className="w-20" value={d.dying} min={0} max={4} onChange={(v) => setD({ dying: v })} aria-label="Dying value" />
|
||||
</Field>
|
||||
<Field label="Wounded">
|
||||
<NumberField className="w-20" value={d.wounded} min={0} onChange={(v) => setD({ wounded: v })} aria-label="Wounded value" />
|
||||
</Field>
|
||||
<Field label="Doomed">
|
||||
<NumberField className="w-20" value={d.doomed} min={0} onChange={(v) => setD({ doomed: v })} aria-label="Doomed value" />
|
||||
</Field>
|
||||
<Field label="Hero Points">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="icon" variant="ghost" onClick={() => setD({ heroPoints: Math.max(0, d.heroPoints - 1) })} aria-label="Spend hero point">−</Button>
|
||||
<span className="font-display text-xl font-semibold text-accent">{d.heroPoints}</span>
|
||||
<Button size="icon" variant="ghost" onClick={() => setD({ heroPoints: d.heroPoints + 1 })} aria-label="Gain hero point">+</Button>
|
||||
</div>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SheetSection>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="rounded-md border border-line bg-panel px-3 py-2">
|
||||
<div className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted">{label}</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user