235 lines
9.4 KiB
Python
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"}
|