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