""" Crafting router. Auto-generated from main.py migration. """ from fastapi import APIRouter, HTTPException, Depends, status from fastapi.security import HTTPAuthorizationCredentials from typing import Optional, Dict, Any from datetime import datetime import random import json import logging from ..core.security import get_current_user, security, verify_internal_key from ..services.models import * from ..services.helpers import calculate_distance, calculate_stamina_cost, calculate_player_capacity, calculate_crafting_stamina_cost, get_locale_string from .. import database as db from ..items import ItemsManager from .. import game_logic from ..core.websockets import manager from .equipment import consume_tool_durability logger = logging.getLogger(__name__) # These will be injected by main.py LOCATIONS = None ITEMS_MANAGER = None WORLD = None def init_router_dependencies(locations, items_manager, world): """Initialize router with game data dependencies""" global LOCATIONS, ITEMS_MANAGER, WORLD LOCATIONS = locations ITEMS_MANAGER = items_manager WORLD = world router = APIRouter(tags=["crafting"]) # Endpoints @router.get("/api/game/craftable") async def get_craftable_items(current_user: dict = Depends(get_current_user)): """Get all craftable items with material requirements and availability""" try: player = current_user # current_user is already the character dict if not player: raise HTTPException(status_code=404, detail="Player not found") # Get player's inventory with quantities inventory = await db.get_inventory(current_user['id']) inventory_counts = {} for inv_item in inventory: item_id = inv_item['item_id'] quantity = inv_item.get('quantity', 1) inventory_counts[item_id] = inventory_counts.get(item_id, 0) + quantity craftable_items = [] for item_id, item_def in ITEMS_MANAGER.items.items(): if not getattr(item_def, 'craftable', False): continue craft_materials = getattr(item_def, 'craft_materials', []) if not craft_materials: continue # Check material availability materials_info = [] can_craft = True for material in craft_materials: mat_item_id = material['item_id'] required = material['quantity'] available = inventory_counts.get(mat_item_id, 0) mat_item_def = ITEMS_MANAGER.items.get(mat_item_id) materials_info.append({ 'item_id': mat_item_id, 'name': mat_item_def.name if mat_item_def else mat_item_id, 'emoji': mat_item_def.emoji if mat_item_def else '📦', 'required': required, 'available': available, 'has_enough': available >= required }) if available < required: can_craft = False # Check tool requirements craft_tools = getattr(item_def, 'craft_tools', []) tools_info = [] for tool_req in craft_tools: tool_id = tool_req['item_id'] durability_cost = tool_req['durability_cost'] tool_def = ITEMS_MANAGER.items.get(tool_id) # Check if player has this tool has_tool = False tool_durability = 0 for inv_item in inventory: # Check if player has this tool (find one with highest durability) has_tool = False tool_durability = 0 best_tool_unique = None for inv_item in inventory: if inv_item['item_id'] == tool_id and inv_item.get('unique_item_id'): unique = await db.get_unique_item(inv_item['unique_item_id']) if unique and unique.get('durability', 0) >= durability_cost: if best_tool_unique is None or unique.get('durability', 0) > best_tool_unique.get('durability', 0): best_tool_unique = unique has_tool = True tool_durability = unique.get('durability', 0) tools_info.append({ 'item_id': tool_id, 'name': tool_def.name if tool_def else tool_id, 'emoji': tool_def.emoji if tool_def else '🔧', 'durability_cost': durability_cost, 'has_tool': has_tool, 'tool_durability': tool_durability }) if not has_tool: can_craft = False # Check level requirement craft_level = getattr(item_def, 'craft_level', 1) player_level = player.get('level', 1) meets_level = player_level >= craft_level # Don't show recipes above player level if player_level < craft_level: continue if not meets_level: can_craft = False craftable_items.append({ 'item_id': item_id, 'name': item_def.name, 'emoji': item_def.emoji, 'description': item_def.description, 'tier': getattr(item_def, 'tier', 1), 'type': item_def.type, 'category': item_def.type, # Add category for filtering 'slot': getattr(item_def, 'slot', None), 'materials': materials_info, 'tools': tools_info, 'craft_level': craft_level, 'meets_level': meets_level, 'uncraftable': getattr(item_def, 'uncraftable', False), 'can_craft': can_craft, 'stamina_cost': calculate_crafting_stamina_cost(getattr(item_def, 'tier', 1), 'craft'), 'stamina_cost': calculate_crafting_stamina_cost(getattr(item_def, 'tier', 1), 'craft'), 'base_stats': {k: int(v) if isinstance(v, (int, float)) else v for k, v in getattr(item_def, 'stats', {}).items()} }) # Sort: craftable items first, then by tier, then by name craftable_items.sort(key=lambda x: (not x['can_craft'], x['tier'], get_locale_string(x['name']))) return {'craftable_items': craftable_items} except Exception as e: print(f"Error getting craftable items: {e}") import traceback traceback.print_exc() raise HTTPException(status_code=500, detail=str(e)) class CraftItemRequest(BaseModel): item_id: str @router.post("/api/game/craft_item") async def craft_item(request: CraftItemRequest, current_user: dict = Depends(get_current_user)): """Craft an item, consuming materials and creating item with random stats for unique items""" try: player = current_user # current_user is already the character dict if not player: raise HTTPException(status_code=404, detail="Player not found") location_id = player['location_id'] location = LOCATIONS.get(location_id) # Check if player is at a workbench if not location or 'workbench' not in getattr(location, 'tags', []): raise HTTPException(status_code=400, detail="You must be at a workbench to craft items") # Get item definition item_def = ITEMS_MANAGER.items.get(request.item_id) if not item_def: raise HTTPException(status_code=404, detail="Item not found") if not getattr(item_def, 'craftable', False): raise HTTPException(status_code=400, detail="This item cannot be crafted") # Check level requirement craft_level = getattr(item_def, 'craft_level', 1) player_level = player.get('level', 1) if player_level < craft_level: raise HTTPException(status_code=400, detail=f"You need to be level {craft_level} to craft this item (you are level {player_level})") craft_materials = getattr(item_def, 'craft_materials', []) if not craft_materials: raise HTTPException(status_code=400, detail="No crafting recipe found") # Check if player has all materials inventory = await db.get_inventory(current_user['id']) inventory_counts = {} inventory_items_map = {} for inv_item in inventory: item_id = inv_item['item_id'] quantity = inv_item.get('quantity', 1) inventory_counts[item_id] = inventory_counts.get(item_id, 0) + quantity if item_id not in inventory_items_map: inventory_items_map[item_id] = [] inventory_items_map[item_id].append(inv_item) # Check tools requirement craft_tools = getattr(item_def, 'craft_tools', []) if craft_tools: success, error_msg, tools_consumed = await consume_tool_durability(current_user['id'], craft_tools, inventory) if not success: raise HTTPException(status_code=400, detail=error_msg) else: tools_consumed = [] # Verify all materials are available for material in craft_materials: required = material['quantity'] available = inventory_counts.get(material['item_id'], 0) if available < required: raise HTTPException( status_code=400, detail=f"Not enough {material['item_id']}. Need {required}, have {available}" ) # Consume materials materials_used = [] for material in craft_materials: item_id = material['item_id'] quantity_needed = material['quantity'] items_of_type = inventory_items_map[item_id] for inv_item in items_of_type: if quantity_needed <= 0: break inv_quantity = inv_item.get('quantity', 1) to_remove = min(quantity_needed, inv_quantity) if inv_quantity > to_remove: # Update quantity await db.update_inventory_item( inv_item['id'], quantity=inv_quantity - to_remove ) else: # Remove entire stack - use item_id string, not inventory row id await db.remove_item_from_inventory(current_user['id'], item_id, to_remove) quantity_needed -= to_remove mat_item_def = ITEMS_MANAGER.items.get(item_id) materials_used.append({ 'item_id': item_id, 'name': mat_item_def.name if mat_item_def else item_id, 'quantity': material['quantity'] }) # Calculate stamina cost stamina_cost = calculate_crafting_stamina_cost(getattr(item_def, 'tier', 1), 'craft') # Check stamina if player['stamina'] < stamina_cost: raise HTTPException(status_code=400, detail=f"Not enough stamina. Need {stamina_cost}, have {player['stamina']}") # Deduct stamina new_stamina = max(0, player['stamina'] - stamina_cost) await db.update_player_stamina(current_user['id'], new_stamina) # Generate random stats for unique items import random created_item = None if hasattr(item_def, 'durability') and item_def.durability: # This is a unique item - generate random stats base_durability = item_def.durability # Random durability: 90-110% of base random_durability = int(base_durability * random.uniform(0.9, 1.1)) # Generate tier based on durability roll durability_percent = (random_durability / base_durability) if durability_percent >= 1.08: tier = 5 # Gold elif durability_percent >= 1.04: tier = 4 # Purple elif durability_percent >= 1.0: tier = 3 # Blue elif durability_percent >= 0.96: tier = 2 # Green else: tier = 1 # White # Generate random stats if item has stats random_stats = {} if hasattr(item_def, 'stats') and item_def.stats: for stat_key, stat_value in item_def.stats.items(): if isinstance(stat_value, (int, float)): # Random stat: 90-110% of base random_stats[stat_key] = int(stat_value * random.uniform(0.9, 1.1)) else: random_stats[stat_key] = stat_value # Create unique item in database unique_item_id = await db.create_unique_item( item_id=request.item_id, durability=random_durability, max_durability=random_durability, tier=tier, unique_stats=random_stats ) # Add to inventory await db.add_item_to_inventory( player_id=current_user['id'], item_id=request.item_id, quantity=1, unique_item_id=unique_item_id ) created_item = { 'item_id': request.item_id, 'name': item_def.name, 'emoji': item_def.emoji, 'tier': tier, 'durability': random_durability, 'max_durability': random_durability, 'stats': random_stats, 'unique': True } else: # Stackable item - just add to inventory await db.add_item_to_inventory( player_id=current_user['id'], item_id=request.item_id, quantity=1 ) created_item = { 'item_id': request.item_id, 'name': item_def.name, 'emoji': item_def.emoji, 'tier': getattr(item_def, 'tier', 1), 'unique': False } return { 'success': True, 'message': f"Successfully crafted {item_def.name}!", 'item': created_item, 'materials_consumed': materials_used, 'tools_consumed': tools_consumed, 'stamina_cost': stamina_cost, 'new_stamina': new_stamina } except Exception as e: print(f"Error crafting item: {e}") import traceback traceback.print_exc() raise HTTPException(status_code=500, detail=str(e)) class UncraftItemRequest(BaseModel): inventory_id: int quantity: int = 1 @router.post("/api/game/uncraft_item") async def uncraft_item(request: UncraftItemRequest, current_user: dict = Depends(get_current_user)): """Uncraft an item, returning materials with a chance of loss""" try: player = current_user # current_user is already the character dict if not player: raise HTTPException(status_code=404, detail="Player not found") location_id = player['location_id'] location = LOCATIONS.get(location_id) # Check if player is at a workbench if not location or 'workbench' not in getattr(location, 'tags', []): raise HTTPException(status_code=400, detail="You must be at a workbench to uncraft items") # Get inventory item inventory = await db.get_inventory(current_user['id']) inv_item = None for item in inventory: if item['id'] == request.inventory_id: inv_item = item break if not inv_item: raise HTTPException(status_code=404, detail="Item not found in inventory") # Check quantity if request.quantity <= 0: raise HTTPException(status_code=400, detail="Quantity must be greater than 0") current_quantity = inv_item.get('quantity', 1) if request.quantity > current_quantity: raise HTTPException(status_code=400, detail=f"Not enough items. Have {current_quantity}, requested {request.quantity}") # Get item definition item_def = ITEMS_MANAGER.items.get(inv_item['item_id']) if not item_def: raise HTTPException(status_code=404, detail="Item definition not found") if not getattr(item_def, 'uncraftable', False): raise HTTPException(status_code=400, detail="This item cannot be uncrafted") uncraft_yield = getattr(item_def, 'uncraft_yield', []) if not uncraft_yield: raise HTTPException(status_code=400, detail="No uncraft recipe found") # Check tools requirement (once per operation? or per item?) # Usually tools are checked once for the operation, but durability cost might be per item. # Logic above for crafting consumes tool durability for the batch? # In craft_item above, it loops through craft_tools but seemingly only once? # Wait, craft_item does NOT loop for quantity because craft_item only crafts 1 at a time (request has no quantity). # For uncrafting multiple, we should multiply tool cost. uncraft_tools = getattr(item_def, 'uncraft_tools', []) tools_consumed = [] if uncraft_tools: # Scale tool cost by quantity scaled_uncraft_tools = [] for tool_req in uncraft_tools: scaled_req = tool_req.copy() scaled_req['durability_cost'] = tool_req['durability_cost'] * request.quantity scaled_uncraft_tools.append(scaled_req) success, error_msg, tools_consumed = await consume_tool_durability(current_user['id'], scaled_uncraft_tools, inventory) if not success: raise HTTPException(status_code=400, detail=error_msg) # Calculate stamina cost base_stamina_cost = calculate_crafting_stamina_cost(getattr(item_def, 'tier', 1), 'uncraft') total_stamina_cost = base_stamina_cost * request.quantity # Check stamina if player['stamina'] < total_stamina_cost: raise HTTPException(status_code=400, detail=f"Not enough stamina. Need {total_stamina_cost}, have {player['stamina']}") # Deduct stamina new_stamina = max(0, player['stamina'] - total_stamina_cost) await db.update_player_stamina(current_user['id'], new_stamina) # Update inventory item if request.quantity == current_quantity: # Remove the item row entirely await db.remove_inventory_row(inv_item['id']) else: # Update quantity await db.update_inventory_item( inv_item['id'], quantity=current_quantity - request.quantity ) # Calculate durability ratio for yield reduction durability_ratio = 1.0 # Default: full yield if inv_item.get('unique_item_id'): unique_item = await db.get_unique_item(inv_item['unique_item_id']) if unique_item: current_durability = unique_item.get('durability', 0) max_durability = unique_item.get('max_durability', 1) if max_durability > 0: durability_ratio = current_durability / max_durability # Re-fetch inventory to get updated capacity inventory = await db.get_inventory(current_user['id']) current_weight, max_weight, current_volume, max_volume = await calculate_player_capacity(inventory, ITEMS_MANAGER) # Calculate materials import random loss_chance = getattr(item_def, 'uncraft_loss_chance', 0.3) materials_yielded_dict = {} materials_lost_dict = {} materials_dropped_dict = {} # Loop for each item being uncrafted to calculate yield fairly for _ in range(request.quantity): for material in uncraft_yield: # Apply durability reduction first base_quantity = material['quantity'] # Calculate adjusted quantity based on durability adjusted_quantity = int(round(base_quantity * durability_ratio)) mat_def = ITEMS_MANAGER.items.get(material['item_id']) mat_name = mat_def.name if mat_def else material['item_id'] loss_key = (material['item_id'], mat_name) # If durability is too low (< 10%), yield nothing for this material if durability_ratio < 0.1 or adjusted_quantity <= 0: if loss_key not in materials_lost_dict: materials_lost_dict[loss_key] = 0 materials_lost_dict[loss_key] += base_quantity continue # Roll for loss chance if random.random() < loss_chance: # Lost this material if loss_key not in materials_lost_dict: materials_lost_dict[loss_key] = 0 materials_lost_dict[loss_key] += adjusted_quantity else: # Check if it fits in inventory (incremental check?) # For simplicity, check per unit or accumulate and check at end. # Checking per unit is safer but slower. # Since we are modifying inventory in loop (potentially), we should be careful. # Actually, we should accumulate yield then add to inventory at end to optimize DB calls? # But we need to check capacity. # Let's accumulate pending yield. yield_key = (material['item_id'], mat_name, mat_def.emoji if mat_def else '📦', mat_def) if yield_key not in materials_yielded_dict: materials_yielded_dict[yield_key] = 0 materials_yielded_dict[yield_key] += adjusted_quantity # Now process the accumulated yield materials_yielded = [] materials_lost = [] materials_dropped = [] # Convert lost dict to list for (item_id, name), qty in materials_lost_dict.items(): materials_lost.append({ 'item_id': item_id, 'name': name, 'quantity': qty, 'reason': 'lost_or_low_durability' }) # Process yield for (item_id, name, emoji, mat_def), qty in materials_yielded_dict.items(): mat_weight = getattr(mat_def, 'weight', 0) * qty mat_volume = getattr(mat_def, 'volume', 0) * qty # Simple check against capacity (assuming current_weight was just updated from DB) # Note: we might fill up mid-loop. ideally we add one by one or check total. # Let's check total. if current_weight + mat_weight <= max_weight and current_volume + mat_volume <= max_volume: # Fits await db.add_item_to_inventory( player_id=current_user['id'], item_id=item_id, quantity=qty ) current_weight += mat_weight current_volume += mat_volume materials_yielded.append({ 'item_id': item_id, 'name': name, 'emoji': emoji, 'quantity': qty }) else: # Drop await db.drop_item_to_world( item_id=item_id, quantity=qty, location_id=player['location_id'] ) materials_dropped.append({ 'item_id': item_id, 'name': name, 'emoji': emoji, 'quantity': qty }) message = f"Uncrafted {request.quantity}x {item_def.name}!" if durability_ratio < 1.0: message += f" (Condition reduced yield)" if materials_lost: message += f" Lost materials." if materials_dropped: message += f" Inventory full! Dropped items." return { 'success': True, 'message': message, 'item_name': item_def.name, 'materials_yielded': materials_yielded, 'materials_lost': materials_lost, 'materials_dropped': materials_dropped, 'tools_consumed': tools_consumed, 'loss_chance': loss_chance, 'durability_ratio': round(durability_ratio, 2), 'stamina_cost': total_stamina_cost, 'new_stamina': new_stamina } except Exception as e: print(f"Error uncrafting item: {e}") import traceback traceback.print_exc() raise HTTPException(status_code=500, detail=str(e))