## 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 ---