# Skills and Abilities System ## Overview The game uses two distinct but interconnected systems for character progression and combat: - **Skills**: Progression system that provides passive bonuses and unlocks abilities - **Abilities**: Combat actions (attacks, spells, skills) that can be used in battle **Key Distinction**: Skills are *unlocked through leveling* and provide permanent benefits. Abilities are *combat actions* that characters can execute during battles. **⚠️ Terminology Note**: The word "skill" and "spell" have specific meanings in this system. See the [Terminology Clarification](#terminology-clarification) section below for important distinctions between Skills (progression nodes), Abilities (combat actions), Spells (a type of ability), and the confusing `AbilityType.SKILL` enum. --- ## Terminology Clarification It's important to understand three distinct concepts that can be confusing due to overlapping names: ### 1. Skills (Skill Tree Nodes) **What**: Nodes in the skill tree progression system that you unlock with skill points earned from leveling up. **Where**: Defined in class YAML files at `/api/app/data/classes/*.yaml` **Model**: `SkillNode` in [app/models/skills.py](../app/models/skills.py) **Purpose**: Provide passive stat bonuses and/or unlock combat abilities **Examples**: - "Shield Bash" skill node (unlocks the Shield Bash ability) - "Fortify" skill node (grants +5 defense bonus) - "Fireball Mastery" skill node (unlocks the Fireball ability) ### 2. Abilities (Combat Actions) **What**: Actions you can perform during combat - the umbrella term for all combat actions. **Where**: Defined in separate YAML files at `/api/app/data/abilities/*.yaml` **Model**: `Ability` in [app/models/abilities.py](../app/models/abilities.py) **Purpose**: Define combat mechanics - damage, effects, costs, targeting, etc. **Examples**: - Fireball ability (deals fire damage) - Shield Bash ability (stuns enemy) - Heal ability (restores HP) - Basic Attack ability (default physical attack) ### 3. Spells (A Type of Ability) **What**: A **category** of ability that uses magic/arcane power (not a separate system). **Type Field**: `ability_type: "spell"` in the Ability YAML **Purpose**: Differentiate magical abilities from physical attacks or special skills **Examples**: - Fireball (ability_type: spell) - Lightning Bolt (ability_type: spell) - Heal (ability_type: spell) ### Ability Types (AbilityType Enum) All abilities have an `ability_type` field that categorizes them. From [app/models/enums.py](../app/models/enums.py): ```python class AbilityType(Enum): """Categories of abilities that can be used in combat or exploration.""" ATTACK = "attack" # Basic physical attack SPELL = "spell" # Magical spell (arcane/divine) SKILL = "skill" # Special class ability (⚠️ different from skill tree!) ITEM_USE = "item_use" # Using a consumable item DEFEND = "defend" # Defensive action ``` **⚠️ Important Confusion to Avoid**: `AbilityType.SKILL` refers to special class abilities used in combat (like a Rogue's "Sneak Attack"). This is **completely different** from "Skills" in the skill tree progression system (`SkillNode`). ### Complete Example: Fireball To illustrate how all three concepts work together: **1. Skill Tree Node** (what you unlock with a skill point): ```yaml # In /api/app/data/classes/arcanist.yaml - skill_id: fireball_mastery name: Fireball Mastery description: Learn to cast the devastating fireball spell tier: 1 prerequisites: [] effects: abilities: - fireball # ← References the ability ``` **2. Ability Definition** (the combat action): ```yaml # In /api/app/data/abilities/fireball.yaml ability_id: "fireball" name: "Fireball" description: "Hurl a ball of fire at your enemies" ability_type: "spell" # ← This makes it a SPELL-type ability base_power: 30 damage_type: "fire" scaling_stat: "intelligence" mana_cost: 15 ``` **3. The Flow**: - Player unlocks "Fireball Mastery" **skill** (spends 1 skill point) - This grants access to the "Fireball" **ability** - Fireball is categorized as a **spell** (ability_type: spell) - Player can now use Fireball in combat ### Summary Table | Term | What It Is | Where Defined | Purpose | |------|------------|---------------|---------| | **Skill** (SkillNode) | Progression node | `/api/app/data/classes/*.yaml` | Unlock abilities, grant bonuses | | **Ability** | Combat action | `/api/app/data/abilities/*.yaml` | Define what you can do in battle | | **Spell** | Type of ability | `ability_type: "spell"` | Magical/arcane combat actions | | **AbilityType.SKILL** | Type of ability | `ability_type: "skill"` | Special class abilities (⚠️ not skill tree) | --- ## Architecture ### Skills **Purpose**: Character progression and passive bonuses **Data Model**: [app/models/skills.py](../app/models/skills.py) - `SkillNode`: Individual skill in a tree - `SkillTree`: Collection of related skills - `PlayerClass`: Character class with multiple skill trees **Data Location**: [app/data/classes/*.yaml](../app/data/classes/) - Skills are embedded within class definitions - Each class YAML contains 2+ skill trees - Example: `vanguard.yaml`, `arcanist.yaml` **Loader**: [app/services/class_loader.py](../app/services/class_loader.py) - `ClassLoader` reads class YAML files - Parses skill trees and nodes during class loading - Caches PlayerClass instances **Storage on Character**: [app/models/character.py](../app/models/character.py) ```python unlocked_skills: List[str] = field(default_factory=list) # Just skill IDs ``` ### Abilities **Purpose**: Combat actions and spells **Data Model**: [app/models/abilities.py](../app/models/abilities.py) - `Ability`: Complete definition of a combat action - Includes damage, effects, costs, targeting, etc. **Data Location**: [app/data/abilities/*.yaml](../app/data/abilities/) - Each ability is a separate YAML file - Example: `fireball.yaml`, `shield_bash.yaml`, `heal.yaml` **Loader**: [app/models/abilities.py](../app/models/abilities.py) - `AbilityLoader` class (lines 164-238) - Loads abilities on-demand or in bulk - Caches Ability instances **Storage on Character**: Not stored directly - Character has `player_class.starting_abilities` - Abilities unlocked through skills are computed dynamically --- ## How Skills Unlock Abilities ### Skill Effects Dictionary Skills can provide multiple types of benefits through their `effects` dictionary: ```yaml # From vanguard.yaml - skill_id: shield_bash name: Shield Bash description: Strike an enemy with your shield, dealing minor damage and stunning them tier: 1 prerequisites: [] effects: abilities: # Unlocks abilities for combat use - shield_bash # References /app/data/abilities/shield_bash.yaml ``` ```yaml # Skills can also provide stat bonuses - skill_id: fortify name: Fortify description: Your defensive training grants you enhanced protection tier: 1 prerequisites: [] effects: stat_bonuses: defense: 5 # Passive stat increase ``` ```yaml # Or both at once - skill_id: perfect_form name: Perfect Form description: Your combat technique reaches perfection tier: 5 prerequisites: - weapon_mastery effects: stat_bonuses: strength: 20 dexterity: 10 combat_bonuses: crit_chance: 0.1 crit_multiplier: 0.5 ``` ### Effect Types Skills can define effects in several categories: 1. **`abilities`**: List of ability IDs to unlock 2. **`stat_bonuses`**: Direct stat increases (strength, defense, etc.) 3. **`combat_bonuses`**: Combat modifiers (crit_chance, crit_multiplier) 4. **`passive_effects`**: Special passive effects (stun_resistance, etc.) 5. **`ability_enhancements`**: Modify existing abilities --- ## Data Flow: Skill Unlock to Ability Usage ### 1. Character Levels Up ```python # Character gains a level and skill point character.level_up() # character.level is now higher # character.available_skill_points increases ``` ### 2. Player Unlocks a Skill ```python # Service validates and unlocks the skill skill_id = "shield_bash" character.unlocked_skills.append(skill_id) # Character saves to database ``` ### 3. Getting Available Abilities ```python # Character.get_unlocked_abilities() - character.py:177-194 def get_unlocked_abilities(self) -> List[str]: # Start with class's built-in abilities abilities = list(self.player_class.starting_abilities) # Get all skill nodes from all trees all_skills = self.player_class.get_all_skills() # Collect abilities from unlocked skills for skill in all_skills: if skill.skill_id in self.unlocked_skills: abilities.extend(skill.get_unlocked_abilities()) return abilities # Returns list of ability IDs ``` ### 4. Extracting Abilities from Skills ```python # SkillNode.get_unlocked_abilities() - skills.py:73-87 def get_unlocked_abilities(self) -> List[str]: abilities = [] if "abilities" in self.effects: ability = self.effects["abilities"] if isinstance(ability, list): abilities.extend(ability) else: abilities.append(ability) return abilities ``` ### 5. Loading Ability Details for Combat ```python # During combat initialization ability_loader = AbilityLoader() ability_ids = character.get_unlocked_abilities() # Load full Ability objects available_abilities = [] for ability_id in ability_ids: ability = ability_loader.load_ability(ability_id) if ability: available_abilities.append(ability) # Now character can use these abilities in combat ``` ### 6. Using an Ability in Combat ```python # Player selects ability ability = ability_loader.load_ability("fireball") # Calculate power with character's stats effective_stats = character.get_effective_stats() damage = ability.calculate_power(effective_stats) # Apply to target target.current_hp -= damage # Apply effects (DoT, buffs, debuffs) effects = ability.get_effects_to_apply() for effect in effects: target.active_effects.append(effect) ``` --- ## Complete Example: Fireball (Step-by-Step Workflow) This example shows the complete journey from skill definition through combat usage. For the YAML definitions, see the [Terminology Clarification](#terminology-clarification) section above. ### Step 1: Define the Skill (in class YAML) See the Fireball Mastery skill node example in the Terminology section. ### Step 2: Define the Ability (separate YAML) See the Fireball ability YAML example in the Terminology section. **Key Point**: The skill's `effects.abilities` list contains `fireball`, which references `/api/app/data/abilities/fireball.yaml`. ### Step 3: Character Progression ```python # Character is created as Arcanist character = Character( name="Merlin", player_class=arcanist_class, level=1, unlocked_skills=[], # No skills yet ) # Starting abilities from class character.get_unlocked_abilities() # Returns: ["basic_attack"] # Character reaches level 2, spends skill point character.unlocked_skills.append("fireball_skill") # Now has fireball ability character.get_unlocked_abilities() # Returns: ["basic_attack", "fireball"] ``` ### Step 4: Combat Usage ```python # Combat begins ability_loader = AbilityLoader() fireball = ability_loader.load_ability("fireball") # Character's intelligence is 15 # Effective stats calculation includes all bonuses effective_stats = character.get_effective_stats() # intelligence = 15 # Calculate damage damage = fireball.calculate_power(effective_stats) # damage = 30 + (15 * 0.5) = 30 + 7.5 = 37 # Apply to enemy enemy.current_hp -= 37 # Apply burning effect burn_effect = fireball.get_effects_to_apply()[0] enemy.active_effects.append(burn_effect) # Enemy will take 5 fire damage per turn for 3 turns ``` --- ## Skill Bonuses vs Abilities ### Passive Bonuses Skills provide **permanent** stat increases calculated by `Character.get_effective_stats()`: ```python # Character._get_skill_bonuses() - character.py:156-175 def _get_skill_bonuses(self) -> Dict[str, int]: bonuses: Dict[str, int] = {} # Get all skill nodes from all trees all_skills = self.player_class.get_all_skills() # Sum up bonuses from unlocked skills for skill in all_skills: if skill.skill_id in self.unlocked_skills: skill_bonuses = skill.get_stat_bonuses() for stat_name, bonus in skill_bonuses.items(): bonuses[stat_name] = bonuses.get(stat_name, 0) + bonus return bonuses ``` ### Extracting Stat Bonuses ```python # SkillNode.get_stat_bonuses() - skills.py:57-71 def get_stat_bonuses(self) -> Dict[str, int]: bonuses = {} for key, value in self.effects.items(): # Look for stat names in effects if key in ["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"]: bonuses[key] = value elif key == "defense" or key == "resistance" or key == "hit_points": bonuses[key] = value return bonuses ``` **Note**: The current implementation looks for direct stat keys in `effects`. The newer YAML format uses nested `stat_bonuses` dictionaries. This may need updating. --- ## Key Design Principles 1. **Separation of Concerns** - Skills = Progression system (what you unlock) - Abilities = Combat system (what you can do) 2. **Data-Driven Design** - Both are defined in YAML files - Game designers can modify without code changes - Easy to balance and iterate 3. **Loose Coupling** - Skills reference abilities by ID only - Abilities are loaded on-demand - Changes to abilities don't affect skill definitions 4. **Efficient Storage** - Character only stores skill IDs (not full objects) - Abilities computed dynamically when needed - Reduces database payload 5. **Flexible Effects System** - Skills can unlock multiple abilities - Skills can provide stat bonuses - Skills can do both simultaneously - Room for future effect types --- ## Related Documentation - [DATA_MODELS.md](DATA_MODELS.md) - Character system and items - [GAME_SYSTEMS.md](GAME_SYSTEMS.md) - Combat mechanics - [API_REFERENCE.md](API_REFERENCE.md) - API endpoints for skills --- ## Future Enhancements Potential improvements to the system: 1. **Skill Levels**: Allow skills to be upgraded multiple times 2. **Ability Enhancements**: Skills that modify existing abilities 3. **Conditional Unlocks**: Require items, quests, or achievements 4. **Skill Synergies**: Bonuses for unlocking related skills 5. **Respec System**: Allow skill point redistribution 6. **Skill Prerequisites Validation**: Runtime validation of skill trees