adding abilities, created skill tree template and unlock mechanics
This commit is contained in:
479
api/docs/SKILLS_AND_ABILITIES.md
Normal file
479
api/docs/SKILLS_AND_ABILITIES.md
Normal file
@@ -0,0 +1,479 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user