""" 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 @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") # 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 uncraft_tools = getattr(item_def, 'uncraft_tools', []) if uncraft_tools: success, error_msg, tools_consumed = await consume_tool_durability(current_user['id'], uncraft_tools, inventory) if not success: raise HTTPException(status_code=400, detail=error_msg) else: tools_consumed = [] # Calculate stamina cost stamina_cost = calculate_crafting_stamina_cost(getattr(item_def, 'tier', 1), 'uncraft') # 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) # Remove the item from inventory # Use remove_inventory_row since we have the inventory ID await db.remove_inventory_row(inv_item['id']) # 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 after removing the item 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 with loss chance and durability reduction import random loss_chance = getattr(item_def, 'uncraft_loss_chance', 0.3) yield_info = { 'base_yield': uncraft_yield, 'loss_chance': loss_chance, 'stamina_cost': calculate_crafting_stamina_cost(getattr(item_def, 'tier', 1), 'uncraft') } materials_yielded = [] materials_lost = [] materials_dropped = [] for material in uncraft_yield: # Apply durability reduction first base_quantity = material['quantity'] # Calculate adjusted quantity based on durability # Use round() to ensure minimum yield of 1 for high durability items (e.g. 90% of 1 = 0.9 -> 1) adjusted_quantity = int(round(base_quantity * durability_ratio)) mat_def = ITEMS_MANAGER.items.get(material['item_id']) # If durability is too low (< 10%), yield nothing for this material if durability_ratio < 0.1 or adjusted_quantity <= 0: materials_lost.append({ 'item_id': material['item_id'], 'name': mat_def.name if mat_def else material['item_id'], 'quantity': base_quantity, 'reason': 'durability_too_low' }) continue # Roll for each material separately with loss chance if random.random() < loss_chance: # Lost this material materials_lost.append({ 'item_id': material['item_id'], 'name': mat_def.name if mat_def else material['item_id'], 'quantity': adjusted_quantity, 'reason': 'random_loss' }) else: # Check if it fits in inventory mat_weight = getattr(mat_def, 'weight', 0) * adjusted_quantity mat_volume = getattr(mat_def, 'volume', 0) * adjusted_quantity if current_weight + mat_weight <= max_weight and current_volume + mat_volume <= max_volume: # Fits in inventory await db.add_item_to_inventory( player_id=current_user['id'], item_id=material['item_id'], quantity=adjusted_quantity ) # Update current capacity tracking current_weight += mat_weight current_volume += mat_volume materials_yielded.append({ 'item_id': material['item_id'], 'name': mat_def.name if mat_def else material['item_id'], 'emoji': mat_def.emoji if mat_def else '📦', 'quantity': adjusted_quantity }) else: # Inventory full - drop to ground await db.drop_item_to_world( item_id=material['item_id'], quantity=adjusted_quantity, location_id=player['location_id'] ) materials_dropped.append({ 'item_id': material['item_id'], 'name': mat_def.name if mat_def else material['item_id'], 'emoji': mat_def.emoji if mat_def else '📦', 'quantity': adjusted_quantity }) message = f"Uncrafted {item_def.name}!" if durability_ratio < 1.0: message += f" (Item condition reduced yield by {int((1 - durability_ratio) * 100)}%)" if materials_lost: message += f" Lost {len(materials_lost)} material type(s)." if materials_dropped: message += f" Inventory full! Dropped {len(materials_dropped)} item(s) to the ground." 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': 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))