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