Files
Bullosseum/mobile_hud.gd
T
richard 981ebf1910 Add blood decals, gore, mobile HUD, web start gate + touch/perf tests
Remove tools/fstest.html scratch page used to probe browser
fullscreen/orientation APIs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EznnY8rH2dXhtono1kwsXg
2026-09-04 14:07:57 +03:00

165 lines
5.8 KiB
GDScript

extends CanvasLayer
## Mobile-only health readouts (instanced by hud.gd only when Controls.use_touch_ui()).
## The desktop HUD's bull pips are tiny and, on a phone, the on-screen thumb cluster crowds
## the bottom edge — so on touch we hide that cluster (hud.gd) and paint health up top where
## it's clear of the joystick and abilities:
## • bull health — a big pip row, top-left
## • boss health — a wide Souls-style bar, top-centre, shown only while a bear is alive
##
## Everything is drawn in one IGNORE-filtered Control (never eats a touch), same immediate-mode
## approach as touch_controls.gd. Cached values are refreshed from each character's
## health_changed signal; the bear is discovered by group (&"bear") because it spawns after the
## HUD, and a level swap frees it so we re-acquire the next one.
const _HP_FULL: Color = Color(0.82, 0.16, 0.12)
const _HP_BONUS: Color = Color(1.00, 0.84, 0.18)
const _SOCKET: Color = Color(0.05, 0.02, 0.02, 0.92)
const _PANEL_BG: Color = Color(0.06, 0.04, 0.02, 0.82)
const _PIP_W: float = 42.0
const _PIP_H: float = 28.0
const _PIP_GAP: float = 7.0
var _surface: Control
var _player: Node = null
var _bull_cur: int = 0
var _bull_max: int = 0
var _bull_bonus: int = 0
var _boss: Node = null
var _boss_alive: bool = false
var _boss_cur: int = 0
var _boss_max: int = 1
func _ready() -> void:
layer = 9 # below touch UI (10) and the HUD/game-over (11) so the outcome screen covers it
process_mode = Node.PROCESS_MODE_ALWAYS
_surface = Control.new()
_surface.set_anchors_preset(Control.PRESET_FULL_RECT)
_surface.mouse_filter = Control.MOUSE_FILTER_IGNORE
_surface.draw.connect(_draw_surface)
add_child(_surface)
get_viewport().size_changed.connect(_surface.queue_redraw)
func _process(_delta: float) -> void:
if not Controls.use_touch_ui():
return
if _player == null:
var players := get_tree().get_nodes_in_group(&"player")
if not players.is_empty():
_player = players[0]
_player.health_changed.connect(_on_bull_health)
_on_bull_health(_player.call(&"get_hp"), _player.call(&"get_max_hp"))
# A level swap frees the old bear; drop the stale ref so the next level's bear is picked up.
if _boss != null and not is_instance_valid(_boss):
_boss = null
_boss_alive = false
_surface.queue_redraw()
if _boss == null:
var bears := get_tree().get_nodes_in_group(&"bear")
if not bears.is_empty():
_boss = bears[0]
_boss_alive = true
_boss.health_changed.connect(_on_boss_health)
if _boss.has_signal(&"killed"):
_boss.killed.connect(_on_boss_killed)
_boss_cur = int(_boss.call(&"get_hp"))
_boss_max = maxi(_boss_cur, int(DP.f("bear_max_hp")))
_surface.queue_redraw()
func _on_bull_health(current: int, max_hp: int) -> void:
_bull_cur = maxi(current, 0)
_bull_max = maxi(max_hp, 0)
_bull_bonus = 0
if _player != null and _player.has_method(&"get_bonus_hp"):
_bull_bonus = int(_player.call(&"get_bonus_hp"))
_surface.queue_redraw()
func _on_boss_health(current: int, max_hp: int) -> void:
_boss_cur = maxi(current, 0)
_boss_max = maxi(max_hp, maxi(_boss_cur, 1))
if _boss_cur <= 0:
_boss_alive = false
_surface.queue_redraw()
func _on_boss_killed() -> void:
_boss_alive = false
_surface.queue_redraw()
# ── Drawing ─────────────────────────────────────────────────────────────────────
func _draw_surface() -> void:
if not Controls.use_touch_ui():
return
var boss_showing := _boss != null and is_instance_valid(_boss) and _boss_alive and _boss_cur > 0
# The boss bar is centred at the very top; the bull pip row is top-left. On a narrow landscape
# phone a long pip row reaches the centre and collides with the boss bar, so when the bar is up
# the bull row drops to a second line just beneath it (guaranteed clear, whatever the HP count).
var bull_top := 22.0
if boss_showing:
bull_top = _draw_boss_bar() + 12.0
_draw_bull_health(bull_top)
# Big pip row in the top-left. Bonus pips (barrel reward) sit at the high end and light gold,
# matching the desktop HUD; base pips light red; spent pips read as dark sockets.
func _draw_bull_health(top: float) -> void:
var font := UiFonts.body()
var x := 24.0
var y := top
if font != null:
var label := "BULL"
var fs := 20
_surface.draw_string(
font, Vector2(x, y + _PIP_H * 0.85), label, HORIZONTAL_ALIGNMENT_LEFT, -1, fs,
UiTheme.GOLD)
x += font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x + 12.0
var base_max := _bull_max - _bull_bonus
for i: int in _bull_max:
var r := Rect2(x + i * (_PIP_W + _PIP_GAP), y, _PIP_W, _PIP_H)
_surface.draw_rect(r, _SOCKET)
if i < _bull_cur:
var c := _HP_BONUS if i >= base_max else _HP_FULL
_surface.draw_rect(r.grow(-2.0), c)
_surface.draw_rect(r, Color(UiTheme.GOLD.r, UiTheme.GOLD.g, UiTheme.GOLD.b, 0.45), false, 2.0)
# Wide boss bar centred at the top: dark trough, red fill scaled to remaining HP, gold trim,
# name centred above it. Returns the bar's bottom Y so the bull pip row can sit clear beneath it.
func _draw_boss_bar() -> float:
var vp := get_viewport().get_visible_rect().size
var bw := clampf(vp.x * 0.55, 280.0, 620.0)
var bh := 30.0
var bx := (vp.x - bw) * 0.5
var by := 20.0
var bar := Rect2(bx, by, bw, bh)
_surface.draw_rect(bar, _PANEL_BG)
var frac := clampf(float(_boss_cur) / float(_boss_max), 0.0, 1.0)
var fill := bar.grow(-3.0)
fill.size.x *= frac
_surface.draw_rect(fill, _HP_FULL)
_surface.draw_rect(bar, UiTheme.BORDER, false, 2.0)
var font := UiFonts.body()
if font != null:
var boss_name := "BEAR"
var fs := 20
var tw := font.get_string_size(boss_name, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
_surface.draw_string(
font, Vector2(bx + (bw - tw.x) * 0.5, by - 6.0), boss_name,
HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiTheme.GOLD)
return by + bh