From dd694477b26fdfead66730322b898fad161e477d Mon Sep 17 00:00:00 2001 From: Nils Briggen Date: Tue, 9 Jun 2026 11:29:36 +0200 Subject: [PATCH] Polish sprint 1/2: restore crash fix, pf2e parity, full glyph purge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crash fix: - restoreBackup (file + cloud pull) now validates each row through its Zod schema, backfilling new fields — an older backup no longer lands characters missing feats/concentration/etc and crashing the sheet. + test. pf2e parity (closing feature gaps vs 5e): - +9 missing classes (Animist, Exemplar, Gunslinger, Inventor, Kineticist, Magus, Psychic, Summoner, Thaumaturge) with data + class tips. - the wizard now enriches pf2e classes with their caster ability, so pf2e spellcasters get the Spells step + "Caster" tag like 5e. - editable Perception rank and spellcasting proficiency on the pf2e sheet (fixes wrong initiative / spell DC at higher levels); rank-10 spell slots. - pf2e monster resistances/weaknesses/immunities now apply in combat (flat amounts: resistance subtracts, weakness adds, 'all' matches any type). + tests. Glyph purge (finishing the emoji→Lucide migration): replaced ~40 leftover text-glyphs (✕ − + ✦ 🤫 ⬆ ⬇ ☑ ☐ ▶ ◀ ‹ › ★ ↻ ▸ ↑ ●) with Lucide icons across Modal (every dialog), the sheet sections, dice, settings, player panels, world pages, map editor, roll tray, and session UI. Gate: 293 unit + e2e green; tsc + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/ui/Modal.tsx | 3 +- src/components/ui/RollTray.tsx | 7 +-- src/features/assistant/SessionPrepCard.tsx | 2 +- src/features/characters/CharacterSheet.tsx | 17 ++++++ .../characters/builder/CreationWizard.tsx | 25 +++++++-- .../characters/sheet/AbilityGenModal.tsx | 3 +- .../characters/sheet/AttacksSection.tsx | 4 +- .../characters/sheet/DefensesSection.tsx | 8 +-- .../characters/sheet/FeatsSection.tsx | 3 +- .../characters/sheet/InventorySection.tsx | 4 +- .../characters/sheet/ResourcesSection.tsx | 8 +-- .../characters/sheet/SpellcastingSection.tsx | 29 +++++++--- src/features/combat/EncounterTracker.tsx | 15 ++++-- src/features/compendium/registry.tsx | 23 ++++---- src/features/dice/DicePage.tsx | 8 +-- src/features/play/HandoutControl.tsx | 4 +- src/features/play/PlayerConnection.tsx | 7 ++- src/features/play/SessionSidebar.tsx | 4 +- src/features/player/ActionGuide.tsx | 10 ++-- src/features/player/MyCharacterPanel.tsx | 16 +++--- src/features/player/PlayerBoards.tsx | 6 +-- src/features/settings/SettingsPage.tsx | 4 +- src/features/world/CalendarPage.tsx | 3 +- src/features/world/HomebrewPage.tsx | 4 +- src/features/world/NpcsPage.tsx | 4 +- src/features/world/QuestsPage.tsx | 6 ++- src/features/world/map/MapEditor.tsx | 8 +-- src/features/world/map/TokenPalette.tsx | 2 +- src/lib/assistant/builder.ts | 54 +++++++++++++++++++ src/lib/combat/engine.test.ts | 10 +++- src/lib/combat/engine.ts | 10 +++- src/lib/io/backup.test.ts | 24 +++++++++ src/lib/io/backup.ts | 23 +++++++- src/lib/mechanics/index.ts | 2 +- .../mechanics/normalize/monsterDefenses.ts | 43 +++++++++++++++ src/lib/mechanics/normalize/normalize.test.ts | 22 +++++++- src/lib/rules/pf2e/progression.ts | 9 ++++ src/lib/schemas/encounter.ts | 5 ++ 38 files changed, 355 insertions(+), 84 deletions(-) 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" /> - +