Files
Code_of_Conquest/api/docs/SKILLS_AND_ABILITIES.md

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 tree
  • SkillTree: Collection of related skills
  • PlayerClass: 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

  • ClassLoader reads 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

  • 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:

# 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:

  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

# 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

  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


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