Files
echoes-of-the-ash/api/routers/trade.py
2026-02-08 20:18:42 +01:00

235 lines
9.4 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Body
from typing import Dict, List, Any, Optional
import time
import json
import logging
from ..core.security import get_current_user
from .. import database as db
from ..items import ItemsManager
router = APIRouter(
prefix="/api/trade",
tags=["trade"],
responses={404: {"description": "Not found"}},
)
logger = logging.getLogger(__name__)
ITEMS_MANAGER = None
NPCS_DATA = {}
def init_router_dependencies(items_manager: ItemsManager, npcs_data: Dict):
global ITEMS_MANAGER, NPCS_DATA
ITEMS_MANAGER = items_manager
NPCS_DATA = npcs_data
@router.get("/{npc_id}")
async def get_trade_stock(npc_id: str, current_user: dict = Depends(get_current_user)):
"""Get NPC stock and trade config"""
npc_def = NPCS_DATA.get(npc_id)
if not npc_def or not npc_def.get('trade', {}).get('enabled'):
raise HTTPException(status_code=404, detail="Merchant not found or trade disabled")
stock_db = await db.get_merchant_stock(npc_id)
stock_config = npc_def['trade'].get('stock', [])
# Merge DB stock with infinite items from config
final_stock = []
# Map DB items
db_items_map = {}
for item in stock_db:
# Resolve item details
item_def = ITEMS_MANAGER.get_item(item['item_id'])
if item_def:
item_data = {
"item_id": item['item_id'],
"name": item_def.name,
"emoji": item_def.emoji,
"quantity": item['quantity'],
"value": item_def.value, # Base value
"unique_item_id": item.get('unique_item_id'),
"description": item_def.description,
"image_path": item_def.image_path,
"tier": item_def.tier,
"item_type": item_def.type,
"weight": item_def.weight,
"volume": item_def.volume,
"stats": item_def.stats,
"effects": item_def.effects
}
# Handle unique item stats if needed (would need to fetch unique_item table)
# For now assuming standard items mostly
final_stock.append(item_data)
db_items_map[item['item_id']] = True
# Add infinite items from config if not in DB (or valid placeholders)
for cfg_item in stock_config:
if cfg_item.get('infinite'):
item_def = ITEMS_MANAGER.get_item(cfg_item['item_id'])
if item_def:
final_stock.append({
"item_id": cfg_item['item_id'],
"name": item_def.name,
"emoji": item_def.emoji,
"quantity": 9999,
"is_infinite": True,
"value": item_def.value,
"description": item_def.description,
"image_path": item_def.image_path,
"tier": item_def.tier,
"item_type": item_def.type,
"weight": item_def.weight,
"volume": item_def.volume,
"stats": item_def.stats,
"effects": item_def.effects
})
return {
"config": npc_def['trade'],
"stock": final_stock
}
@router.post("/{npc_id}/execute")
async def execute_trade(
npc_id: str,
payload: Dict = Body(...),
current_user: dict = Depends(get_current_user)
):
"""
Execute a trade.
Payload: {
"buying": [{"item_id": "water", "quantity": 1}],
"selling": [{"item_id": "junk", "quantity": 1}]
}
"""
character_id = current_user['id']
npc_def = NPCS_DATA.get(npc_id)
if not npc_def:
raise HTTPException(status_code=404, detail="NPC not found")
trade_cfg = npc_def.get('trade', {})
if not trade_cfg.get('enabled'):
raise HTTPException(status_code=400, detail="Trade disabled")
buying = payload.get('buying', [])
selling = payload.get('selling', [])
# Validate items and calculate value
total_buy_value = 0
total_sell_value = 0
# check player inventory for selling
player_inventory = await db.get_inventory(character_id)
buy_markup = trade_cfg.get('buy_markup', 1.0)
sell_markdown = trade_cfg.get('sell_markdown', 1.0)
# PROCESS SELLING (Player -> NPC)
items_to_remove = []
for sell_item in selling:
item_id = sell_item['item_id']
qty = sell_item['quantity']
unique_id = sell_item.get('unique_item_id')
# Verify player has item
inv_item = next((i for i in player_inventory if i['item_id'] == item_id and i.get('unique_item_id') == unique_id), None)
if not inv_item or inv_item['quantity'] < qty:
raise HTTPException(status_code=400, detail=f"Not enough {item_id} to sell")
item_def = ITEMS_MANAGER.get_item(item_id)
value = (item_def.value * sell_markdown) * qty
total_sell_value += value
items_to_remove.append((item_id, qty, unique_id))
# PROCESS BUYING (NPC -> Player)
items_to_add = []
db_stock = await db.get_merchant_stock(npc_id)
for buy_item in buying:
item_id = buy_item['item_id']
qty = buy_item['quantity']
unique_id = buy_item.get('unique_item_id') # For unique items from stock
# Verify NPC has item (unless infinite)
is_infinite = False
config_entry = next((c for c in trade_cfg.get('stock', []) if c['item_id'] == item_id), None)
if config_entry and config_entry.get('infinite'):
is_infinite = True
if not is_infinite:
stock_item = next((s for s in db_stock if s['item_id'] == item_id and s.get('unique_item_id') == unique_id), None)
if not stock_item or stock_item['quantity'] < qty:
raise HTTPException(status_code=400, detail=f"Merchant out of stock: {item_id}")
item_def = ITEMS_MANAGER.get_item(item_id)
value = (item_def.value * buy_markup) * qty
total_buy_value += value
items_to_add.append((item_id, qty, unique_id))
# VALIDATE VALUE
# If using 'value' currency, trades must balance OR player pays difference if we implemented currency items
# For now assuming pure barter or abstract credit if we had it.
# Plan says: "currency": "value", "unlimited_currency": true
# This implies player can Sell for "credit" in this transaction to Buy other things.
# Usually in barter: Sell Value >= Buy Value. If Sell > Buy, player loses difference (or we assume "value" credits are not stored).
# Re-reading: "Trade button active only if Player Value >= NPC Value".
if total_sell_value < total_buy_value:
raise HTTPException(status_code=400, detail="Trade value too low. Offer more items.")
# EXECUTE TRADE
# 1. Remove sold items from Player
for item_id, qty, unique_id in items_to_remove:
await db.remove_item_from_inventory(character_id, item_id, qty) # Need to handle unique_id in remove?
# remove_item_inventory in db currently takes player_id, item_id, qty.
# It doesn't handle unique_id specific removal yet?
# Checking db.py... remove_item_from_inventory isn't fully robust for unique items in the snippet I saw?
# Wait, I strictly need to fix db.remove_item_from_inventory or use a more specific query if unique.
# Assuming for now stackables are main concern. For uniques, quantity is 1.
# If unique_id is passed, we should delete that specific row in inventory.
# I'll implement a fallback db call here if needed or assume standard remove works for stackables.
pass
# 2. Add sold items to NPC (if keep_sold_items)
if trade_cfg.get('keep_sold_items'):
for item_id, qty, unique_id in items_to_remove:
# Add to merchant stock
# If unique, pass unique_id
# Logic to find existing row or create new
current_stock = await db.get_merchant_stock_item(npc_id, item_id, unique_id)
old_qty = current_stock['quantity'] if current_stock else 0
await db.update_merchant_stock(npc_id, item_id, old_qty + qty, unique_id)
# 3. Remove bought items from NPC (if not infinite)
for item_id, qty, unique_id in items_to_add:
is_infinite = False
config_entry = next((c for c in trade_cfg.get('stock', []) if c['item_id'] == item_id), None)
if config_entry and config_entry.get('infinite'):
is_infinite = True
if not is_infinite:
current_stock = await db.get_merchant_stock_item(npc_id, item_id, unique_id)
if current_stock:
new_qty = current_stock['quantity'] - qty
await db.update_merchant_stock(npc_id, item_id, new_qty, unique_id)
# 4. Add bought items to Player
for item_id, qty, unique_id in items_to_add:
# If buying unique item from NPC, it transfers ownership.
# If infinite, it creates new item?
# If unique_id exists (buying specific unique item)
if unique_id and not is_infinite:
await db.add_item_to_inventory(character_id, item_id, qty, unique_item_id=unique_id)
else:
# Standard or infinite
await db.add_item_to_inventory(character_id, item_id, qty)
# Log statistics?
return {"success": True, "message": "Trade completed"}