Phase 4b Abilities and skill trees is finished

This commit is contained in:
2025-11-28 22:02:57 -06:00
parent a8767b34e2
commit 8784fbaa88
3 changed files with 157 additions and 482 deletions

View File

@@ -1269,9 +1269,9 @@ class CombatService:
character = self.character_service.get_character(char_id, user_id) character = self.character_service.get_character(char_id, user_id)
# Add XP and check for level up # Add XP and check for level up
old_level = character.level leveled_up = character.add_experience(xp_per_player)
character.experience += xp_per_player if leveled_up:
# TODO: Add level up logic based on XP thresholds rewards.level_ups.append(char_id)
# Add gold # Add gold
character.gold += gold_per_player character.gold += gold_per_player
@@ -1279,9 +1279,6 @@ class CombatService:
# Save character # Save character
self.character_service.update_character(character, user_id) self.character_service.update_character(character, user_id)
if character.level > old_level:
rewards.level_ups.append(char_id)
except Exception as e: except Exception as e:
logger.error("Failed to distribute rewards to character", logger.error("Failed to distribute rewards to character",
char_id=char_id, char_id=char_id,

View File

@@ -365,17 +365,162 @@ effects_applied:
### Experience & Leveling ### Experience & Leveling
| Source | XP Gain | **XP Sources:**
|--------|---------|
| Combat victory | Based on enemy difficulty | | Source | XP Gain | Notes |
| Quest completion | Fixed quest reward | |--------|---------|-------|
| Story milestones | Major plot points | | Combat victory | Based on enemy `experience_reward` field | Divided evenly among party members |
| Exploration | Discovering new locations | | Quest completion | Fixed quest reward | Defined in quest data |
| Story milestones | Major plot points | AI-driven narrative rewards |
| Exploration | Discovering new locations | Future enhancement |
**Level Progression:** **Level Progression:**
- XP required increases per level (exponential curve)
- Each level grants +1 skill point The XP requirement for each level follows an exponential curve using the formula:
- Stats may increase based on class
```
XP Required = 100 × (current_level ^ 1.5)
```
| Level | XP Required | Cumulative XP |
|-------|-------------|---------------|
| 1→2 | 100 | 100 |
| 2→3 | 282 | 382 |
| 3→4 | 519 | 901 |
| 4→5 | 800 | 1,701 |
| 5→6 | 1,118 | 2,819 |
| 6→7 | 1,469 | 4,288 |
| 7→8 | 1,849 | 6,137 |
| 8→9 | 2,254 | 8,391 |
| 9→10 | 2,683 | 11,074 |
**Leveling Mechanics:**
- Each level grants **+1 skill point** to spend in skill trees
- Skill points calculated: `level - unlocked_skills.length`
- Overflow XP automatically carries to next level
- Level up triggers automatically when threshold reached
- Base stats remain constant (progression via skill trees & equipment)
**Implementation:**
- Leveling logic lives in `Character` model (`add_experience()`, `level_up()` methods)
- No separate service needed (OOP design pattern)
- See `api/app/models/character.py` lines 312-358
### Skill Trees
**Overview:**
Each character class has **2-3 skill trees** representing different specializations or playstyles. Players earn **1 skill point per level** to unlock skills, which provide permanent bonuses and unlock combat abilities.
**Skill Points:**
```
Available Skill Points = Character Level - Unlocked Skills Count
```
**Skill Tree Structure:**
Each skill tree contains **5 tiers** of increasing power:
| Tier | Description | Typical Effects |
|------|-------------|-----------------|
| **1** | Entry-level skills | Basic abilities, small stat bonuses (+3-5) |
| **2** | Intermediate skills | Enhanced abilities, moderate bonuses (+5-8) |
| **3** | Advanced skills | Powerful abilities, passive effects |
| **4** | Expert skills | Ability enhancements, large bonuses (+10-15) |
| **5** | Ultimate skills | Class-defining abilities, massive bonuses (+20+) |
**Prerequisites:**
Skills have prerequisites that create progression paths:
- Tier 1 skills have **no prerequisites** (open choices)
- Higher tier skills require **specific lower-tier skills**
- Cannot skip tiers (must unlock Tier 1 before Tier 2, etc.)
- Can mix between trees within same class
**Skill Effects:**
Skills provide multiple types of benefits:
1. **Stat Bonuses** - Permanent increases to stats
```yaml
effects:
stat_bonuses:
strength: 10
defense: 5
```
2. **Ability Unlocks** - Grant new combat abilities
```yaml
effects:
abilities:
- shield_bash
- riposte
```
3. **Passive Effects** - Special mechanics
```yaml
effects:
passive_effects:
- stun_resistance
- damage_reflection
```
4. **Ability Enhancements** - Modify existing abilities
```yaml
effects:
ability_enhancements:
fireball:
damage_bonus: 15
mana_cost_reduction: 5
```
5. **Combat Bonuses** - Crit chance, crit multiplier, etc.
```yaml
effects:
combat_bonuses:
crit_chance: 0.1 # +10%
crit_multiplier: 0.5 # +0.5x
```
**Example Progression Path:**
**Vanguard - Shield Bearer Tree:**
```
Level 1: No skills yet (0 points)
Level 2: Unlock "Shield Bash" (Tier 1) → Gain shield bash ability
Level 3: Unlock "Fortify" (Tier 1) → +5 defense bonus
Level 4: Unlock "Shield Wall" (Tier 2, requires Shield Bash) → Shield wall ability
Level 5: Unlock "Iron Skin" (Tier 2, requires Fortify) → +5 constitution
Level 6: Unlock "Guardian's Resolve" (Tier 3) → +10 defense + stun resistance
...
```
**Class Specializations:**
Each class offers distinct playstyles through their trees:
| Class | Tree 1 | Tree 2 | Tree 3 |
|-------|--------|--------|--------|
| **Vanguard** | Shield Bearer (Tank) | Weapon Master (DPS) | - |
| **Arcanist** | Pyromancy (Fire) | Cryomancy (Ice) | Electromancy (Lightning) |
| **Wildstrider** | Beast Mastery | Nature Magic | - |
| **Assassin** | Shadow Arts | Poison Master | - |
| **Luminary** | Holy Magic | Divine Protection | - |
| **Necromancer** | Death Magic | Corpse Summoning | - |
| **Lorekeeper** | Arcane Knowledge | Support Magic | - |
| **Oathkeeper** | Divine Wrath | Holy Shield | - |
**Design Notes:**
- Skill choices are **permanent** (no respec system currently)
- Players can mix skills from different trees within same class
- Some skills are **mutually exclusive** by design (different playstyles)
- Skill point allocation encourages specialization vs. generalization
**Implementation:**
- Skills defined in class YAML files at `api/app/data/classes/*.yaml`
- Character stores only `unlocked_skills: List[str]` (skill IDs)
- Bonuses calculated dynamically via `Character.get_effective_stats()`
- Full documentation: [SKILLS_AND_ABILITIES.md](SKILLS_AND_ABILITIES.md)
### Loot System ### Loot System

View File

@@ -1,467 +0,0 @@
## Phase 4B: Skill Trees & Leveling (Week 4)
### Task 4.1: Verify Skill Tree Data (2 hours)
**Objective:** Review skill system
**Files to Review:**
- `/api/app/models/skills.py` - SkillNode, SkillTree, PlayerClass
- `/api/app/data/skills/` - Skill YAML files for all 8 classes
**Verification Checklist:**
- [ ] Skill trees loaded from YAML
- [ ] Each class has 2 skill trees
- [ ] Each tree has 5 tiers
- [ ] Prerequisites work correctly
- [ ] Stat bonuses apply correctly
**Acceptance Criteria:**
- All 8 classes have complete skill trees
- Unlock logic works
- Respec logic implemented
---
### Task 4.2: Create Skill Tree Template (2 days / 16 hours)
**Objective:** Visual skill tree UI
**File:** `/public_web/templates/character/skills.html`
**Layout:**
```
┌─────────────────────────────────────────────────────────────┐
│ CHARACTER SKILL TREES │
├─────────────────────────────────────────────────────────────┤
│ │
│ Skill Points Available: 5 [Respec] ($$$)│
│ │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ TREE 1: Combat │ │ TREE 2: Utility │ │
│ ├────────────────────────┤ ├────────────────────────┤ │
│ │ │ │ │ │
│ │ Tier 5: [⬢] [⬢] │ │ Tier 5: [⬢] [⬢] │ │
│ │ │ │ │ │ │ │ │ │
│ │ Tier 4: [⬢] [⬢] │ │ Tier 4: [⬢] [⬢] │ │
│ │ │ │ │ │ │ │ │ │
│ │ Tier 3: [⬢] [⬢] │ │ Tier 3: [⬢] [⬢] │ │
│ │ │ │ │ │ │ │ │ │
│ │ Tier 2: [✓] [⬢] │ │ Tier 2: [⬢] [✓] │ │
│ │ │ │ │ │ │ │ │ │
│ │ Tier 1: [✓] [✓] │ │ Tier 1: [✓] [✓] │ │
│ │ │ │ │ │
│ └────────────────────────┘ └────────────────────────┘ │
│ │
│ Legend: [✓] Unlocked [⬡] Available [⬢] Locked │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Implementation:**
```html
{% extends "base.html" %}
{% block title %}Skill Trees - {{ character.name }}{% endblock %}
{% block content %}
<div class="skills-container">
<div class="skills-header">
<h1>{{ character.name }}'s Skill Trees</h1>
<div class="skills-info">
<span class="skill-points">Skill Points: <strong>{{ character.skill_points }}</strong></span>
<button class="btn btn-warning btn-respec"
hx-post="/characters/{{ character.character_id }}/skills/respec"
hx-confirm="Respec costs {{ respec_cost }} gold. Continue?"
hx-target=".skills-container"
hx-swap="outerHTML">
Respec ({{ respec_cost }} gold)
</button>
</div>
</div>
<div class="skill-trees-grid">
{% for tree in character.skill_trees %}
<div class="skill-tree">
<h2 class="tree-name">{{ tree.name }}</h2>
<p class="tree-description">{{ tree.description }}</p>
<div class="tree-diagram">
{% for tier in range(5, 0, -1) %}
<div class="skill-tier" data-tier="{{ tier }}">
<span class="tier-label">Tier {{ tier }}</span>
<div class="skill-nodes">
{% for node in tree.get_nodes_by_tier(tier) %}
<div class="skill-node {{ get_node_status(node, character) }}"
data-skill-id="{{ node.skill_id }}"
hx-get="/skills/{{ node.skill_id }}/tooltip"
hx-target="#skill-tooltip"
hx-swap="innerHTML"
hx-trigger="mouseenter">
<div class="node-icon">
{% if node.skill_id in character.unlocked_skills %}
{% elif character.can_unlock(node.skill_id) %}
{% else %}
{% endif %}
</div>
<span class="node-name">{{ node.name }}</span>
{% if character.can_unlock(node.skill_id) and character.skill_points > 0 %}
<button class="btn-unlock"
hx-post="/characters/{{ character.character_id }}/skills/unlock"
hx-vals='{"skill_id": "{{ node.skill_id }}"}'
hx-target=".skills-container"
hx-swap="outerHTML">
Unlock
</button>
{% endif %}
</div>
{# Draw prerequisite lines #}
{% if node.prerequisite_skill_id %}
<div class="prerequisite-line"></div>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{# Skill Tooltip (populated via HTMX) #}
<div id="skill-tooltip" class="skill-tooltip"></div>
</div>
{% endblock %}
```
**Also create `/public_web/templates/character/partials/skill_tooltip.html`:**
```html
<div class="tooltip-content">
<h3 class="skill-name">{{ skill.name }}</h3>
<p class="skill-description">{{ skill.description }}</p>
<div class="skill-bonuses">
<strong>Bonuses:</strong>
<ul>
{% for stat, bonus in skill.stat_bonuses.items() %}
<li>+{{ bonus }} {{ stat|title }}</li>
{% endfor %}
</ul>
</div>
{% if skill.prerequisite_skill_id %}
<p class="prerequisite">
<strong>Requires:</strong> {{ get_skill_name(skill.prerequisite_skill_id) }}
</p>
{% endif %}
</div>
```
**Acceptance Criteria:**
- Dual skill tree layout works
- 5 tiers × 2 nodes per tree displayed
- Locked/available/unlocked states visual
- Prerequisite lines drawn
- Hover shows tooltip
- Mobile responsive
---
### Task 4.3: Skill Unlock HTMX (4 hours)
**Objective:** Click to unlock skills
**File:** `/public_web/app/views/skills.py`
```python
"""
Skill Views
Routes for skill tree UI.
"""
from flask import Blueprint, render_template, request, g
from app.services.api_client import APIClient, APIError
from app.utils.auth import require_auth
from app.utils.logging import get_logger
logger = get_logger(__file__)
skills_bp = Blueprint('skills', __name__)
@skills_bp.route('/<skill_id>/tooltip', methods=['GET'])
@require_auth
def skill_tooltip(skill_id: str):
"""Get skill tooltip (HTMX partial)."""
# Load skill data
# Return rendered tooltip
pass
@skills_bp.route('/characters/<character_id>/skills', methods=['GET'])
@require_auth
def character_skills(character_id: str):
"""Display character skill trees."""
api_client = APIClient()
try:
# Get character
response = api_client.get(f'/characters/{character_id}')
character = response['result']
# Calculate respec cost
respec_cost = character['level'] * 100
return render_template(
'character/skills.html',
character=character,
respec_cost=respec_cost
)
except APIError as e:
logger.error(f"Failed to load skills: {e}")
return render_template('partials/error.html', error=str(e))
@skills_bp.route('/characters/<character_id>/skills/unlock', methods=['POST'])
@require_auth
def unlock_skill(character_id: str):
"""Unlock skill (HTMX endpoint)."""
api_client = APIClient()
skill_id = request.form.get('skill_id')
try:
# Unlock skill via API
response = api_client.post(
f'/characters/{character_id}/skills/unlock',
json={'skill_id': skill_id}
)
# Re-render skill trees
character = response['result']['character']
respec_cost = character['level'] * 100
return render_template(
'character/skills.html',
character=character,
respec_cost=respec_cost
)
except APIError as e:
logger.error(f"Failed to unlock skill: {e}")
return render_template('partials/error.html', error=str(e))
```
**Acceptance Criteria:**
- Click available node unlocks skill
- Skill points decrease
- Stat bonuses apply immediately
- Prerequisites enforced
- UI updates without page reload
---
### Task 4.4: Respec Functionality (4 hours)
**Objective:** Respec button with confirmation
**Implementation:** (in `skills_bp`)
```python
@skills_bp.route('/characters/<character_id>/skills/respec', methods=['POST'])
@require_auth
def respec_skills(character_id: str):
"""Respec all skills."""
api_client = APIClient()
try:
response = api_client.post(f'/characters/{character_id}/skills/respec')
character = response['result']['character']
respec_cost = character['level'] * 100
return render_template(
'character/skills.html',
character=character,
respec_cost=respec_cost,
message="Skills reset! All skill points refunded."
)
except APIError as e:
logger.error(f"Failed to respec: {e}")
return render_template('partials/error.html', error=str(e))
```
**Acceptance Criteria:**
- Respec button costs gold
- Confirmation modal shown
- All skills reset
- Skill points refunded
- Gold deducted
---
### Task 4.5: XP & Leveling System (1 day / 8 hours)
**Objective:** Award XP after combat, level up grants skill points
**File:** `/api/app/services/leveling_service.py`
```python
"""
Leveling Service
Manages XP gain and level ups.
"""
from app.models.character import Character
from app.utils.logging import get_logger
logger = get_logger(__file__)
class LevelingService:
"""Service for XP and leveling."""
@staticmethod
def xp_required_for_level(level: int) -> int:
"""
Calculate XP required for a given level.
Formula: 100 * (level ^ 2)
"""
return 100 * (level ** 2)
@staticmethod
def award_xp(character: Character, xp_amount: int) -> dict:
"""
Award XP to character and check for level up.
Args:
character: Character instance
xp_amount: XP to award
Returns:
Dict with leveled_up, new_level, skill_points_gained
"""
character.experience += xp_amount
leveled_up = False
levels_gained = 0
# Check for level ups (can level multiple times)
while character.experience >= LevelingService.xp_required_for_level(character.level + 1):
character.level += 1
character.skill_points += 1
levels_gained += 1
leveled_up = True
logger.info(f"Character {character.character_id} leveled up to {character.level}")
return {
'leveled_up': leveled_up,
'new_level': character.level if leveled_up else None,
'skill_points_gained': levels_gained,
'xp_gained': xp_amount
}
```
**Update Combat Results Endpoint:**
```python
# In /api/app/api/combat.py
@combat_bp.route('/<combat_id>/results', methods=['GET'])
@require_auth
def get_combat_results(combat_id: str):
"""Get combat results with XP/loot."""
combat_service = CombatService(get_appwrite_service())
encounter = combat_service.get_encounter(combat_id)
if encounter.status != CombatStatus.VICTORY:
return error_response("Combat not won", 400)
# Calculate XP (based on enemy difficulty)
xp_gained = sum(enemy.level * 50 for enemy in encounter.combatants if not enemy.is_player)
# Award XP to character
char_service = get_character_service()
character = char_service.get_character(encounter.character_id, g.user_id)
from app.services.leveling_service import LevelingService
level_result = LevelingService.award_xp(character, xp_gained)
# Award gold
gold_gained = sum(enemy.level * 25 for enemy in encounter.combatants if not enemy.is_player)
character.gold += gold_gained
# Generate loot (TODO: implement loot tables)
loot = []
# Save character
char_service.update_character(character)
return success_response({
'victory': True,
'xp_gained': xp_gained,
'gold_gained': gold_gained,
'loot': loot,
'level_up': level_result
})
```
**Create Level Up Modal Template:**
**File:** `/public_web/templates/game/partials/level_up_modal.html`
```html
<div class="modal-overlay">
<div class="modal-content level-up-modal">
<div class="modal-header">
<h2>🎉 LEVEL UP! 🎉</h2>
</div>
<div class="modal-body">
<p class="level-up-text">
Congratulations! You've reached <strong>Level {{ new_level }}</strong>!
</p>
<div class="level-up-rewards">
<p>You gained:</p>
<ul>
<li>+1 Skill Point</li>
<li>+{{ stat_increases.vitality }} Vitality</li>
<li>+{{ stat_increases.spirit }} Spirit</li>
</ul>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeModal()">Awesome!</button>
<a href="/characters/{{ character_id }}/skills" class="btn btn-secondary">
View Skill Trees
</a>
</div>
</div>
</div>
```
**Acceptance Criteria:**
- XP awarded after combat victory
- Level up triggers at XP threshold
- Skill points granted on level up
- Level up modal shown
- Character stats increase
---