Feature 02: Character Creation
Overview
Players create characters by choosing a species, class, background, and distributing attribute points. Each user can have up to 2 characters for free (up to 6 with purchased slots). Characters are the core identity — all subsequent features operate on a selected character.
Dependencies
- Feature 01 (Authentication)
Technical Tasks
1. Static Game Data — Species, Classes, Backgrounds
- Create
data/species/directory with TOML files defining all 9 species:- Each species: name, description, attribute_bonuses (e.g.,
{ might: 2, fortitude: 1 }), species_skill (name, description, passive effect) - Species: Ironborn, Verdani, Kharren, Sylphari, Thornkin, Ashenmere, Glimkin, Vexari, Ferathi
- Each species: name, description, attribute_bonuses (e.g.,
- Create
data/classes/directory with TOML files defining all 9 classes:- Each class: name, description, primary_resource (stamina/mana/fury/etc.), starting_skills (list of skill IDs), subclasses (3 per class, with name + description — details deferred to Feature 04)
- Classes: Vanguard, Shade, Arcanist, Warden, Pathfinder, Berserker, Songweaver, Oathblade, Hexbinder
- Create
data/backgrounds/directory with TOML files defining all 10 backgrounds:- Each background: name, description, attribute_bonus (+1 to one attribute), passive_perk (name, effect description)
- Create
crates/game/src/data.rs:- Load functions:
load_species(),load_classes(),load_backgrounds() - Use
once_cell::sync::Lazyfor global static access - Validate data on load (no missing references, attribute bonuses sum correctly)
- Load functions:
2. Type Definitions (crates/types)
- Create
src/character.rs:Speciesenum (9 variants) with Serialize/Deserialize/sqlx::TypeClassenum (9 variants)Backgroundenum (10 variants)Attributesstruct:{ might, logic, speed, presence, fortitude, luck }— allu8Characterstruct with all fields matching the DB schema
- Create
src/ids.rs:CharacterId(Uuid)newtype with Serialize/Deserialize/FromRow supportUserId(Uuid)newtype
3. Characters Table Migration
- Create migration
0003_create_characters.sql:- Full characters table with all columns from the technical architecture schema
- Indexes on
user_idandname - Check constraint: attributes between 1 and 99
- Unique constraint on
name
4. Character Model & Queries (crates/db)
- Create
src/models/character.rs—Characterstruct with FromRow - Create
src/queries/characters.rs:create_character(pool, params) -> Characterfind_characters_by_user(pool, user_id) -> Vec<Character>find_character_by_id(pool, id) -> Option<Character>find_character_by_name(pool, name) -> Option<Character>delete_character(pool, id)count_characters_by_user(pool, user_id) -> i64
5. Character Creation Validation (crates/game)
- Create
src/character_creation.rs:validate_attributes(attributes: &Attributes, species: Species) -> Result<()>:- Base points: each attribute starts at 5, player distributes 40 points
- Range: each base attribute 5–15 (before species bonuses)
- Total distributed must equal 40
- After species bonuses, final values must be 1–99
validate_name(name: &str) -> Result<()>:- Length: 2–24 characters
- Allowed: alphanumeric + spaces + hyphens + apostrophes
- No leading/trailing whitespace
- No consecutive spaces
- Profanity filter (basic word list — can be expanded later)
calculate_starting_attributes(base: &Attributes, species: Species, background: Background) -> Attributes:- Apply species bonuses
- Apply background bonus (+1 to one attribute)
get_starting_equipment(class: Class) -> Vec<String>:- Return list of item template IDs for starting gear (deferred to Feature 05, return empty for now)
6. Character Routes (crates/api)
- Create
src/routes/characters.rs:GET /api/characters:- Auth required
- Return all characters owned by the authenticated user
- Include: id, name, species, class, level, gold (summary view)
POST /api/characters:- Auth required
- Body:
{ name, species, class, background, attributes: { might, logic, speed, presence, fortitude, luck } } - Validate user hasn’t exceeded character slot limit
- Validate name uniqueness
- Validate name format
- Validate attribute distribution
- Calculate final attributes (base + species + background bonuses)
- Create character record
- Return full character object
GET /api/characters/:id:- Auth required
- Validate character belongs to authenticated user
- Return full character details including all attributes, skills, gold, level, etc.
DELETE /api/characters/:id:- Auth required
- Validate character belongs to authenticated user
- Soft delete or hard delete (hard delete for now — cascade will handle related data)
- Return 204
7. Character Ownership Middleware
- Create
src/middleware/character.rs:- Axum extractor
CharacterOwner(Character):- Reads
:idpath parameter - Loads character from DB
- Verifies
character.user_id == auth_user.user_id - Returns 404 if character not found or not owned by user
- Reads
- This extractor will be reused by every endpoint that operates on a character
- Axum extractor
8. Client — Character Selection & Creation
- Create
routes/(game)/+page.svelte:- Character selection screen (list of user’s characters)
- “Create New Character” button (disabled if at slot limit)
- Selecting a character sets it as active in the Svelte store
- Create
routes/(game)/create-character/+page.svelte:- Step 1: Choose species (cards showing name, description, bonuses, species skill)
- Step 2: Choose class (cards showing name, description, resource type, starting skills)
- Step 3: Choose background (cards showing name, bonus, perk)
- Step 4: Distribute attributes (point-buy UI with +/- buttons, live preview of final stats with bonuses)
- Step 5: Choose name (text input with validation feedback)
- Step 6: Review & confirm
- Create
lib/types/character.ts— TypeScript types matching the Rust character types - Create
lib/api/queries/characters.ts— TanStack Query hooks for character CRUD
Tests
Unit Tests
validate_attributes: accepts valid 40-point distributionvalidate_attributes: rejects total != 40validate_attributes: rejects individual attribute < 5 or > 15validate_attributes: correctly applies species bonusesvalidate_name: accepts “Aldric the Bold”validate_name: rejects empty stringvalidate_name: rejects names > 24 charsvalidate_name: rejects names with special characters (e.g.,<script>)validate_name: rejects names with consecutive spacescalculate_starting_attributes: correctly sums base + species + backgroundcalculate_starting_attributes: Ironborn gets +2 Fortitude, +1 Might- Game data loading: all 9 species load without error
- Game data loading: all 9 classes load without error
- Game data loading: all 10 backgrounds load without error
Integration Tests
POST /api/characterswith valid data → 201, character created with correct attributesPOST /api/characterswith duplicate name → 409POST /api/characterswhen at slot limit → 403 with clear error messagePOST /api/characterswith invalid attribute total → 422POST /api/characterswith invalid species → 422GET /api/charactersreturns only characters owned by authenticated userGET /api/characters/:idfor own character → 200GET /api/characters/:idfor another user’s character → 404DELETE /api/characters/:idfor own character → 204, character goneDELETE /api/characters/:idfor another user’s character → 404- Character count correctly limits creation per user