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