File size: 8,444 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
"""
Module for level-related functionality in the ARCEngine.
"""

import copy
from typing import Any, List, Optional, Tuple

from .enums import BlockingMode, PlaceableArea
from .sprites import Sprite


class Level:
    """A level that manages a collection of sprites."""

    _sprites: List[Sprite]
    _sorted_sprites: List[Sprite] | None  # Sorted High to low
    _grid_size: Tuple[int, int] | None
    _data: dict[str, Any]
    _name: str
    _placeable_areas: List[PlaceableArea]
    _need_sort: bool

    def __init__(
        self,
        sprites: Optional[List[Sprite]] = None,
        grid_size: Tuple[int, int] | None = None,
        data: dict[str, Any] = {},
        name: str = "Level",
        placeable_areas: Optional[List[PlaceableArea]] = None,
    ):
        """Initialize a new level.

        Args:
            sprites: List of sprites to add to the level
            grid_size: Tuple of width and height of the grid
            data: Dictionary of data to store in the level
            name: Name of the level
            placeable_areas: List of placeable areas in the level
        """
        self._sprites = []
        self._grid_size = grid_size
        self._data = data
        self._name = name
        self._placeable_areas = placeable_areas if placeable_areas is not None else []
        self._need_sort = True

        if sprites:
            # Add first (fast path), then do one-time merge+sort.
            self._sprites.extend(sprites)
            self._merge_sys_static_pixel_perfect_on_init()

    def _merge_sys_static_pixel_perfect_on_init(self) -> None:
        """
        Merge any sprites that are:
          - PIXEL_PERFECT
          - have tag "sys_static"
        into ONE sprite per layer.

        This runs only during construction.
        """
        if not self._sprites:
            return

        # Partition sprites into merge-candidates (by layer) and others.
        by_layer: dict[int, List[Sprite]] = {}
        others: List[Sprite] = []

        for s in self._sprites:
            if s.blocking == BlockingMode.PIXEL_PERFECT and "sys_static" in s.tags:
                by_layer.setdefault(s.layer, []).append(s)
            else:
                others.append(s)

        merged: List[Sprite] = []
        for layer, group in by_layer.items():
            if not group:
                continue
            if len(group) == 1:
                merged.append(group[0])
                continue

            # Merge left-to-right; merge() returns a NEW Sprite each time.
            base = group[0]
            for nxt in group[1:]:
                base = base.merge(nxt)

            # Ensure the merged sprite stays on this layer.
            # (merge() uses max layer, but all are same layer anyway; set explicitly for safety.)
            base.set_layer(layer)

            # Ensure sys_static remains (merge unions tags, so it should already be present)
            if "sys_static" not in base.tags:
                base.tags.append("sys_static")

            merged.append(base)

        self._sprites = others + merged

    def remove_all_sprites(self) -> None:
        """Remove all sprites from the level."""
        self._sprites = []

    def add_sprite(self, sprite: Sprite) -> None:
        """Add a sprite to the level.

        Args:
            sprite: The sprite to add
        """
        self._sprites.append(sprite)
        self._need_sort = True

    def remove_sprite(self, sprite: Sprite) -> None:
        """Remove a sprite from the level.

        Args:
            sprite: The sprite to remove
        """
        if sprite in self._sprites:
            self._sprites.remove(sprite)

    def get_sprites(self) -> List[Sprite]:
        """Get all sprites in the level.

        Returns:
            List[Sprite]: All sprites in the level
        """
        return self._sprites.copy()  # Return copy to prevent external modification

    def get_sprites_by_name(self, name: str) -> List[Sprite]:
        """Get all sprites with the given name.

        Args:
            name: The name to search for

        Returns:
            List[Sprite]: All sprites with the given name
        """
        return [s for s in self._sprites if s.name == name]

    def get_sprites_by_tag(self, tag: str) -> List[Sprite]:
        """Get all sprites that have the given tag.

        Args:
            tag: The tag to search for

        Returns:
            List[Sprite]: All sprites that have the given tag
        """
        return [s for s in self._sprites if tag in s.tags]

    def get_sprites_by_tags(self, tags: List[str]) -> List[Sprite]:
        """Get all sprites that have all of the given tags.

        Args:
            tags: The tags to search for

        Returns:
            List[Sprite]: All sprites that have all of the given tags
        """
        if not tags:
            return []
        return [s for s in self._sprites if all(tag in s.tags for tag in tags)]

    def get_sprites_by_any_tag(self, tags: List[str]) -> List[Sprite]:
        """Get all sprites that have any of the specified tags.

        Args:
            tags: List of tags to search for

        Returns:
            List[Sprite]: List of sprites that have any of the specified tags
        """
        return [sprite for sprite in self._sprites if any(tag in sprite.tags for tag in tags)]

    def get_all_tags(self) -> set[str]:
        """Get all unique tags from all sprites in the level.

        This method collects all tags from all sprites and returns them as a set,
        ensuring each tag appears only once in the result.

        Returns:
            set[str]: A set containing all unique tags from all sprites
        """
        all_tags = set()
        for sprite in self._sprites:
            all_tags.update(sprite.tags)
        return all_tags

    def get_sprite_at(self, x: int, y: int, tag: Optional[str] = None, ignore_collidable: bool = False) -> Sprite | None:
        """Get the sprite at the given coordinates.

        This method returns the first sprite that is at the given coordinates.
        If a tag is provided, it will return the first sprite that has the given tag.

        Args:
            x: The x coordinate
            y: The y coordinate
            tag: The tag to search for
        """
        if self._need_sort or self._sorted_sprites is None or len(self._sorted_sprites) != len(self._sprites):
            self._sorted_sprites = sorted(self._sprites, key=lambda sprite: sprite.layer, reverse=True)
            self._need_sort = False
        for sprite in self._sorted_sprites:
            if (ignore_collidable or sprite.is_collidable) and x >= sprite.x and y >= sprite.y and x < sprite.x + sprite.width and y < sprite.y + sprite.height:
                if sprite.blocking == BlockingMode.PIXEL_PERFECT:
                    pixels = sprite.render()
                    if pixels[y - sprite.y][x - sprite.x] == -1:
                        continue
                if tag is None or tag in sprite.tags:
                    return sprite
        return None

    def collides_with(self, sprite: Sprite, ignoreMode: bool = False) -> List[Sprite]:
        """Checks all sprites in the level for collisions with the given sprite.

        Args:
            sprite: The sprite to check for collisions
        """
        return [s for s in self._sprites if sprite.collides_with(other=s, ignoreMode=ignoreMode)]

    @property
    def name(self) -> str:
        """Get the name of the level."""
        return self._name

    def get_data(self, key: str) -> Any:
        return self._data.get(key)

    @property
    def grid_size(self) -> Tuple[int, int] | None:
        """Get the grid size of the level.

        Returns:
            Tuple[int, int]: The grid size of the level
        """
        return self._grid_size

    @property
    def placeable_areas(self) -> List[PlaceableArea]:
        """Get the placeable areas of the level."""
        return self._placeable_areas

    def clone(self) -> "Level":
        """Create a deep copy of this level.

        Returns:
            Level: A new Level instance with cloned sprites
        """
        # Clone each sprite and create new level
        cloned_sprites = [sprite.clone() for sprite in self._sprites]
        return Level(name=self._name, sprites=cloned_sprites, grid_size=self._grid_size, data=copy.deepcopy(self._data), placeable_areas=self._placeable_areas)