Phase 1: builder/UX quick wins (5e wording, no-campaign, details, ability UI)

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 00:56:26 +02:00
parent f1e15d4011
commit 11a9e87100
9 changed files with 124 additions and 29 deletions
+2 -2
View File
@@ -106,7 +106,7 @@ export const charactersRepo = {
},
async create(
campaignId: string,
campaignId: string | undefined,
draft: Pick<Character, 'name' | 'kind' | 'ancestry' | 'className' | 'level'> & {
system: Character['system'];
},
@@ -115,7 +115,7 @@ export const charactersRepo = {
const sys = getSystem(draft.system);
const character = characterSchema.parse({
id: newId(),
campaignId,
campaignId: campaignId ?? '',
system: draft.system,
kind: draft.kind,
name: draft.name,
+15 -2
View File
@@ -134,7 +134,8 @@ export type Defenses = z.infer<typeof defensesSchema>;
export const characterSchema = z.object({
id: z.string(),
campaignId: z.string(),
/** Owning campaign; '' = unassigned (PCs can be created without a campaign and attached later). */
campaignId: z.string().default(''),
system: systemIdSchema,
kind: characterKindSchema.default('pc'),
@@ -146,6 +147,14 @@ export const characterSchema = z.object({
/** class, free text for MVP */
className: z.string().max(80).default(''),
level: int.min(1).max(20).default(1),
/** background / heritage (promoted out of the notes string). */
background: z.string().max(80).default(''),
/** alignment, free text (e.g. "Chaotic Good", "Unaligned"). */
alignment: z.string().max(40).default(''),
/** physical description / looks. */
appearance: z.string().max(4000).default(''),
/** personality, behaviour, bonds, ideals, flaws. */
personality: z.string().max(4000).default(''),
abilities: abilityScoresSchema,
hp: hpSchema,
@@ -204,9 +213,13 @@ export type CharacterDraft = z.infer<typeof characterDraftSchema>;
/** Default values for the Phase-1 depth fields. Used by create + DB migration. */
export function characterDefaults(): Pick<
Character,
'currency' | 'inventory' | 'feats' | 'attacks' | 'resources' | 'spellcasting' | 'concentration' | 'equippedArmor' | 'defenses' | 'conditions'
'currency' | 'inventory' | 'feats' | 'attacks' | 'resources' | 'spellcasting' | 'concentration' | 'equippedArmor' | 'defenses' | 'conditions' | 'background' | 'alignment' | 'appearance' | 'personality'
> {
return {
background: '',
alignment: '',
appearance: '',
personality: '',
currency: { cp: 0, sp: 0, ep: 0, gp: 0, pp: 0 },
inventory: [],
feats: [],