Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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
  • 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::Lazy for global static access
    • Validate data on load (no missing references, attribute bonuses sum correctly)

2. Type Definitions (crates/types)

  • Create src/character.rs:
    • Species enum (9 variants) with Serialize/Deserialize/sqlx::Type
    • Class enum (9 variants)
    • Background enum (10 variants)
    • Attributes struct: { might, logic, speed, presence, fortitude, luck } — all u8
    • Character struct with all fields matching the DB schema
  • Create src/ids.rs:
    • CharacterId(Uuid) newtype with Serialize/Deserialize/FromRow support
    • UserId(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_id and name
    • Check constraint: attributes between 1 and 99
    • Unique constraint on name

4. Character Model & Queries (crates/db)

  • Create src/models/character.rsCharacter struct with FromRow
  • Create src/queries/characters.rs:
    • create_character(pool, params) -> Character
    • find_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 :id path parameter
      • Loads character from DB
      • Verifies character.user_id == auth_user.user_id
      • Returns 404 if character not found or not owned by user
    • This extractor will be reused by every endpoint that operates on a character

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 distribution
  • validate_attributes: rejects total != 40
  • validate_attributes: rejects individual attribute < 5 or > 15
  • validate_attributes: correctly applies species bonuses
  • validate_name: accepts “Aldric the Bold”
  • validate_name: rejects empty string
  • validate_name: rejects names > 24 chars
  • validate_name: rejects names with special characters (e.g., <script>)
  • validate_name: rejects names with consecutive spaces
  • calculate_starting_attributes: correctly sums base + species + background
  • calculate_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/characters with valid data → 201, character created with correct attributes
  • POST /api/characters with duplicate name → 409
  • POST /api/characters when at slot limit → 403 with clear error message
  • POST /api/characters with invalid attribute total → 422
  • POST /api/characters with invalid species → 422
  • GET /api/characters returns only characters owned by authenticated user
  • GET /api/characters/:id for own character → 200
  • GET /api/characters/:id for another user’s character → 404
  • DELETE /api/characters/:id for own character → 204, character gone
  • DELETE /api/characters/:id for another user’s character → 404
  • Character count correctly limits creation per user