nodes/tile_map.pyΒΆ

Part of Tanks of Freedom.

  1"""TankTileMap: wraps simvx TileMap(mode="isometric") with terrain layout.
  2
  3Owns the procedurally-generated tile sprites, the terrain grid, and helpers
  4for converting between screen / map / world coordinates. Buildings and units
  5are added as Y-sorted children of this TileMap so the engine's isometric
  6draw path renders them in correct depth.
  7"""
  8
  9from __future__ import annotations
 10
 11import numpy as np
 12
 13from simvx.core import Node2D
 14from simvx.core.animation.sprite import Sprite2D
 15from simvx.core.tilemap import TileData, TileMap, TileSet
 16
 17from .data import (
 18    MAP_H,
 19    MAP_LAYOUT_DEFAULT,
 20    MAP_W,
 21    TERR_GRASS,
 22    TERR_MOUNTAIN,
 23    TERR_TREE,
 24    TERR_WATER,
 25    TERRAIN_FROM_CHAR,
 26    TILE_H,
 27    TILE_W,
 28    WORLD_OFFSET_X,
 29    WORLD_OFFSET_Y,
 30)
 31from .textures import make_tile
 32
 33
 34class TankTileMap(TileMap):
 35    """Iso TileMap pre-loaded with a TileSet of procedural terrain tiles."""
 36
 37    def __init__(self, layout=None, **kwargs):
 38        super().__init__(name="TankTileMap", **kwargs)
 39        self.mode = "isometric"
 40        self.cell_size = (TILE_W, TILE_H)
 41        self.position = (WORLD_OFFSET_X, WORLD_OFFSET_Y)
 42        # Units and buildings are direct children that carry their own sprite
 43        # and flag/health-bar children. Sorting each of them as one entity is
 44        # exactly what we want here, so silence the nested-child warning.
 45        self.warn_on_nested_ysort = False
 46
 47        # Build a TileSet with one entry per terrain id; we don't actually use
 48        # texture_region (we render via Sprite2D children for each cell), so
 49        # we just store the terrain_type for auto-tile/neighbour queries.
 50        ts = TileSet(tile_size=(TILE_W, TILE_H))
 51        # Map terrain id -> tile id in tileset (1:1)
 52        self._terrain_tile_id: dict[int, int] = {}
 53        for terr in (TERR_GRASS, TERR_WATER, TERR_TREE, TERR_MOUNTAIN):
 54            tid = ts.add_tile(TileData(terrain_type=str(terr)))
 55            self._terrain_tile_id[terr] = tid
 56        self.tile_set = ts
 57
 58        # 2D terrain grid (row-major, [y][x]).
 59        self.terrain: list[list[int]] = [[TERR_GRASS] * MAP_W for _ in range(MAP_H)]
 60
 61        # Per-cell sprite cache (so we can re-tint or replace if needed).
 62        self._tile_sprites: dict[tuple[int, int], Sprite2D] = {}
 63
 64        # Pre-generate one numpy array per terrain so all sprites for a kind
 65        # share a single GPU texture upload.
 66        self._tile_textures: dict[int, np.ndarray] = {
 67            t: make_tile(t) for t in (TERR_GRASS, TERR_WATER, TERR_TREE, TERR_MOUNTAIN)
 68        }
 69
 70        # Decorative root that holds the per-cell tile sprites. Stored before
 71        # buildings/units so the iso Y-sort visits the floor first.
 72        self._floor = Node2D(name="Floor")
 73        self.add_child(self._floor)
 74
 75        self._apply_layout(layout if layout is not None else MAP_LAYOUT_DEFAULT)
 76
 77    # ------------------------------------------------------------------ build
 78    def _apply_layout(self, layout: list[str]) -> None:
 79        for y, row in enumerate(layout[:MAP_H]):
 80            for x, ch in enumerate(row[:MAP_W]):
 81                terr = TERRAIN_FROM_CHAR.get(ch, TERR_GRASS)
 82                self.set_terrain(x, y, terr)
 83
 84    def set_terrain(self, x: int, y: int, terr: int) -> None:
 85        self.terrain[y][x] = terr
 86        self.set_cell(0, x, y, self._terrain_tile_id[terr])
 87
 88        # Replace / add tile sprite.
 89        old = self._tile_sprites.pop((x, y), None)
 90        if old is not None:
 91            old.destroy()
 92
 93        wx, wy = self.map_to_world((x, y))
 94        sprite = Sprite2D(
 95            texture=self._tile_textures[terr],
 96            position=(wx, wy),
 97            width=TILE_W,
 98            height=TILE_H,
 99            filter="nearest",
100            name=f"tile_{x}_{y}",
101        )
102        self._floor.add_child(sprite)
103        self._tile_sprites[(x, y)] = sprite
104
105    # -------------------------------------------------------- terrain queries
106    def in_bounds(self, x: int, y: int) -> bool:
107        return 0 <= x < MAP_W and 0 <= y < MAP_H
108
109    def get_terrain(self, x: int, y: int) -> int:
110        if not self.in_bounds(x, y):
111            return TERR_MOUNTAIN
112        return self.terrain[y][x]
113
114    def is_passable(self, x: int, y: int, *, is_air: bool) -> bool:
115        if not self.in_bounds(x, y):
116            return False
117        t = self.terrain[y][x]
118        if is_air:
119            return True
120        if t in (TERR_WATER, TERR_MOUNTAIN, TERR_TREE):
121            return False
122        return True