""" Module for user display interfaces in the ARCEngine. """ from abc import ABC, abstractmethod import numpy as np from .enums import InteractionMode from .sprites import Sprite class RenderableUserDisplay(ABC): """Abstract base class for UI elements that can be rendered by the camera. This class defines the interface for UI elements that can be rendered by the camera. It is used as the final step in the camera's rendering pipeline to produce the 64x64 output frame. """ @abstractmethod def render_interface(self, frame: np.ndarray) -> np.ndarray: """Render this UI element onto the given frame. Args: frame: The 64x64 numpy array to render onto """ return frame def draw_sprite(self, frame: np.ndarray, sprite: Sprite, start_x: int, start_y: int) -> np.ndarray: sprite_pixels = sprite.render() sprite_height, sprite_width = sprite_pixels.shape # Calculate sprite boundaries end_x = start_x + sprite_width end_y = start_y + sprite_height # Only render if sprite is at least partially visible if start_x < 64 and start_y < 64 and end_x > 0 and end_y > 0: # Calculate the visible portion of the sprite sprite_start_y = max(0, -start_y) sprite_start_x = max(0, -start_x) sprite_end_y = sprite_height - max(0, end_y - 64) sprite_end_x = sprite_width - max(0, end_x - 64) # Calculate frame boundaries frame_start_y = max(0, start_y) frame_start_x = max(0, start_x) frame_end_y = min(64, end_y) frame_end_x = min(64, end_x) # Only render non-negative pixels frame[frame_start_y:frame_end_y, frame_start_x:frame_end_x] = np.where( sprite_pixels[sprite_start_y:sprite_end_y, sprite_start_x:sprite_end_x] >= 0, sprite_pixels[sprite_start_y:sprite_end_y, sprite_start_x:sprite_end_x], frame[frame_start_y:frame_end_y, frame_start_x:frame_end_x], ) return frame class ToggleableUserDisplay(RenderableUserDisplay): """A UI element that manages a collection of sprite pairs (enabled/disabled states). This class provides methods to toggle between enabled and disabled states of sprite pairs, and renders the appropriate sprite based on the current state. """ _sprite_pairs: list[tuple[Sprite, Sprite]] def __init__(self, sprite_pairs: list[tuple[Sprite, Sprite]] = []): """Initialize the toggleable interface with sprite pairs. Each sprite pair consists of two sprites: - The first sprite is used when the object is enabled - The second sprite is used when the object is disabled Args: sprite_pairs: List of sprite pairs to initialize with. Each pair will be cloned to prevent external modification. Defaults to an empty list. """ self._sprite_pairs: list[tuple[Sprite, Sprite]] = [] if sprite_pairs: for sprite_pair in sprite_pairs: self._sprite_pairs.append((sprite_pair[0].clone(), sprite_pair[1].clone())) def clone(self) -> "ToggleableUserDisplay": """Create a deep copy of this toggleable interface. This method creates a new ToggleableInterface instance with cloned sprite pairs. Each sprite in each pair is cloned, ensuring that the new interface has completely independent sprites from the original. Returns: ToggleableInterface: A new instance with cloned sprite pairs """ # Create new instance with empty sprite pairs cloned_pairs: list[tuple[Sprite, Sprite]] = [] # Clone each sprite pair for sprite_pair in self._sprite_pairs: cloned_pairs.append((sprite_pair[0].clone(), sprite_pair[1].clone())) return ToggleableUserDisplay(cloned_pairs) def is_enabled(self, index: int) -> bool: """Check if a sprite pair is enabled. Args: index: The index of the sprite pair to check. Returns: bool: True if the pair is enabled, False otherwise. Raises: ValueError: If the index is out of bounds. """ if index < 0 or index >= len(self._sprite_pairs): raise ValueError(f"Index {index} is out of bounds") return self._sprite_pairs[index][0].interaction != InteractionMode.REMOVED def enable(self, index: int) -> None: """Enable the sprite pair at the given index. This will make the first sprite in the pair visible and interactive, while making the second sprite invisible and non-interactive. Args: index: The index of the sprite pair to enable. Raises: ValueError: If the index is out of bounds. """ if index < 0 or index >= len(self._sprite_pairs): raise ValueError(f"Index {index} is out of bounds") self._enable_sprite_pair(self._sprite_pairs[index]) def disable(self, index: int) -> None: """Disable the sprite pair at the given index. This will make the first sprite in the pair invisible and non-interactive, while making the second sprite visible and interactive. Args: index: The index of the sprite pair to disable. Raises: ValueError: If the index is out of bounds. """ if index < 0 or index >= len(self._sprite_pairs): raise ValueError(f"Index {index} is out of bounds") self._disable_sprite_pair(self._sprite_pairs[index]) def enable_all_by_tag(self, tag: str) -> None: """Enable all sprite pairs that have the given tag. This will enable all sprite pairs where either sprite in the pair has the specified tag. Args: tag: The tag to search for in the sprites. """ sprites = self._find_sprites_by_tag(tag) for sprite in sprites: self._enable_sprite_pair(sprite) def disabled_all_by_tag(self, tag: str) -> None: """Disable all sprite pairs that have the given tag. This will disable all sprite pairs where either sprite in the pair has the specified tag. Args: tag: The tag to search for in the sprites. """ sprites = self._find_sprites_by_tag(tag) for sprite in sprites: self._disable_sprite_pair(sprite) def enable_first_by_tag(self, tag: str) -> bool: """Enable the first disabled sprite pair that has the given tag. This will find the first sprite pair with the given tag where the first sprite is currently disabled, and enable it. Args: tag: The tag to search for in the sprites. Returns: bool: True if a sprite pair was enabled, False if no disabled pairs with the tag were found. """ sprites = self._find_sprites_by_tag(tag) for sprite in sprites: if sprite[0].interaction == InteractionMode.REMOVED: self._enable_sprite_pair(sprite) return True return False def disabled_first_by_tag(self, tag: str) -> bool: """Disable the first enabled sprite pair that has the given tag. This will find the first sprite pair with the given tag where the first sprite is currently enabled, and disable it. Args: tag: The tag to search for in the sprites. Returns: bool: True if a sprite pair was disabled, False if no enabled pairs with the tag were found. """ sprites = self._find_sprites_by_tag(tag) for sprite in sprites: if sprite[0].interaction == InteractionMode.INTANGIBLE: self._disable_sprite_pair(sprite) return True return False def _find_sprites_by_tag(self, tag: str) -> list[tuple[Sprite, Sprite]]: """Find all sprite pairs that have the given tag. Args: tag: The tag to search for in the sprites. Returns: list[Sprite]: List of sprite pairs where either sprite has the tag. """ return [sprite_pair for sprite_pair in self._sprite_pairs if tag in sprite_pair[0].tags] def _enable_sprite_pair(self, sprite_pair: tuple[Sprite, Sprite]) -> None: """Enable a sprite pair by setting appropriate interaction modes. This makes the first sprite visible and interactive, while making the second sprite invisible and non-interactive. Args: sprite_pair: The pair of sprites to enable. """ sprite_pair[0].set_interaction(InteractionMode.INTANGIBLE) sprite_pair[1].set_interaction(InteractionMode.REMOVED) def _disable_sprite_pair(self, sprite_pair: tuple[Sprite, Sprite]) -> None: """Disable a sprite pair by setting appropriate interaction modes. This makes the first sprite invisible and non-interactive, while making the second sprite visible and interactive. Args: sprite_pair: The pair of sprites to disable. """ sprite_pair[0].set_interaction(InteractionMode.REMOVED) sprite_pair[1].set_interaction(InteractionMode.INTANGIBLE) def render_interface(self, frame: np.ndarray) -> np.ndarray: """Render all visible sprites to the given frame. This method renders all sprites that are currently visible (not in REMOVED mode) to the provided frame. The frame is modified in-place and returned. Args: frame: A 64x64 numpy array to render the sprites onto. Returns: np.ndarray: The modified frame with all visible sprites rendered. """ # Get all sprites from pairs all_sprites: list[Sprite] = [] for pair in self._sprite_pairs: all_sprites.extend(pair) # Render each visible sprite for sprite in all_sprites: if sprite.interaction != InteractionMode.REMOVED: frame = self.draw_sprite(frame, sprite, sprite.x, sprite.y) return frame