2025-11-16 22:48:50 +00:00
|
|
|
extends CanvasLayer
|
|
|
|
|
## HUD Manager - Main UI controller for the WoW-style interface
|
|
|
|
|
## This autoload manages all UI components and connects to the local player
|
|
|
|
|
|
|
|
|
|
# UI Component references
|
|
|
|
|
var action_bar: Control = null
|
|
|
|
|
var unit_frame: Control = null
|
|
|
|
|
var target_frame: Control = null
|
|
|
|
|
var character_sheet: Control = null
|
|
|
|
|
var tab_hint: Control = null
|
2026-06-30 22:57:11 +01:00
|
|
|
var keybind_hint: Control = null
|
2025-11-16 23:11:51 +00:00
|
|
|
var escape_menu: Control = null
|
2026-07-01 23:37:56 +01:00
|
|
|
var run_results: Control = null
|
2025-11-16 22:48:50 +00:00
|
|
|
|
|
|
|
|
# Player reference
|
|
|
|
|
var local_player: Character = null
|
|
|
|
|
|
|
|
|
|
func _ready():
|
|
|
|
|
# Create main HUD container
|
|
|
|
|
name = "HUD"
|
|
|
|
|
layer = 100 # Ensure UI is above game world
|
|
|
|
|
|
|
|
|
|
print("[HUD] HUD Manager initialized")
|
|
|
|
|
|
|
|
|
|
## Set the local player and connect signals
|
|
|
|
|
func set_local_player(player: Character):
|
|
|
|
|
if local_player:
|
|
|
|
|
_disconnect_player_signals()
|
|
|
|
|
|
|
|
|
|
local_player = player
|
|
|
|
|
|
|
|
|
|
if not local_player:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print("[HUD] Local player set: ", local_player.name)
|
|
|
|
|
_connect_player_signals()
|
|
|
|
|
_create_ui_components()
|
|
|
|
|
|
|
|
|
|
## Connect to player signals
|
|
|
|
|
func _connect_player_signals():
|
|
|
|
|
if not local_player:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Health signals (from BaseUnit)
|
|
|
|
|
local_player.health_changed.connect(_on_player_health_changed)
|
|
|
|
|
local_player.died.connect(_on_player_died)
|
|
|
|
|
local_player.respawned.connect(_on_player_respawned)
|
|
|
|
|
|
|
|
|
|
# Ability/weapon signals
|
|
|
|
|
local_player.dash_cooldown_updated.connect(_on_dash_cooldown_updated)
|
|
|
|
|
local_player.attack_cooldown_updated.connect(_on_attack_cooldown_updated)
|
|
|
|
|
local_player.weapon_equipped_changed.connect(_on_weapon_changed)
|
|
|
|
|
|
|
|
|
|
print("[HUD] Connected to player signals")
|
|
|
|
|
|
|
|
|
|
## Disconnect from player signals
|
|
|
|
|
func _disconnect_player_signals():
|
|
|
|
|
if not local_player:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if local_player.health_changed.is_connected(_on_player_health_changed):
|
|
|
|
|
local_player.health_changed.disconnect(_on_player_health_changed)
|
|
|
|
|
if local_player.died.is_connected(_on_player_died):
|
|
|
|
|
local_player.died.disconnect(_on_player_died)
|
|
|
|
|
if local_player.respawned.is_connected(_on_player_respawned):
|
|
|
|
|
local_player.respawned.disconnect(_on_player_respawned)
|
|
|
|
|
if local_player.dash_cooldown_updated.is_connected(_on_dash_cooldown_updated):
|
|
|
|
|
local_player.dash_cooldown_updated.disconnect(_on_dash_cooldown_updated)
|
|
|
|
|
if local_player.attack_cooldown_updated.is_connected(_on_attack_cooldown_updated):
|
|
|
|
|
local_player.attack_cooldown_updated.disconnect(_on_attack_cooldown_updated)
|
|
|
|
|
if local_player.weapon_equipped_changed.is_connected(_on_weapon_changed):
|
|
|
|
|
local_player.weapon_equipped_changed.disconnect(_on_weapon_changed)
|
|
|
|
|
|
|
|
|
|
## Create all UI components
|
|
|
|
|
func _create_ui_components():
|
|
|
|
|
print("[HUD] Creating UI components...")
|
|
|
|
|
|
|
|
|
|
_create_action_bar()
|
|
|
|
|
_create_unit_frame()
|
|
|
|
|
_create_character_sheet()
|
|
|
|
|
_create_tab_hint()
|
2026-06-30 22:57:11 +01:00
|
|
|
_create_keybind_hint()
|
2025-11-16 23:11:51 +00:00
|
|
|
_create_escape_menu()
|
2025-11-16 22:48:50 +00:00
|
|
|
|
|
|
|
|
## Create action bar at bottom of screen
|
|
|
|
|
func _create_action_bar():
|
|
|
|
|
var action_bar_scene = load("res://level/ui/scenes/action_bar.tscn")
|
|
|
|
|
if action_bar_scene:
|
|
|
|
|
action_bar = action_bar_scene.instantiate()
|
|
|
|
|
add_child(action_bar)
|
|
|
|
|
if action_bar and local_player:
|
|
|
|
|
action_bar.set_player(local_player)
|
|
|
|
|
print("[HUD] Action bar created")
|
|
|
|
|
else:
|
|
|
|
|
push_error("[HUD] Failed to load action_bar.tscn")
|
|
|
|
|
|
|
|
|
|
## Create unit frame (character portrait)
|
|
|
|
|
func _create_unit_frame():
|
|
|
|
|
var unit_frame_scene = load("res://level/ui/scenes/unit_frame.tscn")
|
|
|
|
|
if unit_frame_scene:
|
|
|
|
|
unit_frame = unit_frame_scene.instantiate()
|
|
|
|
|
add_child(unit_frame)
|
|
|
|
|
if unit_frame and local_player:
|
|
|
|
|
unit_frame.set_player(local_player)
|
|
|
|
|
print("[HUD] Unit frame created")
|
|
|
|
|
else:
|
|
|
|
|
push_error("[HUD] Failed to load unit_frame.tscn")
|
|
|
|
|
|
|
|
|
|
## Create character sheet (toggle with Tab)
|
|
|
|
|
func _create_character_sheet():
|
|
|
|
|
var character_sheet_scene = load("res://level/ui/scenes/character_sheet.tscn")
|
|
|
|
|
if character_sheet_scene:
|
|
|
|
|
character_sheet = character_sheet_scene.instantiate()
|
|
|
|
|
add_child(character_sheet)
|
|
|
|
|
if character_sheet and local_player:
|
|
|
|
|
character_sheet.set_player(local_player)
|
|
|
|
|
print("[HUD] Character sheet created")
|
|
|
|
|
else:
|
|
|
|
|
push_error("[HUD] Failed to load character_sheet.tscn")
|
|
|
|
|
|
|
|
|
|
## Create Tab hint (always visible on left side)
|
|
|
|
|
func _create_tab_hint():
|
|
|
|
|
var tab_hint_scene = load("res://level/ui/scenes/tab_hint.tscn")
|
|
|
|
|
if tab_hint_scene:
|
|
|
|
|
tab_hint = tab_hint_scene.instantiate()
|
|
|
|
|
add_child(tab_hint)
|
|
|
|
|
print("[HUD] Tab hint created")
|
|
|
|
|
else:
|
|
|
|
|
push_error("[HUD] Failed to load tab_hint.tscn")
|
|
|
|
|
|
2026-06-30 22:57:11 +01:00
|
|
|
## Create keybind hint (always visible, top-right corner)
|
|
|
|
|
func _create_keybind_hint():
|
|
|
|
|
var keybind_hint_scene = load("res://level/ui/scenes/keybind_hint.tscn")
|
|
|
|
|
if keybind_hint_scene:
|
|
|
|
|
keybind_hint = keybind_hint_scene.instantiate()
|
|
|
|
|
add_child(keybind_hint)
|
|
|
|
|
print("[HUD] Keybind hint created")
|
|
|
|
|
else:
|
|
|
|
|
push_error("[HUD] Failed to load keybind_hint.tscn")
|
|
|
|
|
|
2025-11-16 23:11:51 +00:00
|
|
|
## Create escape menu (toggle with Escape)
|
|
|
|
|
func _create_escape_menu():
|
|
|
|
|
var escape_menu_scene = load("res://level/ui/scenes/escape_menu.tscn")
|
|
|
|
|
if escape_menu_scene:
|
|
|
|
|
escape_menu = escape_menu_scene.instantiate()
|
|
|
|
|
add_child(escape_menu)
|
|
|
|
|
if escape_menu and local_player:
|
|
|
|
|
escape_menu.set_player(local_player)
|
|
|
|
|
print("[HUD] Escape menu created")
|
|
|
|
|
else:
|
|
|
|
|
push_error("[HUD] Failed to load escape_menu.tscn")
|
|
|
|
|
|
2025-11-16 22:48:50 +00:00
|
|
|
## Show all UI components
|
|
|
|
|
func show_hud():
|
|
|
|
|
show()
|
|
|
|
|
|
|
|
|
|
## Hide all UI components
|
|
|
|
|
func hide_hud():
|
|
|
|
|
hide()
|
|
|
|
|
|
2026-07-01 23:37:56 +01:00
|
|
|
## Tear down all UI components (used when returning to the menu after a run).
|
|
|
|
|
## The HUD is an autoload, so its children survive scene reloads unless freed.
|
|
|
|
|
func reset():
|
|
|
|
|
if local_player and is_instance_valid(local_player):
|
|
|
|
|
_disconnect_player_signals()
|
|
|
|
|
local_player = null
|
|
|
|
|
|
|
|
|
|
for child in get_children():
|
|
|
|
|
child.queue_free()
|
|
|
|
|
|
|
|
|
|
action_bar = null
|
|
|
|
|
unit_frame = null
|
|
|
|
|
target_frame = null
|
|
|
|
|
character_sheet = null
|
|
|
|
|
tab_hint = null
|
|
|
|
|
keybind_hint = null
|
|
|
|
|
escape_menu = null
|
|
|
|
|
run_results = null
|
|
|
|
|
print("[HUD] Reset - all UI components removed")
|
|
|
|
|
|
2025-11-16 22:48:50 +00:00
|
|
|
## Player signal callbacks
|
|
|
|
|
func _on_player_health_changed(old_health: float, new_health: float):
|
|
|
|
|
# Update unit frame
|
|
|
|
|
if unit_frame:
|
|
|
|
|
unit_frame.update_health(new_health, local_player.max_health)
|
|
|
|
|
|
|
|
|
|
func _on_player_died(killer_id: int):
|
2026-07-01 23:37:56 +01:00
|
|
|
print("[HUD] Player died - ending run")
|
|
|
|
|
_show_run_results()
|
|
|
|
|
|
|
|
|
|
## End the local player's run and show the results screen.
|
|
|
|
|
## GameState.end_run() is guarded against double-calls (died can fire twice on host).
|
|
|
|
|
func _show_run_results():
|
|
|
|
|
if run_results and is_instance_valid(run_results):
|
|
|
|
|
return # Already showing
|
|
|
|
|
|
|
|
|
|
var stats = GameState.end_run()
|
|
|
|
|
|
|
|
|
|
var results_script = load("res://level/ui/scripts/run_results.gd")
|
|
|
|
|
run_results = results_script.new()
|
|
|
|
|
run_results.setup(stats)
|
|
|
|
|
add_child(run_results)
|
2025-11-16 22:48:50 +00:00
|
|
|
|
|
|
|
|
func _on_player_respawned():
|
|
|
|
|
print("[HUD] Player respawned")
|
|
|
|
|
# Refresh all UI elements
|
|
|
|
|
if local_player:
|
|
|
|
|
_on_player_health_changed(0, local_player.current_health)
|
|
|
|
|
|
|
|
|
|
## Cooldown signal handlers
|
|
|
|
|
func _on_dash_cooldown_updated(remaining: float, total: float):
|
|
|
|
|
if action_bar:
|
|
|
|
|
action_bar.update_dash_cooldown(remaining, total)
|
|
|
|
|
|
|
|
|
|
func _on_attack_cooldown_updated(remaining: float, total: float):
|
|
|
|
|
if action_bar:
|
|
|
|
|
action_bar.update_attack_cooldown(remaining, total)
|
|
|
|
|
|
|
|
|
|
func _on_weapon_changed():
|
|
|
|
|
if action_bar:
|
|
|
|
|
action_bar.on_weapon_changed()
|