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))