""" ui.py - User Interface Components for McRogueFace Roguelike Contains the health bar and message log UI elements. """ from typing import List, Tuple, Optional from dataclasses import dataclass import mcrfpy from constants import ( HP_BAR_X, HP_BAR_Y, HP_BAR_WIDTH, HP_BAR_HEIGHT, MSG_LOG_X, MSG_LOG_Y, MSG_LOG_WIDTH, MSG_LOG_HEIGHT, MSG_LOG_MAX_LINES, LEVEL_DISPLAY_X, LEVEL_DISPLAY_Y, COLOR_UI_BG, COLOR_UI_BORDER, COLOR_TEXT, COLOR_HP_BAR_BG, COLOR_HP_BAR_FILL, COLOR_HP_BAR_WARNING, COLOR_HP_BAR_CRITICAL, COLOR_MSG_DEFAULT ) @dataclass class Message: """A message in the message log.""" text: str color: Tuple[int, int, int, int] class HealthBar: """ Visual health bar displaying player HP. Uses nested Frames: an outer background frame and an inner fill frame that resizes based on HP percentage. """ def __init__(self, x: int = HP_BAR_X, y: int = HP_BAR_Y, width: int = HP_BAR_WIDTH, height: int = HP_BAR_HEIGHT, font: mcrfpy.Font = None): """ Create a health bar. Args: x: X position y: Y position width: Total width of the bar height: Height of the bar font: Font for the HP text """ self.x = x self.y = y self.width = width self.height = height self.font = font or mcrfpy.default_font # Background frame self.bg_frame = mcrfpy.Frame(x, y, width, height) self.bg_frame.fill_color = mcrfpy.Color(*COLOR_HP_BAR_BG) self.bg_frame.outline = 2 self.bg_frame.outline_color = mcrfpy.Color(*COLOR_UI_BORDER) # Fill frame (inside background) self.fill_frame = mcrfpy.Frame(x + 2, y + 2, width - 4, height - 4) self.fill_frame.fill_color = mcrfpy.Color(*COLOR_HP_BAR_FILL) self.fill_frame.outline = 0 # HP text self.hp_text = mcrfpy.Caption("HP: 0 / 0", self.font, x + 8, y + 4) self.hp_text.fill_color = mcrfpy.Color(*COLOR_TEXT) self._max_fill_width = width - 4 def add_to_scene(self, ui: mcrfpy.UICollection) -> None: """Add all health bar components to a scene.""" ui.append(self.bg_frame) ui.append(self.fill_frame) ui.append(self.hp_text) def update(self, current_hp: int, max_hp: int) -> None: """ Update the health bar display. Args: current_hp: Current hit points max_hp: Maximum hit points """ # Calculate fill percentage if max_hp <= 0: percent = 0.0 else: percent = max(0.0, min(1.0, current_hp / max_hp)) # Update fill bar width self.fill_frame.w = int(self._max_fill_width * percent) # Update color based on HP percentage if percent > 0.6: color = COLOR_HP_BAR_FILL elif percent > 0.3: color = COLOR_HP_BAR_WARNING else: color = COLOR_HP_BAR_CRITICAL self.fill_frame.fill_color = mcrfpy.Color(*color) # Update text self.hp_text.text = f"HP: {current_hp} / {max_hp}" class MessageLog: """ Scrolling message log displaying game events. Uses a Frame container with Caption children for each line. """ def __init__(self, x: int = MSG_LOG_X, y: int = MSG_LOG_Y, width: int = MSG_LOG_WIDTH, height: int = MSG_LOG_HEIGHT, max_lines: int = MSG_LOG_MAX_LINES, font: mcrfpy.Font = None): """ Create a message log. Args: x: X position y: Y position width: Width of the log height: Height of the log max_lines: Maximum number of visible lines font: Font for the messages """ self.x = x self.y = y self.width = width self.height = height self.max_lines = max_lines self.font = font or mcrfpy.default_font # Container frame self.frame = mcrfpy.Frame(x, y, width, height) self.frame.fill_color = mcrfpy.Color(*COLOR_UI_BG) self.frame.outline = 1 self.frame.outline_color = mcrfpy.Color(*COLOR_UI_BORDER) # Message storage self.messages: List[Message] = [] self.captions: List[mcrfpy.Caption] = [] # Line height (approximate based on font) self.line_height = 18 # Create caption objects for each line self._init_captions() def _init_captions(self) -> None: """Initialize caption objects for message display.""" for i in range(self.max_lines): caption = mcrfpy.Caption( "", self.font, self.x + 5, self.y + 5 + i * self.line_height ) caption.fill_color = mcrfpy.Color(*COLOR_MSG_DEFAULT) self.captions.append(caption) def add_to_scene(self, ui: mcrfpy.UICollection) -> None: """Add the message log to a scene.""" ui.append(self.frame) for caption in self.captions: ui.append(caption) def add_message(self, text: str, color: Tuple[int, int, int, int] = COLOR_MSG_DEFAULT) -> None: """ Add a message to the log. Args: text: Message text color: Text color as (R, G, B, A) """ self.messages.append(Message(text, color)) # Trim old messages if len(self.messages) > 100: self.messages = self.messages[-100:] # Update display self._update_display() def _update_display(self) -> None: """Update the displayed messages.""" # Get the most recent messages recent = self.messages[-self.max_lines:] for i, caption in enumerate(self.captions): if i < len(recent): msg = recent[i] caption.text = msg.text caption.fill_color = mcrfpy.Color(*msg.color) else: caption.text = "" def clear(self) -> None: """Clear all messages.""" self.messages.clear() self._update_display() class LevelDisplay: """Simple display showing current dungeon level.""" def __init__(self, x: int = LEVEL_DISPLAY_X, y: int = LEVEL_DISPLAY_Y, font: mcrfpy.Font = None): """ Create a level display. Args: x: X position y: Y position font: Font for the text """ self.font = font or mcrfpy.default_font self.caption = mcrfpy.Caption("Level: 1", self.font, x, y) self.caption.fill_color = mcrfpy.Color(*COLOR_TEXT) def add_to_scene(self, ui: mcrfpy.UICollection) -> None: """Add to a scene.""" ui.append(self.caption) def update(self, level: int) -> None: """Update the displayed level.""" self.caption.text = f"Dungeon Level: {level}" class GameUI: """ Container for all UI elements. Provides a single point of access for updating the entire UI. """ def __init__(self, font: mcrfpy.Font = None): """ Create the game UI. Args: font: Font for all UI elements """ self.font = font or mcrfpy.default_font # Create UI components self.health_bar = HealthBar(font=self.font) self.message_log = MessageLog(font=self.font) self.level_display = LevelDisplay(font=self.font) def add_to_scene(self, ui: mcrfpy.UICollection) -> None: """Add all UI elements to a scene.""" self.health_bar.add_to_scene(ui) self.message_log.add_to_scene(ui) self.level_display.add_to_scene(ui) def update_hp(self, current_hp: int, max_hp: int) -> None: """Update the health bar.""" self.health_bar.update(current_hp, max_hp) def add_message(self, text: str, color: Tuple[int, int, int, int] = COLOR_MSG_DEFAULT) -> None: """Add a message to the log.""" self.message_log.add_message(text, color) def update_level(self, level: int) -> None: """Update the dungeon level display.""" self.level_display.update(level) def clear_messages(self) -> None: """Clear the message log.""" self.message_log.clear() class DeathScreen: """Game over screen shown when player dies.""" def __init__(self, font: mcrfpy.Font = None): """ Create the death screen. Args: font: Font for text """ self.font = font or mcrfpy.default_font self.elements: List = [] # Semi-transparent overlay self.overlay = mcrfpy.Frame(0, 0, 1024, 768) self.overlay.fill_color = mcrfpy.Color(0, 0, 0, 180) self.elements.append(self.overlay) # Death message self.death_text = mcrfpy.Caption( "YOU HAVE DIED", self.font, 362, 300 ) self.death_text.fill_color = mcrfpy.Color(255, 0, 0, 255) self.death_text.outline = 2 self.death_text.outline_color = mcrfpy.Color(0, 0, 0, 255) self.elements.append(self.death_text) # Restart prompt self.restart_text = mcrfpy.Caption( "Press R to restart", self.font, 400, 400 ) self.restart_text.fill_color = mcrfpy.Color(200, 200, 200, 255) self.elements.append(self.restart_text) def add_to_scene(self, ui: mcrfpy.UICollection) -> None: """Add death screen elements to a scene.""" for element in self.elements: ui.append(element) def remove_from_scene(self, ui: mcrfpy.UICollection) -> None: """Remove death screen elements from a scene.""" for element in self.elements: try: ui.remove(element) except (ValueError, RuntimeError): pass