Files
Code_of_Conquest/api/docs/SKILLS_AND_ABILITIES.md

480 lines
14 KiB
Markdown

# 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