nodes/cursor.pyΒΆ
Part of Tanks of Freedom.
1"""Hover cursor + selection / range overlays.
2
3`Cursor` is a single sprite that follows the mouse-hovered cell.
4`OverlayLayer` is a node that pools a set of Sprite2D children to highlight
5movement / attack range cells. Reusing the pool avoids per-frame churn, which
6matters when a unit exposes 30+ reachable cells on hover.
7"""
8
9from __future__ import annotations
10
11from simvx.core import Node2D, Property
12from simvx.core.animation.sprite import Sprite2D
13
14from .data import TILE_H, TILE_W
15from .textures import make_attack_overlay, make_cursor, make_path_dot, make_range_overlay
16
17
18class Cursor(Node2D):
19 visible = Property(
20 False,
21 coerce=bool,
22 hint="Whether this node and its subtree are drawn",
23 on_change="_on_visible_changed",
24 )
25
26 def __init__(self, tile_map, **kwargs):
27 super().__init__(name="Cursor", **kwargs)
28 self._tile_map = tile_map
29 self._sprite = Sprite2D(
30 texture=make_cursor(),
31 width=TILE_W,
32 height=TILE_H,
33 filter="nearest",
34 )
35 self.add_child(self._sprite)
36 self.cell: tuple[int, int] | None = None
37
38 def show_at(self, cell: tuple[int, int]) -> None:
39 if cell is None:
40 self.visible = False
41 return
42 self.cell = cell
43 wx, wy = self._tile_map.map_to_world(cell)
44 self.position = (wx, wy)
45 self.visible = True
46
47
48class OverlayLayer(Node2D):
49 """Dynamic pool of overlay diamond sprites for move-range / attack-range."""
50
51 def __init__(self, tile_map, **kwargs):
52 super().__init__(name="Overlay", **kwargs)
53 self._tile_map = tile_map
54 self._tex_move = make_range_overlay()
55 self._tex_atk = make_attack_overlay()
56 self._tex_path = make_path_dot()
57
58 # Pre-allocated pools.
59 self._move_pool: list[Sprite2D] = []
60 self._atk_pool: list[Sprite2D] = []
61 self._path_pool: list[Sprite2D] = []
62
63 def _resize_pool(self, pool: list[Sprite2D], texture, count: int, *, w: int, h: int) -> None:
64 while len(pool) < count:
65 s = Sprite2D(texture=texture, width=w, height=h, filter="nearest")
66 s.visible = False
67 self.add_child(s)
68 pool.append(s)
69 for i, s in enumerate(pool):
70 s.visible = i < count
71
72 def show_move_range(self, cells) -> None:
73 self._resize_pool(self._move_pool, self._tex_move, len(cells), w=TILE_W, h=TILE_H)
74 for sprite, cell in zip(self._move_pool, cells, strict=False):
75 wx, wy = self._tile_map.map_to_world(cell)
76 sprite.position = (wx, wy)
77
78 def show_attack_range(self, cells) -> None:
79 self._resize_pool(self._atk_pool, self._tex_atk, len(cells), w=TILE_W, h=TILE_H)
80 for sprite, cell in zip(self._atk_pool, cells, strict=False):
81 wx, wy = self._tile_map.map_to_world(cell)
82 sprite.position = (wx, wy)
83
84 def show_path(self, cells) -> None:
85 # Drop the first cell, that's the unit's current position.
86 path_cells = cells[1:] if cells else []
87 self._resize_pool(self._path_pool, self._tex_path, len(path_cells), w=14, h=8)
88 for sprite, cell in zip(self._path_pool, path_cells, strict=False):
89 wx, wy = self._tile_map.map_to_world(cell)
90 sprite.position = (wx, wy)
91
92 def clear(self) -> None:
93 for pool in (self._move_pool, self._atk_pool, self._path_pool):
94 for s in pool:
95 s.visible = False