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

15 KiB
Raw Blame History

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:

{% 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:

<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

"""
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)

@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

"""
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:

# 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

<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