irregular6612's picture
refactor: restructure proteus into game/web subpackages
426093b
Raw
History Blame Contribute Delete
8.44 kB
"""
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)