updating docs

This commit is contained in:
2025-11-27 11:50:06 -06:00
parent 58f0c1b8f6
commit 19b537d8b0

View File

@@ -81,8 +81,8 @@ This phase implements the core combat and progression systems for Code of Conque
| Sub-Phase | Duration | Focus | | Sub-Phase | Duration | Focus |
|-----------|----------|-------| |-----------|----------|-------|
| **Phase 4A** | 2-3 weeks | Combat Foundation | | **Phase 4A** | 2-3 weeks | Combat Foundation |
| **Phase 4B** | 1-2 weeks | Skill Trees & Leveling | | **Phase 4B** | 1-2 weeks | Skill Trees & Leveling | See [`/PHASE4b.md`](/PHASE4b.md)
| **Phase 4C** | 3-4 days | NPC Shop | | **Phase 4C** | 3-4 days | NPC Shop | [`/PHASE4c.md`](/PHASE4c.md)
**Total Estimated Time:** 4-5 weeks (~140-175 hours) **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) ## Phase 4B: Skill Trees & Leveling (Week 4)
See [`/PHASE4b.md`](/PHASE4b.md)
### 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) ## 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 %}
<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 ## Success Criteria - Phase 4 Complete