updating docs
This commit is contained in:
513
docs/Phase4c.md
Normal file
513
docs/Phase4c.md
Normal file
@@ -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 %}
|
||||
<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
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user