"""Tilemap module - tile storage, serialisation, placement rules, and rendering. Todo: * Add prop system for Trees, etc. (like off_grid), handle blocked with game objects over props """ import json import pygame class Tilemap: """Stores and manages the tile grid, waypoints, and rendering. Attributes: tile_size (int): Width and height of a single tile in pixels. tilemap (dict): Tile data keyed by ``"x;y"`` string positions. planned: offgrid_tiles (list): Tiles placed outside the main grid (e.g. decorations). waypoints (list): Ordered world positions enemies follow. tower_placeable (list[dict]): Tile descriptors that allow tower placement. """ def __init__(self, game, tile=16): self.game = game self.tile_size = tile self.tilemap = {} self.offgrid_tiles = [] self.waypoints=[] self.tower_placeable = [{"type": "desert", "variant": 0}] def save(self, path): """Serialise the tilemap, tile size, and waypoints to a JSON file. Args: path (str): File path to write to. """ with open(path, 'w', encoding='utf-8') as f: json.dump({ 'tilemap': self.tilemap, 'tile_size': self.tile_size, 'waypoints': self.waypoints }, f) f.close() def load(self, path): """Load tilemap data from a JSON file, replacing current state. Args: path (str): File path to read from. """ with open(path, 'r', encoding='utf-8') as f: map_data = json.load(f) f.close() self.tilemap = map_data['tilemap'] self.tile_size = map_data['tile_size'] self.waypoints = map_data['waypoints'] def placeable(self, tile_pos): """Check whether a tower can be placed on the tile at tile_pos. Args: tile_pos (tuple[int, int]): Grid position ``(x, y)`` to check. Returns: bool: ``True`` if the tile exists and is in ``tower_placeable``. """ str_tile_pos = f'{tile_pos[0]};{tile_pos[1]}' if str_tile_pos not in self.tilemap: return False tile = self.tilemap[str_tile_pos] for ti in self.tower_placeable: if tile['type'] == ti['type'] and tile['variant'] == ti['variant']: return True return False def render(self, surf, offset=(0, 0)): """Draw only the tiles visible within surf, using a pixel offset (editor). Args: surf (pygame.Surface): Target surface to draw onto. offset (tuple[int, int]): Pixel scroll offset ``(x, y)``. """ start_x = offset[0] // self.tile_size end_x = (offset[0] + surf.get_width()) // self.tile_size + 1 start_y = offset[1] // self.tile_size end_y = (offset[1] + surf.get_height()) // self.tile_size + 1 for x in range(start_x, end_x): for y in range(start_y, end_y): loc = str(x) + ';' + str(y) if loc in self.tilemap: tile = self.tilemap[loc] surf.blit( self.game.assets[tile['type']][tile['variant']], ( tile['pos'][0] * self.tile_size - offset[0], tile['pos'][1] * self.tile_size - offset[1] ) ) def render_game(self, surf, camera): """Draw only the tiles visible within surf, scaled to the camera zoom and pixel offset. Args: surf (pygame.Surface): Target surface to draw onto. camera (Camera): Active camera for zoom level and coordinate conversion. Todo: document this code below, not full understanding """ tile_size = self.tile_size zoom = camera.zoom scaled_tile_size = int(tile_size * zoom) start_x = int(camera.offset.x // tile_size) start_y = int(camera.offset.y // tile_size) end_x = int((camera.offset.x + surf.get_width() / zoom) // tile_size) + 2 end_y = int((camera.offset.y + surf.get_height() / zoom) // tile_size) + 2 for x in range(start_x, end_x): for y in range(start_y, end_y): loc = f"{x};{y}" if loc in self.tilemap: tile = self.tilemap[loc] image = self.game.assets[tile["type"]][tile["variant"]] surf.blit(pygame.transform.scale( image, (scaled_tile_size, scaled_tile_size) ), camera.world_to_screen( ( tile["pos"][0] * tile_size, tile["pos"][1] * tile_size ) ))