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