@@ -470,133 +470,130 @@ def rev(seq: list[int], start: int, stop: int) -> enumerate[int]:
470470
471471class 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