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 playattribute_growth.toml: attribute points gained per level (per design doc milestones)feats.toml: all selectable feats with name, description, prerequisites, effectssubclasses.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, 30species_skills.toml: one unique passive skill per speciesweapon_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 tablecheck_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:WeaponTypeenum: Sword, Axe, Dagger, Bow, Staff, Wand, Shield, Mace, Speargain_proficiency(current: u16, amount: u16) -> u16: capped at 100skills_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 Nonevalid_subclasses(class: Class) -> Vec<SubclassInfo>: returns 3 options for the classapply_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_proficienciestable: character_id, weapon_type, proficiency (PK: character_id + weapon_type)skill_queuestable: 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 extendcharacters.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
- Body:
POST /api/characters/:id/choose-feat:- Body:
{ feat_id } - Validate feat slot available, prerequisites met
- Body:
POST /api/characters/:id/allocate-attributes:- Body:
{ might: 1, speed: 1 }(unspent points from leveling) - Validate character has unspent points
- Body:
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, 50check_level_up: returns None when XP insufficientcheck_level_up: returns correct result for single level upcheck_level_up: handles multi-level-up correctlycan_select_feat: accepts valid feat selectioncan_select_feat: rejects feat already selectedcan_select_feat: rejects when no feat slot availablecan_select_feat: rejects when prerequisites not metcan_choose_subclass: true at level 10 with no subclasscan_choose_subclass: false at level 9can_choose_subclass: false when subclass already chosenvalid_subclasses: returns exactly 3 for each classgain_proficiency: caps at 100skills_unlocked_at: returns correct skills at proficiency milestonescheck_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/progressionreturns all progression dataPOST /api/characters/:id/choose-subclasspermanently sets subclassPOST /api/characters/:id/choose-subclasssecond call → 409 (already chosen)POST /api/characters/:id/choose-featadds feat to characterPOST /api/characters/:id/choose-featat limit → 409POST /api/characters/:id/allocate-attributesdistributes unspent pointsPOST /api/characters/:id/allocate-attributesrejects if no unspent points- Weapon proficiency persists across requests