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