nodes/hud.pyΒΆ

Part of SNKRX.

  1"""Arena HUD overlay: wave / kills / snake hp via on_draw."""
  2
  3from __future__ import annotations
  4
  5from simvx.core import Node
  6
  7from .colours import FG, GREEN, RED, YELLOW
  8
  9
 10class HUD(Node):
 11    """Read-only HUD; arena pushes data into ``set_state`` once per frame."""
 12
 13    def __init__(self, **kwargs):
 14        super().__init__(name="HUD", **kwargs)
 15        self._visible_hud = False
 16        self.wave = 1
 17        self.total_waves = 5
 18        self.kills = 0
 19        self.gold = 0
 20        self.snake_hp = 0
 21        self.snake_max_hp = 0
 22        self.time_scale = 1.0
 23        self._drawn: tuple | None = None
 24
 25    @property
 26    def visible_hud(self) -> bool:
 27        """Whether the overlay draws. Toggled by the root when the phase changes."""
 28        return self._visible_hud
 29
 30    @visible_hud.setter
 31    def visible_hud(self, value: bool) -> None:
 32        value = bool(value)
 33        if value != self._visible_hud:
 34            self._visible_hud = value
 35            # Plain properties are not Property descriptors, so the retained 2D
 36            # cache has to be dirtied by hand or the HUD lingers after a hide.
 37            self.queue_redraw()
 38
 39    def _screen(self) -> tuple[int, int]:
 40        w, h = self.tree.screen_size if self.tree is not None else (1280, 720)
 41        return int(w), int(h)
 42
 43    def set_state(self, *, wave, total_waves, kills, gold, snake_hp, snake_max_hp, time_scale):
 44        self.wave = wave
 45        self.total_waves = total_waves
 46        self.kills = kills
 47        self.gold = gold
 48        self.snake_hp = snake_hp
 49        self.snake_max_hp = snake_max_hp
 50        self.time_scale = time_scale
 51
 52        # The HUD only changes on discrete events (wave/kills/gold tick, hp
 53        # ticks on damage, slow-mo ramp, a window resize). ``set_state`` is the
 54        # single mutation site for those, so dirty the retained 2D cache here
 55        # and only when the drawn snapshot actually changed, not every frame.
 56        # Show/hide is handled by the ``visible_hud`` setter.
 57        snapshot = (
 58            wave,
 59            total_waves,
 60            kills,
 61            gold,
 62            snake_hp,
 63            snake_max_hp,
 64            round(time_scale, 3),
 65            self._screen(),
 66        )
 67        if snapshot != self._drawn:
 68            self._drawn = snapshot
 69            self.queue_redraw()
 70
 71    def on_draw(self, renderer):
 72        if not self.visible_hud:
 73            return
 74        w, h = self._screen()
 75
 76        # Top strip
 77        renderer.draw_rect((0, 0), (w, 36), colour=(0.05, 0.06, 0.08, 0.85), filled=True)
 78
 79        # Chain WAVE / KILLS / GOLD with measured offsets so glyphs never
 80        # overlap regardless of digit width.
 81        wave_str = f"WAVE {self.wave}/{self.total_waves}"
 82        renderer.draw_text(wave_str, (16, 8), scale=3, colour=YELLOW)
 83        x_cursor = 16 + renderer.text_width(wave_str, 3) + 24
 84
 85        kills_str = f"KILLS {self.kills}"
 86        renderer.draw_text(kills_str, (x_cursor, 14), scale=2, colour=FG)
 87        x_cursor += renderer.text_width(kills_str, 2) + 24
 88
 89        gold_str = f"GOLD {self.gold}"
 90        renderer.draw_text(gold_str, (x_cursor, 14), scale=2, colour=YELLOW)
 91
 92        # HP bar centred
 93        bar_w = 320
 94        bar_h = 12
 95        x = w // 2 - bar_w // 2
 96        y = 12
 97        ratio = max(0.0, min(1.0, self.snake_hp / max(1, self.snake_max_hp)))
 98        renderer.draw_rect((x, y), (bar_w, bar_h), colour=(0.15, 0.15, 0.18, 1.0), filled=True)
 99        renderer.draw_rect((x, y), (bar_w * ratio, bar_h), colour=GREEN if ratio > 0.4 else RED, filled=True)
100        renderer.draw_rect((x, y), (bar_w, bar_h), colour=(0.55, 0.58, 0.65, 1.0), filled=False, thickness=1.0)
101
102        hp_str = f"{int(self.snake_hp)}/{int(self.snake_max_hp)}"
103        hw = renderer.text_width(hp_str, 2)
104        renderer.draw_text(hp_str, (w // 2 - hw // 2, y + bar_h + 4), scale=2, colour=FG)
105
106        # Slow-mo overlay
107        if self.time_scale < 0.95:
108            t_str = f"x{self.time_scale:.2f}"
109            tw_ = renderer.text_width(t_str, 4)
110            renderer.draw_text(t_str, (w - 16 - tw_, 8), scale=4, colour=(1.0, 0.6, 0.85, 1.0))
111
112        # Bottom controls strip (light-grey, per port UX baseline)
113        bottom = h - 56
114        renderer.draw_rect((0, bottom), (w, 56), colour=(0.85, 0.85, 0.88, 1.0), filled=True)
115        controls = "MOUSE / A D  STEER       AUTO-ATTACK       ESC  QUIT"
116        cw = renderer.text_width(controls, 2)
117        renderer.draw_text(controls, (w // 2 - cw // 2, bottom + 20), scale=2, colour=(0.10, 0.10, 0.12, 1.0))