from telegram import InlineKeyboardButton, InlineKeyboardMarkup from data.world_loader import game_world from data.items import ITEMS # ... (main_menu_keyboard, move_keyboard are unchanged) ... def main_menu_keyboard() -> InlineKeyboardMarkup: keyboard = [[InlineKeyboardButton("πŸ—ΊοΈ Move", callback_data="move_menu"), InlineKeyboardButton("πŸ‘€ Inspect Area", callback_data="inspect_area")], [InlineKeyboardButton("πŸ‘€ Profile", callback_data="profile"), InlineKeyboardButton("πŸŽ’ Inventory", callback_data="inventory_menu")]] return InlineKeyboardMarkup(keyboard) async def move_keyboard(current_location_id: str, player_id: int) -> InlineKeyboardMarkup: """ Create a movement keyboard with stamina costs. Layout: [ North (⚑5) ] [ West (⚑5) ] [ East (⚑5) ] [ South (⚑5) ] [ Other exits (inside, down, etc.) ] [ Back ] """ from bot import database, logic keyboard = [] location = game_world.get_location(current_location_id) player = await database.get_player(player_id) inventory = await database.get_inventory(player_id) if location and player: # Dictionary to hold direction buttons compass_directions = {} other_exits = [] for direction, destination_id in location.exits.items(): destination = game_world.get_location(destination_id) if destination: # Calculate stamina cost for this specific route stamina_cost = logic.calculate_travel_stamina_cost(player, inventory, location, destination) # Map direction to emoji and label direction_lower = direction.lower() if direction_lower == "north": emoji = "⬆️" compass_directions["north"] = InlineKeyboardButton( f"{emoji} {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" ) elif direction_lower == "south": emoji = "⬇️" compass_directions["south"] = InlineKeyboardButton( f"{emoji} {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" ) elif direction_lower == "east": emoji = "➑️" compass_directions["east"] = InlineKeyboardButton( f"{emoji} {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" ) elif direction_lower == "west": emoji = "⬅️" compass_directions["west"] = InlineKeyboardButton( f"{emoji} {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" ) elif direction_lower == "northeast": emoji = "↗️" compass_directions["northeast"] = InlineKeyboardButton( f"{emoji} {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" ) elif direction_lower == "northwest": emoji = "↖️" compass_directions["northwest"] = InlineKeyboardButton( f"{emoji} {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" ) elif direction_lower == "southeast": emoji = "β†˜οΈ" compass_directions["southeast"] = InlineKeyboardButton( f"{emoji} {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" ) elif direction_lower == "southwest": emoji = "↙️" compass_directions["southwest"] = InlineKeyboardButton( f"{emoji} {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" ) elif direction_lower == "inside": emoji = "πŸšͺ" other_exits.append(InlineKeyboardButton( f"{emoji} Enter {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" )) elif direction_lower == "outside": emoji = "πŸšͺ" other_exits.append(InlineKeyboardButton( f"{emoji} Exit to {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" )) elif direction_lower == "down": emoji = "⬇️" other_exits.append(InlineKeyboardButton( f"{emoji} Descend to {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" )) elif direction_lower == "up": emoji = "⬆️" other_exits.append(InlineKeyboardButton( f"{emoji} Ascend to {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" )) else: # Generic fallback for any other direction emoji = "πŸ”€" other_exits.append(InlineKeyboardButton( f"{emoji} {destination.name} (⚑{stamina_cost})", callback_data=f"move:{destination_id}" )) # Build compass layout # Row 1: Northwest, North, Northeast top_row = [] if "northwest" in compass_directions: top_row.append(compass_directions["northwest"]) if "north" in compass_directions: top_row.append(compass_directions["north"]) if "northeast" in compass_directions: top_row.append(compass_directions["northeast"]) if top_row: keyboard.append(top_row) # Row 2: West and/or East middle_row = [] if "west" in compass_directions: middle_row.append(compass_directions["west"]) if "east" in compass_directions: middle_row.append(compass_directions["east"]) if middle_row: keyboard.append(middle_row) # Row 3: Southwest, South, Southeast bottom_row = [] if "southwest" in compass_directions: bottom_row.append(compass_directions["southwest"]) if "south" in compass_directions: bottom_row.append(compass_directions["south"]) if "southeast" in compass_directions: bottom_row.append(compass_directions["southeast"]) if bottom_row: keyboard.append(bottom_row) # Add other exits (inside, outside, up, down, etc.) for exit_button in other_exits: keyboard.append([exit_button]) keyboard.append([InlineKeyboardButton("⬅️ Back", callback_data="main_menu")]) return InlineKeyboardMarkup(keyboard) async def inspect_keyboard(location_id: str, dropped_items: list, wandering_enemies: list = None) -> InlineKeyboardMarkup: from bot import database from data.npcs import NPCS keyboard = [] location = game_world.get_location(location_id) # Show wandering enemies first if present (in pairs, emoji only) if wandering_enemies: row = [] for enemy in wandering_enemies: npc_def = NPCS.get(enemy['npc_id']) if npc_def: button = InlineKeyboardButton( f"⚠️ {npc_def.emoji} {npc_def.name}", callback_data=f"attack_wandering:{enemy['id']}" ) row.append(button) if len(row) == 2: keyboard.append(row) row = [] if row: # Add remaining enemy if odd number keyboard.append(row) if wandering_enemies: keyboard.append([InlineKeyboardButton("--- Environment ---", callback_data="no_op")]) # Show interactables in pairs when text is short enough if location: row = [] for instance_id, interactable in location.interactables.items(): label = interactable.name # Check if ANY action is available (not on cooldown) has_available_action = False for action_id in interactable.actions.keys(): cooldown_key = f"{instance_id}:{action_id}" if await database.get_cooldown(cooldown_key) == 0: has_available_action = True break if not has_available_action and len(interactable.actions) > 0: label += " ⏳" # Include location_id in callback data for efficient lookup button = InlineKeyboardButton(label, callback_data=f"inspect:{location_id}:{instance_id}") # If text is short (< 20 chars), try to pair it if len(label) < 20: row.append(button) if len(row) == 2: keyboard.append(row) row = [] else: # Long text, add any pending row first, then add this one alone if row: keyboard.append(row) row = [] keyboard.append([button]) # Add remaining button if odd number if row: keyboard.append(row) # Show player corpse bags player_corpses = await database.get_player_corpses_in_location(location_id) if player_corpses: keyboard.append([InlineKeyboardButton("--- Fallen survivors ---", callback_data="no_op")]) row = [] for corpse in player_corpses: button = InlineKeyboardButton( f"πŸŽ’ {corpse['player_name']}'s bag", callback_data=f"loot_player_corpse:{corpse['id']}" ) row.append(button) if len(row) == 2: keyboard.append(row) row = [] if row: keyboard.append(row) # Show NPC corpses npc_corpses = await database.get_npc_corpses_in_location(location_id) if npc_corpses: if not player_corpses: # Only add separator if not already added keyboard.append([InlineKeyboardButton("--- Corpses ---", callback_data="no_op")]) row = [] for corpse in npc_corpses: from data.npcs import NPCS npc_def = NPCS.get(corpse['npc_id']) if npc_def: button = InlineKeyboardButton( f"{npc_def.emoji} {npc_def.name}", callback_data=f"scavenge_npc_corpse:{corpse['id']}" ) row.append(button) if len(row) == 2: keyboard.append(row) row = [] if row: keyboard.append(row) if dropped_items: keyboard.append([InlineKeyboardButton("--- Items on the ground ---", callback_data="no_op")]) row = [] for item in dropped_items: item_def = ITEMS.get(item['item_id'], {}) emoji = item_def.get('emoji', '❔') quantity_text = f" x{item['quantity']}" if item['quantity'] > 1 else "" button = InlineKeyboardButton( f"{emoji} {item_def.get('name', 'Unknown')}{quantity_text}", callback_data=f"pickup_menu:{item['id']}" ) row.append(button) if len(row) == 2: keyboard.append(row) row = [] if row: keyboard.append(row) keyboard.append([InlineKeyboardButton("⬅️ Back", callback_data="main_menu")]) return InlineKeyboardMarkup(keyboard) def pickup_options_keyboard(item_id: int, item_name: str, quantity: int) -> InlineKeyboardMarkup: """Create pickup options keyboard with x1, x5, x10, and All options.""" keyboard = [] if quantity == 1: # Just show a single "Pick" button for single items keyboard.append([InlineKeyboardButton("πŸ“¦ Pick", callback_data=f"pickup:{item_id}:1")]) else: # Build pickup row with available options pickup_row = [InlineKeyboardButton("πŸ“¦ Pick x1", callback_data=f"pickup:{item_id}:1")] if quantity >= 5: pickup_row.append(InlineKeyboardButton("πŸ“¦ Pick x5", callback_data=f"pickup:{item_id}:5")) if quantity >= 10: pickup_row.append(InlineKeyboardButton("πŸ“¦ Pick x10", callback_data=f"pickup:{item_id}:10")) # Split into rows if more than 2 buttons if len(pickup_row) > 2: keyboard.append(pickup_row[:2]) keyboard.append(pickup_row[2:]) else: keyboard.append(pickup_row) # Add "Pick All" option keyboard.append([InlineKeyboardButton(f"πŸ“¦ Pick All ({quantity})", callback_data=f"pickup:{item_id}:all")]) # Back button keyboard.append([InlineKeyboardButton("⬅️ Back", callback_data="inspect_area")]) return InlineKeyboardMarkup(keyboard) async def actions_keyboard(location_id: str, instance_id: str) -> InlineKeyboardMarkup: from bot import database keyboard = [] location = game_world.get_location(location_id) if location: interactable = location.get_interactable(instance_id) if interactable: for action_id, action in interactable.actions.items(): cooldown_key = f"{instance_id}:{action_id}" cooldown = await database.get_cooldown(cooldown_key) label = action.label # Add stamina cost to the label if action.stamina_cost > 0: label += f" (⚑{action.stamina_cost})" if cooldown > 0: label += " ⏳" keyboard.append([InlineKeyboardButton(label, callback_data=f"action:{location_id}:{instance_id}:{action_id}")]) keyboard.append([InlineKeyboardButton("⬅️ Back", callback_data=f"inspect_area_menu:{location_id}")]) return InlineKeyboardMarkup(keyboard) # ... (inventory_keyboard, inventory_item_actions_keyboard are unchanged) ... def inventory_keyboard(inventory_items: list) -> InlineKeyboardMarkup: keyboard = [] if inventory_items: # Categorize and sort items # Group items by item_id and equipped status to handle stacking properly item_groups = {} for item in inventory_items: item_def = ITEMS.get(item['item_id'], {}) item_type = item_def.get('type', 'resource') item_name = item_def.get('name', 'Unknown') is_equipped = item.get('is_equipped', False) # Create a unique key for grouping: item_id + equipped status group_key = (item['item_id'], is_equipped) if group_key not in item_groups: item_groups[group_key] = { 'name': item_name, 'def': item_def, 'type': item_type, 'is_equipped': is_equipped, 'items': [] } item_groups[group_key]['items'].append(item) # Categorize groups equipped = [] consumables = [] weapons = [] equipment = [] resources = [] quest_items = [] for group_key, group_data in item_groups.items(): item_name = group_data['name'] item_def = group_data['def'] item_type = group_data['type'] is_equipped = group_data['is_equipped'] items_list = group_data['items'] # Calculate total quantity and weight/volume for this group total_quantity = sum(itm['quantity'] for itm in items_list) weight_per_item = item_def.get('weight', 0) volume_per_item = item_def.get('volume', 0) total_weight = weight_per_item * total_quantity total_volume = volume_per_item * total_quantity # Use the first item's ID for the callback (they're all the same item type) first_item_id = items_list[0]['id'] # Create item data tuple: (name, item_def, first_item_id, quantity, weight, volume, is_equipped) item_tuple = (item_name, item_def, first_item_id, total_quantity, total_weight, total_volume, is_equipped) # Only equipped items go to equipped section if is_equipped: equipped.append(item_tuple) elif item_type == 'consumable': consumables.append(item_tuple) elif item_type == 'weapon': weapons.append(item_tuple) elif item_type == 'equipment': equipment.append(item_tuple) elif item_type == 'quest': quest_items.append(item_tuple) else: resources.append(item_tuple) # Sort each category alphabetically by name equipped.sort(key=lambda x: x[0]) consumables.sort(key=lambda x: x[0]) weapons.sort(key=lambda x: x[0]) equipment.sort(key=lambda x: x[0]) resources.sort(key=lambda x: x[0]) quest_items.sort(key=lambda x: x[0]) # Build keyboard sections def add_section(section_name, items_list): if items_list: keyboard.append([InlineKeyboardButton(f"--- {section_name} ---", callback_data="no_op")]) row = [] for item_name, item_def, item_id, quantity, weight, volume, is_equipped in items_list: emoji = item_def.get('emoji', '❔') quantity_text = f" x{quantity}" if quantity > 1 else "" equipped_marker = " βœ“" if is_equipped else "" # Round to 2 decimals weight_vol_text = f" ({weight:.2f}kg, {volume:.2f}vol)" if quantity > 0 else "" button = InlineKeyboardButton( f"{emoji} {item_name}{quantity_text}{equipped_marker}{weight_vol_text}", callback_data=f"inventory_item:{item_id}" ) row.append(button) if len(row) == 2: keyboard.append(row) row = [] # Add remaining item if odd number if row: keyboard.append(row) # Add sections in order add_section("Equipped", equipped) add_section("Consumables", consumables) add_section("Weapons", weapons) add_section("Equipment", equipment) add_section("Resources", resources) add_section("Quest Items", quest_items) if not keyboard: keyboard.append([InlineKeyboardButton("--- Inventory is empty ---", callback_data="no_op")]) else: keyboard.append([InlineKeyboardButton("--- Inventory is empty ---", callback_data="no_op")]) keyboard.append([InlineKeyboardButton("⬅️ Back", callback_data="main_menu")]) return InlineKeyboardMarkup(keyboard) def inventory_item_actions_keyboard(item_db_id: int, item_def: dict, is_equipped: bool = False, quantity: int = 1) -> InlineKeyboardMarkup: keyboard = [] # Use button for consumables if item_def.get('type') == 'consumable': keyboard.append([InlineKeyboardButton("➑️ Use Item", callback_data=f"inventory_use:{item_db_id}")]) # Equip/Unequip button for weapons and equipment if item_def.get('type') in ["weapon", "equipment"]: if is_equipped: keyboard.append([InlineKeyboardButton("❌ Unequip", callback_data=f"inventory_unequip:{item_db_id}")]) else: keyboard.append([InlineKeyboardButton("βœ… Equip", callback_data=f"inventory_equip:{item_db_id}")]) # Drop buttons - simplified for single items if quantity == 1: # Just show a single "Drop" button keyboard.append([InlineKeyboardButton("πŸ—‘οΈ Drop", callback_data=f"inventory_drop:{item_db_id}:all")]) else: # Show x1, x5, x10 options based on quantity drop_row = [InlineKeyboardButton("πŸ—‘οΈ Drop x1", callback_data=f"inventory_drop:{item_db_id}:1")] if quantity >= 5: drop_row.append(InlineKeyboardButton("πŸ—‘οΈ Drop x5", callback_data=f"inventory_drop:{item_db_id}:5")) if quantity >= 10: drop_row.append(InlineKeyboardButton("πŸ—‘οΈ Drop x10", callback_data=f"inventory_drop:{item_db_id}:10")) # Split into rows if more than 2 buttons if len(drop_row) > 2: keyboard.append(drop_row[:2]) keyboard.append(drop_row[2:]) else: keyboard.append(drop_row) # Add "Drop All" option keyboard.append([InlineKeyboardButton(f"πŸ—‘οΈ Drop All ({quantity})", callback_data=f"inventory_drop:{item_db_id}:all")]) keyboard.append([InlineKeyboardButton("⬅️ Back to Inventory", callback_data="inventory_menu")]) return InlineKeyboardMarkup(keyboard) async def combat_keyboard(player_id: int) -> InlineKeyboardMarkup: """Create combat action keyboard.""" from bot import database keyboard = [] # Attack option keyboard.append([InlineKeyboardButton("βš”οΈ Attack", callback_data="combat_attack")]) # Flee option keyboard.append([InlineKeyboardButton("πŸƒ Try to Flee", callback_data="combat_flee")]) # Use item option (show consumables) inventory_items = await database.get_inventory(player_id) consumables = [item for item in inventory_items if ITEMS.get(item['item_id'], {}).get('type') == 'consumable'] if consumables: keyboard.append([InlineKeyboardButton("πŸ’Š Use Item", callback_data="combat_use_item_menu")]) return InlineKeyboardMarkup(keyboard) async def combat_items_keyboard(player_id: int) -> InlineKeyboardMarkup: """Show consumable items during combat.""" from bot import database keyboard = [] inventory_items = await database.get_inventory(player_id) consumables = [item for item in inventory_items if ITEMS.get(item['item_id'], {}).get('type') == 'consumable'] if consumables: keyboard.append([InlineKeyboardButton("--- Select item to use ---", callback_data="no_op")]) for item in consumables: item_def = ITEMS.get(item['item_id'], {}) emoji = item_def.get('emoji', '❔') keyboard.append([InlineKeyboardButton( f"{emoji} {item_def.get('name', 'Unknown')} x{item['quantity']}", callback_data=f"combat_use_item:{item['id']}" )]) keyboard.append([InlineKeyboardButton("⬅️ Back to Combat", callback_data="combat_back")]) return InlineKeyboardMarkup(keyboard) def corpse_keyboard(corpse_id: int, corpse_type: str) -> InlineKeyboardMarkup: """Create keyboard for interacting with corpses.""" keyboard = [] if corpse_type == "player": keyboard.append([InlineKeyboardButton("πŸŽ’ Loot Bag", callback_data=f"loot_player_corpse:{corpse_id}")]) else: # NPC corpse keyboard.append([InlineKeyboardButton("πŸ”ͺ Scavenge Corpse", callback_data=f"scavenge_npc_corpse:{corpse_id}")]) keyboard.append([InlineKeyboardButton("⬅️ Back", callback_data="inspect_area")]) return InlineKeyboardMarkup(keyboard) def player_corpse_loot_keyboard(corpse_id: int, items: list) -> InlineKeyboardMarkup: """Show items in a player corpse bag.""" keyboard = [] if items: keyboard.append([InlineKeyboardButton("--- Take items ---", callback_data="no_op")]) for i, item_data in enumerate(items): item_def = ITEMS.get(item_data['item_id'], {}) emoji = item_def.get('emoji', '❔') keyboard.append([InlineKeyboardButton( f"{emoji} Take {item_def.get('name', 'Unknown')} x{item_data['quantity']}", callback_data=f"take_corpse_item:{corpse_id}:{i}" )]) else: keyboard.append([InlineKeyboardButton("--- Bag is empty ---", callback_data="no_op")]) keyboard.append([InlineKeyboardButton("⬅️ Back", callback_data="inspect_area")]) return InlineKeyboardMarkup(keyboard) def npc_corpse_scavenge_keyboard(corpse_id: int, loot_items: list) -> InlineKeyboardMarkup: """Show scavenging options for NPC corpse.""" keyboard = [] if loot_items: keyboard.append([InlineKeyboardButton("--- Scavenge for materials ---", callback_data="no_op")]) for i, loot_data in enumerate(loot_items): item_def = ITEMS.get(loot_data['item_id'], {}) emoji = item_def.get('emoji', '❔') label = f"{emoji} {item_def.get('name', 'Unknown')}" if loot_data.get('required_tool'): tool_def = ITEMS.get(loot_data['required_tool'], {}) label += f" (needs {tool_def.get('name', 'tool')})" keyboard.append([InlineKeyboardButton( label, callback_data=f"scavenge_corpse_item:{corpse_id}:{i}" )]) else: keyboard.append([InlineKeyboardButton("--- Nothing left to scavenge ---", callback_data="no_op")]) keyboard.append([InlineKeyboardButton("⬅️ Back", callback_data="inspect_area")]) return InlineKeyboardMarkup(keyboard) def spend_points_keyboard() -> InlineKeyboardMarkup: """Create keyboard for spending stat points.""" keyboard = [ [ InlineKeyboardButton("❀️ Max HP (+10)", callback_data="spend_point:max_hp"), InlineKeyboardButton("⚑ Stamina (+5)", callback_data="spend_point:max_stamina") ], [ InlineKeyboardButton("πŸ’ͺ Strength (+1)", callback_data="spend_point:strength"), InlineKeyboardButton("πŸƒ Agility (+1)", callback_data="spend_point:agility") ], [ InlineKeyboardButton("πŸ’š Endurance (+1)", callback_data="spend_point:endurance"), InlineKeyboardButton("🧠 Intellect (+1)", callback_data="spend_point:intellect") ], [InlineKeyboardButton("⬅️ Back to Profile", callback_data="profile")] ] return InlineKeyboardMarkup(keyboard)