Files
echoes-of-the-ash/bot/keyboards.py

604 lines
26 KiB
Python

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)