Files
Code_of_Conquest/docs/PHASE4_COMBAT_IMPLEMENTATION.md

1837 lines
56 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 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:**
```html
{% 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:**
```python
"""
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`:**
```python
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:**
```html
{# 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`:**
```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:**
```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
---
## 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`
```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`
```python
"""
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`:**
```python
"""
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`
```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`:**
```python
"""
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`
```python
"""
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:**
```python
# 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!** 🚀