- 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 guidelinesDashfix
parent
d475c0abf9
commit
15865dd15f
24 changed files with 1386 additions and 58 deletions
@ -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 @@ |
||||
uid://duqq7n1lsn7md |
||||
@ -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 @@ |
||||
uid://7qld22ugcdsp |
||||
@ -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 @@ |
||||
uid://bneg4vj8klvgs |
||||
@ -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 @@ |
||||
uid://ctm8decmcqnuj |
||||
@ -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 @@ |
||||
uid://qnymjhgipuke |
||||
@ -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 |
||||
Loading…
Reference in new issue