diff --git a/docs/PHASE4_COMBAT_IMPLEMENTATION.md b/docs/PHASE4_COMBAT_IMPLEMENTATION.md index 3872ff0..e696220 100644 --- a/docs/PHASE4_COMBAT_IMPLEMENTATION.md +++ b/docs/PHASE4_COMBAT_IMPLEMENTATION.md @@ -81,8 +81,8 @@ This phase implements the core combat and progression systems for Code of Conque | 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 | +| **Phase 4B** | 1-2 weeks | Skill Trees & Leveling | See [`/PHASE4b.md`](/PHASE4b.md) +| **Phase 4C** | 3-4 days | NPC Shop | [`/PHASE4c.md`](/PHASE4c.md) **Total Estimated Time:** 4-5 weeks (~140-175 hours) @@ -746,985 +746,13 @@ app.register_blueprint(combat_bp, url_prefix='/combat') --- + ## Phase 4B: Skill Trees & Leveling (Week 4) - -### Task 4.1: Verify Skill Tree Data (2 hours) - -**Objective:** Review skill system - -**Files to Review:** -- `/api/app/models/skills.py` - SkillNode, SkillTree, PlayerClass -- `/api/app/data/skills/` - Skill YAML files for all 8 classes - -**Verification Checklist:** -- [ ] Skill trees loaded from YAML -- [ ] Each class has 2 skill trees -- [ ] Each tree has 5 tiers -- [ ] Prerequisites work correctly -- [ ] Stat bonuses apply correctly - -**Acceptance Criteria:** -- All 8 classes have complete skill trees -- Unlock logic works -- Respec logic implemented - ---- - -### Task 4.2: Create Skill Tree Template (2 days / 16 hours) - -**Objective:** Visual skill tree UI - -**File:** `/public_web/templates/character/skills.html` - -**Layout:** - -``` -┌─────────────────────────────────────────────────────────────┐ -│ CHARACTER SKILL TREES │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ Skill Points Available: 5 [Respec] ($$$)│ -│ │ -│ ┌────────────────────────┐ ┌────────────────────────┐ │ -│ │ TREE 1: Combat │ │ TREE 2: Utility │ │ -│ ├────────────────────────┤ ├────────────────────────┤ │ -│ │ │ │ │ │ -│ │ Tier 5: [⬢] [⬢] │ │ Tier 5: [⬢] [⬢] │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ Tier 4: [⬢] [⬢] │ │ Tier 4: [⬢] [⬢] │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ Tier 3: [⬢] [⬢] │ │ Tier 3: [⬢] [⬢] │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ Tier 2: [✓] [⬢] │ │ Tier 2: [⬢] [✓] │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ Tier 1: [✓] [✓] │ │ Tier 1: [✓] [✓] │ │ -│ │ │ │ │ │ -│ └────────────────────────┘ └────────────────────────┘ │ -│ │ -│ Legend: [✓] Unlocked [⬡] Available [⬢] Locked │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -**Implementation:** - -```html -{% extends "base.html" %} - -{% block title %}Skill Trees - {{ character.name }}{% endblock %} - -{% block content %} -
-
-

{{ character.name }}'s Skill Trees

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

{{ tree.name }}

-

{{ tree.description }}

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

{{ skill.name }}

-

{{ skill.description }}

- -
- Bonuses: - -
- - {% if skill.prerequisite_skill_id %} -

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

- {% endif %} -
-``` - -**Acceptance Criteria:** -- Dual skill tree layout works -- 5 tiers × 2 nodes per tree displayed -- Locked/available/unlocked states visual -- Prerequisite lines drawn -- Hover shows tooltip -- Mobile responsive - ---- - -### Task 4.3: Skill Unlock HTMX (4 hours) - -**Objective:** Click to unlock skills - -**File:** `/public_web/app/views/skills.py` - -```python -""" -Skill Views - -Routes for skill tree UI. -""" - -from flask import Blueprint, render_template, request, g - -from app.services.api_client import APIClient, APIError -from app.utils.auth import require_auth -from app.utils.logging import get_logger - -logger = get_logger(__file__) - -skills_bp = Blueprint('skills', __name__) - - -@skills_bp.route('//tooltip', methods=['GET']) -@require_auth -def skill_tooltip(skill_id: str): - """Get skill tooltip (HTMX partial).""" - # Load skill data - # Return rendered tooltip - pass - - -@skills_bp.route('/characters//skills', methods=['GET']) -@require_auth -def character_skills(character_id: str): - """Display character skill trees.""" - api_client = APIClient() - - try: - # Get character - response = api_client.get(f'/characters/{character_id}') - character = response['result'] - - # Calculate respec cost - respec_cost = character['level'] * 100 - - return render_template( - 'character/skills.html', - character=character, - respec_cost=respec_cost - ) - - except APIError as e: - logger.error(f"Failed to load skills: {e}") - return render_template('partials/error.html', error=str(e)) - - -@skills_bp.route('/characters//skills/unlock', methods=['POST']) -@require_auth -def unlock_skill(character_id: str): - """Unlock skill (HTMX endpoint).""" - api_client = APIClient() - skill_id = request.form.get('skill_id') - - try: - # Unlock skill via API - response = api_client.post( - f'/characters/{character_id}/skills/unlock', - json={'skill_id': skill_id} - ) - - # Re-render skill trees - character = response['result']['character'] - respec_cost = character['level'] * 100 - - return render_template( - 'character/skills.html', - character=character, - respec_cost=respec_cost - ) - - except APIError as e: - logger.error(f"Failed to unlock skill: {e}") - return render_template('partials/error.html', error=str(e)) -``` - -**Acceptance Criteria:** -- Click available node unlocks skill -- Skill points decrease -- Stat bonuses apply immediately -- Prerequisites enforced -- UI updates without page reload - ---- - -### Task 4.4: Respec Functionality (4 hours) - -**Objective:** Respec button with confirmation - -**Implementation:** (in `skills_bp`) - -```python -@skills_bp.route('/characters//skills/respec', methods=['POST']) -@require_auth -def respec_skills(character_id: str): - """Respec all skills.""" - api_client = APIClient() - - try: - response = api_client.post(f'/characters/{character_id}/skills/respec') - character = response['result']['character'] - respec_cost = character['level'] * 100 - - return render_template( - 'character/skills.html', - character=character, - respec_cost=respec_cost, - message="Skills reset! All skill points refunded." - ) - - except APIError as e: - logger.error(f"Failed to respec: {e}") - return render_template('partials/error.html', error=str(e)) -``` - -**Acceptance Criteria:** -- Respec button costs gold -- Confirmation modal shown -- All skills reset -- Skill points refunded -- Gold deducted - ---- - -### Task 4.5: XP & Leveling System (1 day / 8 hours) - -**Objective:** Award XP after combat, level up grants skill points - -**File:** `/api/app/services/leveling_service.py` - -```python -""" -Leveling Service - -Manages XP gain and level ups. -""" - -from app.models.character import Character -from app.utils.logging import get_logger - -logger = get_logger(__file__) - - -class LevelingService: - """Service for XP and leveling.""" - - @staticmethod - def xp_required_for_level(level: int) -> int: - """ - Calculate XP required for a given level. - - Formula: 100 * (level ^ 2) - """ - return 100 * (level ** 2) - - @staticmethod - def award_xp(character: Character, xp_amount: int) -> dict: - """ - Award XP to character and check for level up. - - Args: - character: Character instance - xp_amount: XP to award - - Returns: - Dict with leveled_up, new_level, skill_points_gained - """ - character.experience += xp_amount - - leveled_up = False - levels_gained = 0 - - # Check for level ups (can level multiple times) - while character.experience >= LevelingService.xp_required_for_level(character.level + 1): - character.level += 1 - character.skill_points += 1 - levels_gained += 1 - leveled_up = True - - logger.info(f"Character {character.character_id} leveled up to {character.level}") - - return { - 'leveled_up': leveled_up, - 'new_level': character.level if leveled_up else None, - 'skill_points_gained': levels_gained, - 'xp_gained': xp_amount - } -``` - -**Update Combat Results Endpoint:** - -```python -# In /api/app/api/combat.py - -@combat_bp.route('//results', methods=['GET']) -@require_auth -def get_combat_results(combat_id: str): - """Get combat results with XP/loot.""" - combat_service = CombatService(get_appwrite_service()) - encounter = combat_service.get_encounter(combat_id) - - if encounter.status != CombatStatus.VICTORY: - return error_response("Combat not won", 400) - - # Calculate XP (based on enemy difficulty) - xp_gained = sum(enemy.level * 50 for enemy in encounter.combatants if not enemy.is_player) - - # Award XP to character - char_service = get_character_service() - character = char_service.get_character(encounter.character_id, g.user_id) - - from app.services.leveling_service import LevelingService - level_result = LevelingService.award_xp(character, xp_gained) - - # Award gold - gold_gained = sum(enemy.level * 25 for enemy in encounter.combatants if not enemy.is_player) - character.gold += gold_gained - - # Generate loot (TODO: implement loot tables) - loot = [] - - # Save character - char_service.update_character(character) - - return success_response({ - 'victory': True, - 'xp_gained': xp_gained, - 'gold_gained': gold_gained, - 'loot': loot, - 'level_up': level_result - }) -``` - -**Create Level Up Modal Template:** - -**File:** `/public_web/templates/game/partials/level_up_modal.html` - -```html - -``` - -**Acceptance Criteria:** -- XP awarded after combat victory -- Level up triggers at XP threshold -- Skill points granted on level up -- Level up modal shown -- Character stats increase - ---- +See [`/PHASE4b.md`](/PHASE4b.md) ## Phase 4C: NPC Shop (Days 15-18) +See [`/PHASE4c.md`](/PHASE4c.md) -### 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 %} -
-
-

🏪 {{ shop_name }}

-

Shopkeeper: {{ shopkeeper_name }}

-

Your Gold: {{ character.gold }}

-
- -
- {% for item_entry in inventory %} -
-
-

{{ item_entry.item.name }}

- {{ item_entry.price }} gold -
- -

{{ item_entry.item.description }}

- -
- {% if item_entry.item.item_type == 'weapon' %} - ⚔️ Damage: {{ item_entry.item.damage }} - {% elif item_entry.item.item_type == 'armor' %} - 🛡️ Defense: {{ item_entry.item.defense }} - {% elif item_entry.item.item_type == 'consumable' %} - ❤️ Restores: {{ item_entry.item.hp_restore }} HP - {% endif %} -
- - -
- {% endfor %} -
-
-{% 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