Files
Code_of_Conquest/docs/PHASE4b.md
2025-11-27 11:51:21 -06:00

468 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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
---