561 lines
23 KiB
Python
561 lines
23 KiB
Python
"""
|
|
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
|
|
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'], 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)) |