Add visual progress bars and refactor handler modules

- Implement visual HP/Stamina/XP bars using Unicode characters (██░)
- Refactor handlers.py (1308 → 377 lines) into specialized modules:
  * action_handlers.py - World interaction and status display
  * inventory_handlers.py - Inventory management
  * combat_handlers.py - Combat actions
  * profile_handlers.py - Character stats with visual bars
  * corpse_handlers.py - Looting system
  * pickup_handlers.py - Item collection
- Add utility functions: create_progress_bar(), format_stat_bar()
- Organize all documentation into docs/ structure
- Create comprehensive documentation index with navigation
- Add UI examples showing before/after visual improvements
This commit is contained in:
Joan
2025-10-19 00:23:44 +02:00
parent ab13bdb9f1
commit 861f3b8a36
15 changed files with 3150 additions and 1062 deletions

371
bot/action_handlers.py Normal file
View File

@@ -0,0 +1,371 @@
"""
Action handlers for button callbacks.
This module contains organized handler functions for different types of player actions.
"""
import logging
import json
import random
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from . import database, keyboards, logic
from data.world_loader import game_world
from data.items import ITEMS
logger = logging.getLogger(__name__)
# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================
async def get_player_status_text(telegram_id: int) -> str:
"""Generate player status text with location and stats."""
from .utils import format_stat_bar
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')}")
# Build status with visual bars
status = f"<b>📍 Location:</b> {location.name}\n"
status += f"{format_stat_bar('HP', '❤️', player['hp'], player['max_hp'])}\n"
status += f"{format_stat_bar('Stamina', '', 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
# ============================================================================
# INSPECTION & WORLD INTERACTION HANDLERS
# ============================================================================
async def handle_inspect_area(query, user_id: int, player: dict):
"""Handle the inspect area action."""
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)
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
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text="You scan the area. You notice...",
reply_markup=keyboard,
image_path=image_path
)
async def handle_attack_wandering(query, user_id: int, player: dict, data: list):
"""Handle attacking 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
from .handlers import send_or_edit_with_image
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
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)
from .handlers import send_or_edit_with_image
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)
async def handle_inspect_interactable(query, user_id: int, player: dict, data: list):
"""Handle inspecting an interactable object."""
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
from .handlers import send_or_edit_with_image
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
)
async def handle_action(query, user_id: int, player: dict, data: list):
"""Handle performing an action on an interactable object."""
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("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
await query.answer()
# Set cooldown
await database.set_cooldown(cooldown_key)
# Resolve action
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 = [f"<i>{outcome.text}</i>"]
if action_obj.stamina_cost > 0:
result_details.append(f"⚡️ <b>Stamina:</b> -{action_obj.stamina_cost}")
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():
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
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=final_text,
reply_markup=keyboards.main_menu_keyboard(),
image_path=location_image
)
# ============================================================================
# NAVIGATION & MOVEMENT HANDLERS
# ============================================================================
async def handle_main_menu(query, user_id: int, player: dict):
"""Return to 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
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=status_text,
reply_markup=keyboards.main_menu_keyboard(),
image_path=location_image
)
async def handle_move_menu(query, user_id: int, player: dict):
"""Show movement options."""
await query.answer()
location = game_world.get_location(player['location_id'])
location_image = location.image_path if location else None
from .handlers import send_or_edit_with_image
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
)
async def handle_move(query, user_id: int, player: dict, data: list):
"""Handle player movement to a new location."""
destination_id = data[1]
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
inventory = await database.get_inventory(user_id)
stamina_cost = logic.calculate_travel_stamina_cost(player, inventory, from_location, to_location)
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
player = await database.get_player(user_id)
# Check for random NPC encounter
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
logger.info(f"Encounter triggered at {destination_id} (rate: {encounter_rate})")
npc_id = get_random_npc_for_location(destination_id)
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)
from .handlers import send_or_edit_with_image
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
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=status_text,
reply_markup=keyboards.main_menu_keyboard(),
image_path=location_image
)

171
bot/combat_handlers.py Normal file
View File

@@ -0,0 +1,171 @@
"""
Combat-related action handlers.
"""
import logging
from . import database, keyboards
from data.world_loader import game_world
logger = logging.getLogger(__name__)
async def handle_combat_attack(query, user_id: int, player: dict):
"""Handle player attack action in combat."""
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
from .handlers import send_or_edit_with_image
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:
from .handlers import send_or_edit_with_image
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)
from .handlers import send_or_edit_with_image
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)
async def handle_combat_flee(query, user_id: int, player: dict):
"""Handle flee attempt in combat."""
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
from .handlers import send_or_edit_with_image
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:
from .handlers import send_or_edit_with_image
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)
from .handlers import send_or_edit_with_image
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)
async def handle_combat_use_item_menu(query, user_id: int, player: dict):
"""Show menu of items that can be used in combat."""
await query.answer()
keyboard = await keyboards.combat_items_keyboard(user_id)
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text="💊 Select an item to use:",
reply_markup=keyboard
)
async def handle_combat_use_item(query, user_id: int, player: dict, data: list):
"""Use an item during combat."""
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:
from .handlers import send_or_edit_with_image
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!"
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=full_message,
reply_markup=keyboard,
image_path=npc_def.image_url if npc_def else None
)
async def handle_combat_back(query, user_id: int, player: dict):
"""Return to combat menu from item selection."""
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..."
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=message,
reply_markup=keyboard,
image_path=npc_def.image_url if npc_def else None
)

234
bot/corpse_handlers.py Normal file
View File

@@ -0,0 +1,234 @@
"""
Corpse looting handlers (player and NPC corpses).
"""
import logging
import json
import random
from . import database, keyboards, logic
from data.world_loader import game_world
from data.items import ITEMS
logger = logging.getLogger(__name__)
async def handle_loot_player_corpse(query, user_id: int, player: dict, data: list):
"""Show player corpse loot menu."""
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)
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..."
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=text,
reply_markup=keyboard,
image_path=image_path
)
async def handle_take_corpse_item(query, user_id: int, player: dict, data: list):
"""Take an item from a player corpse."""
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)
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"
from .handlers import send_or_edit_with_image
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'])
wandering_enemies = await database.get_wandering_enemies_in_location(player['location_id'])
keyboard = await keyboards.inspect_keyboard(player['location_id'], dropped_items, wandering_enemies)
from .handlers import send_or_edit_with_image
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
)
async def handle_scavenge_npc_corpse(query, user_id: int, player: dict, data: list):
"""Show NPC corpse scavenging menu."""
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)
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}"
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=text,
reply_markup=keyboard,
image_path=image_path
)
async def handle_scavenge_corpse_item(query, user_id: int, player: dict, data: list):
"""Scavenge a specific item from an NPC corpse."""
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)
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"
from .handlers import send_or_edit_with_image
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'])
wandering_enemies = await database.get_wandering_enemies_in_location(player['location_id'])
keyboard = await keyboards.inspect_keyboard(player['location_id'], dropped_items, wandering_enemies)
from .handlers import send_or_edit_with_image
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
)

File diff suppressed because it is too large Load Diff

355
bot/inventory_handlers.py Normal file
View File

@@ -0,0 +1,355 @@
"""
Inventory-related action handlers.
"""
import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from . import database, keyboards, logic
from data.world_loader import game_world
from data.items import ITEMS
logger = logging.getLogger(__name__)
async def handle_inventory_menu(query, user_id: int, player: dict):
"""Show player inventory."""
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
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=text,
reply_markup=keyboards.inventory_keyboard(inventory_items),
image_path=location_image
)
async def handle_inventory_item(query, user_id: int, player: dict, data: list):
"""Show details for a specific 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"
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>"
location = game_world.get_location(player['location_id'])
location_image = location.image_path if location else None
from .handlers import send_or_edit_with_image
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
)
async def handle_inventory_use(query, user_id: int, player: dict, data: list):
"""Use a consumable item from inventory."""
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'], {})
if item_def.get('type') != 'consumable':
await query.answer("This item cannot be used.", show_alert=False)
return
await query.answer()
# Apply item effects
result_parts = []
updates = {}
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!")
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!")
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)
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}"
location = game_world.get_location(player['location_id'])
location_image = location.image_path if location else None
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=text,
reply_markup=keyboards.inventory_keyboard(inventory_items),
image_path=location_image
)
async def handle_inventory_drop(query, user_id: int, player: dict, data: list):
"""Drop an item from inventory to the world."""
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":
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_amount = int(drop_amount_str)
if drop_amount >= item['quantity']:
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:
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)
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."
location = game_world.get_location(player['location_id'])
location_image = location.image_path if location else None
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=text,
reply_markup=keyboards.inventory_keyboard(inventory_items),
image_path=location_image
)
async def handle_inventory_equip(query, user_id: int, player: dict, data: list):
"""Equip an item from inventory."""
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, split the stack
if item['quantity'] > 1:
await database.update_inventory_item(item_db_id, quantity=item['quantity'] - 1)
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)
item = await database.get_inventory_item(new_item_id)
item_db_id = new_item_id
else:
await database.update_inventory_item(item_db_id, is_equipped=True)
await query.answer(f"Equipped {item_def.get('name')}!", show_alert=False)
item = await database.get_inventory_item(item_db_id)
# Refresh the item view
emoji = item_def.get('emoji', '')
text = f"{emoji} <b>{item_def.get('name', 'Unknown')}</b>\n"
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"
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
from .handlers import send_or_edit_with_image
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
)
async def handle_inventory_unequip(query, user_id: int, player: dict, data: list):
"""Unequip an item."""
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
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)
await database.remove_item_from_inventory(item_db_id)
await query.answer(f"Unequipped {item_def.get('name')}.", show_alert=False)
item = await database.get_inventory_item(existing_stack['id'])
item_db_id = existing_stack['id']
else:
# Just unequip
await database.update_inventory_item(item_db_id, is_equipped=False)
await query.answer(f"Unequipped {item_def.get('name')}.", show_alert=False)
item = await database.get_inventory_item(item_db_id)
# Refresh the item view
emoji = item_def.get('emoji', '')
text = f"{emoji} <b>{item_def.get('name', 'Unknown')}</b>\n"
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"
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
from .handlers import send_or_edit_with_image
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
)

135
bot/pickup_handlers.py Normal file
View File

@@ -0,0 +1,135 @@
"""
Pickup and item collection handlers.
"""
import logging
from . import database, keyboards, logic
from data.world_loader import game_world
from data.items import ITEMS
logger = logging.getLogger(__name__)
async def handle_pickup_menu(query, user_id: int, player: dict, data: list):
"""Show pickup options for a dropped 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)
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
from .handlers import send_or_edit_with_image
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']
)
location = game_world.get_location(player['location_id'])
image_path = location.image_path if location else None
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=text,
reply_markup=keyboard,
image_path=image_path
)
async def handle_pickup(query, user_id: int, player: dict, data: list):
"""Pick up a dropped item from the world."""
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)
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
from .handlers import send_or_edit_with_image
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
item_def = ITEMS.get(item_to_pickup['item_id'], {})
if remaining > 0:
await database.update_dropped_item(dropped_item_id, remaining)
await query.answer(
f"Picked up {pickup_amount}x {item_def.get('name', 'item')}. {remaining} remaining.",
show_alert=False
)
else:
await database.remove_dropped_item(dropped_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)
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
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text="You scan the area. You notice...",
reply_markup=keyboard,
image_path=image_path
)

152
bot/profile_handlers.py Normal file
View File

@@ -0,0 +1,152 @@
"""
Profile and character stat management handlers.
"""
import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from . import database, keyboards
from data.world_loader import game_world
logger = logging.getLogger(__name__)
async def handle_profile(query, user_id: int, player: dict):
"""Show player profile and stats."""
await query.answer()
from bot import combat
from .utils import format_stat_bar, create_progress_bar
# 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)
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)
# Build profile with visual bars
profile_text = f"👤 <b>{player['name']}</b>\n"
profile_text += f"━━━━━━━━━━━━━━━━━━━━\n\n"
profile_text += f"<b>Level:</b> {player['level']}\n"
# XP bar
xp_bar = create_progress_bar(xp_progress, xp_level_requirement, length=10)
profile_text += f"⭐ XP: {xp_bar} {progress_percent}% ({xp_current}/{xp_needed})\n"
if unspent > 0:
profile_text += f"💎 <b>Unspent Points:</b> {unspent}\n"
profile_text += f"\n{format_stat_bar('HP', '❤️', player['hp'], player['max_hp'])}\n"
profile_text += f"{format_stat_bar('Stamina', '', 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)
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text=profile_text,
reply_markup=back_keyboard,
image_path=location_image
)
async def handle_spend_points_menu(query, user_id: int, player: dict):
"""Show stat point spending 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()
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(query, text=text, reply_markup=keyboard)
async def handle_spend_point(query, user_id: int, player: dict, data: list):
"""Spend a stat point on a specific attribute."""
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()
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(query, text=text, reply_markup=keyboard)

View File

@@ -10,6 +10,64 @@ from telegram.ext import ContextTypes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def create_progress_bar(current: int, maximum: int, length: int = 10, filled_char: str = "", empty_char: str = "") -> str:
"""
Create a visual progress bar.
Args:
current: Current value
maximum: Maximum value
length: Length of the bar in characters (default 10)
filled_char: Character for filled portion (default █)
empty_char: Character for empty portion (default ░)
Returns:
String representation of progress bar
Examples:
>>> create_progress_bar(75, 100)
"███████░░░"
>>> create_progress_bar(0, 100)
"░░░░░░░░░░"
>>> create_progress_bar(100, 100)
"██████████"
"""
if maximum <= 0:
return empty_char * length
percentage = min(1.0, max(0.0, current / maximum))
filled_length = int(length * percentage)
empty_length = length - filled_length
return filled_char * filled_length + empty_char * empty_length
def format_stat_bar(label: str, emoji: str, current: int, maximum: int, bar_length: int = 10) -> str:
"""
Format a stat (HP, Stamina, etc.) with visual progress bar.
Args:
label: Stat label (e.g., "HP", "Stamina")
emoji: Emoji to display (e.g., "❤️", "")
current: Current value
maximum: Maximum value
bar_length: Length of the progress bar
Returns:
Formatted string with bar and percentage
Examples:
>>> format_stat_bar("HP", "❤️", 75, 100)
"❤️ HP: ███████░░░ 75% (75/100)"
>>> format_stat_bar("Stamina", "", 50, 100)
"⚡ Stamina: █████░░░░░ 50% (50/100)"
"""
bar = create_progress_bar(current, maximum, bar_length)
percentage = int((current / maximum * 100)) if maximum > 0 else 0
return f"{emoji} {label}: {bar} {percentage}% ({current}/{maximum})"
def get_admin_ids(): def get_admin_ids():
"""Get the list of admin user IDs from environment variable.""" """Get the list of admin user IDs from environment variable."""
admin_ids_str = os.getenv("ADMIN_IDS", "") admin_ids_str = os.getenv("ADMIN_IDS", "")

140
docs/README.md Normal file
View File

@@ -0,0 +1,140 @@
# Echoes of the Ashes - Documentation Index
## 📚 Documentation Overview
This directory contains all project documentation organized by category.
## 📁 Directory Structure
### `/docs/development/`
Technical documentation for developers
- **[BOT_MODULE.md](development/BOT_MODULE.md)** - Bot module architecture and handler system
- **[HANDLER_REFACTORING_SUMMARY.md](development/HANDLER_REFACTORING_SUMMARY.md)** - Code refactoring summary
- **[REFACTORING_NOTES.md](development/REFACTORING_NOTES.md)** - Detailed refactoring notes
- **[VISUAL_IMPROVEMENTS.md](development/VISUAL_IMPROVEMENTS.md)** - UI improvements and progress bars
- **[UI_EXAMPLES.md](development/UI_EXAMPLES.md)** - Before/after UI comparisons and visual mockups
### `/docs/game/`
Game design and mechanics documentation
- **[MECHANICS.md](game/MECHANICS.md)** - Complete game mechanics overview
### `/docs/api/`
API documentation and integration guides
- **Telegram Bot API** - Bot command reference
- **Database Schema** - Data models and relationships
- **Web Map Editor** - Map editor API and usage
## 🚀 Quick Links
### For Developers
- [Bot Module Documentation](development/BOT_MODULE.md) - Start here to understand the codebase
- [Handler System](development/HANDLER_REFACTORING_SUMMARY.md) - Handler architecture
- [Contributing Guide](../README.md#contributing) - How to contribute
### For Game Designers
- [Game Mechanics](game/) - Game systems and balance
- [World Editor](../web-map/README.md) - Map editing guide
### For Players
- [README](../README.md) - Project overview and setup
- [Game Guide](game/) - How to play
## 📝 Documentation Standards
### Markdown Guidelines
- Use clear, descriptive headings
- Include code examples where relevant
- Keep lines under 100 characters for readability
- Use emoji sparingly for visual organization
### File Naming
- Use `SCREAMING_SNAKE_CASE.md` for major documentation
- Use `PascalCase.md` for component-specific docs
- Use `kebab-case.md` for guides and tutorials
### Document Structure
```markdown
# Title
## Overview
Brief description
## Table of Contents
- [Section 1](#section-1)
- [Section 2](#section-2)
## Content
Detailed information
## See Also
Related documentation
```
## 🔄 Recent Updates
### October 19, 2025
- ✅ Reorganized documentation into structured folders
- ✅ Created documentation index (this file)
- ✅ Moved development docs from root to docs/development/
-**Added visual HP/Stamina bars** - Progress bars for better UI feedback
- ✅ Refactored handler system into modular architecture
- ✅ Created comprehensive bot module documentation
- ✅ Added utility functions for visual progress displays
### October 18, 2025
- ✅ Refactored handler system into modular architecture
- ✅ Created comprehensive bot module documentation
- ✅ Added refactoring notes and summaries
## 📖 Contributing to Documentation
When adding new documentation:
1. **Choose the right folder:**
- `development/` - Technical/code documentation
- `game/` - Game design and mechanics
- `api/` - API references and integration
2. **Update this index** with links to new documents
3. **Follow naming conventions** as outlined above
4. **Include:**
- Clear title and overview
- Table of contents for longer docs
- Code examples where applicable
- Links to related documentation
5. **Keep it updated** - Documentation should match the code
## 🔍 Finding Documentation
### By Topic
- **Setup & Installation** → [README.md](../README.md)
- **Bot Development** → [development/BOT_MODULE.md](development/BOT_MODULE.md)
- **Code Architecture** → [development/](development/)
- **Game Mechanics** → [game/](game/)
- **Map Editor** → [../web-map/README.md](../web-map/README.md)
- **Database** → [api/](api/)
### By File Type
- **README files** - Quick overviews and getting started guides
- **Technical specs** - Detailed architecture and implementation
- **Guides** - Step-by-step tutorials
- **Reference** - API documentation and data schemas
## 📧 Questions?
If you can't find what you're looking for:
1. Check the [main README](../README.md)
2. Search through existing documentation
3. Look at code comments and docstrings
4. Create an issue for missing documentation
---
**Last Updated:** October 19, 2025
**Maintained by:** Development Team

View File

@@ -0,0 +1,298 @@
# Bot Module Documentation
## Overview
The bot module contains all the Telegram bot logic for "Echoes of the Ashes" RPG game.
## Module Structure
### Core Modules
- **`handlers.py`** (14KB) - Main message router and utility functions
- **`database.py`** - Database operations and queries
- **`keyboards.py`** - Telegram keyboard layouts
- **`logic.py`** - Game logic and calculations
- **`combat.py`** (17KB) - Combat system implementation
- **`spawn_manager.py`** - Enemy spawning system
- **`utils.py`** - Utility functions and decorators
### Handler Modules (Refactored)
Organized by functionality for better maintainability:
#### **`action_handlers.py`** (14KB, 367 lines)
World interaction and inspection
- `get_player_status_text()` - Player status display
- `handle_inspect_area()` - Inspect locations
- `handle_attack_wandering()` - Attack wandering enemies
- `handle_inspect_interactable()` - Inspect objects
- `handle_action()` - Perform actions
- `handle_main_menu()` - Main menu navigation
- `handle_move_menu()` - Movement menu
- `handle_move()` - Travel between locations
#### **`inventory_handlers.py`** (13KB, 355 lines)
Inventory management system
- `handle_inventory_menu()` - Show inventory
- `handle_inventory_item()` - Item details
- `handle_inventory_use()` - Use consumables
- `handle_inventory_drop()` - Drop items
- `handle_inventory_equip()` - Equip gear
- `handle_inventory_unequip()` - Unequip gear
#### **`pickup_handlers.py`** (5.1KB, 135 lines)
Item collection from world
- `handle_pickup_menu()` - Pickup options
- `handle_pickup()` - Pick up items
#### **`combat_handlers.py`** (6.3KB, 171 lines)
Combat action handlers
- `handle_combat_attack()` - Attack enemy
- `handle_combat_flee()` - Flee combat
- `handle_combat_use_item_menu()` - Combat items menu
- `handle_combat_use_item()` - Use item in combat
- `handle_combat_back()` - Return to combat menu
#### **`profile_handlers.py`** (6.0KB, 147 lines)
Character progression
- `handle_profile()` - View profile
- `handle_spend_points_menu()` - Stat points menu
- `handle_spend_point()` - Allocate stat points
#### **`corpse_handlers.py`** (8.2KB, 234 lines)
Looting system
- `handle_loot_player_corpse()` - Loot player corpses
- `handle_take_corpse_item()` - Take items from player corpse
- `handle_scavenge_npc_corpse()` - Scavenge NPC corpses
- `handle_scavenge_corpse_item()` - Take items from NPC corpse
## Architecture
### Message Flow
```
Telegram Update
main.py (Application setup)
handlers.py::button_handler (Router)
Specialized Handler (action_handlers, combat_handlers, etc.)
database.py (Data operations)
keyboards.py (UI response)
Response to User
```
### Handler Pattern
All handlers follow a consistent pattern:
```python
async def handle_action(query, user_id: int, player: dict, data: list = None):
"""
Handle specific action.
Args:
query: Telegram callback query object
user_id: Telegram user ID
player: Player data dict
data: Optional list of parameters from callback_data
"""
# 1. Validate input
# 2. Perform action
# 3. Update database
# 4. Send response with send_or_edit_with_image()
```
### Shared Utilities
#### `send_or_edit_with_image()`
Central function for sending/editing messages with images:
```python
await send_or_edit_with_image(
query,
text="Message text",
reply_markup=keyboard,
image_path="/path/to/image.png" # Optional
)
```
Features:
- Smooth image transitions
- Image caching for performance
- Automatic fallback to text-only
- Edit vs. send detection
## Design Principles
### 1. Separation of Concerns
Each module handles a specific domain:
- Handlers → User interaction
- Database → Data persistence
- Logic → Game rules
- Combat → Battle mechanics
### 2. Single Responsibility
Each function has one clear purpose:
-`handle_inventory_use()` - Use items
-`handle_combat_attack()` - Attack in combat
-~~One giant function that does everything~~
### 3. Consistency
All handlers:
- Accept same parameter pattern
- Use async/await
- Handle errors gracefully
- Log important actions
### 4. Error Handling
Centralized in `button_handler`:
```python
try:
await handle_specific_action(...)
except Exception as e:
logger.error(f"Error: {e}", exc_info=True)
await query.answer("An error occurred.", show_alert=True)
```
## Adding New Handlers
### Step 1: Create Handler Function
```python
# In appropriate *_handlers.py module
async def handle_new_action(query, user_id: int, player: dict, data: list):
"""Handle new action."""
await query.answer()
# Your logic here
from .handlers import send_or_edit_with_image
await send_or_edit_with_image(
query,
text="Action result",
reply_markup=some_keyboard()
)
```
### Step 2: Import in handlers.py
```python
from .your_handlers import handle_new_action
```
### Step 3: Add Route
```python
# In button_handler()
elif action_type == "new_action":
await handle_new_action(query, user_id, player, data)
```
### Step 4: Create Keyboard Button
```python
# In keyboards.py
InlineKeyboardButton(
"New Action",
callback_data="new_action:param1:param2"
)
```
## Testing
### Manual Testing
1. Start the bot: `python main.py`
2. Send `/start` to the bot
3. Test each button action
4. Check logs for errors
### Unit Testing (Future)
```python
# tests/test_handlers.py
async def test_handle_inventory_use():
result = await handle_inventory_use(
mock_query,
user_id=123,
player=mock_player,
data=['inventory_use', '1']
)
assert result is not None
```
## Performance Considerations
### Image Caching
Images are cached after first upload:
```python
cached_file_id = await database.get_cached_image(image_path)
if not cached_file_id:
# Upload and cache
await database.cache_image(image_path, file_id)
```
### Database Queries
- Use indexed lookups (user_id, location_id)
- Batch operations where possible
- Async all the way
### Memory
- Don't store large objects in memory
- Use database for persistence
- Clean up old data periodically
## Debugging
### Enable Debug Logging
```python
# In main.py
logging.basicConfig(
level=logging.DEBUG, # Change from INFO
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
```
### Common Issues
**Issue:** Handler not being called
- Check callback_data format: `"action_type:param1:param2"`
- Verify route exists in `button_handler()`
- Check import statement
**Issue:** Image not showing
- Verify image path exists
- Check image cache
- Look for upload errors in logs
**Issue:** Database errors
- Check player exists
- Verify foreign key relationships
- Look at database schema
## Migration Guide
### From Old handlers.py
Old code in backup files:
- `backups/handlers_original.py`
- `backups/handlers.py.old`
All functionality preserved, just reorganized into modules.
## Future Enhancements
1. **Type Hints** - Add proper type annotations
2. **Unit Tests** - Test coverage for all handlers
3. **Decorators** - Common validations (requires_combat, requires_stamina)
4. **Base Classes** - Handler base class with common functionality
5. **Event System** - Decouple actions from responses
## Contributing
When adding new features:
1. Choose the appropriate handler module (or create new one)
2. Follow the existing pattern
3. Add docstrings
4. Log important actions
5. Handle errors gracefully
6. Update this documentation
## Questions?
See also:
- `REFACTORING_NOTES.md` - Refactoring details
- `HANDLER_REFACTORING_SUMMARY.md` - Before/after comparison
- `README.md` - Project overview

View File

@@ -0,0 +1,180 @@
# Handler Refactoring Summary
## Code Metrics
### Before Refactoring
- **Single file:** `bot/handlers.py` (~1,308 lines)
- **Giant function:** `button_handler()` with 1000+ lines of if/elif chains
- **Maintainability:** Very difficult to navigate and modify
### After Refactoring
- **7 organized modules:** Total ~1,786 lines (well-structured)
- **Main router:** `handlers.py` (377 lines) - clean routing logic
- **6 specialized modules:**
- `action_handlers.py` (367 lines) - World interaction
- `inventory_handlers.py` (355 lines) - Inventory management
- `corpse_handlers.py` (234 lines) - Corpse looting
- `combat_handlers.py` (171 lines) - Combat actions
- `profile_handlers.py` (147 lines) - Profile & stats
- `pickup_handlers.py` (135 lines) - Item pickup
## Architecture
```
bot/
├── handlers.py # Main router & utilities
│ ├── send_or_edit_with_image()
│ ├── start()
│ ├── export_map()
│ ├── spawn_stats()
│ └── button_handler() # Routes to specialized handlers
├── action_handlers.py # World & Inspection
│ ├── get_player_status_text()
│ ├── handle_inspect_area()
│ ├── handle_attack_wandering()
│ ├── handle_inspect_interactable()
│ ├── handle_action()
│ ├── handle_main_menu()
│ ├── handle_move_menu()
│ └── handle_move()
├── inventory_handlers.py # Inventory Management
│ ├── handle_inventory_menu()
│ ├── handle_inventory_item()
│ ├── handle_inventory_use()
│ ├── handle_inventory_drop()
│ ├── handle_inventory_equip()
│ └── handle_inventory_unequip()
├── pickup_handlers.py # Item Collection
│ ├── handle_pickup_menu()
│ └── handle_pickup()
├── combat_handlers.py # Combat System
│ ├── handle_combat_attack()
│ ├── handle_combat_flee()
│ ├── handle_combat_use_item_menu()
│ ├── handle_combat_use_item()
│ └── handle_combat_back()
├── profile_handlers.py # Character Stats
│ ├── handle_profile()
│ ├── handle_spend_points_menu()
│ └── handle_spend_point()
└── corpse_handlers.py # Looting System
├── handle_loot_player_corpse()
├── handle_take_corpse_item()
├── handle_scavenge_npc_corpse()
└── handle_scavenge_corpse_item()
```
## Key Improvements
### 1. Separation of Concerns
Each module handles a specific domain of functionality:
- **Action Handlers** → World exploration & interaction
- **Inventory Handlers** → Item management
- **Combat Handlers** → Battle mechanics
- **Profile Handlers** → Character progression
- **Corpse Handlers** → Looting system
- **Pickup Handlers** → Item collection
### 2. Single Responsibility Principle
Each function has one clear purpose:
-`handle_inventory_use()` - Use items
-`handle_combat_attack()` - Attack in combat
-`handle_pickup()` - Pick up items
-~~`button_handler()` - Do everything~~
### 3. Improved Error Handling
```python
# Centralized error handling in router
try:
if action_type == "inspect_area":
await handle_inspect_area(query, user_id, player)
# ... more routes
except Exception as e:
logger.error(f"Error handling {action_type}: {e}", exc_info=True)
await query.answer("An error occurred.", show_alert=True)
```
### 4. Better Code Navigation
- Jump to specific functionality in seconds
- IDE autocomplete works better
- Easier to review code changes
- Reduced cognitive load
### 5. Testability
Each handler can now be tested independently:
```python
# Easy to test
await handle_inventory_use(mock_query, user_id, player, data)
# vs. testing 1000 lines of if/elif
```
## Migration Path
All functionality has been preserved. The refactoring only changed the organization, not the behavior.
### Verified Compatible
- ✅ All action types still handled
- ✅ Same function signatures
- ✅ Same error handling behavior
- ✅ No breaking changes to external code
### Backup Files
Original files saved in `backups/`:
- `handlers_original.py`
- `handlers.py.old`
## Future Enhancements
1. **Add Type Hints**
```python
async def handle_inventory_use(
query: CallbackQuery,
user_id: int,
player: dict,
data: list[str]
) -> None:
```
2. **Create Base Handler Class**
```python
class BaseHandler:
def __init__(self, query, user_id, player):
self.query = query
self.user_id = user_id
self.player = player
```
3. **Add Unit Tests**
```python
def test_handle_inventory_use():
# Test consumable usage
# Test inventory updates
# Test error cases
```
4. **Add Handler Decorators**
```python
@requires_combat
async def handle_combat_attack(...):
@requires_stamina(10)
async def handle_action(...):
```
## Conclusion
This refactoring transforms a monolithic 1,308-line file with a 1000+ line function into a well-organized, modular architecture with:
- ✅ Clear separation of concerns
- ✅ Easy navigation and maintenance
- ✅ Better error handling
- ✅ Improved testability
- ✅ No breaking changes
**Result:** The codebase is now much easier to understand, modify, and extend.

View File

@@ -0,0 +1,127 @@
# Code Refactoring - Handler Organization
## Overview
The `bot/handlers.py` file has been refactored to improve code organization, readability, and maintainability. The massive `button_handler` function (previously 1000+ lines) has been broken down into logical, focused modules.
## New Structure
### Main Module: `bot/handlers.py`
**Purpose:** Core routing and utility functions
- `send_or_edit_with_image()` - Image handling utility
- `start()` - /start command handler
- `export_map()` - Admin command for map export
- `spawn_stats()` - Admin command for spawn statistics
- `button_handler()` - Main router that delegates to specific handlers
### Action Handlers: `bot/action_handlers.py`
**Purpose:** World interaction and inspection
- `get_player_status_text()` - Generate player status display
- `handle_inspect_area()` - Inspect current location
- `handle_attack_wandering()` - Attack wandering enemies
- `handle_inspect_interactable()` - Inspect objects
- `handle_action()` - Perform actions on interactables
- `handle_main_menu()` - Return to main menu
- `handle_move_menu()` - Show movement options
- `handle_move()` - Handle player movement
### Inventory Handlers: `bot/inventory_handlers.py`
**Purpose:** Inventory management
- `handle_inventory_menu()` - Show inventory
- `handle_inventory_item()` - Show item details
- `handle_inventory_use()` - Use consumable items
- `handle_inventory_drop()` - Drop items to world
- `handle_inventory_equip()` - Equip items
- `handle_inventory_unequip()` - Unequip items
### Pickup Handlers: `bot/pickup_handlers.py`
**Purpose:** Item collection from world
- `handle_pickup_menu()` - Show pickup options
- `handle_pickup()` - Pick up dropped items
### Combat Handlers: `bot/combat_handlers.py`
**Purpose:** Combat actions
- `handle_combat_attack()` - Attack enemy
- `handle_combat_flee()` - Attempt to flee
- `handle_combat_use_item_menu()` - Show usable items in combat
- `handle_combat_use_item()` - Use item during combat
- `handle_combat_back()` - Return to combat menu
### Profile Handlers: `bot/profile_handlers.py`
**Purpose:** Character profile and stat management
- `handle_profile()` - Show player profile
- `handle_spend_points_menu()` - Show stat point menu
- `handle_spend_point()` - Spend stat points
### Corpse Handlers: `bot/corpse_handlers.py`
**Purpose:** Looting corpses
- `handle_loot_player_corpse()` - Show player corpse loot
- `handle_take_corpse_item()` - Take item from player corpse
- `handle_scavenge_npc_corpse()` - Show NPC corpse loot
- `handle_scavenge_corpse_item()` - Scavenge from NPC corpse
## Benefits
### 1. **Improved Readability**
- Each module focuses on a specific domain
- Function names clearly describe their purpose
- Easier to find and understand specific functionality
### 2. **Better Maintainability**
- Changes to one feature don't affect others
- Easier to test individual components
- Reduced risk of merge conflicts
### 3. **Logical Organization**
- Related functions grouped together
- Clear separation of concerns
- Follows Single Responsibility Principle
### 4. **Easier Navigation**
- Jump to specific functionality quickly
- No need to scroll through 1000+ lines
- Clear module imports show dependencies
### 5. **Error Handling**
- Centralized error handling in router
- Consistent error messages
- Better logging
## Migration Notes
### Old Structure
```python
async def button_handler():
# 1000+ lines of if/elif statements
if action_type == "inspect_area":
# 50 lines of code
elif action_type == "inventory_menu":
# 30 lines of code
# ... hundreds more lines
```
### New Structure
```python
async def button_handler():
# Clean router - delegates to specialized handlers
if action_type == "inspect_area":
await handle_inspect_area(query, user_id, player)
elif action_type == "inventory_menu":
await handle_inventory_menu(query, user_id, player)
```
## Future Improvements
1. **Add unit tests** for each handler module
2. **Create handler base class** for common functionality
3. **Add type hints** for better IDE support
4. **Document return types** and error conditions
5. **Add handler decorators** for common validations
## Backup Files
Old versions have been preserved in `backups/`:
- `handlers_original.py` - Last version before refactoring
- `handlers.py.old` - Additional backup
## Date
Refactored: October 19, 2025

View File

@@ -0,0 +1,240 @@
# Visual UI Examples
## Before & After Comparison
### Main Menu / Player Status
#### Before
```
Location: Downtown Plaza
Status: Healthy
❤️ HP: 70/100 | ⚡️ Stamina: 50/100
🎒 Load: 15/50 kg | 30/100 vol
⚔️ Equipped: 🔧 Wrench, 🎒 Backpack
━━━━━━━━━━━━━━━━━━━━
A desolate plaza, once bustling with life...
```
#### After
```
📍 Location: Downtown Plaza
❤️ HP: ███████░░░ 70% (70/100)
⚡ Stamina: █████░░░░░ 50% (50/100)
🎒 Load: 15/50 kg | 30/100 vol
⚔️ Equipped: 🔧 Wrench, 🎒 Backpack
━━━━━━━━━━━━━━━━━━━━
A desolate plaza, once bustling with life...
```
### Player Profile
#### Before
```
👤 PlayerName
━━━━━━━━━━━━━━━━━━━━
Level: 5
XP: 240/600 (40%)
⭐ Unspent Points: 2
Health: 70/100 ❤️
Stamina: 50/100 ⚡
Stats:
💪 Strength: 12
🏃 Agility: 8
💚 Endurance: 10
🧠 Intellect: 5
Combat:
⚔️ Base Damage: 13
🛡️ Flee Chance: 58%
💚 Stamina Regen: 2/cycle
```
#### After
```
👤 PlayerName
━━━━━━━━━━━━━━━━━━━━
Level: 5
⭐ XP: ████░░░░░░ 40% (240/600)
💎 Unspent Points: 2
❤️ HP: ███████░░░ 70% (70/100)
⚡ Stamina: █████░░░░░ 50% (50/100)
Stats:
💪 Strength: 12
🏃 Agility: 8
💚 Endurance: 10
🧠 Intellect: 5
Combat:
⚔️ Base Damage: 13
🛡️ Flee Chance: 58%
💚 Stamina Regen: 2/cycle
```
## Visual States
### Critical Health
```
❤️ HP: ██░░░░░░░░ 20% (20/100)
```
### Low Stamina
```
⚡ Stamina: ██░░░░░░░░ 20% (20/100)
```
### Half Values
```
❤️ HP: █████░░░░░ 50% (50/100)
⚡ Stamina: █████░░░░░ 50% (50/100)
```
### Full Values
```
❤️ HP: ██████████ 100% (100/100)
⚡ Stamina: ██████████ 100% (100/100)
```
### Empty/Dead
```
❤️ HP: ░░░░░░░░░░ 0% (0/100)
⚡ Stamina: ░░░░░░░░░░ 0% (0/100)
```
## XP Progress Examples
### Just Leveled Up
```
⭐ XP: ░░░░░░░░░░ 0% (0/600)
```
### Quarter Progress
```
⭐ XP: ██░░░░░░░░ 25% (150/600)
```
### Half Progress
```
⭐ XP: █████░░░░░ 50% (300/600)
```
### Almost Level Up
```
⭐ XP: █████████░ 95% (570/600)
```
## Character Build Examples
### Tank Build (High HP/Endurance)
```
👤 TankWarrior
━━━━━━━━━━━━━━━━━━━━
Level: 10
⭐ XP: ███████░░░ 65% (780/1200)
❤️ HP: ████████░░ 80% (160/200)
⚡ Stamina: ██████░░░░ 60% (90/150)
Stats:
💪 Strength: 15
🏃 Agility: 5
💚 Endurance: 20
🧠 Intellect: 5
```
### Glass Cannon (High Damage, Low HP)
```
👤 GlassCannon
━━━━━━━━━━━━━━━━━━━━
Level: 10
⭐ XP: ███████░░░ 65% (780/1200)
❤️ HP: ██████░░░░ 60% (60/100)
⚡ Stamina: ████████░░ 80% (120/150)
Stats:
💪 Strength: 25
🏃 Agility: 10
💚 Endurance: 5
🧠 Intellect: 5
```
### Balanced Build
```
👤 AllRounder
━━━━━━━━━━━━━━━━━━━━
Level: 10
⭐ XP: ███████░░░ 65% (780/1200)
❤️ HP: ███████░░░ 70% (105/150)
⚡ Stamina: ███████░░░ 70% (88/125)
Stats:
💪 Strength: 15
🏃 Agility: 12
💚 Endurance: 12
🧠 Intellect: 6
```
## Combat Display Examples
### Player vs Enemy (Active Combat)
```
⚔️ Combat with 🐕 Feral Dog!
A mangy, aggressive dog with matted fur...
🐕 Enemy HP: ██████░░░░ 60% (30/50)
❤️ Your HP: ███████░░░ 70% (70/100)
🎯 Your turn! What will you do?
[⚔️ Attack] [🏃 Flee] [💊 Use Item]
```
### Low Health Warning
```
⚠️ A 🧟 Infected Human appears!
A shambling, infected survivor...
🧟 Enemy HP: ████████░░ 80% (80/100)
❤️ Your HP: ██░░░░░░░░ 20% (20/100) ⚠️ CRITICAL!
🎯 Your turn! What will you do?
```
## Inventory Display
### Inventory Load Bars (Future Enhancement)
```
🎒 Your Inventory:
📊 Weight: █████░░░░░ 50% (25/50 kg)
📦 Volume: ███░░░░░░░ 30% (30/100 vol)
Items:
🔧 Wrench x1
🥫 Canned Food x3
💊 Bandage x5
```
## Mobile Display
All bars are designed to work perfectly on mobile:
- Unicode characters supported on all platforms
- Monospaced alignment for consistent display
- Clear even on small screens
- Works in all Telegram clients
---
**Implementation Date:** October 19, 2025
**Status:** ✅ Live in Production

View File

@@ -0,0 +1,297 @@
# Visual UI Improvements
## Overview
This document describes the visual improvements made to the game's user interface, particularly the addition of progress bars for stats.
## Progress Bars
### Implementation
Visual progress bars have been added to display HP, Stamina, and XP using Unicode block characters.
### Format
```
❤️ HP: ███████░░░ 70% (70/100)
⚡ Stamina: █████░░░░░ 50% (50/100)
⭐ XP: ████░░░░░░ 40% (240/600)
```
### Components
- **Emoji** - Visual indicator of stat type
- **Label** - Stat name (HP, Stamina, etc.)
- **Progress Bar** - Visual representation using █ (filled) and ░ (empty)
- **Percentage** - Numeric percentage value
- **Values** - Current/Maximum in parentheses
### Characters Used
- `█` (U+2588) - Full Block - Represents filled portion
- `░` (U+2591) - Light Shade - Represents empty portion
## Utility Functions
### `create_progress_bar()`
Creates a visual progress bar from current and maximum values.
**Signature:**
```python
def create_progress_bar(
current: int,
maximum: int,
length: int = 10,
filled_char: str = "",
empty_char: str = ""
) -> str
```
**Parameters:**
- `current` - Current value of the stat
- `maximum` - Maximum possible value
- `length` - Number of characters in the bar (default: 10)
- `filled_char` - Character for filled portion (default: █)
- `empty_char` - Character for empty portion (default: ░)
**Returns:**
String containing the progress bar
**Examples:**
```python
>>> create_progress_bar(75, 100)
"███████░░░"
>>> create_progress_bar(0, 100)
"░░░░░░░░░░"
>>> create_progress_bar(100, 100)
"██████████"
>>> create_progress_bar(33, 100, length=6)
"██░░░░"
```
### `format_stat_bar()`
Formats a complete stat line with label, bar, and values.
**Signature:**
```python
def format_stat_bar(
label: str,
emoji: str,
current: int,
maximum: int,
bar_length: int = 10
) -> str
```
**Parameters:**
- `label` - Name of the stat (e.g., "HP", "Stamina")
- `emoji` - Emoji to display (e.g., "❤️", "⚡")
- `current` - Current value
- `maximum` - Maximum value
- `bar_length` - Length of the progress bar (default: 10)
**Returns:**
Formatted string with emoji, label, bar, percentage, and values
**Examples:**
```python
>>> format_stat_bar("HP", "❤️", 75, 100)
"❤️ HP: ███████░░░ 75% (75/100)"
>>> format_stat_bar("Stamina", "", 50, 100)
"⚡ Stamina: █████░░░░░ 50% (50/100)"
>>> format_stat_bar("XP", "", 240, 600)
"⭐ XP: ████░░░░░░ 40% (240/600)"
```
## Updated Displays
### Player Status (Main Menu)
**Before:**
```
Location: Downtown Plaza
Status: Healthy
❤️ HP: 70/100 | ⚡️ Stamina: 50/100
🎒 Load: 5/50 kg | 10/100 vol
```
**After:**
```
📍 Location: Downtown Plaza
❤️ HP: ███████░░░ 70% (70/100)
⚡ Stamina: █████░░░░░ 50% (50/100)
🎒 Load: 5/50 kg | 10/100 vol
```
### Player Profile
**Before:**
```
👤 PlayerName
━━━━━━━━━━━━━━━━━━━━
Level: 5
XP: 240/600 (40%)
Health: 70/100 ❤️
Stamina: 50/100 ⚡
```
**After:**
```
👤 PlayerName
━━━━━━━━━━━━━━━━━━━━
Level: 5
⭐ XP: ████░░░░░░ 40% (240/600)
❤️ HP: ███████░░░ 70% (70/100)
⚡ Stamina: █████░░░░░ 50% (50/100)
```
## Benefits
### 1. **Visual Clarity**
- Instant understanding of stat levels at a glance
- No mental math required to assess health/stamina
- Color-coded through emojis (❤️ = health, ⚡ = stamina)
### 2. **Better Gameplay**
- Players can quickly assess danger
- Easy to see when healing/rest is needed
- Visual feedback is more engaging
### 3. **Mobile Friendly**
- Unicode characters work on all platforms
- No images required
- Works in all Telegram clients
### 4. **Accessibility**
- Percentage and numeric values still provided
- Multiple representations of same data
- Screen readers can parse text values
## Customization
### Changing Bar Length
Modify the `bar_length` parameter:
```python
# Shorter bars (5 characters)
format_stat_bar("HP", "❤️", 75, 100, bar_length=5)
# Output: "❤️ HP: ███░░ 75% (75/100)"
# Longer bars (20 characters)
format_stat_bar("HP", "❤️", 75, 100, bar_length=20)
# Output: "❤️ HP: ███████████████░░░░░ 75% (75/100)"
```
### Alternative Characters
Different visual styles can be achieved with different characters:
**Style 1 - Blocks (Default):**
```python
create_progress_bar(50, 100, filled_char="", empty_char="")
# Output: "█████░░░░░"
```
**Style 2 - Squares:**
```python
create_progress_bar(50, 100, filled_char="", empty_char="")
# Output: "■■■■■□□□□□"
```
**Style 3 - Circles:**
```python
create_progress_bar(50, 100, filled_char="", empty_char="")
# Output: "●●●●●○○○○○"
```
**Style 4 - Bars:**
```python
create_progress_bar(50, 100, filled_char="", empty_char="")
# Output: "▰▰▰▰▰▱▱▱▱▱"
```
## Color Variations
While Telegram doesn't support custom colors in text, we use emoji for color coding:
- ❤️ Red heart = Health (HP)
- ⚡ Lightning = Energy (Stamina)
- ⭐ Star = Experience (XP)
- 🎒 Backpack = Inventory capacity
- 💎 Gem = Unspent points
## Performance
### Optimization
- Progress bars are generated on-demand
- No database storage required
- Minimal computation (simple division and multiplication)
- No external dependencies
### Caching
Progress bars are not cached as they change frequently and are cheap to generate.
## Future Enhancements
### Potential Improvements
1. **Color thresholds** - Different bars at low health
```
❤️ HP: ██░░░░░░░░ 20% (20/100) ⚠️ LOW
```
2. **Animated transitions** - Show changes over time
(Limited by Telegram's edit rate limits)
3. **More stats** - Apply to other numeric values
- Hunger bar
- Thirst bar
- Status effect duration
4. **Inventory load bars**
```
🎒 Weight: █████░░░░░ 50% (25/50 kg)
📦 Volume: ███░░░░░░░ 30% (30/100 vol)
```
## Testing
### Test Cases
```python
# Test edge cases
assert create_progress_bar(0, 100) == "░░░░░░░░░░"
assert create_progress_bar(100, 100) == "██████████"
assert create_progress_bar(50, 100) == "█████░░░░░"
# Test invalid values
assert create_progress_bar(-10, 100) == "░░░░░░░░░░" # Negative clamped to 0
assert create_progress_bar(150, 100) == "██████████" # Over max clamped to 100%
# Test zero maximum
assert create_progress_bar(10, 0) == "░░░░░░░░░░" # Avoid division by zero
```
## Files Modified
- **`bot/utils.py`** - Added utility functions
- **`bot/action_handlers.py`** - Updated `get_player_status_text()`
- **`bot/profile_handlers.py`** - Updated `handle_profile()`
## Migration
No database migration required - this is purely a display change.
## Rollback
To revert to the old format, simply remove the `format_stat_bar()` calls and use plain text:
```python
# Old format
status += f"❤️ HP: {player['hp']}/{player['max_hp']}\n"
# New format (can be reverted)
status += f"{format_stat_bar('HP', '❤️', player['hp'], player['max_hp'])}\n"
```
---
**Implementation Date:** October 19, 2025
**Status:** ✅ Complete and Deployed

221
docs/game/MECHANICS.md Normal file
View File

@@ -0,0 +1,221 @@
# Game Mechanics Overview
## Core Systems
### 1. Health & Stamina System
#### Health Points (HP)
- **Purpose:** Measure of character's life force
- **Starting Value:** 100 HP
- **Range:** 0 to max_hp
- **Death:** Occurs when HP reaches 0
- **Regeneration:** Does not auto-regenerate (requires items or rest)
- **Display:** Visual bar with percentage
```
❤️ HP: ███████░░░ 70% (70/100)
```
#### Stamina
- **Purpose:** Resource for actions and movement
- **Starting Value:** 100 Stamina
- **Range:** 0 to max_stamina
- **Usage:**
- Movement between locations (varies by distance and inventory weight)
- Actions on interactables (searching, opening, etc.)
- **Regeneration:** Passive over time (1 + endurance/10 per cycle)
- **Display:** Visual bar with percentage
```
⚡ Stamina: █████░░░░░ 50% (50/100)
```
### 2. Character Progression
#### Experience (XP)
- **Gained From:**
- Defeating enemies
- Completing actions
- Exploring new locations
- **Display:** Progress bar showing advancement to next level
```
⭐ XP: ████░░░░░░ 40% (240/600)
```
#### Leveling
- **Formula:** XP required = 100 * (level ^ 1.5)
- **Benefits:**
- Stat point to allocate
- Increased base damage
- Access to new areas
#### Stats
- **Strength (💪)**
- Increases melee damage
- Formula: Base Damage = 5 + (strength / 2) + level
- **Agility (🏃)**
- Increases flee chance in combat
- Formula: Flee Chance = 50% + (agility / 100)
- **Endurance (💚)**
- Increases max HP when leveled
- Increases stamina regeneration
- Formula: Stamina Regen = 1 + (endurance / 10)
- **Intellect (🧠)**
- Reserved for future mechanics
- May affect crafting, dialogue, etc.
#### Stat Points
- **Earned:** 1 point per level
- **Allocation Options:**
- +10 Max HP
- +5 Max Stamina
- +1 Strength
- +1 Agility
- +1 Endurance
- +1 Intellect
### 3. Inventory System
#### Capacity
- **Weight Limit:** 50 kg (base) + equipped backpack bonus
- **Volume Limit:** 100 vol (base) + equipped backpack bonus
- **Overencumbered:** Cannot pick up items when at capacity
- **Movement Penalty:** Higher inventory weight increases stamina cost for travel
#### Item Types
- **Weapons:** Equippable, adds damage
- **Armor:** Equippable, adds protection
- **Consumables:** Single-use, restores HP/Stamina
- **Tools:** Required for certain actions
- **Materials:** Crafting components (future)
- **Quest Items:** Special items for progression
#### Equipment Slots
- **Weapon:** Primary attack tool
- **Backpack:** Increases carrying capacity
- **Armor:** Protection (future implementation)
### 4. Combat System
#### Turn-Based Combat
- Player turn → NPC turn → Repeat until victory or defeat
#### Actions
- **Attack:** Deal damage to enemy
- **Flee:** Attempt to escape (chance-based)
- **Use Item:** Consume healing/buff items
#### Damage Calculation
```python
Base Damage = 5 + (strength / 2) + level
Weapon Damage = random(weapon_min, weapon_max)
Total Damage = Base Damage + Weapon Damage
```
#### Enemy Spawning
- **Static Spawns:** Fixed enemies at locations
- **Random Encounters:** Chance-based when moving
- **Wandering Enemies:** NPCs that patrol locations
#### Death & Respawn
- **Player Death:** Lose all inventory, respawn at start location
- **Corpses:** Player corpses can be looted by others (multiplayer aspect)
- **Enemy Death:** Drop loot, award XP
### 5. World Interaction
#### Locations
- **Types:** Safe zones, dangerous areas, dungeons
- **Connections:** Graph-based navigation
- **Images:** Visual representation of each location
- **Interactables:** Objects to search/interact with
#### Actions
- **Inspect Area:** View location details and items
- **Move:** Travel to connected locations
- **Search Objects:** Loot interactables for items
- **Attack Enemies:** Engage wandering NPCs
#### Cooldowns
- **Purpose:** Prevent infinite resource farming
- **Duration:** Varies by action (typically 5-30 minutes)
- **Per-Object:** Each interactable has independent cooldown
### 6. Item Collection
#### Dropped Items
- **Sources:**
- Enemy loot
- Interactable rewards
- Player-dropped items
- **Pickup:** Choose quantity to take
- **Capacity Check:** Must have room in inventory
#### Looting
- **NPC Corpses:**
- Requires tools for certain materials
- Random quantities
- Disappears when fully looted
- **Player Corpses:**
- Contains all inventory from death
- Can be looted by any player
- Encourages retrieval runs
## Game Loop
### Basic Cycle
1. **Explore** → Move to new location
2. **Inspect** → Find interactables and enemies
3. **Action** → Search, fight, or collect
4. **Manage** → Organize inventory, use items
5. **Progress** → Gain XP, level up, allocate stats
6. **Repeat**
### Resource Management
- Monitor HP and Stamina
- Use consumables strategically
- Rest when needed
- Manage inventory weight
### Risk vs. Reward
- Dangerous areas have better loot
- Higher-level enemies give more XP
- Overextending can lead to death
- Strategic retreats preserve progress
## Progression Path
### Early Game (Levels 1-5)
- Learn basic mechanics
- Gather starting equipment
- Explore safe zones
- Build initial stats
### Mid Game (Levels 6-15)
- Venture into dangerous areas
- Face tougher enemies
- Collect better equipment
- Specialize character build
### Late Game (Levels 16+)
- Challenge end-game content
- Optimize builds
- Collect rare items
- Explore all areas
## Future Mechanics
### Planned Features
- **Crafting System:** Combine materials into items
- **Quests:** Story-driven objectives
- **Factions:** Reputation and alliances
- **Trading:** Player-to-player economy
- **Skills:** Special abilities beyond stats
- **Housing:** Personal storage and rest areas
---
**Last Updated:** October 19, 2025
**Status:** Living document - Updated as mechanics evolve