480 lines
14 KiB
Markdown
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
|