UI/UX: Improve visual clarity and consistency

- Align status bars with label padding (HP, Stamina, XP)
- Add clear combat turn indicators (YOUR TURN / ENEMY TURN)
- Display HP/Stamina bars in inventory menu and usage
- Add consistent separators between sections
- Improve feedback for item usage with visible stat changes

Files modified:
- bot/utils.py: Added label_width parameter to format_stat_bar()
- bot/combat.py: Turn headers and separators
- bot/inventory_handlers.py: HP/Stamina display
- bot/action_handlers.py: Separator placement fix

See docs/development/UI_UX_IMPROVEMENTS.md for details
This commit is contained in:
Joan
2025-10-20 12:44:16 +02:00
parent d243ec571f
commit dfea27f9cb
5 changed files with 273 additions and 28 deletions

View File

@@ -50,7 +50,8 @@ async def get_player_status_text(telegram_id: int) -> str:
if equipped_items:
status += f"⚔️ <b>Equipped:</b> {', '.join(equipped_items)}\n"
status += f"━━━━━━━━━━━━━━━━━━━━\n<i>{location.description}</i>"
status += "━━━━━━━━━━━━━━━━━━━━\n"
status += f"<i>{location.description}</i>"
return status

View File

@@ -128,7 +128,8 @@ async def player_attack(player_id: int) -> Tuple[str, bool, bool]:
actual_damage = int(actual_damage * 1.5)
new_npc_hp = max(0, combat['npc_hp'] - actual_damage)
message = f"⚔️ You attack the {npc_def.name} for {actual_damage} damage!"
message = "━━━ YOUR TURN ━━━\n"
message += f"⚔️ You attack the {npc_def.name} for {actual_damage} damage!"
if is_crit:
message += " 💥 CRITICAL HIT!"
@@ -174,11 +175,13 @@ async def player_attack(player_id: int) -> Tuple[str, bool, bool]:
'player_status_effects': json.dumps(player_effects)
})
message += "\n" + format_stat_bar(f"{npc_def.emoji} {npc_def.name}", "", new_npc_hp, combat['npc_max_hp'])
message += "\n━━━━━━━━━━━━━━━━━━━━\n"
message += format_stat_bar(f"{npc_def.emoji} {npc_def.name}", "", new_npc_hp, combat['npc_max_hp'])
return (message, False, True)
async def npc_attack(player_id: int) -> Tuple[str, bool]:
"""
NPC attacks the player.
@@ -214,7 +217,8 @@ async def npc_attack(player_id: int) -> Tuple[str, bool]:
new_player_hp = max(0, player['hp'] - damage)
await database.update_player(player_id, {'hp': new_player_hp})
message = f"💥 The {npc_def.name} attacks you for {damage} damage!"
message = "━━━ ENEMY TURN ━━━\n"
message += f"💥 The {npc_def.name} attacks you for {damage} damage!"
# Check for status effect infliction
player_effects = json.loads(combat['player_status_effects'])
@@ -251,8 +255,9 @@ async def npc_attack(player_id: int) -> Tuple[str, bool]:
'npc_status_effects': json.dumps(npc_effects)
})
message += "\n" + format_stat_bar("Your HP", "❤️", new_player_hp, player['max_hp'])
message += "\n" + format_stat_bar(f"{npc_def.emoji} {npc_def.name}", "", combat['npc_hp'], combat['npc_max_hp'])
message += "\n━━━━━━━━━━━━━━━━━━━━\n"
message += format_stat_bar("Your HP", "❤️", new_player_hp, player['max_hp']) + "\n"
message += format_stat_bar(f"{npc_def.emoji} {npc_def.name}", "", combat['npc_hp'], combat['npc_max_hp'])
return (message, False)

View File

@@ -12,21 +12,22 @@ logger = logging.getLogger(__name__)
async def handle_inventory_menu(query, user_id: int, player: dict, data: list = None):
"""Display player inventory with item management options."""
from .utils import format_stat_bar
await query.answer()
inventory_items = await database.get_inventory(user_id)
# Calculate inventory summary
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"{format_stat_bar('HP', '❤️', player['hp'], player['max_hp'])}\n"
text += f"{format_stat_bar('Stamina', '', player['stamina'], player['max_stamina'])}\n"
text += f"📊 Weight: {current_weight}/{max_weight} kg\n"
text += f"📦 Volume: {current_volume}/{max_volume} vol\n\n"
text += f"📦 Volume: {current_volume}/{max_volume} vol\n"
if not inventory_items:
text += "It's empty."
text += "\n<i>Your inventory is empty.</i>"
# Keep current location image for context
location = game_world.get_location(player['location_id'])
location_image = location.image_path if location else None
@@ -92,6 +93,8 @@ async def handle_inventory_item(query, user_id: int, player: dict, data: list):
async def handle_inventory_use(query, user_id: int, player: dict, data: list):
"""Use a consumable item from inventory."""
from .utils import format_stat_bar
item_db_id = int(data[1])
item = await database.get_inventory_item(item_db_id)
@@ -127,40 +130,42 @@ async def handle_inventory_use(query, user_id: int, player: dict, data: list):
actual_gain = new_stamina - player['stamina']
updates['stamina'] = new_stamina
if actual_gain > 0:
result_parts.append(f" Stamina: +{actual_gain}")
result_parts.append(f"⚡ Stamina: +{actual_gain}")
else:
result_parts.append(f" Stamina: Already at maximum!")
result_parts.append(f"⚡ Stamina: Already at maximum!")
if updates:
await database.update_player(user_id, updates)
# Refresh player data to get updated stats
player = await database.get_player(user_id)
# 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)
# Build status section with HP/Stamina bars
text = "<b>🎒 Your Inventory:</b>\n"
text += f"{format_stat_bar('HP', '❤️', player['hp'], player['max_hp'])}\n"
text += f"{format_stat_bar('Stamina', '', player['stamina'], player['max_stamina'])}\n"
text += f"📊 Weight: {current_weight}/{max_weight} kg\n"
text += f"📦 Volume: {current_volume}/{max_volume} vol\n\n"
text += f"📦 Volume: {current_volume}/{max_volume} vol\n"
text += "━━━━━━━━━━━━━━━━━━━━\n"
if not inventory_items:
text += "It's empty."
# Build result message
emoji = item_def.get('emoji', '')
text += f"<b>✨ Used {emoji} {item_def.get('name')}</b>\n"
if result_parts:
text += "\n".join(result_parts)
else:
text += f"{result_text}"
text += "No effect."
location = game_world.get_location(player['location_id'])
location_image = location.image_path if location else None

View File

@@ -43,7 +43,7 @@ def create_progress_bar(current: int, maximum: int, length: int = 10, filled_cha
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:
def format_stat_bar(label: str, emoji: str, current: int, maximum: int, bar_length: int = 10, label_width: int = 7) -> str:
"""
Format a stat (HP, Stamina, etc.) with visual progress bar.
@@ -53,20 +53,25 @@ def format_stat_bar(label: str, emoji: str, current: int, maximum: int, bar_leng
current: Current value
maximum: Maximum value
bar_length: Length of the progress bar
label_width: Width to pad label to for alignment (default 7)
Returns:
Formatted string with bar and percentage
Examples:
>>> format_stat_bar("HP", "❤️", 75, 100)
"❤️ HP: ███████░░░ 75% (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})"
# Pad label for alignment
padded_label = f"{label}:".ljust(label_width + 1)
return f"{emoji} {padded_label} {bar} {percentage}% ({current}/{maximum})"
def get_admin_ids():