File size: 10,287 Bytes
764e34c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
"""
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