diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 221b512..4c40ced 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, type ReactNode } from 'react'; +import { X } from 'lucide-react'; import { cn } from '@/lib/cn'; import { Button } from './Button'; @@ -82,7 +83,7 @@ export function Modal({ open, onClose, title, children, footer, className }: Mod

{title}

{children}
diff --git a/src/components/ui/RollTray.tsx b/src/components/ui/RollTray.tsx index 661b84b..6df711e 100644 --- a/src/components/ui/RollTray.tsx +++ b/src/components/ui/RollTray.tsx @@ -1,3 +1,4 @@ +import { Sparkles, Skull, X } from 'lucide-react'; import { useRollStore, type TrayRoll } from '@/stores/rollStore'; import { DEGREE_COLOR, DEGREE_LABEL } from '@/lib/dice/check'; import { naturalD20 } from '@/lib/dice/notation'; @@ -35,8 +36,8 @@ export function RollTray() {
{last.label &&
{last.label}
} - {crit === 'crit' &&
✦ Critical ✦
} - {crit === 'fumble' &&
✦ Fumble ✦
} + {crit === 'crit' &&
Critical
} + {crit === 'fumble' &&
Fumble
} {last.degree && (
{DEGREE_LABEL[last.degree]} @@ -44,7 +45,7 @@ export function RollTray() {
)}
- +
{last.result.total}
{last.result.breakdown}
diff --git a/src/features/assistant/SessionPrepCard.tsx b/src/features/assistant/SessionPrepCard.tsx index 7256b0b..3cdcc99 100644 --- a/src/features/assistant/SessionPrepCard.tsx +++ b/src/features/assistant/SessionPrepCard.tsx @@ -49,7 +49,7 @@ export function SessionPrepCard({ campaign, characters, notes, quests }: Props) Generate three distinct session opening hooks grounded in your campaign's current threads.

{message &&

{message}

} diff --git a/src/features/characters/CharacterSheet.tsx b/src/features/characters/CharacterSheet.tsx index be5455f..58bb631 100644 --- a/src/features/characters/CharacterSheet.tsx +++ b/src/features/characters/CharacterSheet.tsx @@ -229,6 +229,23 @@ export function CharacterSheet({ character }: { character: Character }) { + {/* Perception (pf2e core proficiency — advances to legendary; drives initiative) */} + {c.system === 'pf2e' && ( +
+ Perception +
+ update({ perceptionRank: rank })} + system={c.system} + /> +
+
+ )} + {/* Saves */}
Saving Throws diff --git a/src/features/characters/builder/CreationWizard.tsx b/src/features/characters/builder/CreationWizard.tsx index 60e9f6a..4b8749f 100644 --- a/src/features/characters/builder/CreationWizard.tsx +++ b/src/features/characters/builder/CreationWizard.tsx @@ -1,10 +1,10 @@ import { useEffect, useMemo, useState } from 'react'; import { useNavigate } from '@tanstack/react-router'; -import { Lightbulb } from 'lucide-react'; +import { Lightbulb, RotateCcw, Star } from 'lucide-react'; import { type Campaign, type Character, type SpellEntry, newSpellEntry } from '@/lib/schemas'; import { charactersRepo } from '@/lib/db/repositories'; import { - getSystem, ABILITY_ABBR, abilityModifier, buildCharacter, SYSTEM_OPTIONS, + getSystem, ABILITY_ABBR, abilityModifier, buildCharacter, getClassDef, SYSTEM_OPTIONS, type AbilityKey, type AbilityScores, type ProficiencyRank, type SystemId, } from '@/lib/rules'; import { STANDARD_ARRAY, rollAbilityScores, pointBuyRemaining, POINT_BUY_MIN, POINT_BUY_MAX } from '@/lib/rules/abilityGen'; @@ -65,7 +65,22 @@ export function CreationWizard({ campaign, onClose }: { campaign: Campaign; onCl const [backgrounds, setBackgrounds] = useState([]); const [allSpells, setAllSpells] = useState([]); - useEffect(() => { let on = true; void loadClasses(system).then((c) => on && setClasses([...c].sort((a, b) => a.name.localeCompare(b.name)))); return () => { on = false; }; }, [system]); + useEffect(() => { + let on = true; + void loadClasses(system).then((c) => { + if (!on) return; + // The pf2e class data loads with caster:'none'; merge the curated caster + // ability so pf2e spellcasters get the Spells step + "Caster" tag like 5e. + const enriched = system === 'pf2e' + ? c.map((rc) => { + const def = getClassDef('pf2e', rc.name); + return def && def.caster !== 'none' && def.spellAbility ? { ...rc, caster: def.spellAbility } : rc; + }) + : c; + setClasses([...enriched].sort((a, b) => a.name.localeCompare(b.name))); + }); + return () => { on = false; }; + }, [system]); useEffect(() => { let on = true; if (system === '5e') { @@ -381,7 +396,7 @@ export function CreationWizard({ campaign, onClose }: { campaign: Campaign; onCl ))} - {method === 'roll' && } + {method === 'roll' && } {method === 'pointbuy' &&

Points remaining: {pointBuyRemaining(pb)} / 27

} {selectedClass && (() => { const tip = getClassTip(system, selectedClass.slug); @@ -402,7 +417,7 @@ export function CreationWizard({ campaign, onClose }: { campaign: Campaign; onCl const isKey = selectedClass?.keyAbilities.includes(a); return (
-
{ABILITY_ABBR[a]}{isKey ? ' ★' : ''}
+
{ABILITY_ABBR[a]}{isKey && }
{usesPool ? ( patch(f.id, { name: e.target.value })} aria-label="Feat name" /> - +