""" Inventory API Blueprint Endpoints for managing character inventory and equipment. All endpoints require authentication and enforce ownership validation. Endpoints: - GET /api/v1/characters//inventory - Get character inventory and equipped items - POST /api/v1/characters//inventory/equip - Equip an item - POST /api/v1/characters//inventory/unequip - Unequip an item - POST /api/v1/characters//inventory/use - Use a consumable item - DELETE /api/v1/characters//inventory/ - Drop an item """ from flask import Blueprint, request from app.services.inventory_service import ( get_inventory_service, ItemNotFoundError, CannotEquipError, InvalidSlotError, CannotUseItemError, InventoryFullError, VALID_SLOTS, MAX_INVENTORY_SIZE, ) from app.services.character_service import ( get_character_service, CharacterNotFound, ) from app.utils.response import ( success_response, error_response, not_found_response, validation_error_response, ) from app.utils.auth import require_auth, get_current_user from app.utils.logging import get_logger # Initialize logger logger = get_logger(__file__) # Create blueprint inventory_bp = Blueprint('inventory', __name__) # ============================================================================= # API Endpoints # ============================================================================= @inventory_bp.route('/api/v1/characters//inventory', methods=['GET']) @require_auth def get_inventory(character_id: str): """ Get character inventory and equipped items. Args: character_id: Character ID Returns: 200: Inventory and equipment data 401: Not authenticated 404: Character not found or not owned by user 500: Internal server error Example Response: { "result": { "inventory": [ { "item_id": "gen_abc123", "name": "Flaming Dagger", "item_type": "weapon", "rarity": "rare", ... } ], "equipped": { "weapon": {...}, "helmet": null, ... }, "inventory_count": 5, "max_inventory": 100 } } """ try: user = get_current_user() logger.info("Getting inventory", user_id=user.id, character_id=character_id) # Get character (validates ownership) char_service = get_character_service() character = char_service.get_character(character_id, user.id) # Get inventory service inventory_service = get_inventory_service() # Get inventory items inventory_items = inventory_service.get_inventory(character) # Get equipped items equipped_items = inventory_service.get_equipped_items(character) # Build equipped dict with all slots (None for empty slots) equipped_response = {} for slot in VALID_SLOTS: item = equipped_items.get(slot) equipped_response[slot] = item.to_dict() if item else None logger.info("Inventory retrieved successfully", user_id=user.id, character_id=character_id, item_count=len(inventory_items)) return success_response( result={ "inventory": [item.to_dict() for item in inventory_items], "equipped": equipped_response, "inventory_count": len(inventory_items), "max_inventory": MAX_INVENTORY_SIZE, } ) except CharacterNotFound as e: logger.warning("Character not found", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, error=str(e)) return not_found_response(message=str(e)) except Exception as e: logger.error("Failed to get inventory", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, error=str(e)) return error_response( code="INVENTORY_GET_ERROR", message="Failed to retrieve inventory", status=500 ) @inventory_bp.route('/api/v1/characters//inventory/equip', methods=['POST']) @require_auth def equip_item(character_id: str): """ Equip an item from inventory to a specified slot. Args: character_id: Character ID Request Body: { "item_id": "gen_abc123", "slot": "weapon" } Returns: 200: Item equipped successfully 400: Cannot equip item (wrong type, level requirement, etc.) 401: Not authenticated 404: Character or item not found 422: Validation error (invalid slot) 500: Internal server error Example Response: { "result": { "message": "Equipped Flaming Dagger to weapon slot", "equipped": {...}, "unequipped_item": null } } """ try: user = get_current_user() # Get request data data = request.get_json() if not data: return validation_error_response( message="Request body is required", details={"error": "Request body is required"} ) item_id = data.get('item_id', '').strip() slot = data.get('slot', '').strip().lower() # Validate required fields validation_errors = {} if not item_id: validation_errors['item_id'] = "item_id is required" if not slot: validation_errors['slot'] = "slot is required" if validation_errors: return validation_error_response( message="Validation failed", details=validation_errors ) logger.info("Equipping item", user_id=user.id, character_id=character_id, item_id=item_id, slot=slot) # Get character (validates ownership) char_service = get_character_service() character = char_service.get_character(character_id, user.id) # Get inventory service inventory_service = get_inventory_service() # Equip item previous_item = inventory_service.equip_item( character, item_id, slot, user.id ) # Get item name for message equipped_item = character.equipped.get(slot) item_name = equipped_item.get_display_name() if equipped_item else item_id # Build equipped response equipped_response = {} for s in VALID_SLOTS: item = character.equipped.get(s) equipped_response[s] = item.to_dict() if item else None logger.info("Item equipped successfully", user_id=user.id, character_id=character_id, item_id=item_id, slot=slot, previous_item=previous_item.item_id if previous_item else None) return success_response( result={ "message": f"Equipped {item_name} to {slot} slot", "equipped": equipped_response, "unequipped_item": previous_item.to_dict() if previous_item else None, } ) except CharacterNotFound as e: logger.warning("Character not found for equip", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, error=str(e)) return not_found_response(message=str(e)) except ItemNotFoundError as e: logger.warning("Item not found for equip", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, item_id=item_id if 'item_id' in locals() else 'unknown', error=str(e)) return not_found_response(message=str(e)) except InvalidSlotError as e: logger.warning("Invalid slot for equip", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, slot=slot if 'slot' in locals() else 'unknown', error=str(e)) return validation_error_response( message=str(e), details={"slot": str(e)} ) except CannotEquipError as e: logger.warning("Cannot equip item", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, item_id=item_id if 'item_id' in locals() else 'unknown', error=str(e)) return error_response( code="CANNOT_EQUIP", message=str(e), status=400 ) except Exception as e: logger.error("Failed to equip item", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, error=str(e)) return error_response( code="EQUIP_ERROR", message="Failed to equip item", status=500 ) @inventory_bp.route('/api/v1/characters//inventory/unequip', methods=['POST']) @require_auth def unequip_item(character_id: str): """ Unequip an item from a specified slot (returns to inventory). Args: character_id: Character ID Request Body: { "slot": "weapon" } Returns: 200: Item unequipped successfully (or slot was empty) 400: Inventory full, cannot unequip 401: Not authenticated 404: Character not found 422: Validation error (invalid slot) 500: Internal server error Example Response: { "result": { "message": "Unequipped Flaming Dagger from weapon slot", "unequipped_item": {...}, "equipped": {...} } } """ try: user = get_current_user() # Get request data data = request.get_json() if not data: return validation_error_response( message="Request body is required", details={"error": "Request body is required"} ) slot = data.get('slot', '').strip().lower() if not slot: return validation_error_response( message="Validation failed", details={"slot": "slot is required"} ) logger.info("Unequipping item", user_id=user.id, character_id=character_id, slot=slot) # Get character (validates ownership) char_service = get_character_service() character = char_service.get_character(character_id, user.id) # Get inventory service inventory_service = get_inventory_service() # Unequip item unequipped_item = inventory_service.unequip_item(character, slot, user.id) # Build equipped response equipped_response = {} for s in VALID_SLOTS: item = character.equipped.get(s) equipped_response[s] = item.to_dict() if item else None # Build message if unequipped_item: message = f"Unequipped {unequipped_item.get_display_name()} from {slot} slot" else: message = f"Slot '{slot}' was already empty" logger.info("Item unequipped", user_id=user.id, character_id=character_id, slot=slot, unequipped_item=unequipped_item.item_id if unequipped_item else None) return success_response( result={ "message": message, "unequipped_item": unequipped_item.to_dict() if unequipped_item else None, "equipped": equipped_response, } ) except CharacterNotFound as e: logger.warning("Character not found for unequip", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, error=str(e)) return not_found_response(message=str(e)) except InvalidSlotError as e: logger.warning("Invalid slot for unequip", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, slot=slot if 'slot' in locals() else 'unknown', error=str(e)) return validation_error_response( message=str(e), details={"slot": str(e)} ) except InventoryFullError as e: logger.warning("Inventory full, cannot unequip", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, error=str(e)) return error_response( code="INVENTORY_FULL", message=str(e), status=400 ) except Exception as e: logger.error("Failed to unequip item", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, error=str(e)) return error_response( code="UNEQUIP_ERROR", message="Failed to unequip item", status=500 ) @inventory_bp.route('/api/v1/characters//inventory/use', methods=['POST']) @require_auth def use_item(character_id: str): """ Use a consumable item from inventory. Args: character_id: Character ID Request Body: { "item_id": "health_potion_small" } Returns: 200: Item used successfully 400: Cannot use item (not consumable) 401: Not authenticated 404: Character or item not found 500: Internal server error Example Response: { "result": { "item_used": "Small Health Potion", "effects_applied": [ { "effect_name": "Healing", "effect_type": "hot", "value": 25, "message": "Restored 25 HP" } ], "hp_restored": 25, "mp_restored": 0, "message": "Used Small Health Potion: Restored 25 HP" } } """ try: user = get_current_user() # Get request data data = request.get_json() if not data: return validation_error_response( message="Request body is required", details={"error": "Request body is required"} ) item_id = data.get('item_id', '').strip() if not item_id: return validation_error_response( message="Validation failed", details={"item_id": "item_id is required"} ) logger.info("Using item", user_id=user.id, character_id=character_id, item_id=item_id) # Get character (validates ownership) char_service = get_character_service() character = char_service.get_character(character_id, user.id) # Get inventory service inventory_service = get_inventory_service() # Use consumable result = inventory_service.use_consumable(character, item_id, user.id) logger.info("Item used successfully", user_id=user.id, character_id=character_id, item_id=item_id, hp_restored=result.hp_restored, mp_restored=result.mp_restored) return success_response(result=result.to_dict()) except CharacterNotFound as e: logger.warning("Character not found for use item", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, error=str(e)) return not_found_response(message=str(e)) except ItemNotFoundError as e: logger.warning("Item not found for use", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, item_id=item_id if 'item_id' in locals() else 'unknown', error=str(e)) return not_found_response(message=str(e)) except CannotUseItemError as e: logger.warning("Cannot use item", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, item_id=item_id if 'item_id' in locals() else 'unknown', error=str(e)) return error_response( code="CANNOT_USE_ITEM", message=str(e), status=400 ) except Exception as e: logger.error("Failed to use item", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, error=str(e)) return error_response( code="USE_ITEM_ERROR", message="Failed to use item", status=500 ) @inventory_bp.route('/api/v1/characters//inventory/', methods=['DELETE']) @require_auth def drop_item(character_id: str, item_id: str): """ Drop (remove) an item from inventory. Args: character_id: Character ID item_id: Item ID to drop Returns: 200: Item dropped successfully 401: Not authenticated 404: Character or item not found 500: Internal server error Example Response: { "result": { "message": "Dropped Rusty Sword", "dropped_item": {...}, "inventory_count": 4 } } """ try: user = get_current_user() logger.info("Dropping item", user_id=user.id, character_id=character_id, item_id=item_id) # Get character (validates ownership) char_service = get_character_service() character = char_service.get_character(character_id, user.id) # Get inventory service inventory_service = get_inventory_service() # Drop item dropped_item = inventory_service.drop_item(character, item_id, user.id) logger.info("Item dropped successfully", user_id=user.id, character_id=character_id, item_id=item_id, item_name=dropped_item.get_display_name()) return success_response( result={ "message": f"Dropped {dropped_item.get_display_name()}", "dropped_item": dropped_item.to_dict(), "inventory_count": len(character.inventory), } ) except CharacterNotFound as e: logger.warning("Character not found for drop", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, error=str(e)) return not_found_response(message=str(e)) except ItemNotFoundError as e: logger.warning("Item not found for drop", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, item_id=item_id, error=str(e)) return not_found_response(message=str(e)) except Exception as e: logger.error("Failed to drop item", user_id=user.id if 'user' in locals() else 'unknown', character_id=character_id, item_id=item_id, error=str(e)) return error_response( code="DROP_ITEM_ERROR", message="Failed to drop item", status=500 )