nodes/hud.pyΒΆ

Part of Q1K3.

  1"""Heads-up display: health, ammo, weapon name, message banner, crosshair."""
  2
  3from __future__ import annotations
  4
  5from typing import TYPE_CHECKING
  6
  7from simvx.core import (
  8    Anchor,
  9    Control,
 10    HBoxContainer,
 11    Label,
 12    Panel,
 13    PointerFilter,
 14    VBoxContainer,
 15    Vec2,
 16)
 17from simvx.core.ui import Button
 18
 19if TYPE_CHECKING:  # pragma: no cover
 20    from .root import Q1K3Root
 21
 22
 23#: The controls with no key of their own to press, printed beside the buttons.
 24KEYBOARD_HINT = "WASD move   Mouse look   Left click fire"
 25#: How tall the controls row along the bottom edge is, and how far it sits above it.
 26ROW_HEIGHT = 40.0
 27ROW_GAP = 10.0
 28
 29
 30class HUD(Control):
 31    """Anchors-FULL_RECT overlay containing all HUD widgets.
 32
 33    Children:
 34    - health_label (bottom-left, or top-left in pointer mode)
 35    - ammo_label + weapon_label (bottom-right, or top-right in pointer mode)
 36    - message_label (top-centre, fades after 2s)
 37    - crosshair    (centre)
 38    - controls row (along the bottom edge, keyboard mode): a button per key
 39      action, each showing its key and pressing it
 40    """
 41
 42    def __init__(self, game: Q1K3Root) -> None:
 43        super().__init__()
 44        self.game = game
 45        self.anchor = Anchor.FULL_RECT
 46
 47        # The controls along the very bottom edge: the actions a key presses
 48        # are buttons that show the key and press it, the rest a line of text.
 49        # Everything else in the bottom band is offset up to clear it.
 50        self.controls = HBoxContainer(
 51            name="Controls", anchor=Anchor.BOTTOM_WIDE, size=(0, ROW_HEIGHT), inset=(16, 0, 16, ROW_GAP)
 52        )
 53        self.controls.alignment = "centre"
 54        self.controls.separation = 10.0
 55        self.controls.pointer_filter = PointerFilter.IGNORE
 56        self.add_child(self.controls)
 57        self.keyboard_hint = self.controls.add_child(Label(text=KEYBOARD_HINT, font_size=14.0))
 58        self.key_buttons = [
 59            self.controls.add_child(Button(text, action=action, name=name))
 60            for text, action, name in (
 61                ("Jump", "jump", "Jump"),
 62                ("Prev weapon", "weapon_prev", "WeaponPrev"),
 63                ("Next weapon", "weapon_next", "WeaponNext"),
 64            )
 65        ]
 66        self.key_buttons.append(self.controls.add_child(Button("Menu", action="menu", name="Menu")))
 67
 68        self.health_label = Label(text="HEALTH 100", font_size=28.0)
 69        # Top-left, the readout keeps the size it measured for its first text.
 70        self._health_size_top = Vec2(self.health_label.size)
 71        self.add_child(self.health_label)
 72
 73        self.weapon_box = VBoxContainer(size=(220, 64))
 74        self.ammo_label = Label(text="AMMO INF", font_size=28.0)
 75        self.weapon_label = Label(text="SHOTGUN", font_size=18.0)
 76        self.weapon_box.add_child(self.ammo_label)
 77        self.weapon_box.add_child(self.weapon_label)
 78        self.add_child(self.weapon_box)
 79
 80        self.set_pointer_mode(False)
 81
 82        # Top-centre message banner
 83        self.message_label = Label(text="", font_size=24.0, anchor=Anchor.TOP_WIDE, size=(0, 0), inset=(24, 48, 24, 0))
 84        self.message_label.alignment = "centre"
 85        self.add_child(self.message_label)
 86        self._message_until = 0.0
 87
 88        # Crosshair (centre, small dot)
 89        self.crosshair = Panel(anchor=Anchor.CENTER, size=(6, 6))
 90        self.crosshair.bg_colour = (1.0, 1.0, 1.0, 0.8)
 91        self.add_child(self.crosshair)
 92
 93        # Death overlay (hidden by default)
 94        self.death_panel = Panel(anchor=Anchor.FULL_RECT)
 95        self.death_panel.bg_colour = (0.4, 0.0, 0.0, 0.6)
 96        self.death_panel.visible = False
 97        self.add_child(self.death_panel)
 98        self.death_label = Label(text="YOU DIED", font_size=48.0, anchor=Anchor.CENTER, size=(360, 80))
 99        self.death_panel.add_child(self.death_label)
100
101    def set_pointer_mode(self, pointer_only: bool) -> None:
102        """Re-anchor the readouts and the controls row for the input in use.
103
104        The on-screen stick and buttons occupy the bottom corners, so in
105        pointer mode the health and ammo readouts move to the top ones; in
106        keyboard mode they sit just above the controls row.
107        """
108        # The keyboard's row runs along the bottom-left. In pointer mode the
109        # on-screen controls (TouchControls) carry their own buttons, Menu too.
110        self.controls.visible = not pointer_only
111        if pointer_only:
112            self.health_label.anchor = Anchor.TOP_LEFT
113            self.health_label.size = self._health_size_top
114            self.weapon_box.anchor = Anchor.TOP_RIGHT
115            inset = (24, 20)
116        else:
117            self.health_label.anchor = Anchor.BOTTOM_LEFT
118            self.health_label.size = (220, 40)
119            self.weapon_box.anchor = Anchor.BOTTOM_RIGHT
120            inset = (24, 64)
121        self.health_label.inset = self.weapon_box.inset = inset
122
123    def show_message(self, text: str, duration: float = 2.0) -> None:
124        self.message_label.text = text
125        self._message_until = self.game.game_time + duration
126
127    def on_update(self, dt: float) -> None:
128        if self.game.player and not self.game.player._dead:
129            p = self.game.player
130            self.health_label.text = f"HEALTH {max(0, p.health)}"
131            self.ammo_label.text = f"AMMO {p.weapon.display_ammo()}"
132            self.weapon_label.text = p.weapon.name.upper()
133        if self.game.game_time >= self._message_until:
134            self.message_label.text = ""
135        if self.game.player and self.game.player._dead:
136            self.death_panel.visible = True
137        else:
138            self.death_panel.visible = False