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