Commit
This commit is contained in:
561
api/routers/crafting.py
Normal file
561
api/routers/crafting.py
Normal file
@@ -0,0 +1,561 @@
|
||||
"""
|
||||
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))
|
||||
Reference in New Issue
Block a user