783 lines
33 KiB
Python
783 lines
33 KiB
Python
"""
|
|
Equipment 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
|
|
|
|
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=["equipment"])
|
|
|
|
|
|
|
|
# Endpoints
|
|
|
|
@router.post("/api/game/equip")
|
|
async def equip_item(
|
|
equip_req: EquipItemRequest,
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""Equip an item from inventory"""
|
|
player_id = current_user['id']
|
|
|
|
# Get the inventory item
|
|
inv_item = await db.get_inventory_item_by_id(equip_req.inventory_id)
|
|
if not inv_item or inv_item['character_id'] != player_id:
|
|
raise HTTPException(status_code=404, detail="Item not found in inventory")
|
|
|
|
# Get item definition
|
|
item_def = ITEMS_MANAGER.get_item(inv_item['item_id'])
|
|
if not item_def:
|
|
raise HTTPException(status_code=404, detail="Item definition not found")
|
|
|
|
# Check if item is equippable
|
|
if not item_def.equippable or not item_def.slot:
|
|
raise HTTPException(status_code=400, detail="This item cannot be equipped")
|
|
|
|
# Check if slot is valid
|
|
valid_slots = ['head', 'torso', 'legs', 'feet', 'weapon', 'offhand', 'backpack']
|
|
if item_def.slot not in valid_slots:
|
|
raise HTTPException(status_code=400, detail=f"Invalid equipment slot: {item_def.slot}")
|
|
|
|
# Check if slot is already occupied
|
|
current_equipped = await db.get_equipped_item_in_slot(player_id, item_def.slot)
|
|
unequipped_item_name = None
|
|
|
|
if current_equipped and current_equipped.get('item_id'):
|
|
# Get the old item's name for the message
|
|
old_inv_item = await db.get_inventory_item_by_id(current_equipped['item_id'])
|
|
if old_inv_item:
|
|
old_item_def = ITEMS_MANAGER.get_item(old_inv_item['item_id'])
|
|
unequipped_item_name = old_item_def.name if old_item_def else "previous item"
|
|
|
|
# Unequip current item first
|
|
await db.unequip_item(player_id, item_def.slot)
|
|
# Mark as not equipped in inventory
|
|
await db.update_inventory_item(current_equipped['item_id'], is_equipped=False)
|
|
|
|
# Equip the new item
|
|
await db.equip_item(player_id, item_def.slot, equip_req.inventory_id)
|
|
|
|
# Mark as equipped in inventory
|
|
await db.update_inventory_item(equip_req.inventory_id, is_equipped=True)
|
|
|
|
# Initialize unique_item if this is first time equipping an equippable with durability
|
|
if inv_item.get('unique_item_id') is None and item_def.durability:
|
|
# Create a unique_item instance for this equipment
|
|
# Save base stats to unique_stats
|
|
base_stats = {k: int(v) if isinstance(v, (int, float)) else v for k, v in item_def.stats.items()} if item_def.stats else {}
|
|
unique_item_id = await db.create_unique_item(
|
|
item_id=item_def.id,
|
|
durability=item_def.durability,
|
|
max_durability=item_def.durability,
|
|
tier=item_def.tier if hasattr(item_def, 'tier') else 1,
|
|
unique_stats=base_stats
|
|
)
|
|
# Link the inventory item to this unique_item
|
|
await db.update_inventory_item(
|
|
equip_req.inventory_id,
|
|
unique_item_id=unique_item_id
|
|
)
|
|
|
|
# Build message
|
|
if unequipped_item_name:
|
|
message = f"Unequipped {unequipped_item_name}, equipped {item_def.name}"
|
|
else:
|
|
message = f"Equipped {item_def.name}"
|
|
|
|
return {
|
|
"success": True,
|
|
"message": message,
|
|
"slot": item_def.slot,
|
|
"unequipped_item": unequipped_item_name
|
|
}
|
|
|
|
|
|
@router.post("/api/game/unequip")
|
|
async def unequip_item(
|
|
unequip_req: UnequipItemRequest,
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""Unequip an item from equipment slot"""
|
|
player_id = current_user['id']
|
|
|
|
# Check if slot is valid
|
|
valid_slots = ['head', 'torso', 'legs', 'feet', 'weapon', 'offhand', 'backpack']
|
|
if unequip_req.slot not in valid_slots:
|
|
raise HTTPException(status_code=400, detail=f"Invalid equipment slot: {unequip_req.slot}")
|
|
|
|
# Get currently equipped item
|
|
equipped = await db.get_equipped_item_in_slot(player_id, unequip_req.slot)
|
|
if not equipped:
|
|
raise HTTPException(status_code=400, detail=f"No item equipped in {unequip_req.slot} slot")
|
|
|
|
# Get inventory item and item definition
|
|
inv_item = await db.get_inventory_item_by_id(equipped['item_id'])
|
|
item_def = ITEMS_MANAGER.get_item(inv_item['item_id'])
|
|
|
|
# Check if inventory has space (volume-wise)
|
|
inventory = await db.get_inventory(player_id)
|
|
total_volume = sum(
|
|
ITEMS_MANAGER.get_item(i['item_id']).volume * i['quantity']
|
|
for i in inventory
|
|
if ITEMS_MANAGER.get_item(i['item_id']) and not i['is_equipped']
|
|
)
|
|
|
|
# Get max volume (base 10 + backpack bonus)
|
|
max_volume = 10.0
|
|
for inv in inventory:
|
|
if inv['is_equipped']:
|
|
item = ITEMS_MANAGER.get_item(inv['item_id'])
|
|
if item:
|
|
# Use unique_stats if this is a unique item, otherwise fall back to default stats
|
|
if inv.get('unique_item_id'):
|
|
unique_item = await db.get_unique_item(inv['unique_item_id'])
|
|
if unique_item and unique_item.get('unique_stats'):
|
|
max_volume += unique_item['unique_stats'].get('volume_capacity', 0)
|
|
elif item.stats:
|
|
max_volume += item.stats.get('volume_capacity', 0)
|
|
|
|
# If unequipping backpack, check if items will fit
|
|
if unequip_req.slot == 'backpack':
|
|
# Get the backpack's volume capacity from unique_stats if available
|
|
backpack_volume = 0
|
|
if inv_item.get('unique_item_id'):
|
|
unique_item = await db.get_unique_item(inv_item['unique_item_id'])
|
|
if unique_item and unique_item.get('unique_stats'):
|
|
backpack_volume = unique_item['unique_stats'].get('volume_capacity', 0)
|
|
elif item_def.stats:
|
|
backpack_volume = item_def.stats.get('volume_capacity', 0)
|
|
|
|
if backpack_volume > 0 and total_volume > (max_volume - backpack_volume):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Cannot unequip backpack: inventory would exceed volume capacity"
|
|
)
|
|
|
|
# Check if adding this item would exceed volume
|
|
if total_volume + item_def.volume > max_volume:
|
|
# Drop to ground instead
|
|
await db.unequip_item(player_id, unequip_req.slot)
|
|
await db.update_inventory_item(equipped['item_id'], is_equipped=False)
|
|
await db.drop_item(player_id, inv_item['item_id'], 1, current_user['location_id'])
|
|
await db.remove_from_inventory(player_id, inv_item['item_id'], 1)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Unequipped {item_def.name} (dropped to ground - inventory full)",
|
|
"dropped": True
|
|
}
|
|
|
|
# Unequip the item
|
|
await db.unequip_item(player_id, unequip_req.slot)
|
|
await db.update_inventory_item(equipped['item_id'], is_equipped=False)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Unequipped {item_def.name}",
|
|
"dropped": False
|
|
}
|
|
|
|
|
|
@router.get("/api/game/equipment")
|
|
async def get_equipment(current_user: dict = Depends(get_current_user)):
|
|
"""Get all equipped items"""
|
|
player_id = current_user['id']
|
|
|
|
equipment = await db.get_all_equipment(player_id)
|
|
|
|
# Enrich with item data
|
|
enriched = {}
|
|
for slot, item_data in equipment.items():
|
|
if item_data:
|
|
inv_item = await db.get_inventory_item_by_id(item_data['item_id'])
|
|
item_def = ITEMS_MANAGER.get_item(inv_item['item_id'])
|
|
if item_def:
|
|
enriched[slot] = {
|
|
"inventory_id": item_data['item_id'],
|
|
"item_id": item_def.id,
|
|
"name": item_def.name,
|
|
"description": item_def.description,
|
|
"emoji": item_def.emoji,
|
|
"image_path": item_def.image_path,
|
|
"durability": inv_item.get('durability'),
|
|
"max_durability": inv_item.get('max_durability'),
|
|
"tier": inv_item.get('tier', 1),
|
|
"stats": item_def.stats,
|
|
"encumbrance": item_def.encumbrance
|
|
}
|
|
else:
|
|
enriched[slot] = None
|
|
|
|
return {"equipment": enriched}
|
|
|
|
|
|
@router.post("/api/game/repair_item")
|
|
async def repair_item(
|
|
repair_req: RepairItemRequest,
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""Repair an item using materials at a workbench location"""
|
|
player_id = current_user['id']
|
|
|
|
# Get player's location
|
|
player = await db.get_player_by_id(player_id)
|
|
location = LOCATIONS.get(player['location_id'])
|
|
|
|
if not location:
|
|
raise HTTPException(status_code=404, detail="Location not found")
|
|
|
|
# Check if location has workbench
|
|
location_tags = getattr(location, 'tags', [])
|
|
if 'workbench' not in location_tags and 'repair_station' not in location_tags:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="You need to be at a location with a workbench to repair items. Try the Gas Station!"
|
|
)
|
|
|
|
# Get inventory item
|
|
inv_item = await db.get_inventory_item(repair_req.inventory_id)
|
|
if not inv_item or inv_item['character_id'] != player_id:
|
|
raise HTTPException(status_code=404, detail="Item not found in inventory")
|
|
|
|
# Get item definition
|
|
item_def = ITEMS_MANAGER.get_item(inv_item['item_id'])
|
|
if not item_def:
|
|
raise HTTPException(status_code=404, detail="Item definition not found")
|
|
|
|
# Check if item is repairable
|
|
if not getattr(item_def, 'repairable', False):
|
|
raise HTTPException(status_code=400, detail=f"{item_def.name} cannot be repaired")
|
|
|
|
# Check if item has durability (unique item)
|
|
if not inv_item.get('unique_item_id'):
|
|
raise HTTPException(status_code=400, detail="This item doesn't have durability tracking")
|
|
|
|
# Get unique item data
|
|
unique_item = await db.get_unique_item(inv_item['unique_item_id'])
|
|
if not unique_item:
|
|
raise HTTPException(status_code=500, detail="Unique item data not found")
|
|
|
|
current_durability = unique_item.get('durability', 0)
|
|
max_durability = unique_item.get('max_durability', 100)
|
|
|
|
# Check if item needs repair
|
|
if current_durability >= max_durability:
|
|
raise HTTPException(status_code=400, detail=f"{item_def.name} is already at full durability")
|
|
|
|
# Get repair materials
|
|
repair_materials = getattr(item_def, 'repair_materials', [])
|
|
if not repair_materials:
|
|
raise HTTPException(status_code=500, detail="Item repair configuration missing")
|
|
|
|
# Get repair tools
|
|
repair_tools = getattr(item_def, 'repair_tools', [])
|
|
|
|
# Check if player has all required materials and tools
|
|
player_inventory = await db.get_inventory(player_id)
|
|
inventory_dict = {item['item_id']: item['quantity'] for item in player_inventory}
|
|
|
|
missing_materials = []
|
|
for material in repair_materials:
|
|
required_qty = material.get('quantity', 1)
|
|
available_qty = inventory_dict.get(material['item_id'], 0)
|
|
if available_qty < required_qty:
|
|
material_def = ITEMS_MANAGER.get_item(material['item_id'])
|
|
material_name = material_def.name if material_def else material['item_id']
|
|
missing_materials.append(f"{material_name} ({available_qty}/{required_qty})")
|
|
|
|
if missing_materials:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Missing materials: {', '.join(missing_materials)}"
|
|
)
|
|
|
|
# Check and consume tools if required
|
|
tools_consumed = []
|
|
if repair_tools:
|
|
success, error_msg, tools_consumed = await consume_tool_durability(player_id, repair_tools, player_inventory)
|
|
if not success:
|
|
raise HTTPException(status_code=400, detail=error_msg)
|
|
|
|
# Calculate stamina cost
|
|
stamina_cost = calculate_crafting_stamina_cost(unique_item.get('tier', 1), 'repair')
|
|
|
|
# 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(player_id, new_stamina)
|
|
|
|
# Consume materials
|
|
for material in repair_materials:
|
|
await db.remove_item_from_inventory(player_id, material['item_id'], material['quantity'])
|
|
|
|
# Calculate repair amount
|
|
repair_percentage = getattr(item_def, 'repair_percentage', 25)
|
|
repair_amount = int((max_durability * repair_percentage) / 100)
|
|
new_durability = min(current_durability + repair_amount, max_durability)
|
|
|
|
# Update unique item durability
|
|
await db.update_unique_item(inv_item['unique_item_id'], durability=new_durability)
|
|
|
|
# Build materials consumed message
|
|
materials_used = []
|
|
for material in repair_materials:
|
|
material_def = ITEMS_MANAGER.get_item(material['item_id'])
|
|
emoji = material_def.emoji if material_def and hasattr(material_def, 'emoji') else '📦'
|
|
name = material_def.name if material_def else material['item_id']
|
|
materials_used.append(f"{emoji} {name} x{material['quantity']}")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Repaired {item_def.name}! Restored {repair_amount} durability.",
|
|
"item_name": item_def.name,
|
|
"old_durability": current_durability,
|
|
"new_durability": new_durability,
|
|
"max_durability": max_durability,
|
|
"materials_consumed": materials_used,
|
|
"tools_consumed": tools_consumed,
|
|
"repair_amount": repair_amount,
|
|
"stamina_cost": stamina_cost,
|
|
"new_stamina": new_stamina
|
|
}
|
|
|
|
|
|
|
|
|
|
async def reduce_armor_durability(player_id: int, damage_taken: int) -> tuple:
|
|
"""
|
|
Reduce durability of equipped armor pieces when taking damage.
|
|
Formula: durability_loss = max(1, (damage_taken / armor_value) * base_reduction_rate)
|
|
Base reduction rate: 0.5 (so 10 damage with 5 armor = 1 durability loss)
|
|
Returns: (armor_damage_absorbed, broken_armor_pieces)
|
|
"""
|
|
equipment = await db.get_all_equipment(player_id)
|
|
armor_pieces = ['head', 'torso', 'legs', 'feet']
|
|
|
|
total_armor = 0
|
|
equipped_armor = []
|
|
|
|
# Collect all equipped armor
|
|
for slot in armor_pieces:
|
|
if equipment.get(slot) and equipment[slot]:
|
|
armor_slot = equipment[slot]
|
|
inv_item = await db.get_inventory_item_by_id(armor_slot['item_id'])
|
|
if inv_item and inv_item.get('unique_item_id'):
|
|
item_def = ITEMS_MANAGER.get_item(inv_item['item_id'])
|
|
if item_def and item_def.stats and 'armor' in item_def.stats:
|
|
armor_value = item_def.stats['armor']
|
|
total_armor += armor_value
|
|
equipped_armor.append({
|
|
'slot': slot,
|
|
'inv_item_id': armor_slot['item_id'],
|
|
'unique_item_id': inv_item['unique_item_id'],
|
|
'item_id': inv_item['item_id'],
|
|
'item_def': item_def,
|
|
'armor_value': armor_value
|
|
})
|
|
|
|
if not equipped_armor:
|
|
return 0, []
|
|
|
|
# Calculate damage absorbed by armor (total armor reduces damage)
|
|
armor_absorbed = min(damage_taken // 2, total_armor) # Armor absorbs up to half the damage
|
|
|
|
# Calculate durability loss for each armor piece
|
|
# Balanced formula: armor should last many combats (10-20+ hits for low tier)
|
|
base_reduction_rate = 0.1 # Reduced from 0.5 to make armor more durable
|
|
broken_armor = []
|
|
|
|
for armor in equipped_armor:
|
|
# Each piece takes durability loss proportional to its armor value
|
|
proportion = armor['armor_value'] / total_armor if total_armor > 0 else 0
|
|
# Formula: durability_loss = (damage_taken * proportion / armor_value) * base_rate
|
|
# This means higher armor value = less durability loss per hit
|
|
# With base_rate = 0.1, a 5 armor piece taking 10 damage loses ~0.2 durability per hit
|
|
durability_loss = max(1, int((damage_taken * proportion / max(armor['armor_value'], 1)) * base_reduction_rate * 10))
|
|
|
|
# Get current durability
|
|
unique_item = await db.get_unique_item(armor['unique_item_id'])
|
|
if unique_item:
|
|
current_durability = unique_item.get('durability', 0)
|
|
new_durability = max(0, current_durability - durability_loss)
|
|
|
|
await db.update_unique_item(armor['unique_item_id'], durability=new_durability)
|
|
|
|
# If armor broke, unequip and remove from inventory
|
|
if new_durability <= 0:
|
|
await db.unequip_item(player_id, armor['slot'])
|
|
await db.remove_inventory_row(armor['inv_item_id'])
|
|
broken_armor.append({
|
|
'name': armor['item_def'].name,
|
|
'emoji': armor['item_def'].emoji,
|
|
'slot': armor['slot']
|
|
})
|
|
|
|
return armor_absorbed, broken_armor
|
|
|
|
|
|
async def consume_tool_durability(user_id: int, tools: list, inventory: list) -> tuple:
|
|
"""
|
|
Consume durability from required tools.
|
|
Returns: (success, error_message, consumed_tools_info)
|
|
"""
|
|
consumed_tools = []
|
|
tools_map = {}
|
|
|
|
# Build map of available tools with durability
|
|
for inv_item in inventory:
|
|
if inv_item.get('unique_item_id'):
|
|
unique_item = await db.get_unique_item(inv_item['unique_item_id'])
|
|
if unique_item:
|
|
item_id = inv_item['item_id']
|
|
durability = unique_item.get('durability', 0)
|
|
if item_id not in tools_map:
|
|
tools_map[item_id] = []
|
|
tools_map[item_id].append({
|
|
'inventory_id': inv_item['id'],
|
|
'unique_item_id': inv_item['unique_item_id'],
|
|
'durability': durability,
|
|
'max_durability': unique_item.get('max_durability', 100)
|
|
})
|
|
|
|
# Check and consume tools
|
|
for tool_req in tools:
|
|
tool_id = tool_req['item_id']
|
|
durability_cost = tool_req['durability_cost']
|
|
|
|
if tool_id not in tools_map or not tools_map[tool_id]:
|
|
tool_def = ITEMS_MANAGER.items.get(tool_id)
|
|
tool_name = tool_def.name if tool_def else tool_id
|
|
return False, f"Missing required tool: {tool_name}", []
|
|
|
|
# Find tool with enough durability
|
|
tool_found = None
|
|
for tool in tools_map[tool_id]:
|
|
if tool['durability'] >= durability_cost:
|
|
tool_found = tool
|
|
break
|
|
|
|
if not tool_found:
|
|
tool_def = ITEMS_MANAGER.items.get(tool_id)
|
|
tool_name = tool_def.name if tool_def else tool_id
|
|
return False, f"Tool {tool_name} doesn't have enough durability (need {durability_cost})", []
|
|
|
|
# Consume durability
|
|
new_durability = tool_found['durability'] - durability_cost
|
|
await db.update_unique_item(tool_found['unique_item_id'], durability=new_durability)
|
|
|
|
# If tool breaks, remove from inventory
|
|
if new_durability <= 0:
|
|
await db.remove_inventory_row(tool_found['inventory_id'])
|
|
|
|
tool_def = ITEMS_MANAGER.items.get(tool_id)
|
|
consumed_tools.append({
|
|
'item_id': tool_id,
|
|
'name': tool_def.name if tool_def else tool_id,
|
|
'durability_cost': durability_cost,
|
|
'broke': new_durability <= 0
|
|
})
|
|
|
|
return True, "", consumed_tools
|
|
|
|
|
|
@router.get("/api/game/repairable")
|
|
async def get_repairable_items(current_user: dict = Depends(get_current_user)):
|
|
"""Get all repairable items from inventory and equipped slots"""
|
|
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 repair station
|
|
if not location or 'repair_station' not in getattr(location, 'tags', []):
|
|
raise HTTPException(status_code=400, detail="You must be at a repair station to repair items")
|
|
|
|
repairable_items = []
|
|
|
|
# Check inventory items
|
|
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
|
|
|
|
for inv_item in inventory:
|
|
if inv_item.get('unique_item_id'):
|
|
unique_item = await db.get_unique_item(inv_item['unique_item_id'])
|
|
if not unique_item:
|
|
continue
|
|
|
|
item_def = ITEMS_MANAGER.items.get(inv_item['item_id'])
|
|
if not item_def or not getattr(item_def, 'repairable', False):
|
|
continue
|
|
|
|
current_durability = unique_item.get('durability', 0)
|
|
max_durability = unique_item.get('max_durability', 100)
|
|
needs_repair = current_durability < max_durability
|
|
|
|
# Check materials availability
|
|
repair_materials = getattr(item_def, 'repair_materials', [])
|
|
materials_info = []
|
|
has_materials = True
|
|
for material in repair_materials:
|
|
mat_item_def = ITEMS_MANAGER.items.get(material['item_id'])
|
|
available = inventory_counts.get(material['item_id'], 0)
|
|
required = material['quantity']
|
|
materials_info.append({
|
|
'item_id': material['item_id'],
|
|
'name': mat_item_def.name if mat_item_def else material['item_id'],
|
|
'emoji': mat_item_def.emoji if mat_item_def else '📦',
|
|
'quantity': required,
|
|
'available': available,
|
|
'has_enough': available >= required
|
|
})
|
|
if available < required:
|
|
has_materials = False
|
|
|
|
# Check tools availability
|
|
repair_tools = getattr(item_def, 'repair_tools', [])
|
|
tools_info = []
|
|
has_tools = True
|
|
for tool_req in repair_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 (find one with highest durability)
|
|
tool_found = False
|
|
tool_durability = 0
|
|
best_tool_unique = None
|
|
|
|
for check_item in inventory:
|
|
if check_item['item_id'] == tool_id and check_item.get('unique_item_id'):
|
|
unique = await db.get_unique_item(check_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
|
|
tool_found = 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': tool_found,
|
|
'tool_durability': tool_durability
|
|
})
|
|
if not tool_found:
|
|
has_tools = False
|
|
|
|
can_repair = needs_repair and has_materials and has_tools
|
|
|
|
repairable_items.append({
|
|
'inventory_id': inv_item['id'],
|
|
'unique_item_id': inv_item['unique_item_id'],
|
|
'item_id': inv_item['item_id'],
|
|
'name': item_def.name,
|
|
'emoji': item_def.emoji,
|
|
'unique_item_data': {k: int(v) if isinstance(v, (int, float)) and k != 'durability_percent' else v for k, v in unique_item.items()},
|
|
'tier': unique_item.get('tier', 1),
|
|
'current_durability': current_durability,
|
|
'max_durability': max_durability,
|
|
'durability_percent': int((current_durability / max_durability) * 100),
|
|
'repair_percentage': getattr(item_def, 'repair_percentage', 25),
|
|
'needs_repair': needs_repair,
|
|
'materials': materials_info,
|
|
'tools': tools_info,
|
|
'can_repair': can_repair,
|
|
'location': 'equipped' if inv_item.get('is_equipped') else 'inventory',
|
|
'stamina_cost': calculate_crafting_stamina_cost(unique_item.get('tier', 1), 'repair'),
|
|
'type': getattr(item_def, 'type', 'misc')
|
|
})
|
|
|
|
# Sort: repairable items first (can_repair=True), then by durability percent (lowest first), then by name
|
|
repairable_items.sort(key=lambda x: (not x['can_repair'], -x['durability_percent'], x['name']))
|
|
|
|
return {'repairable_items': repairable_items}
|
|
|
|
except Exception as e:
|
|
print(f"Error getting repairable items: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/api/game/salvageable")
|
|
async def get_salvageable_items(current_user: dict = Depends(get_current_user)):
|
|
"""Get list of salvageable (uncraftable) items from inventory with their unique stats"""
|
|
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', []):
|
|
return {'salvageable_items': [], 'at_workbench': False}
|
|
|
|
# Get inventory
|
|
inventory = await db.get_inventory(current_user['id'])
|
|
|
|
salvageable_items = []
|
|
for inv_item in inventory:
|
|
item_id = inv_item['item_id']
|
|
item_def = ITEMS_MANAGER.items.get(item_id)
|
|
|
|
if not item_def or not getattr(item_def, 'uncraftable', False):
|
|
continue
|
|
|
|
# Get unique item details if it exists
|
|
unique_item_data = None
|
|
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)
|
|
durability_percent = int((current_durability / max_durability) * 100) if max_durability > 0 else 0
|
|
|
|
# Get item stats from definition merged with unique stats
|
|
item_stats = {}
|
|
if item_def.stats:
|
|
item_stats = dict(item_def.stats)
|
|
if unique_item.get('unique_stats'):
|
|
item_stats.update(unique_item.get('unique_stats'))
|
|
|
|
unique_item_data = {
|
|
'current_durability': current_durability,
|
|
'max_durability': max_durability,
|
|
'durability_percent': durability_percent,
|
|
'tier': unique_item.get('tier', 1),
|
|
'unique_stats': item_stats # Includes both base stats and unique overrides
|
|
}
|
|
|
|
# Get uncraft yield
|
|
uncraft_yield = getattr(item_def, 'uncraft_yield', [])
|
|
yield_info = []
|
|
for material in uncraft_yield:
|
|
mat_def = ITEMS_MANAGER.items.get(material['item_id'])
|
|
yield_info.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': material['quantity']
|
|
})
|
|
|
|
# Check tools availability for uncrafting
|
|
uncraft_tools = getattr(item_def, 'uncraft_tools', [])
|
|
tools_info = []
|
|
has_tools = True
|
|
for tool_req in uncraft_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 (find one with highest durability)
|
|
tool_found = False
|
|
tool_durability = 0
|
|
best_tool_unique = None
|
|
|
|
for check_item in inventory:
|
|
if check_item['item_id'] == tool_id and check_item.get('unique_item_id'):
|
|
unique = await db.get_unique_item(check_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
|
|
tool_found = 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': tool_found,
|
|
'tool_durability': tool_durability
|
|
})
|
|
|
|
if not tool_found:
|
|
has_tools = False
|
|
|
|
can_uncraft = has_tools
|
|
|
|
# Build item entry
|
|
item_entry = {
|
|
'inventory_id': inv_item['id'],
|
|
'unique_item_id': inv_item.get('unique_item_id'),
|
|
'item_id': item_id,
|
|
'name': item_def.name,
|
|
'emoji': item_def.emoji,
|
|
'image_path': getattr(item_def, 'image_path', None),
|
|
'tier': getattr(item_def, 'tier', 1),
|
|
'quantity': inv_item['quantity'],
|
|
'base_yield': yield_info,
|
|
'loss_chance': getattr(item_def, 'uncraft_loss_chance', 0.3),
|
|
'stamina_cost': calculate_crafting_stamina_cost(getattr(item_def, 'tier', 1), 'uncraft'),
|
|
'can_uncraft': can_uncraft,
|
|
'uncraft_tools': tools_info,
|
|
'location': 'equipped' if inv_item.get('is_equipped') else 'inventory',
|
|
'type': getattr(item_def, 'type', 'misc')
|
|
}
|
|
|
|
# Add unique item data if available
|
|
if unique_item_data:
|
|
item_entry['unique_item_data'] = unique_item_data
|
|
item_entry['unique_stats'] = unique_item_data.get('unique_stats', {})
|
|
item_entry['current_durability'] = unique_item_data.get('current_durability')
|
|
item_entry['max_durability'] = unique_item_data.get('max_durability')
|
|
item_entry['durability_percent'] = unique_item_data.get('durability_percent')
|
|
|
|
salvageable_items.append(item_entry)
|
|
|
|
return {
|
|
'salvageable_items': salvageable_items,
|
|
'at_workbench': True
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"Error getting salvageable items: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
class LootCorpseRequest(BaseModel):
|
|
corpse_id: str
|
|
item_index: Optional[int] = None # Index of specific item to loot (None = all) |