From 94c4ca9e9531af94b57d62f9150a9c5e84ca7b69 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Thu, 27 Nov 2025 11:51:21 -0600 Subject: [PATCH] updating docs --- docs/PHASE4b.md | 467 +++++++++++++++++++++++++++++++++++++++++++ docs/Phase4c.md | 513 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 980 insertions(+) create mode 100644 docs/PHASE4b.md create mode 100644 docs/Phase4c.md diff --git a/docs/PHASE4b.md b/docs/PHASE4b.md new file mode 100644 index 0000000..9be4ba9 --- /dev/null +++ b/docs/PHASE4b.md @@ -0,0 +1,467 @@ + +## 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: +
    + {% for stat, bonus in skill.stat_bonuses.items() %} +
  • +{{ bonus }} {{ stat|title }}
  • + {% endfor %} +
+
+ + {% 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 + +--- diff --git a/docs/Phase4c.md b/docs/Phase4c.md new file mode 100644 index 0000000..0f6485d --- /dev/null +++ b/docs/Phase4c.md @@ -0,0 +1,513 @@ + +## 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 %} +
+
+

🏪 {{ 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 + +---