import logging import math import json import random from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton from telegram.ext import ContextTypes from telegram.error import BadRequest from . import database, keyboards, logic from .utils import admin_only from data.world_loader import game_world from data.items import ITEMS logger = logging.getLogger(__name__) # ... (get_player_status_text, send_or_edit_with_image, start are unchanged) ... async def get_player_status_text(telegram_id: int) -> str: player = await database.get_player(telegram_id) if not player: return "Could not find player data." location = game_world.get_location(player["location_id"]) if not location: return "Error: Player is in an unknown location." inventory = await database.get_inventory(telegram_id) weight, volume = logic.calculate_inventory_load(inventory) max_weight, max_volume = logic.get_player_capacity(inventory, player) # Get equipped items equipped_items = [] for item in inventory: if item.get('is_equipped'): item_def = ITEMS.get(item['item_id'], {}) emoji = item_def.get('emoji', '❔') equipped_items.append(f"{emoji} {item_def.get('name', 'Unknown')}") status = f"Location: {location.name}\nStatus: Healthy\n" status += f"❤️ HP: {player['hp']}/{player['max_hp']} | ⚡️ Stamina: {player['stamina']}/{player['max_stamina']}\n" status += f"🎒 Load: {weight}/{max_weight} kg | {volume}/{max_volume} vol\n" if equipped_items: status += f"⚔️ Equipped: {', '.join(equipped_items)}\n" status += f"━━━━━━━━━━━━━━━━━━━━\n{location.description}" return status async def send_or_edit_with_image(query, text: str, reply_markup: InlineKeyboardMarkup, image_path: str = None, parse_mode='HTML'): """ Send a message with an image (as caption) or edit existing message. Uses edit_message_media for smooth transitions when changing images. """ import os from telegram import InputMediaPhoto # Check if we should edit or send new current_message = query.message has_photo = bool(current_message.photo) if image_path: # Get or upload image cached_file_id = await database.get_cached_image(image_path) if not cached_file_id and os.path.exists(image_path): # Upload new image try: with open(image_path, 'rb') as img_file: temp_msg = await current_message.reply_photo( photo=img_file, caption=text, reply_markup=reply_markup, parse_mode=parse_mode ) if temp_msg.photo: cached_file_id = temp_msg.photo[-1].file_id await database.cache_image(image_path, cached_file_id) # Delete old message to keep chat clean try: await current_message.delete() except: pass return except Exception as e: logger.error(f"Error uploading image: {e}") cached_file_id = None if cached_file_id: # Check if current message has same photo if has_photo: current_file_id = current_message.photo[-1].file_id if current_file_id == cached_file_id: # Same image, just edit caption try: await query.edit_message_caption( caption=text, reply_markup=reply_markup, parse_mode=parse_mode ) return except BadRequest as e: if "Message is not modified" in str(e): return # Failed to edit, fall through to send new else: # Different image - use edit_message_media for smooth transition try: media = InputMediaPhoto( media=cached_file_id, caption=text, parse_mode=parse_mode ) await query.edit_message_media( media=media, reply_markup=reply_markup ) return except Exception as e: logger.error(f"Error editing message media: {e}") # Fall through to delete and send new # Current message has no photo - try to edit to photo if not has_photo: # Can't edit text message to photo message, need to delete and send new try: await current_message.delete() except: pass try: await current_message.reply_photo( photo=cached_file_id, caption=text, reply_markup=reply_markup, parse_mode=parse_mode ) except Exception as e: logger.error(f"Error sending cached image: {e}") else: # No image requested if has_photo: # Current message has photo, need to delete and send text-only try: await current_message.delete() except: pass await current_message.reply_html(text=text, reply_markup=reply_markup) else: # Both text-only, just edit try: await query.edit_message_text(text=text, reply_markup=reply_markup, parse_mode=parse_mode) except BadRequest as e: if "Message is not modified" not in str(e): await current_message.reply_html(text=text, reply_markup=reply_markup) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: import os user = update.effective_user player = await database.get_player(user.id) if not player: await database.create_player(user.id, user.first_name) await update.message.reply_html(f"Welcome, {user.mention_html()}! Your story is just beginning.") # Get player status and location image player = await database.get_player(user.id) status_text = await get_player_status_text(user.id) location = game_world.get_location(player['location_id']) # Send with image if available if location and location.image_path: cached_file_id = await database.get_cached_image(location.image_path) if cached_file_id: await update.message.reply_photo( photo=cached_file_id, caption=status_text, reply_markup=keyboards.main_menu_keyboard(), parse_mode='HTML' ) elif os.path.exists(location.image_path): with open(location.image_path, 'rb') as img_file: msg = await update.message.reply_photo( photo=img_file, caption=status_text, reply_markup=keyboards.main_menu_keyboard(), parse_mode='HTML' ) if msg.photo: await database.cache_image(location.image_path, msg.photo[-1].file_id) else: await update.message.reply_html(status_text, reply_markup=keyboards.main_menu_keyboard()) else: await update.message.reply_html(status_text, reply_markup=keyboards.main_menu_keyboard()) @admin_only async def export_map(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Export map data as JSON for external visualization.""" from data.world_loader import export_map_data import json map_data = export_map_data() json_str = json.dumps(map_data, indent=2) # Send as text file from io import BytesIO file = BytesIO(json_str.encode('utf-8')) file.name = "map_data.json" await update.message.reply_document( document=file, filename="map_data.json", caption="🗺️ Game Map Data\n\nThis JSON file contains all locations, coordinates, and connections.\nYou can use it to visualize the game map in external tools." ) @admin_only async def spawn_stats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Show wandering enemy spawn statistics (debug command).""" from bot.spawn_manager import get_spawn_stats stats = await get_spawn_stats() text = "📊 Wandering Enemy Statistics\n\n" text += f"Total Active Enemies: {stats['total_active']}\n\n" if stats['by_location']: text += "Enemies by Location:\n" for loc_id, count in stats['by_location'].items(): from data.world_loader import game_world location = game_world.get_location(loc_id) loc_name = location.name if location else loc_id text += f"• {loc_name}: {count}\n" else: text += "No wandering enemies currently active." await update.message.reply_html(text) async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: query = update.callback_query user_id = query.from_user.id data = query.data.split(':') action_type = data[0] player = await database.get_player(user_id) if not player or player['is_dead']: await query.answer() await send_or_edit_with_image(query, text="💀 Your journey has ended. You died in the wasteland. Create a new character with /start to begin again.", reply_markup=None) return # Check if player is in combat - restrict most actions combat = await database.get_combat(user_id) if combat and action_type not in ['combat_attack', 'combat_flee', 'combat_use_item_menu', 'combat_use_item', 'combat_back', 'no_op']: await query.answer("You're in combat! Focus on the fight!", show_alert=False) return # --- Inspection & World Interaction --- if action_type == "inspect_area": await query.answer() location_id = player['location_id'] location = game_world.get_location(location_id) dropped_items = await database.get_dropped_items_in_location(location_id) # Get wandering enemies from database wandering_enemies = await database.get_wandering_enemies_in_location(location_id) keyboard = await keyboards.inspect_keyboard(location_id, dropped_items, wandering_enemies) image_path = location.image_path if location else None await send_or_edit_with_image(query, text="You scan the area. You notice...", reply_markup=keyboard, image_path=image_path) elif action_type == "attack_wandering": # Player initiates combat with a wandering enemy enemy_db_id = int(data[1]) await query.answer() # Get the enemy from database wandering_enemies = await database.get_wandering_enemies_in_location(player['location_id']) enemy_data = next((e for e in wandering_enemies if e['id'] == enemy_db_id), None) if not enemy_data: await query.answer("That enemy has already moved on!", show_alert=True) # Refresh inspect menu location_id = player['location_id'] location = game_world.get_location(location_id) dropped_items = await database.get_dropped_items_in_location(location_id) wandering_enemies = await database.get_wandering_enemies_in_location(location_id) keyboard = await keyboards.inspect_keyboard(location_id, dropped_items, wandering_enemies) image_path = location.image_path if location else None await send_or_edit_with_image(query, text="You scan the area. You notice...", reply_markup=keyboard, image_path=image_path) return npc_id = enemy_data['npc_id'] # Remove enemy from wandering table (they're now in combat) await database.remove_wandering_enemy(enemy_db_id) from data.npcs import NPCS from bot import combat # Initiate combat with from_wandering_enemy=True so it respawns on flee/death combat_data = await combat.initiate_combat(user_id, npc_id, player['location_id'], from_wandering_enemy=True) if combat_data: npc_def = NPCS.get(npc_id) message = f"⚔️ You engage the {npc_def.emoji} {npc_def.name}!\n\n" message += f"{npc_def.description}\n\n" message += f"{npc_def.emoji} Enemy HP: {combat_data['npc_hp']}/{combat_data['npc_max_hp']}\n" message += f"❤️ Your HP: {player['hp']}/{player['max_hp']}\n\n" message += "🎯 Your turn! What will you do?" keyboard = await keyboards.combat_keyboard(user_id) await send_or_edit_with_image(query, text=message, reply_markup=keyboard, image_path=npc_def.image_url if npc_def else None) else: await query.answer("Failed to initiate combat.", show_alert=True) elif action_type == "inspect": location_id, instance_id = data[1], data[2] location = game_world.get_location(location_id) if not location: await query.answer("Location not found.", show_alert=True) return interactable = location.get_interactable(instance_id) if not interactable: await query.answer("Object not found.", show_alert=False) return # Check if ALL actions are on cooldown all_on_cooldown = True for action_id in interactable.actions.keys(): cooldown_key = f"{instance_id}:{action_id}" if await database.get_cooldown(cooldown_key) == 0: all_on_cooldown = False break if all_on_cooldown and len(interactable.actions) > 0: await query.answer(f"The {interactable.name} has already been searched. Try again later.", show_alert=False) return # Show action menu await query.answer() image_path = interactable.image_path if interactable else None await send_or_edit_with_image(query, text=f"You focus on the {interactable.name}. What do you do?", reply_markup=await keyboards.actions_keyboard(location_id, instance_id), image_path=image_path) elif action_type == "action": location_id, instance_id, action_id = data[1], data[2], data[3] cooldown_key = f"{instance_id}:{action_id}" cooldown = await database.get_cooldown(cooldown_key) if cooldown > 0: await query.answer(f"Someone got to it just before you!", show_alert=False) return location = game_world.get_location(location_id) if not location: await query.answer("Location not found.", show_alert=True) return action_obj = location.get_interactable(instance_id).get_action(action_id) if player['stamina'] < action_obj.stamina_cost: await query.answer("You are too tired to do that!", show_alert=False) return # Answer the callback to dismiss loading state await query.answer() # FIX: Set cooldown ON ACTION, not after result. await database.set_cooldown(cooldown_key) outcome = logic.resolve_action(player, action_obj) new_stamina = player['stamina'] - action_obj.stamina_cost new_hp = player['hp'] - outcome.damage_taken await database.update_player(user_id, {"stamina": new_stamina, "hp": new_hp}) # Build detailed action result result_details = [] # Add the outcome text result_details.append(f"{outcome.text}") # Add stamina cost if action_obj.stamina_cost > 0: result_details.append(f"⚡️ Stamina: -{action_obj.stamina_cost}") # Add HP damage if any if outcome.damage_taken > 0: result_details.append(f"❤️ HP: -{outcome.damage_taken}") # Add items gained if outcome.items_reward: items_text = [] items_failed = [] for item_id, quantity in outcome.items_reward.items(): # Check if item can be added can_add, reason = await logic.can_add_item_to_inventory(user_id, item_id, quantity) if can_add: await database.add_item_to_inventory(user_id, item_id, quantity) item_def = ITEMS.get(item_id, {}) emoji = item_def.get('emoji', '❔') item_name = item_def.get('name', item_id) items_text.append(f"{emoji} {item_name} x{quantity}") else: item_def = ITEMS.get(item_id, {}) item_name = item_def.get('name', item_id) items_failed.append(f"{item_name} ({reason})") if items_text: result_details.append(f"🎁 Gained: {', '.join(items_text)}") if items_failed: result_details.append(f"⚠️ Couldn't take: {', '.join(items_failed)}") final_text = await get_player_status_text(user_id) final_text += f"\n\n━━━ Action Result ━━━\n" + "\n".join(result_details) # Get location image for the result screen current_location = game_world.get_location(player['location_id']) location_image = current_location.image_path if current_location else None await send_or_edit_with_image(query, text=final_text, reply_markup=keyboards.main_menu_keyboard(), image_path=location_image) # ... (Other handlers like pickup, inventory, move are mostly unchanged) ... elif action_type == "main_menu": await query.answer() status_text = await get_player_status_text(user_id) location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=status_text, reply_markup=keyboards.main_menu_keyboard(), image_path=location_image) elif action_type == "profile": await query.answer() from bot import combat # Calculate stats xp_current = player['xp'] xp_needed = combat.xp_for_level(player['level'] + 1) xp_for_current_level = combat.xp_for_level(player['level']) xp_progress = max(0, xp_current - xp_for_current_level) # Ensure non-negative xp_level_requirement = xp_needed - xp_for_current_level progress_percent = int((xp_progress / xp_level_requirement) * 100) if xp_level_requirement > 0 else 0 unspent = player.get('unspent_points', 0) profile_text = f"👤 {player['name']}\n" profile_text += f"━━━━━━━━━━━━━━━━━━━━\n\n" profile_text += f"Level: {player['level']}\n" profile_text += f"XP: {xp_current}/{xp_needed} ({progress_percent}%)\n" if unspent > 0: profile_text += f"⭐ Unspent Points: {unspent}\n" profile_text += f"\nHealth: {player['hp']}/{player['max_hp']} ❤️\n" profile_text += f"Stamina: {player['stamina']}/{player['max_stamina']} ⚡\n\n" profile_text += f"Stats:\n" profile_text += f"💪 Strength: {player['strength']}\n" profile_text += f"🏃 Agility: {player['agility']}\n" profile_text += f"💚 Endurance: {player['endurance']}\n" profile_text += f"🧠 Intellect: {player['intellect']}\n\n" profile_text += f"Combat:\n" profile_text += f"⚔️ Base Damage: {5 + player['strength'] // 2 + player['level']}\n" profile_text += f"🛡️ Flee Chance: {int((0.5 + player['agility'] / 100) * 100)}%\n" profile_text += f"💚 Stamina Regen: {1 + player['endurance'] // 10}/cycle\n" location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None # Add spend points button if player has unspent points keyboard_buttons = [] if unspent > 0: keyboard_buttons.append([InlineKeyboardButton("⭐ Spend Stat Points", callback_data="spend_points_menu")]) keyboard_buttons.append([InlineKeyboardButton("⬅️ Back", callback_data="main_menu")]) back_keyboard = InlineKeyboardMarkup(keyboard_buttons) await send_or_edit_with_image(query, text=profile_text, reply_markup=back_keyboard, image_path=location_image) elif action_type == "spend_points_menu": await query.answer() unspent = player.get('unspent_points', 0) if unspent <= 0: await query.answer("You have no points to spend!", show_alert=False) return text = f"⭐ Spend Stat Points\n\n" text += f"Available Points: {unspent}\n\n" text += f"Current Stats:\n" text += f"❤️ Max HP: {player['max_hp']} (+10 per point)\n" text += f"⚡ Max Stamina: {player['max_stamina']} (+5 per point)\n" text += f"💪 Strength: {player['strength']} (+1 per point)\n" text += f"🏃 Agility: {player['agility']} (+1 per point)\n" text += f"💚 Endurance: {player['endurance']} (+1 per point)\n" text += f"🧠 Intellect: {player['intellect']} (+1 per point)\n\n" text += f"💡 Choose wisely! Each point matters." keyboard = keyboards.spend_points_keyboard() await send_or_edit_with_image(query, text=text, reply_markup=keyboard) elif action_type == "spend_point": stat_name = data[1] unspent = player.get('unspent_points', 0) if unspent <= 0: await query.answer("You have no points to spend!", show_alert=False) return # Map stat names to updates stat_mapping = { 'max_hp': ('max_hp', 10, '❤️ Max HP'), 'max_stamina': ('max_stamina', 5, '⚡ Max Stamina'), 'strength': ('strength', 1, '💪 Strength'), 'agility': ('agility', 1, '🏃 Agility'), 'endurance': ('endurance', 1, '💚 Endurance'), 'intellect': ('intellect', 1, '🧠 Intellect'), } if stat_name not in stat_mapping: await query.answer("Invalid stat!", show_alert=False) return db_field, increase, display_name = stat_mapping[stat_name] new_value = player[db_field] + increase new_unspent = unspent - 1 await database.update_player(user_id, { db_field: new_value, 'unspent_points': new_unspent }) # Update local player data player[db_field] = new_value player['unspent_points'] = new_unspent await query.answer(f"+{increase} {display_name}!", show_alert=False) # Refresh the spend points menu text = f"⭐ Spend Stat Points\n\n" text += f"Available Points: {new_unspent}\n\n" text += f"Current Stats:\n" text += f"❤️ Max HP: {player['max_hp']} (+10 per point)\n" text += f"⚡ Max Stamina: {player['max_stamina']} (+5 per point)\n" text += f"💪 Strength: {player['strength']} (+1 per point)\n" text += f"🏃 Agility: {player['agility']} (+1 per point)\n" text += f"💚 Endurance: {player['endurance']} (+1 per point)\n" text += f"🧠 Intellect: {player['intellect']} (+1 per point)\n\n" text += f"💡 Choose wisely! Each point matters." keyboard = keyboards.spend_points_keyboard() await send_or_edit_with_image(query, text=text, reply_markup=keyboard) elif action_type == "move_menu": await query.answer() location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text="Where do you want to go?", reply_markup=await keyboards.move_keyboard(player['location_id'], user_id), image_path=location_image) elif action_type == "move": destination_id = data[1] # Get locations for distance calculation from_location = game_world.get_location(player['location_id']) to_location = game_world.get_location(destination_id) if not from_location or not to_location: await query.answer("Invalid location!", show_alert=True) return # Calculate stamina cost for travel inventory = await database.get_inventory(user_id) stamina_cost = logic.calculate_travel_stamina_cost(player, inventory, from_location, to_location) # Check if player has enough stamina if player['stamina'] < stamina_cost: await query.answer(f"Too tired to travel! Need {stamina_cost} stamina.", show_alert=True) return # Deduct stamina and update location new_stamina = player['stamina'] - stamina_cost await database.update_player(user_id, {"location_id": destination_id, "stamina": new_stamina}) await query.answer(f"⚡️ -{stamina_cost} stamina", show_alert=False) # Refresh player data after update player = await database.get_player(user_id) # Check for random NPC encounter (dynamic chance based on destination danger) from data.npcs import NPCS, get_random_npc_for_location, get_location_encounter_rate encounter_rate = get_location_encounter_rate(destination_id) if random.random() < encounter_rate: from bot import combat logging.info(f"Encounter triggered at {destination_id} (rate: {encounter_rate})") # Select random NPC appropriate for this location npc_id = get_random_npc_for_location(destination_id) # If location has spawns and NPC was selected, initiate combat if npc_id: combat_data = await combat.initiate_combat(user_id, npc_id, destination_id) if combat_data: npc_def = NPCS.get(npc_id) message = f"⚠️ A {npc_def.emoji} {npc_def.name} appears!\n\n" message += f"{npc_def.description}\n\n" message += f"{npc_def.emoji} Enemy HP: {combat_data['npc_hp']}/{combat_data['npc_max_hp']}\n" message += f"❤️ Your HP: {player['hp']}/{player['max_hp']}\n\n" message += "🎯 Your turn! What will you do?" keyboard = await keyboards.combat_keyboard(user_id) await send_or_edit_with_image(query, text=message, reply_markup=keyboard, image_path=npc_def.image_url if npc_def else None) return status_text = await get_player_status_text(user_id) new_location = game_world.get_location(destination_id) location_image = new_location.image_path if new_location else None await send_or_edit_with_image(query, text=status_text, reply_markup=keyboards.main_menu_keyboard(), image_path=location_image) elif action_type == "pickup_menu": # Show pickup options for an item dropped_item_id = int(data[1]) item_to_pickup = await database.get_dropped_item(dropped_item_id) if not item_to_pickup: await query.answer("Someone already picked that up!", show_alert=False) location_id = player['location_id'] location = game_world.get_location(location_id) dropped_items = await database.get_dropped_items_in_location(location_id) keyboard = await keyboards.inspect_keyboard(location_id, dropped_items) image_path = location.image_path if location else None await send_or_edit_with_image(query, text="You scan the area. You notice...", reply_markup=keyboard, image_path=image_path) return item_def = ITEMS.get(item_to_pickup['item_id'], {}) emoji = item_def.get('emoji', '❔') text = f"{emoji} {item_def.get('name', 'Unknown')}\n\n" text += f"Available: {item_to_pickup['quantity']}\n" text += f"Weight: {item_def.get('weight', 0)} kg each\n" text += f"Volume: {item_def.get('volume', 0)} vol each\n\n" text += "How many do you want to pick up?" await query.answer() keyboard = keyboards.pickup_options_keyboard(dropped_item_id, item_def.get('name', 'Unknown'), item_to_pickup['quantity']) # Keep location image for visual continuity location = game_world.get_location(player['location_id']) image_path = location.image_path if location else None await send_or_edit_with_image(query, text=text, reply_markup=keyboard, image_path=image_path) elif action_type == "pickup": dropped_item_id = int(data[1]) pickup_amount_str = data[2] if len(data) > 2 else "all" item_to_pickup = await database.get_dropped_item(dropped_item_id) if not item_to_pickup: await query.answer("Someone already picked that up!", show_alert=False) location_id = player['location_id'] location = game_world.get_location(location_id) dropped_items = await database.get_dropped_items_in_location(location_id) keyboard = await keyboards.inspect_keyboard(location_id, dropped_items) image_path = location.image_path if location else None await send_or_edit_with_image(query, text="You scan the area. You notice...", reply_markup=keyboard, image_path=image_path) return # Determine how much to pick up if pickup_amount_str == "all": pickup_amount = item_to_pickup['quantity'] else: pickup_amount = min(int(pickup_amount_str), item_to_pickup['quantity']) # Check inventory capacity can_add, reason = await logic.can_add_item_to_inventory(user_id, item_to_pickup['item_id'], pickup_amount) if not can_add: await query.answer(reason, show_alert=True) return # Add to inventory await database.add_item_to_inventory(user_id, item_to_pickup['item_id'], pickup_amount) # Update or remove dropped item remaining = item_to_pickup['quantity'] - pickup_amount if remaining > 0: # Update quantity await database.update_dropped_item(dropped_item_id, remaining) item_def = ITEMS.get(item_to_pickup['item_id'], {}) await query.answer(f"Picked up {pickup_amount}x {item_def.get('name', 'item')}. {remaining} remaining.", show_alert=False) else: # Remove item completely await database.remove_dropped_item(dropped_item_id) item_def = ITEMS.get(item_to_pickup['item_id'], {}) await query.answer(f"Picked up {pickup_amount}x {item_def.get('name', 'item')}.", show_alert=False) # Return to inspect area location_id = player['location_id'] location = game_world.get_location(location_id) dropped_items = await database.get_dropped_items_in_location(location_id) keyboard = await keyboards.inspect_keyboard(location_id, dropped_items) image_path = location.image_path if location else None await send_or_edit_with_image(query, text="You scan the area. You notice...", reply_markup=keyboard, image_path=image_path) elif action_type == "inventory_menu": await query.answer() inventory_items = await database.get_inventory(user_id) # Calculate inventory summary current_weight, current_volume = logic.calculate_inventory_load(inventory_items) max_weight, max_volume = logic.get_player_capacity(inventory_items, player) text = "🎒 Your Inventory:\n" text += f"📊 Weight: {current_weight}/{max_weight} kg\n" text += f"📦 Volume: {current_volume}/{max_volume} vol\n\n" if not inventory_items: text += "It's empty." # Keep current location image for context location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=text, reply_markup=keyboards.inventory_keyboard(inventory_items), image_path=location_image) elif action_type == "inventory_item": await query.answer() item_db_id = int(data[1]) item = await database.get_inventory_item(item_db_id) item_def = ITEMS.get(item['item_id'], {}) emoji = item_def.get('emoji', '❔') # Build item details text text = f"{emoji} {item_def.get('name', 'Unknown')}\n" # Add description if available description = item_def.get('description') if description: text += f"{description}\n\n" else: text += "\n" text += f"Weight: {item_def.get('weight', 0)} kg | Volume: {item_def.get('volume', 0)} vol\n" # Add weapon stats if applicable if item_def.get('type') == 'weapon': text += f"Damage: {item_def.get('damage_min', 0)}-{item_def.get('damage_max', 0)}\n" # Add consumable effects if applicable if item_def.get('type') == 'consumable': effects = [] if item_def.get('hp_restore'): effects.append(f"❤️ +{item_def.get('hp_restore')} HP") if item_def.get('stamina_restore'): effects.append(f"⚡ +{item_def.get('stamina_restore')} Stamina") if effects: text += f"Effects: {', '.join(effects)}\n" # Add equipped status if item.get('is_equipped'): text += "\n✅ Currently Equipped" # Keep current location image for context location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=text, reply_markup=keyboards.inventory_item_actions_keyboard(item_db_id, item_def, item.get('is_equipped', False), item['quantity']), image_path=location_image) elif action_type == "inventory_use": item_db_id = int(data[1]) item = await database.get_inventory_item(item_db_id) if not item: await query.answer("Item not found.", show_alert=False) return item_def = ITEMS.get(item['item_id'], {}) # Check if item is consumable if item_def.get('type') != 'consumable': await query.answer("This item cannot be used.", show_alert=False) return # Answer callback before processing await query.answer() # Apply item effects result_parts = [] updates = {} # Check for hp_restore if 'hp_restore' in item_def: hp_gain = item_def['hp_restore'] new_hp = min(player['max_hp'], player['hp'] + hp_gain) actual_gain = new_hp - player['hp'] updates['hp'] = new_hp if actual_gain > 0: result_parts.append(f"❤️ HP: +{actual_gain}") else: result_parts.append(f"❤️ HP: Already at maximum!") # Check for stamina_restore if 'stamina_restore' in item_def: stamina_gain = item_def['stamina_restore'] new_stamina = min(player['max_stamina'], player['stamina'] + stamina_gain) actual_gain = new_stamina - player['stamina'] updates['stamina'] = new_stamina if actual_gain > 0: result_parts.append(f"⚡️ Stamina: +{actual_gain}") else: result_parts.append(f"⚡️ Stamina: Already at maximum!") # Apply all updates at once if updates: await database.update_player(user_id, updates) # Remove one item from inventory if item['quantity'] > 1: await database.update_inventory_item(item['id'], quantity=item['quantity'] - 1) else: await database.remove_item_from_inventory(item['id']) # Build result message emoji = item_def.get('emoji', '❔') result_text = f"Used {emoji} {item_def.get('name')}\n\n" if result_parts: result_text += "\n".join(result_parts) else: result_text += "No effect." # Show updated inventory inventory_items = await database.get_inventory(user_id) # Calculate inventory summary current_weight, current_volume = logic.calculate_inventory_load(inventory_items) max_weight, max_volume = logic.get_player_capacity(inventory_items, player) text = "🎒 Your Inventory:\n" text += f"📊 Weight: {current_weight}/{max_weight} kg\n" text += f"📦 Volume: {current_volume}/{max_volume} vol\n\n" if not inventory_items: text += "It's empty." else: text += f"{result_text}" # Keep current location image for context location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=text, reply_markup=keyboards.inventory_keyboard(inventory_items), image_path=location_image) elif action_type == "inventory_drop": item_db_id = int(data[1]) drop_amount_str = data[2] if len(data) > 2 else None item = await database.get_inventory_item(item_db_id) if not item: await query.answer("Item not found.", show_alert=False) return item_def = ITEMS.get(item['item_id'], {}) # Determine how much to drop if drop_amount_str is None or drop_amount_str == "all": # Drop all - pass the full quantity to remove_item_from_inventory await database.drop_item_to_world(item['item_id'], item['quantity'], player['location_id']) await database.remove_item_from_inventory(item['id'], quantity=item['quantity']) await query.answer(f"You dropped all {item['quantity']}x {item_def.get('name')}.", show_alert=False) else: # Drop partial amount drop_amount = int(drop_amount_str) if drop_amount >= item['quantity']: # Drop all await database.drop_item_to_world(item['item_id'], item['quantity'], player['location_id']) await database.remove_item_from_inventory(item['id'], quantity=item['quantity']) await query.answer(f"You dropped all {item['quantity']}x {item_def.get('name')}.", show_alert=False) else: # Drop partial amount await database.drop_item_to_world(item['item_id'], drop_amount, player['location_id']) await database.update_inventory_item(item['id'], quantity=item['quantity'] - drop_amount) await query.answer(f"You dropped {drop_amount}x {item_def.get('name')}.", show_alert=False) inventory_items = await database.get_inventory(user_id) # Calculate inventory summary current_weight, current_volume = logic.calculate_inventory_load(inventory_items) max_weight, max_volume = logic.get_player_capacity(inventory_items, player) text = "🎒 Your Inventory:\n" text += f"📊 Weight: {current_weight}/{max_weight} kg\n" text += f"📦 Volume: {current_volume}/{max_volume} vol\n\n" if not inventory_items: text += "It's empty." # Keep current location image for context location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=text, reply_markup=keyboards.inventory_keyboard(inventory_items), image_path=location_image) elif action_type == "inventory_equip": item_db_id = int(data[1]) item = await database.get_inventory_item(item_db_id) if not item: await query.answer("Item not found.", show_alert=False) return item_def = ITEMS.get(item['item_id'], {}) item_slot = item_def.get('slot') if not item_slot: await query.answer("This item cannot be equipped.", show_alert=False) return # Unequip any item in the same slot inventory_items = await database.get_inventory(user_id) for inv_item in inventory_items: if inv_item.get('is_equipped'): inv_item_def = ITEMS.get(inv_item['item_id'], {}) if inv_item_def.get('slot') == item_slot: await database.update_inventory_item(inv_item['id'], is_equipped=False) # If equipping from a stack (quantity > 1), split the stack if item['quantity'] > 1: # Reduce the stack by 1 await database.update_inventory_item(item_db_id, quantity=item['quantity'] - 1) # Create a new inventory entry with quantity 1 and equipped=True new_item_id = await database.add_equipped_item_to_inventory(user_id, item['item_id']) await query.answer(f"Equipped {item_def.get('name')}!", show_alert=False) # Refresh the item view with the NEW equipped item item = await database.get_inventory_item(new_item_id) emoji = item_def.get('emoji', '❔') text = f"Item: {emoji} {item_def.get('name', 'Unknown')}\n" text += f"Weight: {item_def.get('weight', 0)} kg | Volume: {item_def.get('volume', 0)} vol\n" if item_def.get('type') == 'weapon': text += f"Damage: {item_def.get('damage_min', 0)}-{item_def.get('damage_max', 0)}\n" text += "\n✅ Currently Equipped" location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=text, reply_markup=keyboards.inventory_item_actions_keyboard(new_item_id, item_def, True, item['quantity']), image_path=location_image) else: # Equip the single item await database.update_inventory_item(item_db_id, is_equipped=True) await query.answer(f"Equipped {item_def.get('name')}!", show_alert=False) # Refresh the item view item = await database.get_inventory_item(item_db_id) emoji = item_def.get('emoji', '❔') text = f"Item: {emoji} {item_def.get('name', 'Unknown')}\n" text += f"Weight: {item_def.get('weight', 0)} kg | Volume: {item_def.get('volume', 0)} vol\n" if item_def.get('type') == 'weapon': text += f"Damage: {item_def.get('damage_min', 0)}-{item_def.get('damage_max', 0)}\n" text += "\n✅ Currently Equipped" location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=text, reply_markup=keyboards.inventory_item_actions_keyboard(item_db_id, item_def, True, item['quantity']), image_path=location_image) elif action_type == "inventory_unequip": item_db_id = int(data[1]) item = await database.get_inventory_item(item_db_id) if not item: await query.answer("Item not found.", show_alert=False) return item_def = ITEMS.get(item['item_id'], {}) # Check if there's an existing unequipped stack of the same item inventory_items = await database.get_inventory(user_id) existing_stack = None for inv_item in inventory_items: if inv_item['item_id'] == item['item_id'] and not inv_item.get('is_equipped') and inv_item['id'] != item_db_id: existing_stack = inv_item break if existing_stack: # Merge into existing stack await database.update_inventory_item(existing_stack['id'], quantity=existing_stack['quantity'] + 1) # Remove the equipped item await database.remove_item_from_inventory(item_db_id) await query.answer(f"Unequipped {item_def.get('name')}.", show_alert=False) # Show the merged stack item = await database.get_inventory_item(existing_stack['id']) emoji = item_def.get('emoji', '❔') text = f"Item: {emoji} {item_def.get('name', 'Unknown')}\n" text += f"Weight: {item_def.get('weight', 0)} kg | Volume: {item_def.get('volume', 0)} vol\n" if item_def.get('type') == 'weapon': text += f"Damage: {item_def.get('damage_min', 0)}-{item_def.get('damage_max', 0)}\n" location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=text, reply_markup=keyboards.inventory_item_actions_keyboard(existing_stack['id'], item_def, False, item['quantity']), image_path=location_image) else: # Just unequip the item await database.update_inventory_item(item_db_id, is_equipped=False) await query.answer(f"Unequipped {item_def.get('name')}.", show_alert=False) # Refresh the item view item = await database.get_inventory_item(item_db_id) emoji = item_def.get('emoji', '❔') text = f"Item: {emoji} {item_def.get('name', 'Unknown')}\n" text += f"Weight: {item_def.get('weight', 0)} kg | Volume: {item_def.get('volume', 0)} vol\n" if item_def.get('type') == 'weapon': text += f"Damage: {item_def.get('damage_min', 0)}-{item_def.get('damage_max', 0)}\n" location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=text, reply_markup=keyboards.inventory_item_actions_keyboard(item_db_id, item_def, False, item['quantity']), image_path=location_image) # --- Combat Actions --- elif action_type == "combat_attack": from bot import combat await query.answer() message, npc_died, turn_ended = await combat.player_attack(user_id) if npc_died: # Combat ended - return to main menu location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=message, reply_markup=keyboards.main_menu_keyboard(), image_path=location_image) elif turn_ended: # NPC's turn - auto-attack npc_message, player_died = await combat.npc_attack(user_id) message += "\n\n" + npc_message if player_died: # Player died - show death message await send_or_edit_with_image(query, text=message, reply_markup=None) else: # Show combat state combat_data = await database.get_combat(user_id) if combat_data: from data.npcs import NPCS npc_def = NPCS.get(combat_data['npc_id']) keyboard = await keyboards.combat_keyboard(user_id) await send_or_edit_with_image(query, text=message, reply_markup=keyboard, image_path=npc_def.image_url if npc_def else None) else: await query.answer(message, show_alert=False) elif action_type == "combat_flee": from bot import combat await query.answer() message, fled, turn_ended = await combat.flee_attempt(user_id) if fled: # Successfully fled - return to main menu location = game_world.get_location(player['location_id']) location_image = location.image_path if location else None await send_or_edit_with_image(query, text=message, reply_markup=keyboards.main_menu_keyboard(), image_path=location_image) elif turn_ended: # Failed to flee - NPC attacks npc_message, player_died = await combat.npc_attack(user_id) message += "\n\n" + npc_message if player_died: await send_or_edit_with_image(query, text=message, reply_markup=None) else: combat_data = await database.get_combat(user_id) if combat_data: from data.npcs import NPCS npc_def = NPCS.get(combat_data['npc_id']) keyboard = await keyboards.combat_keyboard(user_id) await send_or_edit_with_image(query, text=message, reply_markup=keyboard, image_path=npc_def.image_url if npc_def else None) else: await query.answer(message, show_alert=False) elif action_type == "combat_use_item_menu": await query.answer() keyboard = await keyboards.combat_items_keyboard(user_id) await send_or_edit_with_image(query, text="💊 Select an item to use:", reply_markup=keyboard) elif action_type == "combat_use_item": from bot import combat item_db_id = int(data[1]) message, turn_ended = await combat.use_item_in_combat(user_id, item_db_id) await query.answer(message, show_alert=False) if turn_ended: # NPC's turn npc_message, player_died = await combat.npc_attack(user_id) if player_died: await send_or_edit_with_image(query, text=message + "\n\n" + npc_message, reply_markup=None) else: combat_data = await database.get_combat(user_id) if combat_data: from data.npcs import NPCS npc_def = NPCS.get(combat_data['npc_id']) keyboard = await keyboards.combat_keyboard(user_id) full_message = message + "\n\n" + npc_message + "\n\n🎯 Your turn!" await send_or_edit_with_image(query, text=full_message, reply_markup=keyboard, image_path=npc_def.image_url if npc_def else None) elif action_type == "combat_back": await query.answer() combat_data = await database.get_combat(user_id) if combat_data: from data.npcs import NPCS npc_def = NPCS.get(combat_data['npc_id']) keyboard = await keyboards.combat_keyboard(user_id) message = f"⚔️ Combat with {npc_def.emoji} {npc_def.name}!\n" message += f"{npc_def.emoji} Enemy HP: {combat_data['npc_hp']}/{combat_data['npc_max_hp']}\n" message += f"❤️ Your HP: {player['hp']}/{player['max_hp']}\n\n" message += "🎯 Your turn!" if combat_data['turn'] == 'player' else "⏳ Enemy's turn..." await send_or_edit_with_image(query, text=message, reply_markup=keyboard, image_path=npc_def.image_url if npc_def else None) # --- Corpse Looting --- elif action_type == "loot_player_corpse": corpse_id = int(data[1]) corpse = await database.get_player_corpse(corpse_id) if not corpse: await query.answer("Corpse not found.", show_alert=False) return items = json.loads(corpse['items']) keyboard = keyboards.player_corpse_loot_keyboard(corpse_id, items) # Get location image location = game_world.get_location(player['location_id']) image_path = location.image_path if location else None await query.answer() text = f"🎒 {corpse['player_name']}'s bag\n\nYou find the remains of another survivor..." await send_or_edit_with_image(query, text=text, reply_markup=keyboard, image_path=image_path) elif action_type == "take_corpse_item": corpse_id = int(data[1]) item_index = int(data[2]) corpse = await database.get_player_corpse(corpse_id) if not corpse: await query.answer("Corpse not found.", show_alert=False) return items = json.loads(corpse['items']) if item_index >= len(items): await query.answer("Item not found.", show_alert=False) return item_data = items[item_index] item_def = ITEMS.get(item_data['item_id'], {}) # Check inventory capacity can_add, reason = await logic.can_add_item_to_inventory(user_id, item_data['item_id'], item_data['quantity']) if not can_add: await query.answer(reason, show_alert=False) return # Add to inventory await database.add_item_to_inventory(user_id, item_data['item_id'], item_data['quantity']) # Remove from corpse items.pop(item_index) if items: await database.update_player_corpse(corpse_id, json.dumps(items)) keyboard = keyboards.player_corpse_loot_keyboard(corpse_id, items) # Get location image location = game_world.get_location(player['location_id']) image_path = location.image_path if location else None await query.answer(f"Took {item_def.get('name', 'Unknown')}.", show_alert=False) text = f"🎒 {corpse['player_name']}'s bag" await send_or_edit_with_image(query, text=text, reply_markup=keyboard, image_path=image_path) else: # Bag is empty, remove it await database.remove_player_corpse(corpse_id) await query.answer(f"Took {item_def.get('name', 'Unknown')}. The bag is now empty.", show_alert=False) location = game_world.get_location(player['location_id']) dropped_items = await database.get_dropped_items_in_location(player['location_id']) keyboard = await keyboards.inspect_keyboard(player['location_id'], dropped_items) await send_or_edit_with_image(query, text="You scan the area. You notice...", reply_markup=keyboard, image_path=location.image_path if location else None) elif action_type == "scavenge_npc_corpse": corpse_id = int(data[1]) corpse = await database.get_npc_corpse(corpse_id) if not corpse: await query.answer("Corpse not found.", show_alert=False) return from data.npcs import NPCS npc_def = NPCS.get(corpse['npc_id']) loot_items = json.loads(corpse['loot_remaining']) keyboard = keyboards.npc_corpse_scavenge_keyboard(corpse_id, loot_items) # Get location image location = game_world.get_location(player['location_id']) image_path = location.image_path if location else None await query.answer() text = f"🔪 {npc_def.emoji} {npc_def.name} Corpse\n\n{npc_def.description}" await send_or_edit_with_image(query, text=text, reply_markup=keyboard, image_path=image_path) elif action_type == "scavenge_corpse_item": corpse_id = int(data[1]) loot_index = int(data[2]) corpse = await database.get_npc_corpse(corpse_id) if not corpse: await query.answer("Corpse not found.", show_alert=False) return loot_items = json.loads(corpse['loot_remaining']) if loot_index >= len(loot_items): await query.answer("Nothing to scavenge here.", show_alert=False) return loot_data = loot_items[loot_index] required_tool = loot_data.get('required_tool') # Check if player has required tool if required_tool: inventory_items = await database.get_inventory(user_id) has_tool = any(item['item_id'] == required_tool for item in inventory_items) if not has_tool: tool_def = ITEMS.get(required_tool, {}) await query.answer(f"You need a {tool_def.get('name', 'tool')} to scavenge this.", show_alert=False) return # Determine quantity quantity = random.randint(loot_data['quantity_min'], loot_data['quantity_max']) item_def = ITEMS.get(loot_data['item_id'], {}) # Check inventory capacity can_add, reason = await logic.can_add_item_to_inventory(user_id, loot_data['item_id'], quantity) if not can_add: await query.answer(reason, show_alert=False) return # Add to inventory await database.add_item_to_inventory(user_id, loot_data['item_id'], quantity) # Remove from corpse loot_items.pop(loot_index) if loot_items: await database.update_npc_corpse(corpse_id, json.dumps(loot_items)) keyboard = keyboards.npc_corpse_scavenge_keyboard(corpse_id, loot_items) # Get location image location = game_world.get_location(player['location_id']) image_path = location.image_path if location else None await query.answer(f"Scavenged {quantity}x {item_def.get('name', 'Unknown')}.", show_alert=False) from data.npcs import NPCS npc_def = NPCS.get(corpse['npc_id']) text = f"🔪 {npc_def.emoji} {npc_def.name} Corpse" await send_or_edit_with_image(query, text=text, reply_markup=keyboard, image_path=image_path) else: # Nothing left, remove corpse await database.remove_npc_corpse(corpse_id) await query.answer(f"Scavenged {quantity}x {item_def.get('name', 'Unknown')}. Nothing left on the corpse.", show_alert=False) location = game_world.get_location(player['location_id']) dropped_items = await database.get_dropped_items_in_location(player['location_id']) keyboard = await keyboards.inspect_keyboard(player['location_id'], dropped_items) await send_or_edit_with_image(query, text="You scan the area. You notice...", reply_markup=keyboard, image_path=location.image_path if location else None) elif action_type == "no_op": await query.answer() return elif action_type == "inspect_area_menu": await query.answer() location_id = data[1] location = game_world.get_location(location_id) dropped_items = await database.get_dropped_items_in_location(location_id) keyboard = await keyboards.inspect_keyboard(location_id, dropped_items) image_path = location.image_path if location else None await send_or_edit_with_image(query, text="You scan the area. You notice...", reply_markup=keyboard, image_path=image_path)