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
 6from simvx.core.ui import Anchor, Control, Label, PointerFilter
 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", anchor=Anchor.FULL_RECT, **kwargs)
24        # A read-only overlay: a tap anywhere on it is a tap on the game, which
25        # is what starts and restarts a run.
26        self.pointer_filter = PointerFilter.IGNORE
27
28        # Score label: top-centre, anchored to TOP_WIDE so it scales with the
29        # window width and stays centred horizontally.
30        self.score_label = self.add_child(
31            Label("0", name="ScoreLabel", anchor=Anchor.TOP_WIDE, size=(0, 0), inset=(0, 12, 0, 0))
32        )
33        self.score_label.pointer_filter = PointerFilter.IGNORE
34        self.score_label.font_size = 56
35        self.score_label.alignment = "centre"
36        self.score_label.text_colour = (1.0, 1.0, 1.0, 1.0)
37
38        # Splash + restart prompt rendered by Main.on_draw via fit-scaled
39        # text (the Label widget's text_width doesn't handle newlines
40        # or wider-than-rect text, so it renders left-shifted at default size).
41        # We expose plain string state here; Main reads .message_text and
42        # .show_prompt to render them.
43        self.message_text: str = "Dodge the Creeps"
44        self.message_visible: bool = True
45        self.show_prompt: bool = True
46
47        self.message_timer = self.add_child(Timer(duration=self.MESSAGE_FADE_SEC, one_shot=True, name="MessageTimer"))
48        self.message_timer.timeout.connect(self._hide_message)
49
50    def on_update(self, dt: float) -> None:
51        # The prompt is the only thing that arms a (re)start, so the HUD owns
52        # the input that answers it and reports back through `start_game`.
53        # One action covers the keys, a tap and a pad button alike.
54        if self.show_prompt and Input.just_pressed("start"):
55            self.start_game.emit()
56
57    # ------------------------------------------------------------------
58    # Public API mirroring hud.gd
59    # ------------------------------------------------------------------
60
61    def update_score(self, score: int) -> None:
62        self.score_label.text = str(score)
63
64    def show_message(self, text: str) -> None:
65        self.message_text = text
66        self.message_visible = True
67        self.message_timer.start()
68
69    def show_game_over(self) -> Node:
70        """Run the `Game Over` → splash → restart-prompt sequence as a coroutine."""
71        return self.start_coroutine(self._game_over_sequence())
72
73    def _game_over_sequence(self):
74        from simvx.core import wait, wait_signal
75
76        self.show_message("Game Over")
77        yield from wait_signal(self.message_timer.timeout)
78
79        self.message_text = "Dodge the Creeps"
80        self.message_visible = True
81        yield from wait(self.GAME_OVER_HOLD_SEC)
82
83        self.show_prompt = True
84
85    def hide_start_prompt(self) -> None:
86        self.show_prompt = False
87
88    def _hide_message(self) -> None:
89        self.message_visible = False