1269 lines
59 KiB
Python
1269 lines
59 KiB
Python
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"<b>Location:</b> {location.name}\n<b>Status:</b> Healthy\n"
|
|
status += f"❤️ <b>HP:</b> {player['hp']}/{player['max_hp']} | ⚡️ <b>Stamina:</b> {player['stamina']}/{player['max_stamina']}\n"
|
|
status += f"🎒 <b>Load:</b> {weight}/{max_weight} kg | {volume}/{max_volume} vol\n"
|
|
|
|
if equipped_items:
|
|
status += f"⚔️ <b>Equipped:</b> {', '.join(equipped_items)}\n"
|
|
|
|
status += f"━━━━━━━━━━━━━━━━━━━━\n<i>{location.description}</i>"
|
|
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 = "📊 <b>Wandering Enemy Statistics</b>\n\n"
|
|
text += f"<b>Total Active Enemies:</b> {stats['total_active']}\n\n"
|
|
|
|
if stats['by_location']:
|
|
text += "<b>Enemies by Location:</b>\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 += "<i>No wandering enemies currently active.</i>"
|
|
|
|
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"<i>{outcome.text}</i>")
|
|
|
|
# Add stamina cost
|
|
if action_obj.stamina_cost > 0:
|
|
result_details.append(f"⚡️ <b>Stamina:</b> -{action_obj.stamina_cost}")
|
|
|
|
# Add HP damage if any
|
|
if outcome.damage_taken > 0:
|
|
result_details.append(f"❤️ <b>HP:</b> -{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"🎁 <b>Gained:</b> {', '.join(items_text)}")
|
|
if items_failed:
|
|
result_details.append(f"⚠️ <b>Couldn't take:</b> {', '.join(items_failed)}")
|
|
|
|
final_text = await get_player_status_text(user_id)
|
|
final_text += f"\n\n<b>━━━ Action Result ━━━</b>\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"👤 <b>{player['name']}</b>\n"
|
|
profile_text += f"━━━━━━━━━━━━━━━━━━━━\n\n"
|
|
profile_text += f"<b>Level:</b> {player['level']}\n"
|
|
profile_text += f"<b>XP:</b> {xp_current}/{xp_needed} ({progress_percent}%)\n"
|
|
|
|
if unspent > 0:
|
|
profile_text += f"⭐ <b>Unspent Points:</b> {unspent}\n"
|
|
|
|
profile_text += f"\n<b>Health:</b> {player['hp']}/{player['max_hp']} ❤️\n"
|
|
profile_text += f"<b>Stamina:</b> {player['stamina']}/{player['max_stamina']} ⚡\n\n"
|
|
profile_text += f"<b>Stats:</b>\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"<b>Combat:</b>\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"⭐ <b>Spend Stat Points</b>\n\n"
|
|
text += f"Available Points: <b>{unspent}</b>\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"⭐ <b>Spend Stat Points</b>\n\n"
|
|
text += f"Available Points: <b>{new_unspent}</b>\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} <b>{item_def.get('name', 'Unknown')}</b>\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 = "<b>🎒 Your Inventory:</b>\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} <b>{item_def.get('name', 'Unknown')}</b>\n"
|
|
|
|
# Add description if available
|
|
description = item_def.get('description')
|
|
if description:
|
|
text += f"<i>{description}</i>\n\n"
|
|
else:
|
|
text += "\n"
|
|
|
|
text += f"<b>Weight:</b> {item_def.get('weight', 0)} kg | <b>Volume:</b> {item_def.get('volume', 0)} vol\n"
|
|
|
|
# Add weapon stats if applicable
|
|
if item_def.get('type') == 'weapon':
|
|
text += f"<b>Damage:</b> {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"<b>Effects:</b> {', '.join(effects)}\n"
|
|
|
|
# Add equipped status
|
|
if item.get('is_equipped'):
|
|
text += "\n✅ <b>Currently Equipped</b>"
|
|
|
|
# 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"<b>Used {emoji} {item_def.get('name')}</b>\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 = "<b>🎒 Your Inventory:</b>\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 = "<b>🎒 Your Inventory:</b>\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"<b>Item:</b> {emoji} {item_def.get('name', 'Unknown')}\n"
|
|
text += f"<b>Weight:</b> {item_def.get('weight', 0)} kg | <b>Volume:</b> {item_def.get('volume', 0)} vol\n"
|
|
|
|
if item_def.get('type') == 'weapon':
|
|
text += f"<b>Damage:</b> {item_def.get('damage_min', 0)}-{item_def.get('damage_max', 0)}\n"
|
|
|
|
text += "\n✅ <b>Currently Equipped</b>"
|
|
|
|
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"<b>Item:</b> {emoji} {item_def.get('name', 'Unknown')}\n"
|
|
text += f"<b>Weight:</b> {item_def.get('weight', 0)} kg | <b>Volume:</b> {item_def.get('volume', 0)} vol\n"
|
|
|
|
if item_def.get('type') == 'weapon':
|
|
text += f"<b>Damage:</b> {item_def.get('damage_min', 0)}-{item_def.get('damage_max', 0)}\n"
|
|
|
|
text += "\n✅ <b>Currently Equipped</b>"
|
|
|
|
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"<b>Item:</b> {emoji} {item_def.get('name', 'Unknown')}\n"
|
|
text += f"<b>Weight:</b> {item_def.get('weight', 0)} kg | <b>Volume:</b> {item_def.get('volume', 0)} vol\n"
|
|
|
|
if item_def.get('type') == 'weapon':
|
|
text += f"<b>Damage:</b> {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"<b>Item:</b> {emoji} {item_def.get('name', 'Unknown')}\n"
|
|
text += f"<b>Weight:</b> {item_def.get('weight', 0)} kg | <b>Volume:</b> {item_def.get('volume', 0)} vol\n"
|
|
|
|
if item_def.get('type') == 'weapon':
|
|
text += f"<b>Damage:</b> {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)
|