Skip to content

Commit c9eeab1

Browse files
committed
map aggregator
1 parent b5ebc4d commit c9eeab1

2 files changed

Lines changed: 184 additions & 215 deletions

File tree

‎pyscroll/data.py‎

Lines changed: 83 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -470,133 +470,130 @@ def rev(seq: list[int], start: int, stop: int) -> enumerate[int]:
470470

471471
class MapAggregator(PyscrollDataAdapter):
472472
"""
473-
Combine multiple data sources with an offset
474-
475-
Currently this is just in a test phase.
476-
477-
Has the following limitations:
478-
- Tile sizes must be the same for all maps
479-
- No tile animations
480-
- Sprites cannot be under layers
481-
- Cannot remove maps once added
482-
473+
Combine multiple data sources with an offset.
474+
475+
Improvements:
476+
- Correct layer offset handling
477+
- Simplified normalization of positions
478+
- Support for animations
479+
- Delegation for tile image lookups
480+
- Dynamic map removal with re-normalization
483481
"""
484482

485483
def __init__(self, tile_size: Vector2DInt) -> None:
486484
super().__init__()
487485
self.tile_size = tile_size
488-
self.map_size = 0, 0
489-
self.maps: list[tuple[PyscrollDataAdapter, Rect, int]] = []
490-
self._min_x = 0
491-
self._min_y = 0
492-
493-
def _get_tile_image(self, x: int, y: int, l: int) -> Optional[Surface]:
494-
"""
495-
Required for sprite collation - not implemented.
496-
"""
497-
pass
498-
499-
def _get_tile_image_by_id(self, id: int) -> None:
500-
"""
501-
Required for sprite collation - not implemented.
502-
"""
503-
pass
486+
self.map_size: tuple[int, int] = (0, 0)
487+
self.maps: list[tuple[PyscrollDataAdapter, pygame.Rect, int]] = []
488+
self._min_x: int = 0
489+
self._min_y: int = 0
490+
self._animation_map: dict[int, AnimationToken] = {}
491+
self._tracked_gids: set[int] = set()
504492

505493
def add_map(
506494
self, data: PyscrollDataAdapter, offset: Vector2DInt, layer: int = 0
507495
) -> None:
508-
"""
509-
Add map data and position it with an offset.
510-
511-
Args:
512-
data: Data Adapter, such as TiledMapData
513-
offset: Where the upper-left corner is, in tiles
514-
layer: The layer of the map
515-
"""
516496
if data.tile_size != self.tile_size:
517497
raise ValueError("Tile sizes must be the same for all maps.")
518498

519499
rect = pygame.Rect(offset, data.map_size)
520-
self._min_x = min(self._min_x, offset[0])
521-
self._min_y = min(self._min_y, offset[1])
522-
523-
# the renderer cannot deal with negative tile coordinates,
524-
# so we must move all the offsets if there is a negative so
525-
# that all the tile coordinates are >= (0, 0)
526500
self.maps.append((data, rect, layer))
527-
self._adjust_map_positions(offset)
501+
502+
# Update min coords and normalize if needed
503+
self._min_x = min(self._min_x, rect.left)
504+
self._min_y = min(self._min_y, rect.top)
505+
self._normalize_positions()
528506
self._update_map_size()
529507

530508
def remove_map(self, data: PyscrollDataAdapter) -> None:
531-
"""
532-
Removes a map from the aggregator.
533-
534-
Args:
535-
data: The data adapter of the map to remove.
536-
"""
537-
if data not in [m[0] for m in self.maps]:
538-
raise ValueError("Map is not in the aggregator")
509+
initial_len = len(self.maps)
539510
self.maps = [m for m in self.maps if m[0] != data]
540-
self._update_map_size()
511+
if len(self.maps) == initial_len:
512+
raise ValueError("Map is not in the aggregator")
541513

542-
def _adjust_map_positions(self, offset: Vector2DInt) -> None:
543-
"""Adjusts map positions based on negative offsets."""
544-
ox = self._min_x - offset[0]
545-
oy = self._min_y - offset[1]
546-
if ox > 0 or oy > 0:
547-
for _, rect, _ in self.maps:
548-
rect.move_ip((ox, oy))
549-
else:
514+
self._re_normalize_positions()
515+
516+
def _normalize_positions(self) -> None:
517+
"""Shift maps so that top-left is always (0,0)."""
518+
if self._min_x < 0 or self._min_y < 0:
519+
shift_x = -self._min_x
520+
shift_y = -self._min_y
550521
for _, rect, _ in self.maps:
551-
rect.move_ip(-self._min_x, -self._min_y)
522+
rect.move_ip(shift_x, shift_y)
523+
self._min_x, self._min_y = 0, 0
524+
525+
def _re_normalize_positions(self) -> None:
526+
"""Recalculate normalization after removal."""
527+
if not self.maps:
528+
self._min_x, self._min_y = 0, 0
529+
self.map_size = (0, 0)
530+
return
531+
532+
self._min_x = min(rect.left for _, rect, _ in self.maps)
533+
self._min_y = min(rect.top for _, rect, _ in self.maps)
534+
self._normalize_positions()
535+
self._update_map_size()
552536

553537
def _update_map_size(self) -> None:
554-
"""Updates the overall map size."""
555538
mx = 0
556539
my = 0
557540
for _, rect, _ in self.maps:
558541
mx = max(mx, rect.right)
559542
my = max(my, rect.bottom)
560-
self.map_size = mx, my
561-
562-
def get_animations(self) -> None:
563-
"""
564-
Get animations - not implemented
565-
"""
566-
pass
567-
568-
def reload_data(self) -> None:
569-
"""
570-
Reload the tiles - not implemented
571-
"""
572-
pass
543+
self.map_size = (mx, my)
573544

574545
@property
575546
def visible_tile_layers(self) -> list[int]:
576-
"""
577-
Returns a sorted list of all visible tile layers from all added maps.
578-
"""
547+
"""Return all visible layers adjusted by aggregator offsets."""
579548
layers = set()
580-
for data, rect, z in self.maps:
581-
layers.update(list(data.visible_tile_layers))
549+
for data, _, z in self.maps:
550+
layers.update([l + z for l in data.visible_tile_layers])
582551
return sorted(layers)
583552

584553
def get_tile_images_by_rect(
585554
self, view: pygame.Rect
586555
) -> Iterable[tuple[int, int, int, Surface]]:
587-
"""
588-
Yields tile images within the given view rectangle from all added maps.
589-
590-
Args:
591-
view: Rect-like object defining the tile view.
592-
"""
556+
"""Yield tile images within the view, with adjusted coords and layers."""
593557
view = pygame.Rect(view)
594-
for data, rect, z in self.maps:
558+
sorted_maps = sorted(self.maps, key=lambda m: m[2]) # sort by z offset
559+
560+
for data, rect, z in sorted_maps:
595561
ox, oy = rect.topleft
596562
clipped = rect.clip(view).move(-ox, -oy)
597563
if clipped.width > 0 and clipped.height > 0:
598564
for x, y, l, image in data.get_tile_images_by_rect(clipped):
599-
yield x + ox, y + oy, l, image
565+
yield x + ox, y + oy, l + z, image
566+
567+
def _get_tile_image(self, x: int, y: int, l: int) -> Optional[Surface]:
568+
"""Delegate tile image lookup to the correct sub-map."""
569+
for data, rect, z in self.maps:
570+
if rect.collidepoint(x, y) and (l - z) in data.visible_tile_layers:
571+
local_x = x - rect.left
572+
local_y = y - rect.top
573+
local_l = l - z
574+
return data._get_tile_image(local_x, local_y, local_l)
575+
return None
576+
577+
def _get_tile_image_by_id(self, id: int) -> Optional[Surface]:
578+
"""Delegate image lookup by ID to child maps (first match)."""
579+
for data, _, _ in self.maps:
580+
try:
581+
return data._get_tile_image_by_id(id)
582+
except Exception:
583+
continue
584+
return None
585+
586+
def get_animations(self) -> Iterable[tuple[int, list[tuple[int, int]]]]:
587+
"""Aggregate animations from all child maps."""
588+
for data, _, _ in self.maps:
589+
if hasattr(data, "get_animations"):
590+
yield from data.get_animations()
591+
592+
def reload_data(self) -> None:
593+
"""Reload all child maps."""
594+
for data, _, _ in self.maps:
595+
if hasattr(data, "reload_data"):
596+
data.reload_data()
600597

601598
def __repr__(self) -> str:
602599
return f"MapAggregator(tile_size={self.tile_size}, maps={self.maps})"

0 commit comments

Comments
 (0)