nodes/hud.pyΒΆ
Part of Tanks of Freedom.
1"""HUD: top status bar + right-side info panel + end-turn button."""
2
3from __future__ import annotations
4
5from simvx.core import Node2D, Signal, Vec2
6from simvx.core.ui.widgets import Button, Label
7
8from .data import (
9 BUILDING_STATS,
10 HUD_HEIGHT,
11 INFO_PANEL_W,
12 PLAYER_COLOUR,
13 PLAYER_NAME,
14 UNIT_STATS,
15)
16
17
18class Hud(Node2D):
19 def __init__(self, **kwargs):
20 super().__init__(name="Hud", **kwargs)
21 self.end_turn_pressed = Signal()
22 self.new_game_pressed = Signal()
23
24 # Label children so we can update them live.
25 self._turn_label = Label("Turn 1: BLUE")
26 self._turn_label.position = (16, 16)
27 self._turn_label.size = (300, 28)
28 self._turn_label.font_size = 22.0
29 self._turn_label.text_colour = (1.0, 1.0, 1.0, 1.0)
30 self.add_child(self._turn_label)
31
32 self._status_label = Label("")
33 self._status_label.position = (340, 18)
34 self._status_label.size = (700, 22)
35 self._status_label.font_size = 16.0
36 self._status_label.text_colour = (0.9, 0.92, 0.95, 1.0)
37 self.add_child(self._status_label)
38
39 # End-turn button (top right of the top bar). Everything below is
40 # placed by ``_relayout`` instead of here: __init__ runs before the
41 # node is in a tree, so there is no viewport to ask yet.
42 self._end_btn = Button("End Turn")
43 self._end_btn.size = (120, 36)
44 self._end_btn.font_size = 16.0
45 self._end_btn.pressed.connect(lambda: self.end_turn_pressed.emit())
46 self.add_child(self._end_btn)
47
48 # Right-hand info panel labels (unit selection / building info).
49 self._panel_title = Label("")
50 self._panel_title.size = (INFO_PANEL_W - 24, 28)
51 self._panel_title.font_size = 22.0
52 self._panel_title.text_colour = (1.0, 1.0, 1.0, 1.0)
53 self.add_child(self._panel_title)
54
55 self._panel_lines: list[Label] = []
56 for _ in range(8):
57 lbl = Label("")
58 lbl.size = (INFO_PANEL_W - 24, 22)
59 lbl.font_size = 14.0
60 lbl.text_colour = (0.85, 0.88, 0.92, 1.0)
61 self.add_child(lbl)
62 self._panel_lines.append(lbl)
63
64 self._victory_label = Label("")
65 self._victory_label.size = (440, 60)
66 self._victory_label.font_size = 36.0
67 self._victory_label.text_colour = (1.0, 1.0, 1.0, 1.0)
68 self._victory_label.alignment = "centre"
69 self.add_child(self._victory_label)
70
71 self._restart_btn = Button("Play Again")
72 self._restart_btn.size = (150, 40)
73 self._restart_btn.font_size = 16.0
74 self._restart_btn.visible = False
75 self._restart_btn.pressed.connect(lambda: self.new_game_pressed.emit())
76 self.add_child(self._restart_btn)
77
78 self._view = Vec2(0.0, 0.0)
79
80 # ------------------------------------------------------------ layout
81 def on_ready(self) -> None:
82 self._relayout(self.viewport_size)
83
84 def on_update(self, dt: float) -> None:
85 view = self.viewport_size
86 if (view.x, view.y) != (self._view.x, self._view.y):
87 self._relayout(view)
88
89 def _relayout(self, view: Vec2) -> None:
90 """Place everything that hangs off a screen edge, for a viewport of *view*.
91
92 The top bar spans the width, the info panel hugs the right edge and the
93 victory card centres, so all three move when the window does. A
94 `queue_redraw` goes with the move because `on_draw` paints those panels
95 from the same numbers.
96 """
97 self._view = view
98 self._end_btn.position = (view.x - INFO_PANEL_W - 130, 12)
99 panel_x = view.x - INFO_PANEL_W + 12
100 self._panel_title.position = (panel_x, HUD_HEIGHT + 14)
101 for i, lbl in enumerate(self._panel_lines):
102 lbl.position = (panel_x, HUD_HEIGHT + 50 + i * 22)
103 self._victory_label.position = (view.x / 2 - 220, view.y / 2 - 30)
104 self._restart_btn.position = (view.x / 2 - 75, view.y / 2 + 30)
105 self.queue_redraw()
106
107 # ----------------------------------------------------------- updates
108 def set_turn(self, player: int, turn_no: int) -> None:
109 col = PLAYER_COLOUR[player]
110 self._turn_label.text = f"Turn {turn_no}: {PLAYER_NAME[player]}"
111 self._turn_label.text_colour = (col[0], col[1], col[2], 1.0)
112
113 def set_status(self, msg: str) -> None:
114 self._status_label.text = msg
115
116 def set_unit_panel(self, unit) -> None:
117 if unit is None:
118 self._panel_title.text = ""
119 for lbl in self._panel_lines:
120 lbl.text = ""
121 return
122 stats = UNIT_STATS[unit.type]
123 col = PLAYER_COLOUR[unit.owner]
124 self._panel_title.text = stats["name"]
125 self._panel_title.text_colour = (col[0], col[1], col[2], 1.0)
126 lines = [
127 f"Owner : {PLAYER_NAME[unit.owner]}",
128 f"HP : {unit.life} / {unit.max_life}",
129 f"Attack : {stats['attack']}",
130 f"AP : {unit.ap} / {unit.max_ap}",
131 f"Capture : {'yes' if unit.can_capture else 'no'}",
132 "",
133 "Click highlighted tile to move",
134 "Click red tile to attack",
135 ]
136 for lbl, text in zip(self._panel_lines, lines, strict=False):
137 lbl.text = text
138 for lbl in self._panel_lines[len(lines) :]:
139 lbl.text = ""
140
141 def set_building_panel(self, building) -> None:
142 if building is None:
143 return
144 stats = BUILDING_STATS[building.type]
145 col = PLAYER_COLOUR[building.owner]
146 self._panel_title.text = stats["name"]
147 self._panel_title.text_colour = (col[0], col[1], col[2], 1.0)
148 lines = [
149 f"Owner : {PLAYER_NAME[building.owner]}",
150 f"Role : {'win condition' if building.is_hq else 'territory'}",
151 "",
152 "Capture by ending an infantry",
153 "move on this tile.",
154 ]
155 for lbl, text in zip(self._panel_lines, lines, strict=False):
156 lbl.text = text
157 for lbl in self._panel_lines[len(lines) :]:
158 lbl.text = ""
159
160 def show_victory(self, winner: int) -> None:
161 col = PLAYER_COLOUR[winner]
162 self._victory_label.text = f"{PLAYER_NAME[winner]} WINS"
163 self._victory_label.text_colour = (col[0], col[1], col[2], 1.0)
164 self._restart_btn.visible = True
165
166 def hide_victory(self) -> None:
167 self._victory_label.text = ""
168 self._restart_btn.visible = False
169
170 # -------------------------------------------------------------- draw
171 def on_draw(self, renderer) -> None:
172 view = self.viewport_size
173 # Top bar
174 renderer.rect((0, 0), (view.x, HUD_HEIGHT), fill=(0.10, 0.13, 0.18, 0.95))
175 renderer.rect((0, HUD_HEIGHT - 2), (view.x, 2), fill=(0.30, 0.40, 0.55, 1.0))
176 # Right info panel
177 renderer.rect(
178 (view.x - INFO_PANEL_W, HUD_HEIGHT),
179 (INFO_PANEL_W, view.y - HUD_HEIGHT),
180 fill=(0.10, 0.13, 0.18, 0.92),
181 )
182 renderer.rect((view.x - INFO_PANEL_W, HUD_HEIGHT), (2, view.y - HUD_HEIGHT), fill=(0.30, 0.40, 0.55, 1.0))