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:
@@ -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,
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user