Skip to content

Commit 8c5b756

Browse files
committed
data map
1 parent 988c436 commit 8c5b756

File tree

4 files changed

+320
-63
lines changed

4 files changed

+320
-63
lines changed

.github/workflows/python-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ jobs:
3434
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
3535
- name: Test
3636
run: |
37-
python -m unittest tests/pyscroll/test_pyscroll.py
37+
python -m unittest discover -s tests/pyscroll -p "test_*.py"

pyscroll/data.py

Lines changed: 105 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
If you are developing your own map format, please use this
55
as a template. Just fill in values that work for your game.
66
"""
7+
78
from __future__ import annotations
89

910
import time
11+
from collections.abc import Iterable
1012
from heapq import heappop, heappush
1113
from itertools import product
14+
from typing import Any, Optional
1215

1316
import pygame
14-
from pygame import Surface
17+
from pygame import Rect, Surface
1518

1619
try:
1720
# optional pytmx support
@@ -43,22 +46,29 @@ class PyscrollDataAdapter:
4346
# or properties. they are listed here as class
4447
# instances, but use as properties is fine, too.
4548

46-
tile_size = None # (int, int): size of each tile in pixels
47-
map_size = None # (int, int): size of map in tiles
48-
visible_tile_layers = None # list of visible layer integers
49+
# (int, int): size of each tile in pixels
50+
tile_size: Vector2DInt = (0, 0)
51+
# (int, int): size of map in tiles
52+
map_size: Vector2DInt = (0, 0)
53+
# list of visible layer integers
54+
visible_tile_layers: list[int] = []
4955

5056
def __init__(self) -> None:
51-
self._last_time = None # last time map animations were updated
52-
self._animation_queue = list() # list of animation tokens
53-
self._animated_tile = dict() # mapping of tile substitutions when animated
54-
self._tracked_tiles = set() # track the tiles on screen with animations
57+
# last time map animations were updated
58+
self._last_time: float = 0.0
59+
# list of animation tokens
60+
self._animation_queue: list[AnimationToken] = []
61+
# mapping of tile substitutions when animated
62+
self._animated_tile: dict[tuple[int, int, int], Surface] = {}
63+
# track the tiles on screen with animations
64+
self._tracked_tiles = set()
5565

5666
def reload_data(self) -> None:
5767
raise NotImplementedError
5868

5969
def process_animation_queue(
6070
self,
61-
tile_view: RectLike,
71+
tile_view: Rect,
6272
) -> list[tuple[int, int, int, Surface]]:
6373
"""
6474
Given the time and the tile view, process tile changes and return them
@@ -67,7 +77,7 @@ def process_animation_queue(
6777
tile_view: Rect representing tiles on the screen
6878
6979
"""
70-
new_tiles = list()
80+
new_tiles = []
7181

7282
# verify that there are tile substitutions ready
7383
self._update_time()
@@ -131,7 +141,7 @@ def _update_time(self) -> None:
131141
"""
132142
self._last_time = time.time() * 1000
133143

134-
def prepare_tiles(self, tiles: RectLike):
144+
def prepare_tiles(self, tiles: RectLike) -> None:
135145
"""
136146
Somewhat experimental: The renderer will advise data layer of its view
137147
@@ -155,14 +165,13 @@ def reload_animations(self) -> None:
155165
156166
"""
157167
self._update_time()
158-
self._animation_queue = list()
159-
self._tracked_gids = set()
160-
self._animation_map = dict()
168+
self._tracked_gids: set[int] = set()
169+
self._animation_map: dict[int, AnimationToken] = {}
161170

162171
for gid, frame_data in self.get_animations():
163172
self._tracked_gids.add(gid)
164173

165-
frames = list()
174+
frames: list[AnimationFrame] = []
166175
for frame_gid, frame_duration in frame_data:
167176
image = self._get_tile_image_by_id(frame_gid)
168177
frames.append(AnimationFrame(image, frame_duration))
@@ -174,7 +183,7 @@ def reload_animations(self) -> None:
174183
# locations of an animation, but searching for their locations
175184
# is slow. so it will be updated as the map is drawn.
176185

177-
positions = set()
186+
positions: set[tuple[int, int, int]] = set()
178187
ani = AnimationToken(positions, frames, self._last_time)
179188
self._animation_map[gid] = ani
180189
heappush(self._animation_queue, ani)
@@ -221,7 +230,7 @@ def _get_tile_image(self, x: int, y: int, l: int) -> Surface:
221230
"""
222231
raise NotImplementedError
223232

224-
def _get_tile_image_by_id(self, id):
233+
def _get_tile_image_by_id(self, id: int) -> Any:
225234
"""
226235
Return Image by a custom ID.
227236
@@ -245,6 +254,9 @@ def get_animations(self) -> None:
245254
Where Frames is:
246255
[ (ID, Duration), ... ]
247256
257+
[tuple[int, tuple[int, float]]]
258+
[tuple[gid, tuple[frame_gid, frame_duration]]]
259+
248260
And ID is a reference to a tile image.
249261
This will be something accessible using _get_tile_image_by_id
250262
@@ -289,7 +301,7 @@ class TiledMapData(PyscrollDataAdapter):
289301
290302
"""
291303

292-
def __init__(self, tmx) -> None:
304+
def __init__(self, tmx: pytmx.TiledMap) -> None:
293305
super(TiledMapData, self).__init__()
294306
self.tmx = tmx
295307
self.reload_animations()
@@ -308,7 +320,7 @@ def get_animations(self):
308320
yield gid, frames
309321

310322
def convert_surfaces(self, parent: Surface, alpha: bool = False) -> None:
311-
images = list()
323+
images = []
312324
for i in self.tmx.images:
313325
try:
314326
if alpha:
@@ -320,19 +332,19 @@ def convert_surfaces(self, parent: Surface, alpha: bool = False) -> None:
320332
self.tmx.images = images
321333

322334
@property
323-
def tile_size(self):
335+
def tile_size(self) -> Vector2DInt:
324336
return self.tmx.tilewidth, self.tmx.tileheight
325337

326338
@property
327-
def map_size(self):
339+
def map_size(self) -> Vector2DInt:
328340
return self.tmx.width, self.tmx.height
329341

330342
@property
331343
def visible_tile_layers(self):
332344
return self.tmx.visible_tile_layers
333345

334346
@property
335-
def visible_object_layers(self):
347+
def visible_object_layers(self) -> Iterable[pytmx.TiledObjectGroup]:
336348
return (
337349
layer
338350
for layer in self.tmx.visible_layers
@@ -345,11 +357,11 @@ def _get_tile_image(self, x: int, y: int, l: int):
345357
except ValueError:
346358
return None
347359

348-
def _get_tile_image_by_id(self, id) -> Surface:
360+
def _get_tile_image_by_id(self, id: int) -> Surface:
349361
return self.tmx.images[id]
350362

351363
def get_tile_images_by_rect(self, rect: RectLike):
352-
def rev(seq, start, stop):
364+
def rev(seq: list[int], start: int, stop: int) -> enumerate[int]:
353365
if start < 0:
354366
start = 0
355367
return enumerate(seq[start : stop + 1], start)
@@ -393,92 +405,124 @@ class MapAggregator(PyscrollDataAdapter):
393405
394406
"""
395407

396-
def __init__(self, tile_size) -> None:
408+
def __init__(self, tile_size: Vector2DInt) -> None:
397409
super().__init__()
398410
self.tile_size = tile_size
399411
self.map_size = 0, 0
400-
self.maps = list()
412+
self.maps: list[tuple[PyscrollDataAdapter, Rect, int]] = []
401413
self._min_x = 0
402414
self._min_y = 0
403415

404-
def _get_tile_image(self, x: int, y: int, l: int) -> Surface:
416+
def _get_tile_image(self, x: int, y: int, l: int) -> Optional[Surface]:
405417
"""
406-
Required for sprite collation - not implemented
407-
418+
Required for sprite collation - not implemented.
408419
"""
409420
pass
410421

411-
def _get_tile_image_by_id(self, id) -> None:
422+
def _get_tile_image_by_id(self, id: int) -> None:
412423
"""
413-
Required for sprite collation - not implemented
414-
424+
Required for sprite collation - not implemented.
415425
"""
416426
pass
417427

418-
def add_map(self, data: PyscrollDataAdapter, offset: Vector2DInt) -> None:
428+
def add_map(
429+
self, data: PyscrollDataAdapter, offset: Vector2DInt, layer: int = 0
430+
) -> None:
419431
"""
420-
Add map data and position it with an offset
432+
Add map data and position it with an offset.
421433
422434
Args:
423-
data: Data Adapater, such as TiledMapData
435+
data: Data Adapter, such as TiledMapData
424436
offset: Where the upper-left corner is, in tiles
425-
437+
layer: The layer of the map
426438
"""
427-
assert data.tile_size == self.tile_size
439+
if data.tile_size != self.tile_size:
440+
raise ValueError("Tile sizes must be the same for all maps.")
441+
428442
rect = pygame.Rect(offset, data.map_size)
429-
ox = self._min_x - offset[0]
430-
oy = self._min_y - offset[1]
431443
self._min_x = min(self._min_x, offset[0])
432444
self._min_y = min(self._min_y, offset[1])
433-
mx = 0
434-
my = 0
445+
435446
# the renderer cannot deal with negative tile coordinates,
436447
# so we must move all the offsets if there is a negative so
437448
# that all the tile coordinates are >= (0, 0)
438-
self.maps.append((data, rect))
449+
self.maps.append((data, rect, layer))
450+
self._adjust_map_positions(offset)
451+
self._update_map_size()
452+
453+
def remove_map(self, data: PyscrollDataAdapter) -> None:
454+
"""
455+
Removes a map from the aggregator.
456+
457+
Args:
458+
data: The data adapter of the map to remove.
459+
"""
460+
if data not in [m[0] for m in self.maps]:
461+
raise ValueError("Map is not in the aggregator")
462+
self.maps = [m for m in self.maps if m[0] != data]
463+
self._update_map_size()
464+
465+
def _adjust_map_positions(self, offset: Vector2DInt) -> None:
466+
"""Adjusts map positions based on negative offsets."""
467+
ox = self._min_x - offset[0]
468+
oy = self._min_y - offset[1]
439469
if ox > 0 or oy > 0:
440-
for data, rect in self.maps:
470+
for _, rect, _ in self.maps:
441471
rect.move_ip((ox, oy))
442-
mx = max(mx, rect.right)
443-
my = max(my, rect.bottom)
444472
else:
445-
rect.move_ip(-self._min_x, -self._min_y)
473+
for _, rect, _ in self.maps:
474+
rect.move_ip(-self._min_x, -self._min_y)
475+
476+
def _update_map_size(self) -> None:
477+
"""Updates the overall map size."""
478+
mx = 0
479+
my = 0
480+
for _, rect, _ in self.maps:
446481
mx = max(mx, rect.right)
447482
my = max(my, rect.bottom)
448483
self.map_size = mx, my
449484

450-
def remove_map(self, data: PyscrollDataAdapter):
451-
"""
452-
Remove map - not implemented
453-
454-
"""
455-
raise NotImplementedError
456-
457485
def get_animations(self) -> None:
458486
"""
459487
Get animations - not implemented
460-
461488
"""
462489
pass
463490

464491
def reload_data(self) -> None:
465492
"""
466493
Reload the tiles - not implemented
467-
468494
"""
469495
pass
470496

471497
@property
472-
def visible_tile_layers(self):
498+
def visible_tile_layers(self) -> list[int]:
499+
"""
500+
Returns a sorted list of all visible tile layers from all added maps.
501+
"""
473502
layers = set()
474-
for data, offset in self.maps:
503+
for data, rect, z in self.maps:
475504
layers.update(list(data.visible_tile_layers))
476505
return sorted(layers)
477506

478-
def get_tile_images_by_rect(self, view: RectLike):
507+
def get_tile_images_by_rect(
508+
self, view: pygame.Rect
509+
) -> Iterable[tuple[int, int, int, Surface]]:
510+
"""
511+
Yields tile images within the given view rectangle from all added maps.
512+
513+
Args:
514+
view: Rect-like object defining the tile view.
515+
"""
479516
view = pygame.Rect(view)
480-
for data, rect in self.maps:
517+
for data, rect, z in self.maps:
481518
ox, oy = rect.topleft
482519
clipped = rect.clip(view).move(-ox, -oy)
483-
for x, y, l, image in data.get_tile_images_by_rect(clipped):
484-
yield x + ox, y + oy, l, image
520+
if clipped.width > 0 and clipped.height > 0:
521+
for x, y, l, image in data.get_tile_images_by_rect(clipped):
522+
yield x + ox, y + oy, l, image
523+
524+
def __repr__(self) -> str:
525+
return f"MapAggregator(tile_size={self.tile_size}, maps={self.maps})"
526+
527+
def __len__(self) -> int:
528+
return len(self.maps)

pyscroll/orthographic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ def append(rect) -> None:
569569
append((v.left, v.top, v.width, -dy))
570570

571571
@staticmethod
572-
def _calculate_zoom_buffer_size(size: Vector2DInt, value: float) -> tuple[int, int]:
572+
def _calculate_zoom_buffer_size(size: Vector2DInt, value: float) -> Vector2DInt:
573573
if value <= 0:
574574
log.error("zoom level cannot be zero or less")
575575
raise ValueError

0 commit comments

Comments
 (0)