From 11a9e87100d257b388b2d67adc820b810c5cdff4 Mon Sep 17 00:00:00 2001 From: Nils Briggen Date: Wed, 10 Jun 2026 00:56:26 +0200 Subject: [PATCH] Phase 1: builder/UX quick wins (5e wording, no-campaign, details, ability UI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5e skill/save proficiency shows 'Not proficient/Proficient/Expertise' instead of the PF2e rank names (Untrained/Trained/Expert); PF2e keeps its ranks. (shared rankLabel) - Characters can be created without a campaign: campaignId is optional ('' = unassigned), CharactersPage no longer requires an active campaign, the wizard's campaign prop is optional, and the sheet has a Campaign selector to attach/move later. - New character details: appearance, personality/behaviour, alignment, and a real background field (promoted out of notes); a Details wizard step + a Details sheet card. - Ability step shows a clearer total + 'base · +race' breakdown. - Point-buy verified correct (8:0..15:9 / 27, clamped 8-15, Next gated on budget). All schema additions default-safe (characterDefaults updated). 329 tests green, build OK. Co-Authored-By: Claude Opus 4.8 --- server/src/rooms.test.ts | 3 +- src/features/characters/CharacterSheet.tsx | 41 ++++++++++++--- src/features/characters/CharactersPage.tsx | 18 ++++--- .../characters/builder/CreationWizard.tsx | 51 ++++++++++++++++--- .../characters/sheet/AttacksSection.tsx | 3 +- src/features/characters/sheet/labels.ts | 14 +++++ src/lib/db/repositories.ts | 4 +- src/lib/schemas/character.ts | 17 ++++++- tsconfig.app.tsbuildinfo | 2 +- 9 files changed, 124 insertions(+), 29 deletions(-) create mode 100644 src/features/characters/sheet/labels.ts diff --git a/server/src/rooms.test.ts b/server/src/rooms.test.ts index 940d2bf..194e993 100644 --- a/server/src/rooms.test.ts +++ b/server/src/rooms.test.ts @@ -129,8 +129,7 @@ describe('RoomHub', () => { }); it('survives a player reconnect: the seat is reclaimed with the same stable playerId', () => { - let t = 0; - const hub = new RoomHub(() => t); + const hub = new RoomHub(() => 0); const gm = fake(); hub.host(gm); const hosted = lastOf(gm, 'hosted') as Extract; const PID = 'stable-player-1'; diff --git a/src/features/characters/CharacterSheet.tsx b/src/features/characters/CharacterSheet.tsx index 2e744c1..35067be 100644 --- a/src/features/characters/CharacterSheet.tsx +++ b/src/features/characters/CharacterSheet.tsx @@ -19,6 +19,8 @@ import { Badge, Meter } from '@/components/ui/Codex'; import { Input, Select } from '@/components/ui/Input'; import { NumberField } from '@/components/ui/NumberField'; import { formatModifier } from '@/lib/format'; +import { useCampaigns } from '@/features/campaigns/hooks'; +import { rankLabel } from './sheet/labels'; import { InventorySection } from './sheet/InventorySection'; import { AttacksSection } from './sheet/AttacksSection'; import { ResourcesSection } from './sheet/ResourcesSection'; @@ -32,17 +34,11 @@ 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 campaigns = useCampaigns(); const [levelUp, setLevelUp] = useState(false); const [genScores, setGenScores] = useState(false); const [shared, setShared] = useState(false); @@ -184,6 +180,12 @@ export function CharacterSheet({ character }: { character: Character }) { update({ speed })} /> + + + {/* Vital stats */} @@ -313,6 +315,29 @@ export function CharacterSheet({ character }: { character: Character }) { {/* Inventory, currency, encumbrance */} + {/* Details */} +
+ Details +
+ + update({ background: e.target.value })} placeholder="Acolyte, Soldier…" /> + + + update({ alignment: e.target.value })} placeholder="Chaotic Good, Unaligned…" /> + + +