Spaces:
Sleeping
Sleeping
| """ | |
| 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. | |
| """ | |
| 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 | |