91 lines
3.7 KiB
Python
91 lines
3.7 KiB
Python
"""Player HUD: health bar, energy bar, EXP counter, weapon/magic boxes.
|
|
|
|
Drawn from the root via ``on_draw`` on a CanvasLayer so it stays in screen
|
|
space regardless of the camera offset.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from settings import (
|
|
BAR_HEIGHT,
|
|
ENERGY_BAR_WIDTH,
|
|
ENERGY_COLOUR,
|
|
HEALTH_BAR_WIDTH,
|
|
HEALTH_COLOUR,
|
|
ITEM_BOX_SIZE,
|
|
TEXT_COLOUR,
|
|
UI_BG_COLOUR,
|
|
UI_BORDER_COLOUR,
|
|
UI_BORDER_COLOUR_ACTIVE,
|
|
)
|
|
|
|
from simvx.core import Node2D
|
|
|
|
#: Height of the equipped-weapon / equipped-spell chips. Their width follows the
|
|
#: label, with ``ITEM_BOX_SIZE`` as the minimum.
|
|
ITEM_BOX_HEIGHT = 34
|
|
|
|
|
|
class HUD(Node2D):
|
|
"""Static-position HUD drawn directly via on_draw.
|
|
|
|
Lives outside the world camera offset (added under a CanvasLayer-style
|
|
parent) so positions are screen-space. Everything it draws comes from the
|
|
player rather than from its own Properties, hence ``dynamic``.
|
|
"""
|
|
|
|
dynamic = True
|
|
|
|
def __init__(self, player, **kwargs):
|
|
super().__init__(name="HUD", **kwargs)
|
|
self.player = player
|
|
self.z_index = 1000
|
|
|
|
def on_draw(self, renderer):
|
|
p = self.player
|
|
if p is None:
|
|
return
|
|
sw, sh = self.tree.screen_size if self.tree else (1280.0, 720.0)
|
|
|
|
# Status column, top-left: bars, kills, then the weapon and spell boxes.
|
|
# The bottom half of the screen belongs to the on-screen controls.
|
|
self._bar(renderer, 10, 10, HEALTH_BAR_WIDTH, BAR_HEIGHT, p.hp, p.max_hp, HEALTH_COLOUR)
|
|
self._bar(renderer, 10, 34, ENERGY_BAR_WIDTH, BAR_HEIGHT, p.energy, p.max_energy, ENERGY_COLOUR)
|
|
renderer.draw_text(f"Kills {p.kills}", (10, 60), colour=TEXT_COLOUR, scale=1.6)
|
|
|
|
self._item_box(renderer, 10, 88, p.weapon, p.weapon_switching)
|
|
self._item_box(renderer, 10 + ITEM_BOX_SIZE + 8, 88, p.magic, p.magic_switching)
|
|
|
|
# EXP, top-right.
|
|
exp_text = f"XP {int(p.exp)}"
|
|
renderer.draw_rect((sw - 140, 10), (130, 36), colour=UI_BG_COLOUR, filled=True)
|
|
renderer.draw_rect((sw - 140, 10), (130, 36), colour=UI_BORDER_COLOUR, filled=False)
|
|
renderer.draw_text(exp_text, (sw - 130, 18), colour=TEXT_COLOUR, scale=2)
|
|
|
|
# Help strip along the very bottom. Strip height + scale chosen so glyph
|
|
# bounding boxes (incl. descenders) stay inside the strip.
|
|
help_text = "WASD move SPACE attack CTRL magic Q/E swap M upgrade"
|
|
strip_h = 28
|
|
text_scale = 1.4
|
|
text_y = sh - strip_h + (strip_h - 14 * text_scale) / 2
|
|
renderer.draw_rect((0, sh - strip_h), (sw, strip_h), colour=(0.86, 0.86, 0.86, 1.0), filled=True)
|
|
renderer.draw_text(help_text, (10, text_y), colour=(0.10, 0.10, 0.12, 1.0), scale=text_scale)
|
|
|
|
def _bar(self, renderer, x, y, w, h, current, mx, colour):
|
|
# background
|
|
renderer.draw_rect((x, y), (w, h), colour=UI_BG_COLOUR, filled=True)
|
|
# filled
|
|
ratio = max(0.0, min(1.0, current / max(1.0, mx)))
|
|
renderer.draw_rect((x, y), (w * ratio, h), colour=colour, filled=True)
|
|
# border
|
|
renderer.draw_rect((x, y), (w, h), colour=UI_BORDER_COLOUR, filled=False)
|
|
|
|
def _item_box(self, renderer, x, y, label: str, active: bool):
|
|
"""Labelled chip for the equipped weapon / spell, sized to fit its text."""
|
|
scale = 1.4
|
|
width = max(ITEM_BOX_SIZE, renderer.text_width(label, scale) + 20)
|
|
renderer.draw_rect((x, y), (width, ITEM_BOX_HEIGHT), colour=UI_BG_COLOUR, filled=True)
|
|
border = UI_BORDER_COLOUR_ACTIVE if active else UI_BORDER_COLOUR
|
|
renderer.draw_rect((x, y), (width, ITEM_BOX_HEIGHT), colour=border, filled=False)
|
|
renderer.draw_text(label, (x + 10, y + 8), colour=TEXT_COLOUR, scale=scale)
|