Files
Code_of_Conquest/docs/PHASE4_COMBAT_IMPLEMENTATION.md

56 KiB
Raw Blame History

Phase 4: Combat & Progression Systems - Implementation Plan

Status: In Progress - Week 2 Complete, Week 3 Next Timeline: 4-5 weeks Last Updated: November 27, 2025 Document Version: 1.3


Completion Summary

Week 1: Combat Backend - COMPLETE

Task Description Status Tests
1.1 Verify Combat Data Models Complete -
1.2 Implement Combat Service Complete 25 tests
1.3 Implement Damage Calculator Complete 39 tests
1.4 Implement Effect Processor Complete -
1.5 Implement Combat Actions Complete -
1.6 Combat API Endpoints Complete 19 tests
1.7 Manual API Testing ⏭️ Skipped -

Files Created:

  • /api/app/models/enemy.py - EnemyTemplate, LootEntry dataclasses
  • /api/app/services/enemy_loader.py - YAML-based enemy loading
  • /api/app/services/combat_service.py - Combat orchestration service
  • /api/app/services/damage_calculator.py - Damage formula calculations
  • /api/app/api/combat.py - REST API endpoints
  • /api/app/data/enemies/*.yaml - 6 sample enemy definitions
  • /api/tests/test_damage_calculator.py - 39 tests
  • /api/tests/test_enemy_loader.py - 25 tests
  • /api/tests/test_combat_service.py - 25 tests
  • /api/tests/test_combat_api.py - 19 tests

Total Tests: 108 passing

Week 2: Inventory & Equipment - COMPLETE

Task Description Status Tests
2.1 Item Data Models (Affixes) Complete 24 tests
2.2 Item Data Files (YAML) Complete -
2.2.1 Item Generator Service Complete 35 tests
2.3 Inventory Service Complete 24 tests
2.4 Inventory API Endpoints Complete 25 tests
2.5 Character Stats Calculation Complete 17 tests
2.6 Equipment-Combat Integration Complete 140 tests
2.7 Combat Loot Integration Complete 59 tests

Files Created/Modified:

  • /api/app/models/items.py - Item with affix support, spell_power field
  • /api/app/models/affixes.py - Affix, BaseItemTemplate dataclasses
  • /api/app/models/stats.py - spell_power_bonus, updated damage formula
  • /api/app/models/combat.py - Combatant weapon properties
  • /api/app/services/item_generator.py - Procedural item generation
  • /api/app/services/inventory_service.py - Equipment management
  • /api/app/services/damage_calculator.py - Refactored to use stats properties
  • /api/app/services/combat_service.py - Equipment integration
  • /api/app/api/inventory.py - REST API endpoints

Total Tests (Week 2): 324+ passing


Overview

This phase implements the core combat and progression systems for Code of Conquest, enabling turn-based tactical combat, inventory management, equipment, skill trees, and the NPC shop. This is a prerequisite for the story progression and quest systems.

Key Deliverables:

  • Turn-based combat system (API + UI)
  • Inventory & equipment management
  • Skill tree visualization and unlocking
  • XP and leveling system
  • NPC shop

Phase Structure

Sub-Phase Duration Focus
Phase 4A 2-3 weeks Combat Foundation
Phase 4B 1-2 weeks Skill Trees & Leveling
Phase 4C 3-4 days NPC Shop

Total Estimated Time: 4-5 weeks (~140-175 hours)


Phase 4A: Combat Foundation (Weeks 1-3)

Week 1: Combat Backend & Data Models COMPLETE

Task 1.1: Verify Combat Data Models COMPLETE

Files: /api/app/models/combat.py, effects.py, abilities.py, stats.py

Verified: Combatant, CombatEncounter dataclasses, effect types (BUFF, DEBUFF, DOT, HOT, STUN, SHIELD), stacking logic, YAML ability loading, serialization methods.


Task 1.2: Implement Combat Service COMPLETE

File: /api/app/services/combat_service.py

Implemented: CombatService class with initiate_combat(), process_action(), initiative rolling, turn management, death checking, combat end detection.


Task 1.3: Implement Damage Calculator COMPLETE

File: /api/app/services/damage_calculator.py

Implemented: calculate_physical_damage(), calculate_magical_damage(), apply_damage() with shield absorption. Physical formula: weapon.damage + (STR/2) - defense. 39 unit tests.


Task 1.4: Implement Effect Processor COMPLETE

File: /api/app/models/effects.py

Implemented: tick() method for DOT/HOT damage/healing, duration tracking, stat modifiers via get_effective_stats().


Task 1.5: Implement Combat Actions COMPLETE

File: /api/app/services/combat_service.py

Implemented: _execute_attack(), _execute_spell(), _execute_item(), _execute_defend() with mana costs, cooldowns, effect application.


Task 1.6: Combat API Endpoints COMPLETE

File: /api/app/api/combat.py

Endpoints:

  • POST /api/v1/combat/start - Initiate combat
  • POST /api/v1/combat/<combat_id>/action - Take action
  • GET /api/v1/combat/<combat_id>/state - Get state
  • POST /api/v1/combat/<combat_id>/flee - Attempt flee
  • POST /api/v1/combat/<combat_id>/enemy-turn - Enemy AI
  • GET /api/v1/combat/enemies - List templates (public)
  • GET /api/v1/combat/enemies/<id> - Enemy details (public)

19 integration tests passing.


Task 1.7: Manual API Testing ⏭️ SKIPPED

Covered by 108 comprehensive automated tests.


Week 2: Inventory & Equipment System COMPLETE

Task 2.1: Item Data Models COMPLETE

Files: /api/app/models/items.py, affixes.py, enums.py

Implemented: Item dataclass with affix support (applied_affixes, base_template_id, generated_name, is_generated), Affix model (PREFIX/SUFFIX types, MINOR/MAJOR/LEGENDARY tiers), BaseItemTemplate for procedural generation. 24 tests.


Task 2.2: Item Data Files COMPLETE

Directory: /api/app/data/

Created:

  • base_items/weapons.yaml - 13 weapon templates
  • base_items/armor.yaml - 12 armor templates (cloth/leather/chain/plate)
  • affixes/prefixes.yaml - 18 prefixes (elemental, material, quality, legendary)
  • affixes/suffixes.yaml - 11 suffixes (stat bonuses, animal totems, legendary)
  • items/consumables/potions.yaml - Health/mana potions (small/medium/large)

Task 2.2.1: Item Generator Service COMPLETE

Files: /api/app/services/item_generator.py, affix_loader.py, base_item_loader.py

Implemented Diablo-style procedural generation:

  • Affix distribution: COMMON/UNCOMMON (0), RARE (1), EPIC (2), LEGENDARY (3)
  • Name generation: "Flaming Dagger of Strength"
  • Tier weights by rarity (RARE: 80% MINOR, EPIC: 70% MAJOR, LEGENDARY: 50% LEGENDARY)
  • Luck-influenced rarity rolling

35 tests.


Task 2.3: Implement Inventory Service COMPLETE

File: /api/app/services/inventory_service.py

Implemented: add_item(), remove_item(), equip_item(), unequip_item(), use_consumable(), use_consumable_in_combat(). Full object storage for generated items. Validation for slots, levels, item types. 24 tests.


Task 2.4: Inventory API Endpoints COMPLETE

File: /api/app/api/inventory.py

Endpoints:

  • GET /api/v1/characters/<id>/inventory - Get inventory + equipped
  • POST /api/v1/characters/<id>/inventory/equip - Equip item
  • POST /api/v1/characters/<id>/inventory/unequip - Unequip item
  • POST /api/v1/characters/<id>/inventory/use - Use consumable
  • DELETE /api/v1/characters/<id>/inventory/<item_id> - Drop item

25 tests.


Task 2.5: Update Character Stats Calculation COMPLETE

Files: /api/app/models/stats.py, character.py

Added damage_bonus, defense_bonus, resistance_bonus fields to Stats. Updated get_effective_stats() to populate from equipped weapon/armor. 17 tests.


Task 2.6: Equipment-Combat Integration COMPLETE

Files: stats.py, items.py, character.py, combat.py, combat_service.py, damage_calculator.py

Key changes:

  • Damage scaling: int(STR * 0.75) + damage_bonus (was STR // 2)
  • Added spell_power system for magical weapons
  • Combatant weapon properties (crit_chance, crit_multiplier, elemental support)
  • DamageCalculator uses stats.damage directly (removed weapon_damage param)

140 tests.


Task 2.7: Combat Loot Integration COMPLETE

Files: combat_loot_service.py, static_item_loader.py, app/models/enemy.py

Implemented hybrid loot system:

  • Static drops (consumables, materials) via StaticItemLoader
  • Procedural drops (equipment) via ItemGenerator
  • Difficulty bonuses: EASY +0%, MEDIUM +5%, HARD +15%, BOSS +30%
  • Enemy variants: goblin_scout, goblin_warrior, goblin_chieftain

59 tests.


Week 3: Combat UI

Task 3.1: Create Combat Template (1 day / 8 hours)

Objective: Build HTMX-powered combat interface

File: /public_web/templates/game/combat.html

Layout:

┌─────────────────────────────────────────────────────────────┐
│                        COMBAT ENCOUNTER                      │
├───────────────┬─────────────────────────┬───────────────────┤
│               │                         │                   │
│  YOUR         │   COMBAT LOG            │  TURN ORDER       │
│  CHARACTER    │                         │  ───────────      │
│  ─────────    │   Goblin attacks you    │  1. Aragorn ✓     │
│  HP: ████ 80  │   for 12 damage!        │  2. Goblin        │
│  MP: ███  60  │                         │  3. Orc           │
│               │   You attack Goblin     │                   │
│  ENEMY        │   for 18 damage!        │  ACTIVE EFFECTS   │
│  ─────────    │   CRITICAL HIT!         │  ───────────      │
│  Goblin       │                         │  🛡️ Defending     │
│  HP: ██   12  │   Goblin is stunned!    │  (1 turn)         │
│               │                         │                   │
│               │   ─────────────────     │                   │
│               │   ACTION BUTTONS        │                   │
│               │   ─────────────────     │                   │
│               │   [Attack] [Spell]      │                   │
│               │   [Item]   [Defend]     │                   │
│               │                         │                   │
└───────────────┴─────────────────────────┴───────────────────┘

Implementation:

{% extends "base.html" %}

{% block title %}Combat - Code of Conquest{% endblock %}

{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/combat.css') }}">
{% endblock %}

{% block content %}
<div class="combat-container">
    <h1 class="combat-title">⚔️ COMBAT ENCOUNTER</h1>

    <div class="combat-grid">
        {# Left Panel - Combatants #}
        <aside class="combat-panel combat-combatants">
            <div class="combatant-card player-card">
                <h3>{{ character.name }}</h3>
                <div class="hp-bar">
                    <div class="hp-fill" style="width: {{ (character.current_hp / character.stats.max_hp * 100)|int }}%"></div>
                    <span class="hp-text">HP: {{ character.current_hp }} / {{ character.stats.max_hp }}</span>
                </div>
                <div class="mp-bar">
                    <div class="mp-fill" style="width: {{ (character.current_mp / character.stats.max_mp * 100)|int }}%"></div>
                    <span class="mp-text">MP: {{ character.current_mp }} / {{ character.stats.max_mp }}</span>
                </div>
            </div>

            <div class="vs-divider">VS</div>

            {% for enemy in enemies %}
            <div class="combatant-card enemy-card" id="enemy-{{ loop.index0 }}">
                <h3>{{ enemy.name }}</h3>
                <div class="hp-bar">
                    <div class="hp-fill enemy" style="width: {{ (enemy.current_hp / enemy.stats.max_hp * 100)|int }}%"></div>
                    <span class="hp-text">HP: {{ enemy.current_hp }} / {{ enemy.stats.max_hp }}</span>
                </div>
                {% if enemy.current_hp > 0 %}
                <button class="btn btn-target" onclick="selectTarget('{{ enemy.combatant_id }}')">
                    Target
                </button>
                {% else %}
                <span class="defeated-badge">DEFEATED</span>
                {% endif %}
            </div>
            {% endfor %}
        </aside>

        {# Middle Panel - Combat Log & Actions #}
        <section class="combat-panel combat-main">
            <div class="combat-log" id="combat-log">
                <h3>Combat Log</h3>
                <div class="log-entries">
                    {% for entry in combat_log[-10:] %}
                    <div class="log-entry">{{ entry }}</div>
                    {% endfor %}
                </div>
            </div>

            <div class="combat-actions" id="combat-actions">
                <h3>Your Turn</h3>
                <div class="action-buttons">
                    <button class="btn btn-action btn-attack"
                            hx-post="/combat/{{ combat_id }}/action"
                            hx-vals='{"action_type": "attack", "ability_id": "basic_attack", "target_id": ""}'
                            hx-target="#combat-container"
                            hx-swap="outerHTML">
                        ⚔️ Attack
                    </button>

                    <button class="btn btn-action btn-spell"
                            onclick="openSpellMenu()">
                        ✨ Cast Spell
                    </button>

                    <button class="btn btn-action btn-item"
                            onclick="openItemMenu()">
                        🎒 Use Item
                    </button>

                    <button class="btn btn-action btn-defend"
                            hx-post="/combat/{{ combat_id }}/action"
                            hx-vals='{"action_type": "defend"}'
                            hx-target="#combat-container"
                            hx-swap="outerHTML">
                        🛡️ Defend
                    </button>
                </div>
            </div>
        </section>

        {# Right Panel - Turn Order & Effects #}
        <aside class="combat-panel combat-sidebar">
            <div class="turn-order">
                <h3>Turn Order</h3>
                <ol>
                    {% for combatant_id in turn_order %}
                    <li class="{% if loop.index0 == current_turn_index %}active-turn{% endif %}">
                        {{ get_combatant_name(combatant_id) }}
                        {% if loop.index0 == current_turn_index %}✓{% endif %}
                    </li>
                    {% endfor %}
                </ol>
            </div>

            <div class="active-effects">
                <h3>Active Effects</h3>
                {% for effect in character.active_effects %}
                <div class="effect-badge {{ effect.effect_type }}">
                    {{ effect.name }} ({{ effect.duration }})
                </div>
                {% endfor %}
            </div>
        </aside>
    </div>
</div>

{# Modal Container #}
<div id="modal-container"></div>
{% endblock %}

{% block scripts %}
<script>
let selectedTargetId = null;

function selectTarget(targetId) {
    selectedTargetId = targetId;

    // Update UI to show selected target
    document.querySelectorAll('.btn-target').forEach(btn => {
        btn.classList.remove('selected');
    });
    event.target.classList.add('selected');
}

function openSpellMenu() {
    // TODO: Open modal with spell selection
}

function openItemMenu() {
    // TODO: Open modal with item selection
}

// Auto-scroll combat log to bottom
const logDiv = document.querySelector('.log-entries');
if (logDiv) {
    logDiv.scrollTop = logDiv.scrollHeight;
}
</script>
{% endblock %}

Also create /public_web/static/css/combat.css

Acceptance Criteria:

  • 3-column layout works
  • Combat log displays messages
  • HP/MP bars update dynamically
  • Action buttons trigger HTMX requests
  • Turn order displays correctly
  • Active effects shown

Task 3.2: Combat HTMX Integration (1 day / 8 hours)

Objective: Wire combat UI to API via HTMX

File: /public_web/app/views/combat.py

Implementation:

"""
Combat Views

Routes for combat UI.
"""

from flask import Blueprint, render_template, request, g, redirect, url_for

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

combat_bp = Blueprint('combat', __name__)


@combat_bp.route('/<combat_id>')
@require_auth
def combat_view(combat_id: str):
    """Display combat interface."""
    api_client = APIClient()

    try:
        # Get combat state
        response = api_client.get(f'/combat/{combat_id}/state')
        combat_state = response['result']

        return render_template(
            'game/combat.html',
            combat_id=combat_id,
            combat_state=combat_state,
            turn_order=combat_state['turn_order'],
            current_turn_index=combat_state['current_turn_index'],
            combat_log=combat_state['combat_log'],
            character=combat_state['combatants'][0],  # Player is first
            enemies=combat_state['combatants'][1:]     # Rest are enemies
        )

    except APIError as e:
        logger.error(f"Failed to load combat {combat_id}: {e}")
        return redirect(url_for('game.play'))


@combat_bp.route('/<combat_id>/action', methods=['POST'])
@require_auth
def combat_action(combat_id: str):
    """Process combat action (HTMX endpoint)."""
    api_client = APIClient()

    action_data = {
        'action_type': request.form.get('action_type'),
        'ability_id': request.form.get('ability_id'),
        'target_id': request.form.get('target_id'),
        'item_id': request.form.get('item_id')
    }

    try:
        # Submit action to API
        response = api_client.post(f'/combat/{combat_id}/action', json=action_data)
        result = response['result']

        # Check if combat ended
        if result['combat_state']['status'] in ['victory', 'defeat']:
            return redirect(url_for('combat.combat_results', combat_id=combat_id))

        # Re-render combat view with updated state
        return render_template(
            'game/combat.html',
            combat_id=combat_id,
            combat_state=result['combat_state'],
            turn_order=result['combat_state']['turn_order'],
            current_turn_index=result['combat_state']['current_turn_index'],
            combat_log=result['combat_state']['combat_log'],
            character=result['combat_state']['combatants'][0],
            enemies=result['combat_state']['combatants'][1:]
        )

    except APIError as e:
        logger.error(f"Combat action failed: {e}")
        return render_template('partials/error.html', error=str(e))


@combat_bp.route('/<combat_id>/results')
@require_auth
def combat_results(combat_id: str):
    """Display combat results (victory/defeat)."""
    api_client = APIClient()

    try:
        response = api_client.get(f'/combat/{combat_id}/results')
        results = response['result']

        return render_template(
            'game/combat_results.html',
            victory=results['victory'],
            xp_gained=results['xp_gained'],
            gold_gained=results['gold_gained'],
            loot=results['loot']
        )

    except APIError as e:
        logger.error(f"Failed to load combat results: {e}")
        return redirect(url_for('game.play'))

Register blueprint in /public_web/app/__init__.py:

from app.views.combat import combat_bp
app.register_blueprint(combat_bp, url_prefix='/combat')

Acceptance Criteria:

  • Combat view loads from API
  • Action buttons submit to API
  • Combat state updates dynamically
  • Combat results shown at end
  • Errors handled gracefully

Task 3.3: Inventory UI (1 day / 8 hours)

Objective: Add inventory accordion to character panel

File: /public_web/templates/game/partials/character_panel.html

Add Inventory Section:

{# Existing character panel code #}

{# Add Inventory Accordion #}
<div class="panel-accordion" data-accordion="inventory">
    <button class="panel-accordion-header" onclick="togglePanelAccordion(this)">
        <span>Inventory <span class="count">({{ character.inventory|length }}/{{ inventory_max }})</span></span>
        <span class="accordion-icon"></span>
    </button>
    <div class="panel-accordion-content">
        <div class="inventory-grid">
            {% for item in inventory %}
            <div class="inventory-item {{ item.rarity }}"
                 hx-get="/inventory/{{ character.character_id }}/item/{{ item.item_id }}"
                 hx-target="#modal-container"
                 hx-swap="innerHTML">
                <img src="{{ item.icon_url or '/static/img/items/default.png' }}" alt="{{ item.name }}">
                <span class="item-name">{{ item.name }}</span>
            </div>
            {% endfor %}
        </div>
    </div>
</div>

{# Equipment Section #}
<div class="panel-accordion" data-accordion="equipment">
    <button class="panel-accordion-header" onclick="togglePanelAccordion(this)">
        <span>Equipment</span>
        <span class="accordion-icon"></span>
    </button>
    <div class="panel-accordion-content">
        <div class="equipment-slots">
            <div class="equipment-slot">
                <label>Weapon:</label>
                {% if character.equipped.weapon %}
                <span class="equipped-item">{{ get_item_name(character.equipped.weapon) }}</span>
                <button class="btn-small"
                        hx-post="/inventory/{{ character.character_id }}/unequip"
                        hx-vals='{"slot": "weapon"}'
                        hx-target="#character-panel"
                        hx-swap="outerHTML">
                    Unequip
                </button>
                {% else %}
                <span class="empty-slot">Empty</span>
                {% endif %}
            </div>

            <div class="equipment-slot">
                <label>Helmet:</label>
                {# Similar for helmet, chest, boots, etc. #}
            </div>
        </div>
    </div>
</div>

Create /public_web/templates/game/partials/item_modal.html:

<div class="modal-overlay" onclick="closeModal()">
    <div class="modal-content" onclick="event.stopPropagation()">
        <div class="modal-header">
            <h2 class="item-name {{ item.rarity }}">{{ item.name }}</h2>
            <button class="modal-close" onclick="closeModal()">×</button>
        </div>

        <div class="modal-body">
            <p class="item-description">{{ item.description }}</p>

            <div class="item-stats">
                {% if item.item_type == 'weapon' %}
                <p><strong>Damage:</strong> {{ item.damage }}</p>
                <p><strong>Crit Chance:</strong> {{ (item.crit_chance * 100)|int }}%</p>
                {% elif item.item_type == 'armor' %}
                <p><strong>Defense:</strong> {{ item.defense }}</p>
                <p><strong>Resistance:</strong> {{ item.resistance }}</p>
                {% elif item.item_type == 'consumable' %}
                <p><strong>HP Restore:</strong> {{ item.hp_restore }}</p>
                <p><strong>MP Restore:</strong> {{ item.mp_restore }}</p>
                {% endif %}
            </div>

            <p class="item-value">Value: {{ item.value }} gold</p>
        </div>

        <div class="modal-footer">
            {% if item.item_type == 'weapon' %}
            <button class="btn btn-primary"
                    hx-post="/inventory/{{ character_id }}/equip"
                    hx-vals='{"item_id": "{{ item.item_id }}", "slot": "weapon"}'
                    hx-target="#character-panel"
                    hx-swap="outerHTML">
                Equip Weapon
            </button>
            {% elif item.item_type == 'consumable' %}
            <button class="btn btn-primary"
                    hx-post="/inventory/{{ character_id }}/use"
                    hx-vals='{"item_id": "{{ item.item_id }}"}'
                    hx-target="#character-panel"
                    hx-swap="outerHTML">
                Use Item
            </button>
            {% endif %}

            <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
        </div>
    </div>
</div>

Acceptance Criteria:

  • Inventory displays in character panel
  • Click item shows modal with details
  • Equip/unequip works via HTMX
  • Use consumable works
  • Equipment slots show equipped items

Task 3.4: Combat Testing & Polish (1 day / 8 hours)

Objective: Playtest combat and fix bugs

Testing Checklist:

  • Start combat from story session
  • Turn order correct
  • Attack deals damage
  • Critical hits work
  • Spells consume mana
  • Effects apply and tick correctly
  • Items can be used in combat
  • Defend action works
  • Victory awards XP/gold/loot
  • Defeat handling works
  • Combat log readable
  • HP/MP bars update
  • Multiple enemies work
  • Combat state persists (refresh page)

Bug Fixes & Polish:

  • Fix any calculation errors
  • Improve combat log messages
  • Add visual feedback (animations, highlights)
  • Improve mobile responsiveness
  • Add loading states

Acceptance Criteria:

  • Combat flows smoothly start to finish
  • No critical bugs
  • UX feels responsive and clear
  • Ready for real gameplay

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

Phase 4C: NPC Shop (Days 15-18)

Task 5.1: Define Shop Inventory (4 hours)

Objective: Create YAML for shop items

File: /api/app/data/shop/general_store.yaml

shop_id: "general_store"
shop_name: "General Store"
shop_description: "A well-stocked general store with essential supplies."
shopkeeper_name: "Merchant Guildmaster"

inventory:
  # Weapons
  - item_id: "iron_sword"
    stock: -1  # Unlimited stock (-1)
    price: 50

  - item_id: "oak_bow"
    stock: -1
    price: 45

  # Armor
  - item_id: "leather_helmet"
    stock: -1
    price: 30

  - item_id: "leather_chest"
    stock: -1
    price: 60

  # Consumables
  - item_id: "health_potion_small"
    stock: -1
    price: 10

  - item_id: "health_potion_medium"
    stock: -1
    price: 30

  - item_id: "mana_potion_small"
    stock: -1
    price: 15

  - item_id: "antidote"
    stock: -1
    price: 20

Acceptance Criteria:

  • Shop inventory defined in YAML
  • Mix of weapons, armor, consumables
  • Reasonable pricing
  • Unlimited stock for basics

Task 5.2: Shop API Endpoints (4 hours)

Objective: Create shop endpoints

File: /api/app/api/shop.py

"""
Shop API Blueprint

Endpoints:
- GET /api/v1/shop/inventory - Browse shop items
- POST /api/v1/shop/purchase - Purchase item
"""

from flask import Blueprint, request, g

from app.services.shop_service import ShopService
from app.services.character_service import get_character_service
from app.services.appwrite_service import get_appwrite_service
from app.utils.response import success_response, error_response
from app.utils.auth import require_auth
from app.utils.logging import get_logger

logger = get_logger(__file__)

shop_bp = Blueprint('shop', __name__)


@shop_bp.route('/inventory', methods=['GET'])
@require_auth
def get_shop_inventory():
    """Get shop inventory."""
    shop_service = ShopService()
    inventory = shop_service.get_shop_inventory("general_store")

    return success_response({
        'shop_name': "General Store",
        'inventory': [
            {
                'item': item.to_dict(),
                'price': price,
                'in_stock': True
            }
            for item, price in inventory
        ]
    })


@shop_bp.route('/purchase', methods=['POST'])
@require_auth
def purchase_item():
    """
    Purchase item from shop.

    Request JSON:
    {
        "character_id": "char_abc",
        "item_id": "iron_sword",
        "quantity": 1
    }
    """
    data = request.get_json()

    character_id = data.get('character_id')
    item_id = data.get('item_id')
    quantity = data.get('quantity', 1)

    # Get character
    char_service = get_character_service()
    character = char_service.get_character(character_id, g.user_id)

    # Purchase item
    shop_service = ShopService()

    try:
        result = shop_service.purchase_item(
            character,
            "general_store",
            item_id,
            quantity
        )

        # Save character
        char_service.update_character(character)

        return success_response(result)

    except Exception as e:
        return error_response(str(e), 400)

Also create /api/app/services/shop_service.py:

"""
Shop Service

Manages NPC shop inventory and purchases.
"""

import yaml
from typing import List, Tuple

from app.models.items import Item
from app.models.character import Character
from app.services.item_loader import ItemLoader
from app.utils.logging import get_logger

logger = get_logger(__file__)


class ShopService:
    """Service for NPC shops."""

    def __init__(self):
        self.item_loader = ItemLoader()
        self.shops = self._load_shops()

    def _load_shops(self) -> dict:
        """Load all shop data from YAML."""
        shops = {}

        with open('app/data/shop/general_store.yaml', 'r') as f:
            shop_data = yaml.safe_load(f)
            shops[shop_data['shop_id']] = shop_data

        return shops

    def get_shop_inventory(self, shop_id: str) -> List[Tuple[Item, int]]:
        """
        Get shop inventory.

        Returns:
            List of (Item, price) tuples
        """
        shop = self.shops.get(shop_id)
        if not shop:
            return []

        inventory = []
        for item_data in shop['inventory']:
            item = self.item_loader.get_item(item_data['item_id'])
            price = item_data['price']
            inventory.append((item, price))

        return inventory

    def purchase_item(
        self,
        character: Character,
        shop_id: str,
        item_id: str,
        quantity: int = 1
    ) -> dict:
        """
        Purchase item from shop.

        Args:
            character: Character instance
            shop_id: Shop ID
            item_id: Item to purchase
            quantity: Quantity to buy

        Returns:
            Purchase result dict

        Raises:
            ValueError: If insufficient gold or item not found
        """
        shop = self.shops.get(shop_id)
        if not shop:
            raise ValueError("Shop not found")

        # Find item in shop inventory
        item_data = next(
            (i for i in shop['inventory'] if i['item_id'] == item_id),
            None
        )

        if not item_data:
            raise ValueError("Item not available in shop")

        price = item_data['price'] * quantity

        # Check if character has enough gold
        if character.gold < price:
            raise ValueError(f"Not enough gold. Need {price}, have {character.gold}")

        # Deduct gold
        character.gold -= price

        # Add items to inventory
        for _ in range(quantity):
            if item_id not in character.inventory_item_ids:
                character.inventory_item_ids.append(item_id)
            else:
                # Item already exists, increment stack (if stackable)
                # For now, just add multiple entries
                character.inventory_item_ids.append(item_id)

        logger.info(f"Character {character.character_id} purchased {quantity}x {item_id} for {price} gold")

        return {
            'item_purchased': item_id,
            'quantity': quantity,
            'total_cost': price,
            'gold_remaining': character.gold
        }

Acceptance Criteria:

  • Shop inventory endpoint works
  • Purchase endpoint validates gold
  • Items added to inventory
  • Gold deducted
  • Transactions logged

Task 5.3: Shop UI (1 day / 8 hours)

Objective: Shop browse and purchase interface

File: /public_web/templates/shop/index.html

{% extends "base.html" %}

{% block title %}Shop - Code of Conquest{% endblock %}

{% block content %}
<div class="shop-container">
    <div class="shop-header">
        <h1>🏪 {{ shop_name }}</h1>
        <p class="shopkeeper">Shopkeeper: {{ shopkeeper_name }}</p>
        <p class="player-gold">Your Gold: <strong>{{ character.gold }}</strong></p>
    </div>

    <div class="shop-inventory">
        {% for item_entry in inventory %}
        <div class="shop-item-card {{ item_entry.item.rarity }}">
            <div class="item-header">
                <h3>{{ item_entry.item.name }}</h3>
                <span class="item-price">{{ item_entry.price }} gold</span>
            </div>

            <p class="item-description">{{ item_entry.item.description }}</p>

            <div class="item-stats">
                {% if item_entry.item.item_type == 'weapon' %}
                <span>⚔️ Damage: {{ item_entry.item.damage }}</span>
                {% elif item_entry.item.item_type == 'armor' %}
                <span>🛡️ Defense: {{ item_entry.item.defense }}</span>
                {% elif item_entry.item.item_type == 'consumable' %}
                <span>❤️ Restores: {{ item_entry.item.hp_restore }} HP</span>
                {% endif %}
            </div>

            <button class="btn btn-primary btn-purchase"
                    {% if character.gold < item_entry.price %}disabled{% endif %}
                    hx-post="/shop/purchase"
                    hx-vals='{"character_id": "{{ character.character_id }}", "item_id": "{{ item_entry.item.item_id }}"}'
                    hx-target=".shop-container"
                    hx-swap="outerHTML">
                {% if character.gold >= item_entry.price %}
                Purchase
                {% else %}
                Not Enough Gold
                {% endif %}
            </button>
        </div>
        {% endfor %}
    </div>
</div>
{% endblock %}

Create view in /public_web/app/views/shop.py:

"""
Shop Views
"""

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

shop_bp = Blueprint('shop', __name__)


@shop_bp.route('/')
@require_auth
def shop_index():
    """Display shop."""
    api_client = APIClient()

    try:
        # Get shop inventory
        shop_response = api_client.get('/shop/inventory')
        inventory = shop_response['result']['inventory']

        # Get character (for gold display)
        char_response = api_client.get(f'/characters/{g.character_id}')
        character = char_response['result']

        return render_template(
            'shop/index.html',
            shop_name="General Store",
            shopkeeper_name="Merchant Guildmaster",
            inventory=inventory,
            character=character
        )

    except APIError as e:
        logger.error(f"Failed to load shop: {e}")
        return render_template('partials/error.html', error=str(e))


@shop_bp.route('/purchase', methods=['POST'])
@require_auth
def purchase():
    """Purchase item (HTMX endpoint)."""
    api_client = APIClient()

    purchase_data = {
        'character_id': request.form.get('character_id'),
        'item_id': request.form.get('item_id'),
        'quantity': 1
    }

    try:
        response = api_client.post('/shop/purchase', json=purchase_data)

        # Reload shop
        return shop_index()

    except APIError as e:
        logger.error(f"Purchase failed: {e}")
        return render_template('partials/error.html', error=str(e))

Acceptance Criteria:

  • Shop displays all items
  • Item cards show stats and price
  • Purchase button disabled if not enough gold
  • Purchase adds item to inventory
  • Gold updates dynamically
  • UI refreshes after purchase

Task 5.4: Transaction Logging (2 hours)

Objective: Log all shop purchases

File: /api/app/models/transaction.py

"""
Transaction Model

Tracks all gold transactions (shop, trades, etc.)
"""

from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, Any


@dataclass
class Transaction:
    """Represents a gold transaction."""

    transaction_id: str
    transaction_type: str  # "shop_purchase", "trade", "quest_reward", etc.
    character_id: str
    amount: int  # Negative for expenses, positive for income
    description: str
    timestamp: datetime = field(default_factory=datetime.utcnow)
    metadata: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        """Serialize to dict."""
        return {
            "transaction_id": self.transaction_id,
            "transaction_type": self.transaction_type,
            "character_id": self.character_id,
            "amount": self.amount,
            "description": self.description,
            "timestamp": self.timestamp.isoformat(),
            "metadata": self.metadata
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'Transaction':
        """Deserialize from dict."""
        return cls(
            transaction_id=data["transaction_id"],
            transaction_type=data["transaction_type"],
            character_id=data["character_id"],
            amount=data["amount"],
            description=data["description"],
            timestamp=datetime.fromisoformat(data["timestamp"]),
            metadata=data.get("metadata", {})
        )

Update ShopService.purchase_item() to log transaction:

# In shop_service.py

def purchase_item(...):
    # ... existing code ...

    # Log transaction
    from app.models.transaction import Transaction
    import uuid

    transaction = Transaction(
        transaction_id=str(uuid.uuid4()),
        transaction_type="shop_purchase",
        character_id=character.character_id,
        amount=-price,
        description=f"Purchased {quantity}x {item_id} from {shop_id}",
        metadata={
            "shop_id": shop_id,
            "item_id": item_id,
            "quantity": quantity,
            "unit_price": item_data['price']
        }
    )

    # Save to database
    from app.services.appwrite_service import get_appwrite_service
    appwrite = get_appwrite_service()
    appwrite.create_document("transactions", transaction.transaction_id, transaction.to_dict())

    # ... rest of code ...

Acceptance Criteria:

  • All purchases logged to database
  • Transaction records complete
  • Can query transaction history

Success Criteria - Phase 4 Complete

Combat System

  • Turn-based combat works end-to-end
  • Damage calculations correct (physical, magical, critical)
  • Effects process correctly (DOT, HOT, buffs, debuffs, shields, stun)
  • Combat UI functional and responsive
  • Victory awards XP, gold, loot
  • Combat state persists

Inventory System

  • Inventory displays in UI
  • Equip/unequip items works
  • Consumables can be used
  • Equipment affects character stats
  • Item YAML data loaded correctly

Skill Trees

  • Visual skill tree UI works
  • Prerequisites enforced
  • Unlock skills with skill points
  • Respec functionality works
  • Stat bonuses apply immediately

Leveling

  • XP awarded after combat
  • Level up triggers at threshold
  • Skill points granted on level up
  • Level up modal shown
  • Character stats increase

NPC Shop

  • Shop inventory displays
  • Purchase validation works
  • Items added to inventory
  • Gold deducted correctly
  • Transactions logged

Next Steps After Phase 4

Once Phase 4 is complete, you'll have a fully playable combat game with progression. The next logical phases are:

Phase 5: Story Progression & Quests (Original Phase 4 from roadmap)

  • AI-driven story progression
  • Action prompts (button-based gameplay)
  • Quest system (YAML-driven, context-aware)
  • Full gameplay loop: Explore → Combat → Quests → Level Up

Phase 6: Multiplayer Sessions

  • Invite-based co-op
  • Time-limited sessions
  • AI-generated campaigns

Phase 7: Marketplace & Economy

  • Player-to-player trading
  • Auction system
  • Economy balancing

Appendix: Testing Strategy

Manual Testing Checklist

Combat:

  • Start combat from story
  • Turn order correct
  • Attack deals damage
  • Spells work
  • Items usable in combat
  • Defend action
  • Victory conditions
  • Defeat handling

Inventory:

  • Add items
  • Remove items
  • Equip weapons
  • Equip armor
  • Use consumables
  • Inventory UI updates

Skills:

  • View skill trees
  • Unlock skills
  • Prerequisites enforced
  • Stat bonuses apply
  • Respec works

Shop:

  • Browse inventory
  • Purchase items
  • Insufficient gold handling
  • Transaction logging

Document Maintenance

Update this document as you complete tasks:

  • Mark tasks complete with
  • Add notes about implementation decisions
  • Update time estimates based on actual progress
  • Document any blockers or challenges

Good luck with Phase 4 implementation! 🚀