Files
pixel_tower_defense/game/tilemap.py
T
2026-07-15 22:16:18 +02:00

136 lines
4.8 KiB
Python

"""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
)
))