nodes/hud.py

Part of Dodge the Creeps.

 1"""HUD: anchored Label widgets for score / message / start prompt."""
 2
 3from __future__ import annotations
 4
 5from simvx.core import Input, Node, Signal, Timer, Vec2
 6from simvx.core.ui import AnchorPreset, Control, Label
 7
 8
 9class HUD(Control):
10    """Top-of-screen score, centred message, bottom restart prompt.
11
12    Faithful to upstream `hud.gd`: shows "Get Ready" briefly at game start,
13    "Game Over" then "Dodge the Creeps" + a restart prompt at game over,
14    and emits `start_game` when the player answers that prompt.
15    """
16
17    start_game = Signal()
18
19    MESSAGE_FADE_SEC = 2.0
20    GAME_OVER_HOLD_SEC = 1.0
21
22    def __init__(self, **kwargs):
23        super().__init__(name="HUD", **kwargs)
24        self.set_anchor_preset(AnchorPreset.FULL_RECT)
25
26        # Score label: top-centre, anchored to TOP_WIDE so it scales with the
27        # window width and stays centred horizontally.
28        self.score_label = self.add_child(Label("0", name="ScoreLabel"))
29        self.score_label.set_anchor_preset(AnchorPreset.TOP_WIDE)
30        self.score_label.font_size = 56
31        self.score_label.alignment = "center"
32        self.score_label.text_colour = (1.0, 1.0, 1.0, 1.0)
33        self.score_label.size = Vec2(0, 78)
34        self.score_label.margin_top = 12
35
36        # Splash + restart prompt rendered by Main.on_draw via fit-scaled
37        # draw_text (the Label widget's text_width doesn't handle newlines
38        # or wider-than-rect text, so it renders left-shifted at default size).
39        # We expose plain string state here; Main reads .message_text and
40        # .show_prompt to render them.
41        self.message_text: str = "Dodge the Creeps"
42        self.message_visible: bool = True
43        self.show_prompt: bool = True
44
45        self.message_timer = self.add_child(Timer(self.MESSAGE_FADE_SEC, one_shot=True, name="MessageTimer"))
46        self.message_timer.timeout.connect(self._hide_message)
47
48    def on_update(self, dt: float) -> None:
49        # The prompt is the only thing that arms a (re)start, so the HUD owns
50        # the input that answers it and reports back through `start_game`.
51        if self.show_prompt and (
52            Input.is_action_just_pressed("start_game") or Input.is_action_just_pressed("restart_click")
53        ):
54            self.start_game()
55
56    # ------------------------------------------------------------------
57    # Public API mirroring hud.gd
58    # ------------------------------------------------------------------
59
60    def update_score(self, score: int) -> None:
61        self.score_label.text = str(score)
62
63    def show_message(self, text: str) -> None:
64        self.message_text = text
65        self.message_visible = True
66        self.message_timer.start()
67
68    def show_game_over(self) -> Node:
69        """Run the `Game Over` → splash → restart-prompt sequence as a coroutine."""
70        return self.start_coroutine(self._game_over_sequence())
71
72    def _game_over_sequence(self):
73        from simvx.core import wait, wait_signal
74
75        self.show_message("Game Over")
76        yield from wait_signal(self.message_timer.timeout)
77
78        self.message_text = "Dodge the Creeps"
79        self.message_visible = True
80        yield from wait(self.GAME_OVER_HOLD_SEC)
81
82        self.show_prompt = True
83
84    def hide_start_prompt(self) -> None:
85        self.show_prompt = False
86
87    def _hide_message(self) -> None:
88        self.message_visible = False