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"}