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    AnchorPreset,
  9    Control,
 10    Label,
 11    Panel,
 12    VBoxContainer,
 13)
 14from simvx.core.ui import BottomControlsStrip
 15
 16if TYPE_CHECKING:  # pragma: no cover
 17    from .root import Q1K3Root
 18
 19
 20KEYBOARD_HINTS = [
 21    "WASD move",
 22    "MOUSE look",
 23    "LMB fire",
 24    "SPACE jump",
 25    "Q / E weapon",
 26    "ESC menu",
 27]
 28POINTER_HINTS = [
 29    "STICK move",
 30    "DRAG look",
 31    "FIRE / JUMP / WPN buttons",
 32    "ESC menu",
 33]
 34STRIP_HEIGHT = 26.0
 35
 36
 37class HUD(Control):
 38    """Anchors-FULL_RECT overlay containing all HUD widgets.
 39
 40    Children:
 41    - health_label (bottom-left, or top-left in pointer mode)
 42    - ammo_label + weapon_label (bottom-right, or top-right in pointer mode)
 43    - message_label (top-centre, fades after 2s)
 44    - crosshair    (centre)
 45    - controls strip (pinned to the bottom edge)
 46    """
 47
 48    def __init__(self, game: Q1K3Root) -> None:
 49        super().__init__()
 50        self.game = game
 51        self.set_anchor_preset(AnchorPreset.FULL_RECT)
 52
 53        # Persistent controls hint strip along the very bottom edge. Everything
 54        # else in the bottom band is offset up to clear it.
 55        self.strip = BottomControlsStrip(hints=KEYBOARD_HINTS, name="Controls")
 56        self.strip.place_bottom_strip(STRIP_HEIGHT)
 57        self.add_child(self.strip)
 58
 59        self.health_label = Label(text="HEALTH 100", font_size=28.0)
 60        self.add_child(self.health_label)
 61
 62        self.weapon_box = VBoxContainer()
 63        self.ammo_label = Label(text="AMMO INF", font_size=28.0)
 64        self.weapon_label = Label(text="SHOTGUN", font_size=18.0)
 65        self.weapon_box.add_child(self.ammo_label)
 66        self.weapon_box.add_child(self.weapon_label)
 67        self.add_child(self.weapon_box)
 68
 69        self.set_pointer_mode(False)
 70
 71        # Top-centre message banner
 72        self.message_label = Label(text="", font_size=24.0)
 73        self.message_label.set_anchor_preset(AnchorPreset.TOP_WIDE)
 74        self.message_label.alignment = "center"
 75        self.message_label.margin_top = 48
 76        self.message_label.margin_bottom = -88
 77        self.message_label.margin_left = 24
 78        self.message_label.margin_right = -24
 79        self.add_child(self.message_label)
 80        self._message_until = 0.0
 81
 82        # Crosshair (centre, small dot)
 83        self.crosshair = Panel()
 84        self.crosshair.set_anchor_preset(AnchorPreset.CENTER)
 85        self.crosshair.margin_left = -3
 86        self.crosshair.margin_right = 3
 87        self.crosshair.margin_top = -3
 88        self.crosshair.margin_bottom = 3
 89        self.crosshair.bg_colour = (1.0, 1.0, 1.0, 0.8)
 90        self.add_child(self.crosshair)
 91
 92        # Death overlay (hidden by default)
 93        self.death_panel = Panel()
 94        self.death_panel.set_anchor_preset(AnchorPreset.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)
 99        self.death_label.set_anchor_preset(AnchorPreset.CENTER)
100        self.death_label.margin_left = -180
101        self.death_label.margin_right = 180
102        self.death_label.margin_top = -40
103        self.death_label.margin_bottom = 40
104        self.death_panel.add_child(self.death_label)
105
106    def set_pointer_mode(self, pointer_only: bool) -> None:
107        """Re-anchor the readouts and retitle the hint strip.
108
109        The on-screen stick and buttons occupy the bottom corners, so in
110        pointer mode the health and ammo readouts move to the top ones.
111        Margins are relative to the anchored corner: negative top/bottom
112        moves up, negative left/right moves left.
113        """
114        if pointer_only:
115            self.health_label.set_anchor_preset(AnchorPreset.TOP_LEFT)
116            self.health_label.margin_top = 20
117            self.health_label.margin_bottom = 60
118            self.weapon_box.set_anchor_preset(AnchorPreset.TOP_RIGHT)
119            self.weapon_box.margin_top = 20
120            self.weapon_box.margin_bottom = 84
121        else:
122            self.health_label.set_anchor_preset(AnchorPreset.BOTTOM_LEFT)
123            self.health_label.margin_top = -86
124            self.health_label.margin_bottom = -46
125            self.weapon_box.set_anchor_preset(AnchorPreset.BOTTOM_RIGHT)
126            self.weapon_box.margin_top = -110
127            self.weapon_box.margin_bottom = -46
128        self.health_label.margin_left = 24
129        self.health_label.margin_right = 244
130        self.weapon_box.margin_left = -244
131        self.weapon_box.margin_right = -24
132        self.strip.hints = POINTER_HINTS if pointer_only else KEYBOARD_HINTS
133
134    def show_message(self, text: str, duration: float = 2.0) -> None:
135        self.message_label.text = text
136        self._message_until = self.game.game_time + duration
137
138    def on_update(self, dt: float) -> None:
139        if self.game.player and not self.game.player._dead:
140            p = self.game.player
141            self.health_label.text = f"HEALTH {max(0, p.health)}"
142            self.ammo_label.text = f"AMMO {p.weapon.display_ammo()}"
143            self.weapon_label.text = p.weapon.name.upper()
144        if self.game.game_time >= self._message_until:
145            self.message_label.text = ""
146        if self.game.player and self.game.player._dead:
147            self.death_panel.visible = True
148        else:
149            self.death_panel.visible = False