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