Add comprehensive UI system with action bar, unit frame, and character sheet

- Created UI system in level/ui/ folder
  - Action bar with 12 ability slots (Attack, Block, Dash, Jump + 8 expansion slots)
  - Unit frame showing player portrait with health bar
  - Character sheet/spellbook (toggle with Tab) displaying stats, weapons, and abilities
  - Tab hint indicator showing how to open character sheet
  - Custom theme with golden borders and dark backgrounds

- UI Components:
  - HUD Manager autoload handles all UI initialization and player connections
  - Ability buttons with cooldown overlay and keybind display
  - Real-time health and cooldown tracking via signals
  - Scrollable character sheet with two-page layout

- Player Integration:
  - Added UI signals: dash_cooldown_updated, attack_cooldown_updated, weapon_equipped_changed
  - Mouse capture system (Escape to toggle, click to recapture)
  - Synced attack cooldown from weapons to player for UI tracking

- Updated level.gd to connect local player to HUD Manager
- Updated CLAUDE.md with git commit guidelines
Dashfix
Scott 3 weeks ago
parent d475c0abf9
commit 15865dd15f
  1. 3
      CLAUDE.md
  2. 4
      level/scripts/base_weapon.gd
  3. 10
      level/scripts/level.gd
  4. 88
      level/scripts/player.gd
  5. 75
      level/ui/scenes/ability_button.tscn
  6. 64
      level/ui/scenes/action_bar.tscn
  7. 129
      level/ui/scenes/character_sheet.tscn
  8. 69
      level/ui/scenes/resource_bars.tscn
  9. 52
      level/ui/scenes/tab_hint.tscn
  10. 93
      level/ui/scenes/unit_frame.tscn
  11. 139
      level/ui/scripts/ability_button.gd
  12. 1
      level/ui/scripts/ability_button.gd.uid
  13. 157
      level/ui/scripts/action_bar.gd
  14. 1
      level/ui/scripts/action_bar.gd.uid
  15. 220
      level/ui/scripts/character_sheet.gd
  16. 1
      level/ui/scripts/character_sheet.gd.uid
  17. 161
      level/ui/scripts/hud_manager.gd
  18. 1
      level/ui/scripts/hud_manager.gd.uid
  19. 73
      level/ui/scripts/resource_bars.gd
  20. 1
      level/ui/scripts/resource_bars.gd.uid
  21. 83
      level/ui/scripts/unit_frame.gd
  22. 1
      level/ui/scripts/unit_frame.gd.uid
  23. 12
      level/ui/theme/wow_style.tres
  24. 6
      project.godot

@ -62,6 +62,9 @@ Defined in project.godot:
## Development Guidelines ## Development Guidelines
### Git Commits
When creating git commits, do NOT include "🤖 Generated with [Claude Code]" or "Co-Authored-By: Claude" in commit messages. Keep commit messages clean and professional.
### Adding New Components ### Adding New Components
1. Create scripts in `level/scripts/` 1. Create scripts in `level/scripts/`
2. For multiplayer-synchronized components: 2. For multiplayer-synchronized components:

@ -44,6 +44,10 @@ func perform_attack() -> bool:
_attack_timer = weapon_data.attack_cooldown _attack_timer = weapon_data.attack_cooldown
# Notify owner character of attack cooldown (for UI)
if owner_character and owner_character.is_multiplayer_authority():
owner_character._attack_timer = weapon_data.attack_cooldown
# Play attack animation on owner # Play attack animation on owner
if owner_character._body: if owner_character._body:
owner_character._body.play_attack() owner_character._body.play_attack()

@ -182,6 +182,16 @@ func _add_player(id: int, player_info : Dictionary):
player.nickname.text = nick player.nickname.text = nick
# player.rpc("change_nick", nick) # player.rpc("change_nick", nick)
# Set up HUD for local player
if id == multiplayer.get_unique_id():
print("[Level] Setting up HUD for local player")
# Wait a frame to ensure HUD autoload is ready
await get_tree().process_frame
if has_node("/root/HUD"):
get_node("/root/HUD").set_local_player(player)
else:
push_warning("[Level] HUD autoload not found! Make sure to restart Godot to register the new autoload.")
var skin_enum = player_info["skin"] var skin_enum = player_info["skin"]
player.set_player_skin(skin_enum) player.set_player_skin(skin_enum)
# rpc("sync_player_skin", id, skin_enum) # rpc("sync_player_skin", id, skin_enum)

@ -53,6 +53,11 @@ var _dash_cooldown_timer: float = 0.0
var _is_dashing: bool = false var _is_dashing: bool = false
var _dash_direction: Vector3 = Vector3.ZERO var _dash_direction: Vector3 = Vector3.ZERO
# UI Signals
signal dash_cooldown_updated(remaining: float, total: float)
signal attack_cooldown_updated(remaining: float, total: float)
signal weapon_equipped_changed()
func _enter_tree(): func _enter_tree():
super._enter_tree() super._enter_tree()
$SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority() $SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority()
@ -61,6 +66,10 @@ func _ready():
super._ready() super._ready()
set_respawn_point(Vector3(0, 5, 0)) set_respawn_point(Vector3(0, 5, 0))
# Capture mouse for local player
if is_multiplayer_authority():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# Auto-find body node (needed for instanced scenes where @export NodePath doesn't work reliably) # Auto-find body node (needed for instanced scenes where @export NodePath doesn't work reliably)
if _body == null: if _body == null:
for child in get_children(): for child in get_children():
@ -132,10 +141,6 @@ func _ready():
# Update health display # Update health display
_update_health_display() _update_health_display()
# Create 2D UI health bar for local player
if is_multiplayer_authority():
_create_health_ui()
# Setup weapon pickup detection area # Setup weapon pickup detection area
_setup_weapon_pickup_area() _setup_weapon_pickup_area()
@ -208,9 +213,21 @@ func _process(delta):
_check_fall_and_respawn() _check_fall_and_respawn()
# Handle mouse capture toggle (Escape to release, click to recapture)
if Input.is_action_just_pressed("quit"):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# Recapture mouse on click if it was released
if Input.is_action_just_pressed("attack") and Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# Update attack cooldown # Update attack cooldown
if _attack_timer > 0: if _attack_timer > 0:
_attack_timer -= delta _attack_timer -= delta
attack_cooldown_updated.emit(_attack_timer, attack_cooldown)
# Update dash timers # Update dash timers
if _dash_timer > 0: if _dash_timer > 0:
@ -220,6 +237,7 @@ func _process(delta):
if _dash_cooldown_timer > 0: if _dash_cooldown_timer > 0:
_dash_cooldown_timer -= delta _dash_cooldown_timer -= delta
dash_cooldown_updated.emit(_dash_cooldown_timer, dash_cooldown)
# Handle dash input # Handle dash input
if Input.is_action_just_pressed("dash") and _dash_cooldown_timer <= 0 and not is_dead and is_on_floor(): if Input.is_action_just_pressed("dash") and _dash_cooldown_timer <= 0 and not is_dead and is_on_floor():
@ -441,65 +459,15 @@ func _server_apply_damage(target_name: String, damage: float, attacker_id: int,
target.take_damage(damage, attacker_id, knockback, attacker_pos) target.take_damage(damage, attacker_id, knockback, attacker_pos)
## Health display and callbacks ## Health display and callbacks
func _create_health_ui(): # Old 2D health UI - now handled by HUD Manager
# Create a 2D UI for the local player's health # Kept for reference, but no longer used
var canvas = CanvasLayer.new()
canvas.name = "HealthUI"
add_child(canvas)
# Health bar background
var health_bg = ColorRect.new()
health_bg.name = "HealthBG"
health_bg.color = Color(0.2, 0.2, 0.2, 0.8)
health_bg.position = Vector2(20, 20)
health_bg.size = Vector2(200, 30)
canvas.add_child(health_bg)
# Health bar (current health)
var health_bar = ColorRect.new()
health_bar.name = "HealthBar"
health_bar.color = Color(0.0, 0.8, 0.0, 1.0) # Green
health_bar.position = Vector2(22, 22)
health_bar.size = Vector2(196, 26)
canvas.add_child(health_bar)
# Health text
var health_text = Label.new()
health_text.name = "HealthText"
health_text.position = Vector2(20, 20)
health_text.size = Vector2(200, 30)
health_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
health_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
health_text.add_theme_color_override("font_color", Color.WHITE)
health_text.add_theme_color_override("font_outline_color", Color.BLACK)
health_text.add_theme_constant_override("outline_size", 2)
health_text.text = "HP: %d/%d" % [int(current_health), int(max_health)]
canvas.add_child(health_text)
func _update_health_display(): func _update_health_display():
# Update 3D label if it exists # Update 3D label if it exists (for other players to see)
if health_label: if health_label:
health_label.text = "HP: %d/%d" % [int(current_health), int(max_health)] health_label.text = "HP: %d/%d" % [int(current_health), int(max_health)]
# Update 2D UI for local player # 2D UI is now handled by HUD Manager autoload
if is_multiplayer_authority() and has_node("HealthUI"):
var health_bar = get_node_or_null("HealthUI/HealthBar")
var health_text = get_node_or_null("HealthUI/HealthText")
if health_bar:
var health_percent = get_health_percent()
health_bar.size.x = 196 * health_percent
# Change color based on health
if health_percent > 0.6:
health_bar.color = Color(0.0, 0.8, 0.0) # Green
elif health_percent > 0.3:
health_bar.color = Color(1.0, 0.8, 0.0) # Yellow
else:
health_bar.color = Color(0.8, 0.0, 0.0) # Red
if health_text:
health_text.text = "HP: %d/%d" % [int(current_health), int(max_health)]
func _on_health_changed(_old_health: float, _new_health: float): func _on_health_changed(_old_health: float, _new_health: float):
_update_health_display() _update_health_display()
@ -688,6 +656,7 @@ func equip_weapon(data: WeaponData):
if is_multiplayer_authority(): if is_multiplayer_authority():
print("Equipped: ", data.weapon_name, " to ", attach_point.name) print("Equipped: ", data.weapon_name, " to ", attach_point.name)
weapon_equipped_changed.emit()
## Unequip current weapon (local only) ## Unequip current weapon (local only)
func unequip_weapon(is_offhand: bool = false): func unequip_weapon(is_offhand: bool = false):
@ -700,6 +669,9 @@ func unequip_weapon(is_offhand: bool = false):
equipped_weapon.queue_free() equipped_weapon.queue_free()
equipped_weapon = null equipped_weapon = null
if is_multiplayer_authority():
weapon_equipped_changed.emit()
## Sync unequip across all clients ## Sync unequip across all clients
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func _unequip_weapon_sync(is_offhand: bool = false): func _unequip_weapon_sync(is_offhand: bool = false):

@ -0,0 +1,75 @@
[gd_scene load_steps=4 format=3 uid="uid://cq7xyb8n4h6yk"]
[ext_resource type="Script" path="res://level/ui/scripts/ability_button.gd" id="1_script"]
[ext_resource type="Theme" uid="uid://dvsh7tuhulnfm" path="res://level/ui/theme/wow_style.tres" id="2_theme"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
bg_color = Color(0.15, 0.15, 0.15, 0.9)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.6, 0.5, 0.2, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[node name="AbilityButton" type="PanelContainer"]
custom_minimum_size = Vector2(64, 64)
theme = ExtResource("2_theme")
theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
script = ExtResource("1_script")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
[node name="Icon" type="TextureRect" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
expand_mode = 1
stretch_mode = 5
[node name="CooldownOverlay" type="ColorRect" parent="MarginContainer/VBoxContainer/Icon"]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.7)
[node name="CooldownLabel" type="Label" parent="MarginContainer/VBoxContainer/Icon"]
visible = false
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -11.5
offset_right = 20.0
offset_bottom = 11.5
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 24
text = "3"
horizontal_alignment = 1
vertical_alignment = 1
[node name="KeybindLabel" type="Label" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 2
theme_override_font_sizes/font_size = 14
text = "1"
horizontal_alignment = 1

@ -0,0 +1,64 @@
[gd_scene load_steps=3 format=3 uid="uid://b4h0p6nbcq35b"]
[ext_resource type="Script" path="res://level/ui/scripts/action_bar.gd" id="1_script"]
[ext_resource type="PackedScene" uid="uid://cq7xyb8n4h6yk" path="res://level/ui/scenes/ability_button.tscn" id="2_ability_button"]
[node name="ActionBar" type="Control"]
layout_mode = 3
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -400.0
offset_top = -100.0
offset_right = 400.0
grow_horizontal = 2
grow_vertical = 0
script = ExtResource("1_script")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 4
alignment = 1
[node name="Slot1" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot2" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot3" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot4" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot5" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot6" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot7" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot8" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot9" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot10" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot11" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2
[node name="Slot12" parent="HBoxContainer" instance=ExtResource("2_ability_button")]
layout_mode = 2

@ -0,0 +1,129 @@
[gd_scene load_steps=4 format=3 uid="uid://cu2vkr5h5b8hb"]
[ext_resource type="Script" path="res://level/ui/scripts/character_sheet.gd" id="1_script"]
[ext_resource type="Theme" uid="uid://dvsh7tuhulnfm" path="res://level/ui/theme/wow_style.tres" id="2_theme"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_book"]
bg_color = Color(0.12, 0.1, 0.08, 0.95)
border_width_left = 4
border_width_top = 4
border_width_right = 4
border_width_bottom = 4
border_color = Color(0.6, 0.5, 0.2, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[node name="CharacterSheet" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("2_theme")
script = ExtResource("1_script")
[node name="DarkBackground" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.7)
[node name="Panel" type="PanelContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -450.0
offset_top = -300.0
offset_right = 450.0
offset_bottom = 300.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_book")
[node name="MarginContainer" type="MarginContainer" parent="Panel"]
layout_mode = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20
[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="TitleLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 0.8, 0, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 3
theme_override_font_sizes/font_size = 28
text = "CHARACTER SHEET"
horizontal_alignment = 1
[node name="HSeparator" type="HSeparator" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 5
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 20
[node name="LeftPage" type="PanelContainer" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ScrollContainer" type="ScrollContainer" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer/LeftPage"]
layout_mode = 2
horizontal_scroll_mode = 0
[node name="StatsContainer" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer/LeftPage/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 2
[node name="Divider" type="VSeparator" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="RightPage" type="PanelContainer" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ScrollContainer" type="ScrollContainer" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer/RightPage"]
layout_mode = 2
horizontal_scroll_mode = 0
[node name="ContentContainer" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer/RightPage/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 10
[node name="WeaponsContainer" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer/RightPage/ScrollContainer/ContentContainer"]
layout_mode = 2
theme_override_constants/separation = 2
[node name="Spacer" type="Control" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer/RightPage/ScrollContainer/ContentContainer"]
custom_minimum_size = Vector2(0, 15)
layout_mode = 2
[node name="AbilitiesContainer" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer/RightPage/ScrollContainer/ContentContainer"]
layout_mode = 2
theme_override_constants/separation = 2
[node name="CloseHint" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 1
theme_override_font_sizes/font_size = 14
text = "Press TAB to close"
horizontal_alignment = 2

@ -0,0 +1,69 @@
[gd_scene load_steps=3 format=3 uid="uid://bjn6yfwqgp2n7"]
[ext_resource type="Script" path="res://level/ui/scripts/resource_bars.gd" id="1_script"]
[ext_resource type="Theme" uid="uid://dvsh7tuhulnfm" path="res://level/ui/theme/wow_style.tres" id="2_theme"]
[node name="ResourceBars" type="Control"]
layout_mode = 3
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -250.0
offset_top = -140.0
offset_right = 250.0
offset_bottom = -110.0
grow_horizontal = 2
grow_vertical = 0
theme = ExtResource("2_theme")
script = ExtResource("1_script")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 4
[node name="HealthBar" type="Control" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="Background" type="ColorRect" parent="VBoxContainer/HealthBar"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.2, 0.2, 0.2, 0.8)
[node name="Fill" type="ColorRect" parent="VBoxContainer/HealthBar/Background"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 2.0
offset_top = 2.0
offset_right = -2.0
offset_bottom = -2.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0.8, 0, 1)
[node name="HealthText" type="Label" parent="VBoxContainer/HealthBar"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 2
theme_override_font_sizes/font_size = 18
text = "100 / 100 HP"
horizontal_alignment = 1
vertical_alignment = 1

@ -0,0 +1,52 @@
[gd_scene load_steps=3 format=3 uid="uid://d2s8m3fy7ktjv"]
[ext_resource type="Theme" uid="uid://dvsh7tuhulnfm" path="res://level/ui/theme/wow_style.tres" id="1_theme"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hint"]
bg_color = Color(0.15, 0.15, 0.15, 0.85)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.6, 0.5, 0.2, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[node name="TabHint" type="PanelContainer"]
offset_left = 20.0
offset_top = 150.0
offset_right = 180.0
offset_bottom = 220.0
theme = ExtResource("1_theme")
theme_override_styles/panel = SubResource("StyleBoxFlat_hint")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 5
[node name="KeyLabel" type="Label" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 0.8, 0, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 2
theme_override_font_sizes/font_size = 24
text = "TAB"
horizontal_alignment = 1
[node name="DescLabel" type="Label" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 1
theme_override_font_sizes/font_size = 14
text = "Character
Sheet"
horizontal_alignment = 1

@ -0,0 +1,93 @@
[gd_scene load_steps=4 format=3 uid="uid://b7u8yhry4b74a"]
[ext_resource type="Script" path="res://level/ui/scripts/unit_frame.gd" id="1_script"]
[ext_resource type="Theme" uid="uid://dvsh7tuhulnfm" path="res://level/ui/theme/wow_style.tres" id="2_theme"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_frame"]
bg_color = Color(0.1, 0.1, 0.1, 0.85)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.6, 0.5, 0.2, 1)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[node name="UnitFrame" type="PanelContainer"]
offset_left = 20.0
offset_top = 20.0
offset_right = 270.0
offset_bottom = 120.0
theme = ExtResource("2_theme")
theme_override_styles/panel = SubResource("StyleBoxFlat_frame")
script = ExtResource("1_script")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 6
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="LevelLabel" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 2
theme_override_font_sizes/font_size = 20
text = "Lv 1"
[node name="NameLabel" type="Label" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 2
theme_override_font_sizes/font_size = 22
text = "Player Name"
[node name="HealthBarContainer" type="Control" parent="MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 30)
layout_mode = 2
[node name="Background" type="ColorRect" parent="MarginContainer/VBoxContainer/HealthBarContainer"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.2, 0.2, 0.2, 0.9)
[node name="Fill" type="ColorRect" parent="MarginContainer/VBoxContainer/HealthBarContainer/Background"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 2.0
offset_top = 2.0
offset_right = -2.0
offset_bottom = -2.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0.8, 0, 1)
[node name="HealthText" type="Label" parent="MarginContainer/VBoxContainer/HealthBarContainer"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 2
theme_override_font_sizes/font_size = 16
text = "100 / 100"
horizontal_alignment = 1
vertical_alignment = 1

@ -0,0 +1,139 @@
extends PanelContainer
class_name AbilityButton
## Individual ability/action button for the action bar
## Shows icon, keybind, cooldown overlay, and tooltip
signal pressed()
signal hovered()
# References
@onready var icon: TextureRect = $MarginContainer/VBoxContainer/Icon
@onready var keybind_label: Label = $MarginContainer/VBoxContainer/KeybindLabel
@onready var cooldown_overlay: ColorRect = $MarginContainer/VBoxContainer/Icon/CooldownOverlay
@onready var cooldown_label: Label = $MarginContainer/VBoxContainer/Icon/CooldownLabel
# Data
var ability_icon: Texture2D = null
var keybind_text: String = ""
var ability_name: String = ""
var ability_description: String = ""
# Cooldown tracking
var is_on_cooldown: bool = false
var cooldown_remaining: float = 0.0
var cooldown_total: float = 0.0
# Colors
const COOLDOWN_COLOR = Color(0.0, 0.0, 0.0, 0.7) # Dark overlay during cooldown
const EMPTY_SLOT_COLOR = Color(0.3, 0.3, 0.3, 0.5) # Gray for empty slots
func _ready():
# Setup default appearance
cooldown_overlay.visible = false
cooldown_label.visible = false
# Connect mouse events for tooltip
mouse_entered.connect(_on_mouse_entered)
mouse_exited.connect(_on_mouse_exited)
_update_appearance()
func _process(delta):
if is_on_cooldown:
cooldown_remaining -= delta
if cooldown_remaining <= 0:
cooldown_remaining = 0
is_on_cooldown = false
_update_cooldown_display()
else:
_update_cooldown_display()
## Set the ability data
func set_ability(ability_texture: Texture2D, keybind: String, name: String = "", description: String = ""):
ability_icon = ability_texture
keybind_text = keybind
ability_name = name
ability_description = description
_update_appearance()
## Clear the ability (make slot empty)
func clear_ability():
ability_icon = null
ability_name = ""
ability_description = ""
_update_appearance()
## Set only the keybind (for empty slots)
func set_keybind(keybind: String):
keybind_text = keybind
_update_appearance()
## Start a cooldown animation
func start_cooldown(remaining: float, total: float = -1):
# If total is provided, use it; otherwise use remaining as both
if total > 0:
cooldown_total = total
else:
# Only set cooldown_total if we're starting a new cooldown
if not is_on_cooldown:
cooldown_total = remaining
is_on_cooldown = true
cooldown_remaining = remaining
_update_cooldown_display()
## Update visual appearance based on current state
func _update_appearance():
if not is_node_ready():
return
# Update icon
if ability_icon:
icon.texture = ability_icon
icon.modulate = Color.WHITE
else:
icon.texture = null
icon.modulate = EMPTY_SLOT_COLOR
# Update keybind label
if keybind_label:
keybind_label.text = keybind_text
## Update cooldown overlay and label
func _update_cooldown_display():
if not is_node_ready():
return
if is_on_cooldown:
cooldown_overlay.visible = true
cooldown_overlay.color = COOLDOWN_COLOR
# Show timer for cooldowns > 1 second
if cooldown_total > 1.0:
cooldown_label.visible = true
cooldown_label.text = str(ceil(cooldown_remaining))
else:
cooldown_label.visible = false
# Animate overlay height based on remaining cooldown
var percent_remaining = cooldown_remaining / cooldown_total
cooldown_overlay.size.y = icon.size.y * percent_remaining
else:
cooldown_overlay.visible = false
cooldown_label.visible = false
## Mouse hover handlers for tooltip
func _on_mouse_entered():
hovered.emit()
# Tooltip will be implemented later
func _on_mouse_exited():
# Hide tooltip
pass
## Handle button press
func _gui_input(event):
if event is InputEventMouseButton:
if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
if not is_on_cooldown and ability_icon != null:
pressed.emit()

@ -0,0 +1,157 @@
extends Control
class_name ActionBar
## Action bar displaying 12 ability slots at the bottom of the screen
## Pre-populated with Jump, Dash, Attack, Block + 8 empty slots for expansion
# Ability button references (12 slots)
var ability_buttons: Array[AbilityButton] = []
# Player reference
var player: Character = null
# Slot assignments (which slot holds which ability)
const SLOT_ATTACK = 0 # Left Mouse Button
const SLOT_BLOCK = 1 # Right Mouse Button
const SLOT_DASH = 2 # F key
const SLOT_JUMP = 3 # Space
func _ready():
# Ability buttons are children of the HBoxContainer
var container = $HBoxContainer
if container:
for child in container.get_children():
if child is AbilityButton:
ability_buttons.append(child)
print("[ActionBar] Found ", ability_buttons.size(), " ability button slots")
# Initialize button appearances
_initialize_slots()
## Set the player reference and connect signals
func set_player(p: Character):
if player:
_disconnect_player_signals()
player = p
if player:
_connect_player_signals()
_update_all_abilities()
## Initialize all slots with their keybinds
func _initialize_slots():
if ability_buttons.size() < 12:
push_warning("[ActionBar] Expected 12 slots, found ", ability_buttons.size())
return
# Set keybinds for all slots
var keybinds = ["LMB", "RMB", "F", "Space", "5", "6", "7", "8", "9", "0", "-", "="]
for i in range(min(12, ability_buttons.size())):
ability_buttons[i].set_keybind(keybinds[i])
print("[ActionBar] Initialized all ability slots")
## Update all ability icons and data
func _update_all_abilities():
if not player:
return
# Clear all slots first
for button in ability_buttons:
button.clear_ability()
# Re-set keybinds
_initialize_slots()
# SLOT 0: Attack (Main Hand or Unarmed)
if player.equipped_weapon:
var weapon_data = player.equipped_weapon.weapon_data
if weapon_data and weapon_data.icon:
ability_buttons[SLOT_ATTACK].set_ability(
weapon_data.icon,
"LMB",
weapon_data.weapon_name,
"Attack with " + weapon_data.weapon_name
)
else:
# Unarmed attack - use placeholder or no icon
ability_buttons[SLOT_ATTACK].set_ability(
null,
"LMB",
"Unarmed Attack",
"Punch enemies"
)
# SLOT 1: Block (if shield equipped)
if player.equipped_offhand:
var offhand_data = player.equipped_offhand.weapon_data
if offhand_data and offhand_data.can_block and offhand_data.icon:
ability_buttons[SLOT_BLOCK].set_ability(
offhand_data.icon,
"RMB",
"Block",
"Block with " + offhand_data.weapon_name + " (" + str(int(offhand_data.block_reduction * 100)) + "% reduction)"
)
# If main hand can block
elif player.equipped_weapon:
var weapon_data = player.equipped_weapon.weapon_data
if weapon_data and weapon_data.can_block and weapon_data.icon:
ability_buttons[SLOT_BLOCK].set_ability(
weapon_data.icon,
"RMB",
"Block",
"Block with " + weapon_data.weapon_name + " (" + str(int(weapon_data.block_reduction * 100)) + "% reduction)"
)
# SLOT 2: Dash
ability_buttons[SLOT_DASH].set_ability(
null, # TODO: Add dash icon
"F",
"Dash",
"Dash in movement direction\nCooldown: 4s"
)
# SLOT 3: Jump
ability_buttons[SLOT_JUMP].set_ability(
null, # TODO: Add jump icon
"Space",
"Jump",
"Jump into the air"
)
# Slots 4-11 remain empty for future abilities
print("[ActionBar] Updated all ability displays")
## Connect to player signals
func _connect_player_signals():
if not player:
return
# We'll add custom signals to player.gd for weapon changes and cooldowns
# For now, just print
print("[ActionBar] Connected to player signals")
## Disconnect from player signals
func _disconnect_player_signals():
if not player:
return
print("[ActionBar] Disconnected from player signals")
## Update dash cooldown
func update_dash_cooldown(remaining: float, total: float):
if ability_buttons.size() > SLOT_DASH:
if remaining > 0:
ability_buttons[SLOT_DASH].start_cooldown(remaining, total)
## Update attack cooldown
func update_attack_cooldown(remaining: float, total: float):
if ability_buttons.size() > SLOT_ATTACK:
if remaining > 0:
ability_buttons[SLOT_ATTACK].start_cooldown(remaining, total)
## Called when weapon is equipped/unequipped
func on_weapon_changed():
_update_all_abilities()

@ -0,0 +1,220 @@
extends Control
class_name CharacterSheet
## Character sheet / spellbook that displays player stats, weapons, and abilities
## Opens with Tab key
# References
@onready var stats_container: VBoxContainer = $Panel/MarginContainer/VBoxContainer/HBoxContainer/LeftPage/ScrollContainer/StatsContainer
@onready var weapons_container: VBoxContainer = $Panel/MarginContainer/VBoxContainer/HBoxContainer/RightPage/ScrollContainer/ContentContainer/WeaponsContainer
@onready var abilities_container: VBoxContainer = $Panel/MarginContainer/VBoxContainer/HBoxContainer/RightPage/ScrollContainer/ContentContainer/AbilitiesContainer
# Player reference
var player: Character = null
# Visibility
var is_visible: bool = false
func _ready():
# Start hidden
hide()
is_visible = false
func _input(event):
# Toggle on Tab press
if event.is_action_pressed("toggle_character_sheet"):
toggle_sheet()
get_viewport().set_input_as_handled()
## Set the player reference and update display
func set_player(p: Character):
player = p
if player:
refresh_all()
## Toggle character sheet visibility
func toggle_sheet():
is_visible = !is_visible
if is_visible:
show()
refresh_all()
# Release mouse when opening sheet
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
hide()
# Recapture mouse when closing sheet
if player and player.is_multiplayer_authority():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
## Refresh all data in the sheet
func refresh_all():
if not player:
return
_refresh_stats()
_refresh_weapons()
_refresh_abilities()
## Refresh player stats
func _refresh_stats():
if not stats_container:
return
# Clear existing stats
for child in stats_container.get_children():
child.queue_free()
# Add title
var title = Label.new()
title.text = "CHARACTER STATS"
title.add_theme_font_size_override("font_size", 24)
title.add_theme_color_override("font_color", Color(1.0, 0.8, 0.0))
stats_container.add_child(title)
# Spacer
var spacer1 = Control.new()
spacer1.custom_minimum_size = Vector2(0, 10)
stats_container.add_child(spacer1)
# Get player name
var player_id = player.name.to_int()
var player_name = "Player"
if Network.players.has(player_id):
player_name = Network.players[player_id]["nick"]
_add_stat_label("Name: " + player_name)
_add_stat_label("Level: 1") # Hardcoded for now
_add_stat_label("")
# Health stats
_add_stat_label("=== HEALTH ===", Color(0.8, 0.8, 0.8))
_add_stat_label("Current HP: " + str(int(player.current_health)))
_add_stat_label("Max HP: " + str(int(player.max_health)))
_add_stat_label("Health: " + str(int(player.get_health_percent() * 100)) + "%")
_add_stat_label("")
# Movement stats
_add_stat_label("=== MOVEMENT ===", Color(0.8, 0.8, 0.8))
_add_stat_label("Walk Speed: " + str(player.NORMAL_SPEED))
_add_stat_label("Sprint Speed: " + str(player.SPRINT_SPEED))
_add_stat_label("Jump Power: " + str(player.JUMP_VELOCITY))
_add_stat_label("")
# Combat stats
_add_stat_label("=== COMBAT ===", Color(0.8, 0.8, 0.8))
_add_stat_label("Base Damage: " + str(player.attack_damage))
_add_stat_label("Attack Range: " + str(player.attack_range))
_add_stat_label("Attack Cooldown: " + str(player.attack_cooldown) + "s")
## Refresh equipped weapons
func _refresh_weapons():
if not weapons_container:
return
# Clear existing
for child in weapons_container.get_children():
child.queue_free()
# Add title
var title = Label.new()
title.text = "EQUIPPED WEAPONS"
title.add_theme_font_size_override("font_size", 20)
title.add_theme_color_override("font_color", Color(1.0, 0.8, 0.0))
weapons_container.add_child(title)
# Spacer
var spacer = Control.new()
spacer.custom_minimum_size = Vector2(0, 10)
weapons_container.add_child(spacer)
# Main hand weapon
_add_stat_label("--- MAIN HAND ---", Color(0.8, 0.8, 0.8))
if player.equipped_weapon and player.equipped_weapon.weapon_data:
var wd = player.equipped_weapon.weapon_data
_add_stat_label("Name: " + wd.weapon_name, Color(0.0, 1.0, 0.5))
_add_stat_label("Damage: " + str(wd.damage))
_add_stat_label("Range: " + str(wd.attack_range))
_add_stat_label("Cooldown: " + str(wd.attack_cooldown) + "s")
_add_stat_label("Knockback: " + str(wd.knockback_force))
if wd.can_block:
_add_stat_label("Block: " + str(int(wd.block_reduction * 100)) + "%")
else:
_add_stat_label("No weapon equipped", Color(0.7, 0.7, 0.7))
# Spacer
var spacer2 = Control.new()
spacer2.custom_minimum_size = Vector2(0, 10)
weapons_container.add_child(spacer2)
# Off-hand weapon
_add_stat_label("--- OFF-HAND ---", Color(0.8, 0.8, 0.8))
if player.equipped_offhand and player.equipped_offhand.weapon_data:
var wd = player.equipped_offhand.weapon_data
_add_stat_label("Name: " + wd.weapon_name, Color(0.0, 1.0, 0.5))
_add_stat_label("Damage: " + str(wd.damage))
_add_stat_label("Range: " + str(wd.attack_range))
_add_stat_label("Cooldown: " + str(wd.attack_cooldown) + "s")
_add_stat_label("Knockback: " + str(wd.knockback_force))
if wd.can_block:
_add_stat_label("Block: " + str(int(wd.block_reduction * 100)) + "%")
else:
_add_stat_label("No weapon equipped", Color(0.7, 0.7, 0.7))
## Refresh abilities list
func _refresh_abilities():
if not abilities_container:
return
# Clear existing
for child in abilities_container.get_children():
child.queue_free()
# Add title
var title = Label.new()
title.text = "ABILITIES"
title.add_theme_font_size_override("font_size", 20)
title.add_theme_color_override("font_color", Color(1.0, 0.8, 0.0))
abilities_container.add_child(title)
# Spacer
var spacer = Control.new()
spacer.custom_minimum_size = Vector2(0, 10)
abilities_container.add_child(spacer)
# Dash ability
_add_stat_label("--- DASH (F) ---", Color(0.8, 0.8, 0.8))
_add_stat_label("Cooldown: " + str(player.dash_cooldown) + "s")
_add_stat_label("Duration: " + str(player.dash_duration) + "s")
_add_stat_label("Speed: " + str(player.dash_speed_multiplier) + "x")
_add_stat_label("Description: Dash in movement direction")
var dash_remaining = player._dash_cooldown_timer
if dash_remaining > 0:
_add_stat_label("Ready in: " + str(ceil(dash_remaining)) + "s", Color(1.0, 0.5, 0.5))
else:
_add_stat_label("Status: READY", Color(0.0, 1.0, 0.0))
# Spacer
var spacer2 = Control.new()
spacer2.custom_minimum_size = Vector2(0, 10)
abilities_container.add_child(spacer2)
# Jump ability
_add_stat_label("--- JUMP (Space) ---", Color(0.8, 0.8, 0.8))
_add_stat_label("Power: " + str(player.JUMP_VELOCITY))
_add_stat_label("Description: Jump into the air")
_add_stat_label("Status: ALWAYS READY", Color(0.0, 1.0, 0.0))
## Helper to add a stat label
func _add_stat_label(text: String, color: Color = Color.WHITE):
var label = Label.new()
label.text = text
label.add_theme_color_override("font_color", color)
label.add_theme_color_override("font_outline_color", Color.BLACK)
label.add_theme_constant_override("outline_size", 1)
label.add_theme_font_size_override("font_size", 16)
if stats_container:
stats_container.add_child(label)
elif weapons_container:
weapons_container.add_child(label)
elif abilities_container:
abilities_container.add_child(label)

@ -0,0 +1,161 @@
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
# 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 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")
## Show all UI components
func show_hud():
show()
## Hide all UI components
func hide_hud():
hide()
## 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")
# Could show death screen or respawn timer here
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()

@ -0,0 +1,73 @@
extends Control
class_name ResourceBars
## Resource bars (Health, Stamina, etc.) displayed above the action bar
# Health bar references
@onready var health_bg: ColorRect = $VBoxContainer/HealthBar/Background
@onready var health_fill: ColorRect = $VBoxContainer/HealthBar/Background/Fill
@onready var health_text: Label = $VBoxContainer/HealthBar/HealthText
# Future: Stamina bar references
# @onready var stamina_bg: ColorRect = $VBoxContainer/StaminaBar/Background
# @onready var stamina_fill: ColorRect = $VBoxContainer/StaminaBar/Background/Fill
# Player reference
var player: Character = null
# Smoothing
var target_health_width: float = 0.0
var current_health_width: float = 0.0
const HEALTH_LERP_SPEED: float = 10.0
# Colors
const COLOR_HEALTH_HIGH = Color(0.0, 0.8, 0.0, 1.0) # Green (>60%)
const COLOR_HEALTH_MID = Color(1.0, 0.8, 0.0, 1.0) # Yellow (30-60%)
const COLOR_HEALTH_LOW = Color(0.8, 0.0, 0.0, 1.0) # Red (<30%)
func _ready():
# Initialize bars
if health_fill:
current_health_width = health_fill.size.x
target_health_width = health_fill.size.x
print("[ResourceBars] Resource bars initialized")
func _process(delta):
# Smooth health bar animation
if health_fill and abs(current_health_width - target_health_width) > 0.1:
current_health_width = lerp(current_health_width, target_health_width, HEALTH_LERP_SPEED * delta)
health_fill.size.x = current_health_width
## Set player reference
func set_player(p: Character):
player = p
if player:
update_health(player.current_health, player.max_health)
## Update health bar display
func update_health(current: float, maximum: float):
if not health_fill or not health_bg or not health_text:
return
var health_percent = current / maximum if maximum > 0 else 0.0
health_percent = clamp(health_percent, 0.0, 1.0)
# Update fill width
var max_width = health_bg.size.x
target_health_width = max_width * health_percent
# Update color based on health percentage
if health_percent > 0.6:
health_fill.color = COLOR_HEALTH_HIGH
elif health_percent > 0.3:
health_fill.color = COLOR_HEALTH_MID
else:
health_fill.color = COLOR_HEALTH_LOW
# Update text
health_text.text = "%d / %d HP" % [int(current), int(maximum)]
## Update stamina bar (for future use)
func update_stamina(current: float, maximum: float):
# TODO: Implement when stamina system is added
pass

@ -0,0 +1,83 @@
extends PanelContainer
class_name UnitFrame
## Character portrait/unit frame displayed in top-left corner
# References
@onready var character_name: Label = $MarginContainer/VBoxContainer/NameLabel
@onready var health_bar_bg: ColorRect = $MarginContainer/VBoxContainer/HealthBarContainer/Background
@onready var health_bar_fill: ColorRect = $MarginContainer/VBoxContainer/HealthBarContainer/Background/Fill
@onready var health_text: Label = $MarginContainer/VBoxContainer/HealthBarContainer/HealthText
@onready var level_label: Label = $MarginContainer/VBoxContainer/HBoxContainer/LevelLabel
# Player reference
var player: Character = null
# Smoothing
var target_health_width: float = 0.0
var current_health_width: float = 0.0
const HEALTH_LERP_SPEED: float = 10.0
# Colors
const COLOR_HEALTH_HIGH = Color(0.0, 0.8, 0.0, 1.0) # Green (>60%)
const COLOR_HEALTH_MID = Color(1.0, 0.8, 0.0, 1.0) # Yellow (30-60%)
const COLOR_HEALTH_LOW = Color(0.8, 0.0, 0.0, 1.0) # Red (<30%)
func _ready():
# Initialize
if health_bar_fill:
current_health_width = health_bar_fill.size.x
target_health_width = health_bar_fill.size.x
print("[UnitFrame] Unit frame initialized")
func _process(delta):
# Smooth health bar animation
if health_bar_fill and abs(current_health_width - target_health_width) > 0.1:
current_health_width = lerp(current_health_width, target_health_width, HEALTH_LERP_SPEED * delta)
health_bar_fill.size.x = current_health_width
## Set player reference and display info
func set_player(p: Character):
player = p
if not player:
return
# Get player name from Network
var player_id = player.name.to_int() # Player nodes are named with their peer_id
if Network.players.has(player_id):
var player_data = Network.players[player_id]
if character_name:
character_name.text = player_data["nick"]
else:
if character_name:
character_name.text = "Player"
# Set level (hardcoded for now)
if level_label:
level_label.text = "Lv 1"
# Update health
update_health(player.current_health, player.max_health)
## Update health bar
func update_health(current: float, maximum: float):
if not health_bar_fill or not health_bar_bg or not health_text:
return
var health_percent = current / maximum if maximum > 0 else 0.0
health_percent = clamp(health_percent, 0.0, 1.0)
# Update fill width
var max_width = health_bar_bg.size.x
target_health_width = max_width * health_percent
# Update color based on health percentage
if health_percent > 0.6:
health_bar_fill.color = COLOR_HEALTH_HIGH
elif health_percent > 0.3:
health_bar_fill.color = COLOR_HEALTH_MID
else:
health_bar_fill.color = COLOR_HEALTH_LOW
# Update text
health_text.text = "%d / %d" % [int(current), int(maximum)]

@ -0,0 +1 @@
uid://dswkn21tmoaev

@ -0,0 +1,12 @@
[gd_resource type="Theme" load_steps=2 format=3]
[ext_resource type="FontFile" uid="uid://dh88edx2pf6ax" path="res://assets/fonts/Kurland.ttf" id="1_font"]
[resource]
default_font = ExtResource("1_font")
default_font_size = 16
; WoW-style color palette
; Gold accent color for highlights and borders
; Dark backgrounds with semi-transparency
; Health colors: green -> yellow -> red gradient

@ -20,6 +20,7 @@ config/icon="res://icon.png"
[autoload] [autoload]
Network="*res://level/scripts/network.gd" Network="*res://level/scripts/network.gd"
HUD="*res://level/ui/scripts/hud_manager.gd"
[display] [display]
@ -88,6 +89,11 @@ block={
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null) "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null)
] ]
} }
toggle_character_sheet={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
[layer_names] [layer_names]

Loading…
Cancel
Save