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