nodes/ui.pyΒΆ

Part of Clear Code Zelda.

 1"""Player HUD: health bar, energy bar, EXP counter, weapon/magic boxes.
 2
 3Drawn from the root via ``on_draw`` on a CanvasLayer so it stays in screen
 4space regardless of the camera offset.
 5"""
 6
 7from __future__ import annotations
 8
 9from settings import (
10    BAR_HEIGHT,
11    ENERGY_BAR_WIDTH,
12    ENERGY_COLOUR,
13    HEALTH_BAR_WIDTH,
14    HEALTH_COLOUR,
15    ITEM_BOX_SIZE,
16    TEXT_COLOUR,
17    UI_BG_COLOUR,
18    UI_BORDER_COLOUR,
19    UI_BORDER_COLOUR_ACTIVE,
20)
21
22from simvx.core import Node2D
23
24#: Height of the equipped-weapon / equipped-spell chips. Their width follows the
25#: label, with ``ITEM_BOX_SIZE`` as the minimum.
26ITEM_BOX_HEIGHT = 34
27
28
29class HUD(Node2D):
30    """Static-position HUD drawn directly via on_draw.
31
32    Lives outside the world camera offset (added under a CanvasLayer-style
33    parent) so positions are screen-space. Everything it draws comes from the
34    player rather than from its own Properties, hence ``dynamic``.
35    """
36
37    dynamic = True
38
39    def __init__(self, player, **kwargs):
40        super().__init__(name="HUD", **kwargs)
41        self.player = player
42        self.z_index = 1000
43
44    def on_draw(self, renderer):
45        p = self.player
46        if p is None:
47            return
48        sw, sh = self.viewport_size
49
50        # Status column, top-left: bars, kills, then the weapon and spell boxes.
51        # The bottom half of the screen belongs to the on-screen controls.
52        self._bar(renderer, 10, 10, HEALTH_BAR_WIDTH, BAR_HEIGHT, p.hp, p.max_hp, HEALTH_COLOUR)
53        self._bar(renderer, 10, 34, ENERGY_BAR_WIDTH, BAR_HEIGHT, p.energy, p.max_energy, ENERGY_COLOUR)
54        renderer.text(f"Kills {p.kills}", (10, 60), fill=TEXT_COLOUR, scale=1.6)
55
56        self._item_box(renderer, 10, 88, p.weapon, p.weapon_switching)
57        self._item_box(renderer, 10 + ITEM_BOX_SIZE + 8, 88, p.magic, p.magic_switching)
58
59        # EXP, top-right.
60        exp_text = f"XP {int(p.exp)}"
61        renderer.rect((sw - 140, 10), (130, 36), fill=UI_BG_COLOUR)
62        renderer.rect((sw - 140, 10), (130, 36), stroke=UI_BORDER_COLOUR)
63        renderer.text(exp_text, (sw - 130, 18), fill=TEXT_COLOUR, scale=2)
64
65        # Help strip along the very bottom, naming the key and the pad button
66        # for each verb. The scale shrinks to whatever the window can hold, so
67        # the line never runs off the edge.
68        help_text = "move WASD/stick   attack SPACE/A   magic CTRL/B   swap Q,E/shoulders   upgrade M/START"
69        strip_h = 28
70        text_scale = min(1.4, (sw - 20) / max(1.0, renderer.text_width(help_text, 1.0)))
71        text_y = sh - strip_h + (strip_h - 14 * text_scale) / 2
72        renderer.rect((0, sh - strip_h), (sw, strip_h), fill=(0.86, 0.86, 0.86, 1.0))
73        renderer.text(help_text, (10, text_y), fill=(0.10, 0.10, 0.12, 1.0), scale=text_scale)
74
75    def _bar(self, renderer, x, y, w, h, current, mx, colour):
76        # background
77        renderer.rect((x, y), (w, h), fill=UI_BG_COLOUR)
78        # filled
79        ratio = max(0.0, min(1.0, current / max(1.0, mx)))
80        renderer.rect((x, y), (w * ratio, h), fill=colour)
81        # border
82        renderer.rect((x, y), (w, h), stroke=UI_BORDER_COLOUR)
83
84    def _item_box(self, renderer, x, y, label: str, active: bool):
85        """Labelled chip for the equipped weapon / spell, sized to fit its text."""
86        scale = 1.4
87        width = max(ITEM_BOX_SIZE, renderer.text_width(label, scale) + 20)
88        renderer.rect((x, y), (width, ITEM_BOX_HEIGHT), fill=UI_BG_COLOUR)
89        border = UI_BORDER_COLOUR_ACTIVE if active else UI_BORDER_COLOUR
90        renderer.rect((x, y), (width, ITEM_BOX_HEIGHT), stroke=border)
91        renderer.text(label, (x + 10, y + 8), fill=TEXT_COLOUR, scale=scale)