## 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 %}

{{ character.name }}'s Skill Trees

Skill Points: {{ character.skill_points }}
{% for tree in character.skill_trees %}

{{ tree.name }}

{{ tree.description }}

{% for tier in range(5, 0, -1) %}
Tier {{ tier }}
{% for node in tree.get_nodes_by_tier(tier) %}
{% if node.skill_id in character.unlocked_skills %} ✓ {% elif character.can_unlock(node.skill_id) %} ⬡ {% else %} ⬢ {% endif %}
{{ node.name }} {% if character.can_unlock(node.skill_id) and character.skill_points > 0 %} {% endif %}
{# Draw prerequisite lines #} {% if node.prerequisite_skill_id %}
{% endif %} {% endfor %}
{% endfor %}
{% endfor %}
{# Skill Tooltip (populated via HTMX) #}
{% endblock %} ``` **Also create `/public_web/templates/character/partials/skill_tooltip.html`:** ```html

{{ skill.name }}

{{ skill.description }}

Bonuses:
{% if skill.prerequisite_skill_id %}

Requires: {{ get_skill_name(skill.prerequisite_skill_id) }}

{% endif %}
``` **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('//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//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//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//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('//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 ``` **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 ---