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 04: Character Progression

Overview

Leveling (1-50), XP curves, attribute growth per level, subclass selection at level 10, feat selection at milestone levels, non-combat skill advancement, weapon proficiency tracking, and the Paragon system for post-50 horizontal progression.

Dependencies

  • Feature 02 (Character Creation)
  • Feature 03 (Items & Inventory) — for weapon proficiency tracking by weapon type

Technical Tasks

1. Static Game Data — Progression Tables

  • Create data/progression/:
    • xp_table.toml: XP required per level (1-50), designed for 8-12 months of regular play
    • attribute_growth.toml: attribute points gained per level (per design doc milestones)
    • feats.toml: all selectable feats with name, description, prerequisites, effects
    • subclasses.toml: 3 subclasses per class (27 total) with name, description, bonus_skills, passive_effects
  • Create data/skills/:
    • class_skills.toml: starting skills per class + skills unlocked at levels 5, 10, 15, 20, 25, 30
    • species_skills.toml: one unique passive skill per species
    • weapon_skills.toml: 10 skills per weapon type, unlocked at proficiency milestones (10, 20, 30, 40, 50, 60, 70, 80, 90, 100)

2. Progression Logic (crates/game)

  • Create src/progression.rs:
    • xp_for_level(level: u16) -> u64: lookup from XP table
    • check_level_up(current_level: u16, current_xp: u64) -> Option<LevelUpResult>:
      • If XP >= threshold, return new level + attribute points earned + any unlocks
      • Handle multi-level-up (if enough XP for several levels at once)
    • LevelUpResult: new_level, attribute_points, feat_slot_unlocked (bool), skills_unlocked, subclass_eligible (bool)
    • apply_level_up(character: &mut Character, result: &LevelUpResult):
      • Increment level, reduce XP by threshold, apply attribute points
  • Create src/feats.rs:
    • Feat selection rules: slots at levels 5, 12, 20, 28, 35, 42, 48
    • can_select_feat(character: &Character, feat_id: &str) -> Result<()>:
      • Check character has an open feat slot
      • Check prerequisites met (level, class, attribute minimums)
      • Check feat not already selected
    • apply_feat(character: &mut Character, feat_id: &str):
      • Add to character’s feat list

3. Weapon Proficiency (crates/game)

  • Create src/weapon_proficiency.rs:
    • WeaponType enum: Sword, Axe, Dagger, Bow, Staff, Wand, Shield, Mace, Spear
    • gain_proficiency(current: u16, amount: u16) -> u16: capped at 100
    • skills_unlocked_at(proficiency: u16) -> Vec<String>: return skill IDs unlocked at current proficiency
    • Proficiency gained from using the weapon in combat (calculated during simulation)

4. Non-Combat Skills (crates/game)

  • Create src/noncombat_skills.rs:
    • 12 skills: Athletics, Acrobatics, Stealth, Perception, Arcana, Nature, Religion, Persuasion, Deception, Intimidation, Medicine, Survival
    • Skills are 0-100, improved through encounter checks
    • check_skill(skill_value: u16, difficulty: u16, rng: &mut impl Rng) -> SkillCheckResult:
      • d100 roll vs. (skill_value + attribute_modifier)
      • Returns success/failure + margin

5. Subclass System

  • Create src/subclass.rs:
    • can_choose_subclass(character: &Character) -> bool: level >= 10 and subclass is None
    • valid_subclasses(class: Class) -> Vec<SubclassInfo>: returns 3 options for the class
    • apply_subclass(character: &mut Character, subclass_id: &str) -> Result<()>:
      • Validate character is eligible
      • Validate subclass belongs to character’s class
      • Set subclass (permanent, cannot be changed)
      • Grant subclass starting skills

6. Paragon System (crates/game)

  • Create src/paragon.rs:
    • Post-level-50 horizontal progression
    • Paragon XP required per level (uncapped, escalating)
    • Each paragon level grants a small bonus (e.g., +1 to a chosen attribute, +1% to a stat)
    • gain_paragon_xp(character: &mut Character, xp: u64): check for paragon level up
    • Paragon bonuses are marginal — depth, not power

7. Database Migrations

  • Create migration 0005_create_progression.sql:
    • weapon_proficiencies table: character_id, weapon_type, proficiency (PK: character_id + weapon_type)
    • skill_queues table: id, character_id, name, is_active, slots (JSONB), created_at (index on character_id)

8. Progression Queries (crates/db)

  • Create src/queries/progression.rs:
    • get_weapon_proficiencies(pool, character_id) -> Vec<WeaponProficiency>
    • update_weapon_proficiency(pool, character_id, weapon_type, new_value)
    • update_character_level(pool, character_id, new_level, new_xp, attribute_updates)
    • set_subclass(pool, character_id, subclass)
    • add_feat(pool, character_id, feat_id)

9. Progression Routes (crates/api)

  • Create src/routes/progression.rs (or extend characters.rs):
    • GET /api/characters/:id/progression:
      • Return: level, xp, xp_to_next, feats, subclass, weapon_proficiencies, non-combat skills, paragon_level
    • POST /api/characters/:id/choose-subclass:
      • Body: { subclass_id }
      • Validate eligibility, apply permanently
    • POST /api/characters/:id/choose-feat:
      • Body: { feat_id }
      • Validate feat slot available, prerequisites met
    • POST /api/characters/:id/allocate-attributes:
      • Body: { might: 1, speed: 1 } (unspent points from leveling)
      • Validate character has unspent points

10. Client — Character Sheet

  • Create routes/(game)/character/+page.svelte:
    • Character overview: name, species, class, subclass, level, XP bar
    • Attributes panel: all 6 attributes with base + bonus breakdown
    • Feats panel: selected feats + available slots with “Choose Feat” UI
    • Weapon proficiencies: bar chart per weapon type (0-100)
    • Non-combat skills: list with values
    • Subclass selection modal (appears at level 10 if unchosen)

Tests

Unit Tests

  • xp_for_level: returns correct XP for levels 1, 10, 25, 50
  • check_level_up: returns None when XP insufficient
  • check_level_up: returns correct result for single level up
  • check_level_up: handles multi-level-up correctly
  • can_select_feat: accepts valid feat selection
  • can_select_feat: rejects feat already selected
  • can_select_feat: rejects when no feat slot available
  • can_select_feat: rejects when prerequisites not met
  • can_choose_subclass: true at level 10 with no subclass
  • can_choose_subclass: false at level 9
  • can_choose_subclass: false when subclass already chosen
  • valid_subclasses: returns exactly 3 for each class
  • gain_proficiency: caps at 100
  • skills_unlocked_at: returns correct skills at proficiency milestones
  • check_skill: d100 against skill value produces expected pass/fail distribution (statistical test with seeded RNG)
  • Paragon XP escalation: each level requires more than the last

Integration Tests

  • GET /api/characters/:id/progression returns all progression data
  • POST /api/characters/:id/choose-subclass permanently sets subclass
  • POST /api/characters/:id/choose-subclass second call → 409 (already chosen)
  • POST /api/characters/:id/choose-feat adds feat to character
  • POST /api/characters/:id/choose-feat at limit → 409
  • POST /api/characters/:id/allocate-attributes distributes unspent points
  • POST /api/characters/:id/allocate-attributes rejects if no unspent points
  • Weapon proficiency persists across requests