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

@@ -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
---