14 KiB
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 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
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
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:
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):
# 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):
# 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
SkillNode: Individual skill in a treeSkillTree: Collection of related skillsPlayerClass: Character class with multiple skill trees
Data Location: app/data/classes/*.yaml
- Skills are embedded within class definitions
- Each class YAML contains 2+ skill trees
- Example:
vanguard.yaml,arcanist.yaml
Loader: app/services/class_loader.py
ClassLoaderreads class YAML files- Parses skill trees and nodes during class loading
- Caches PlayerClass instances
Storage on Character: app/models/character.py
unlocked_skills: List[str] = field(default_factory=list) # Just skill IDs
Abilities
Purpose: Combat actions and spells
Data Model: app/models/abilities.py
Ability: Complete definition of a combat action- Includes damage, effects, costs, targeting, etc.
Data Location: app/data/abilities/*.yaml
- Each ability is a separate YAML file
- Example:
fireball.yaml,shield_bash.yaml,heal.yaml
Loader: app/models/abilities.py
AbilityLoaderclass (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:
# 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
# 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
# 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:
abilities: List of ability IDs to unlockstat_bonuses: Direct stat increases (strength, defense, etc.)combat_bonuses: Combat modifiers (crit_chance, crit_multiplier)passive_effects: Special passive effects (stun_resistance, etc.)ability_enhancements: Modify existing abilities
Data Flow: Skill Unlock to Ability Usage
1. Character Levels Up
# 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
# Service validates and unlocks the skill
skill_id = "shield_bash"
character.unlocked_skills.append(skill_id)
# Character saves to database
3. Getting Available Abilities
# 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
# 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
# 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
# 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 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
# 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
# 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():
# 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
# 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
-
Separation of Concerns
- Skills = Progression system (what you unlock)
- Abilities = Combat system (what you can do)
-
Data-Driven Design
- Both are defined in YAML files
- Game designers can modify without code changes
- Easy to balance and iterate
-
Loose Coupling
- Skills reference abilities by ID only
- Abilities are loaded on-demand
- Changes to abilities don't affect skill definitions
-
Efficient Storage
- Character only stores skill IDs (not full objects)
- Abilities computed dynamically when needed
- Reduces database payload
-
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 - Character system and items
- GAME_SYSTEMS.md - Combat mechanics
- API_REFERENCE.md - API endpoints for skills
Future Enhancements
Potential improvements to the system:
- Skill Levels: Allow skills to be upgraded multiple times
- Ability Enhancements: Skills that modify existing abilities
- Conditional Unlocks: Require items, quests, or achievements
- Skill Synergies: Bonuses for unlocking related skills
- Respec System: Allow skill point redistribution
- Skill Prerequisites Validation: Runtime validation of skill trees