Files
SurvivalOfTheSnippest/level/ui/scripts/hud_manager.gd
T
Scottlg f5e0642b39 Arena polish: in-run HUD, no free loot, retire-with-winnings
- HUD arena status (top center): live wave number and run gold,
  driven by GameState signals, torn down cleanly on HUD.reset()
- Free floor weapons removed: the initial sword/shield spawn is gone
  and manually placed level.tscn weapons are deleted server-side at
  init. Gear now only enters play via the armory or enemy drops.
- Retire to Camp: new escape-menu button (shown only while alive in
  an active run) banks 100% of run gold and keeps all gear - the
  counterweight to death's 10% settlement. Exit Game and window close
  also bank winnings before quitting.
- CLAUDE.md rewritten for the gladiator arena era: core loop,
  autoloads, economy API, RPC patterns, verification workflow.
2026-07-01 23:47:03 +01:00

287 lines
9.2 KiB
GDScript

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
var keybind_hint: Control = null
var escape_menu: Control = null
var run_results: Control = null
var arena_status: Control = null
var _wave_label: Label = null
var _gold_label: Label = null
# 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()
_create_keybind_hint()
_create_escape_menu()
_create_arena_status()
## 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")
## 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")
## 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")
## Create the arena status readout (wave + run gold, top center)
func _create_arena_status():
arena_status = PanelContainer.new()
arena_status.name = "ArenaStatus"
arena_status.set_anchors_preset(Control.PRESET_CENTER_TOP)
arena_status.offset_top = 10
arena_status.grow_horizontal = Control.GROW_DIRECTION_BOTH
add_child(arena_status)
var margin = MarginContainer.new()
margin.add_theme_constant_override("margin_left", 18)
margin.add_theme_constant_override("margin_right", 18)
margin.add_theme_constant_override("margin_top", 6)
margin.add_theme_constant_override("margin_bottom", 6)
arena_status.add_child(margin)
var hbox = HBoxContainer.new()
hbox.add_theme_constant_override("separation", 24)
margin.add_child(hbox)
_wave_label = Label.new()
_wave_label.add_theme_font_size_override("font_size", 22)
_wave_label.add_theme_color_override("font_color", Color(1.0, 0.8, 0.0))
_wave_label.add_theme_color_override("font_outline_color", Color.BLACK)
_wave_label.add_theme_constant_override("outline_size", 2)
hbox.add_child(_wave_label)
_gold_label = Label.new()
_gold_label.add_theme_font_size_override("font_size", 22)
_gold_label.add_theme_color_override("font_color", Color(1.0, 0.85, 0.2))
_gold_label.add_theme_color_override("font_outline_color", Color.BLACK)
_gold_label.add_theme_constant_override("outline_size", 2)
hbox.add_child(_gold_label)
# Live updates from the economy
GameState.wave_changed.connect(_on_wave_changed)
GameState.run_gold_changed.connect(_on_run_gold_changed)
# Seed with current values (run may already be underway)
_on_wave_changed(GameState.current_wave)
_on_run_gold_changed(GameState.run_gold)
print("[HUD] Arena status created")
func _on_wave_changed(wave: int):
if _wave_label:
_wave_label.text = "Wave %d" % wave if wave > 0 else "Prepare..."
func _on_run_gold_changed(gold: int):
if _gold_label:
_gold_label.text = "Gold +%d" % gold
## Show all UI components
func show_hud():
show()
## Hide all UI components
func hide_hud():
hide()
## 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
# Drop GameState connections so they don't fire on freed labels
# (and don't double-connect when the next run creates a new status bar)
if GameState.wave_changed.is_connected(_on_wave_changed):
GameState.wave_changed.disconnect(_on_wave_changed)
if GameState.run_gold_changed.is_connected(_on_run_gold_changed):
GameState.run_gold_changed.disconnect(_on_run_gold_changed)
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
arena_status = null
_wave_label = null
_gold_label = null
print("[HUD] Reset - all UI components removed")
## 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):
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)
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()