From 15865dd15f8d83c4daa797a9bc9f2351e1042f47 Mon Sep 17 00:00:00 2001 From: Scott Date: Sun, 16 Nov 2025 22:48:50 +0000 Subject: [PATCH] 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 --- CLAUDE.md | 3 + level/scripts/base_weapon.gd | 4 + level/scripts/level.gd | 10 ++ level/scripts/player.gd | 88 ++++------ level/ui/scenes/ability_button.tscn | 75 ++++++++ level/ui/scenes/action_bar.tscn | 64 +++++++ level/ui/scenes/character_sheet.tscn | 129 ++++++++++++++ level/ui/scenes/resource_bars.tscn | 69 ++++++++ level/ui/scenes/tab_hint.tscn | 52 ++++++ level/ui/scenes/unit_frame.tscn | 93 ++++++++++ level/ui/scripts/ability_button.gd | 139 +++++++++++++++ level/ui/scripts/ability_button.gd.uid | 1 + level/ui/scripts/action_bar.gd | 157 +++++++++++++++++ level/ui/scripts/action_bar.gd.uid | 1 + level/ui/scripts/character_sheet.gd | 220 ++++++++++++++++++++++++ level/ui/scripts/character_sheet.gd.uid | 1 + level/ui/scripts/hud_manager.gd | 161 +++++++++++++++++ level/ui/scripts/hud_manager.gd.uid | 1 + level/ui/scripts/resource_bars.gd | 73 ++++++++ level/ui/scripts/resource_bars.gd.uid | 1 + level/ui/scripts/unit_frame.gd | 83 +++++++++ level/ui/scripts/unit_frame.gd.uid | 1 + level/ui/theme/wow_style.tres | 12 ++ project.godot | 6 + 24 files changed, 1386 insertions(+), 58 deletions(-) create mode 100644 level/ui/scenes/ability_button.tscn create mode 100644 level/ui/scenes/action_bar.tscn create mode 100644 level/ui/scenes/character_sheet.tscn create mode 100644 level/ui/scenes/resource_bars.tscn create mode 100644 level/ui/scenes/tab_hint.tscn create mode 100644 level/ui/scenes/unit_frame.tscn create mode 100644 level/ui/scripts/ability_button.gd create mode 100644 level/ui/scripts/ability_button.gd.uid create mode 100644 level/ui/scripts/action_bar.gd create mode 100644 level/ui/scripts/action_bar.gd.uid create mode 100644 level/ui/scripts/character_sheet.gd create mode 100644 level/ui/scripts/character_sheet.gd.uid create mode 100644 level/ui/scripts/hud_manager.gd create mode 100644 level/ui/scripts/hud_manager.gd.uid create mode 100644 level/ui/scripts/resource_bars.gd create mode 100644 level/ui/scripts/resource_bars.gd.uid create mode 100644 level/ui/scripts/unit_frame.gd create mode 100644 level/ui/scripts/unit_frame.gd.uid create mode 100644 level/ui/theme/wow_style.tres diff --git a/CLAUDE.md b/CLAUDE.md index 94c732d..3123032 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,6 +62,9 @@ Defined in project.godot: ## 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 1. Create scripts in `level/scripts/` 2. For multiplayer-synchronized components: diff --git a/level/scripts/base_weapon.gd b/level/scripts/base_weapon.gd index a816f94..36d9dcc 100644 --- a/level/scripts/base_weapon.gd +++ b/level/scripts/base_weapon.gd @@ -44,6 +44,10 @@ func perform_attack() -> bool: _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 if owner_character._body: owner_character._body.play_attack() diff --git a/level/scripts/level.gd b/level/scripts/level.gd index 8cf90e4..361429b 100644 --- a/level/scripts/level.gd +++ b/level/scripts/level.gd @@ -182,6 +182,16 @@ func _add_player(id: int, player_info : Dictionary): player.nickname.text = 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"] player.set_player_skin(skin_enum) # rpc("sync_player_skin", id, skin_enum) diff --git a/level/scripts/player.gd b/level/scripts/player.gd index c8b5c33..72b7633 100644 --- a/level/scripts/player.gd +++ b/level/scripts/player.gd @@ -53,6 +53,11 @@ var _dash_cooldown_timer: float = 0.0 var _is_dashing: bool = false 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(): super._enter_tree() $SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority() @@ -61,6 +66,10 @@ func _ready(): super._ready() 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) if _body == null: for child in get_children(): @@ -132,10 +141,6 @@ func _ready(): # 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_area() @@ -208,9 +213,21 @@ func _process(delta): _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 if _attack_timer > 0: _attack_timer -= delta + attack_cooldown_updated.emit(_attack_timer, attack_cooldown) # Update dash timers if _dash_timer > 0: @@ -220,6 +237,7 @@ func _process(delta): if _dash_cooldown_timer > 0: _dash_cooldown_timer -= delta + dash_cooldown_updated.emit(_dash_cooldown_timer, dash_cooldown) # Handle dash input 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) ## Health display and callbacks -func _create_health_ui(): - # Create a 2D UI for the local player's health - 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) +# Old 2D health UI - now handled by HUD Manager +# Kept for reference, but no longer used func _update_health_display(): - # Update 3D label if it exists + # Update 3D label if it exists (for other players to see) if health_label: health_label.text = "HP: %d/%d" % [int(current_health), int(max_health)] - # Update 2D UI for local player - 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)] + # 2D UI is now handled by HUD Manager autoload func _on_health_changed(_old_health: float, _new_health: float): _update_health_display() @@ -688,6 +656,7 @@ func equip_weapon(data: WeaponData): if is_multiplayer_authority(): print("Equipped: ", data.weapon_name, " to ", attach_point.name) + weapon_equipped_changed.emit() ## Unequip current weapon (local only) func unequip_weapon(is_offhand: bool = false): @@ -700,6 +669,9 @@ func unequip_weapon(is_offhand: bool = false): equipped_weapon.queue_free() equipped_weapon = null + if is_multiplayer_authority(): + weapon_equipped_changed.emit() + ## Sync unequip across all clients @rpc("any_peer", "call_local", "reliable") func _unequip_weapon_sync(is_offhand: bool = false): diff --git a/level/ui/scenes/ability_button.tscn b/level/ui/scenes/ability_button.tscn new file mode 100644 index 0000000..c57d21c --- /dev/null +++ b/level/ui/scenes/ability_button.tscn @@ -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 diff --git a/level/ui/scenes/action_bar.tscn b/level/ui/scenes/action_bar.tscn new file mode 100644 index 0000000..26327ab --- /dev/null +++ b/level/ui/scenes/action_bar.tscn @@ -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 diff --git a/level/ui/scenes/character_sheet.tscn b/level/ui/scenes/character_sheet.tscn new file mode 100644 index 0000000..e8dd358 --- /dev/null +++ b/level/ui/scenes/character_sheet.tscn @@ -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 diff --git a/level/ui/scenes/resource_bars.tscn b/level/ui/scenes/resource_bars.tscn new file mode 100644 index 0000000..3b5f061 --- /dev/null +++ b/level/ui/scenes/resource_bars.tscn @@ -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 diff --git a/level/ui/scenes/tab_hint.tscn b/level/ui/scenes/tab_hint.tscn new file mode 100644 index 0000000..a6a2054 --- /dev/null +++ b/level/ui/scenes/tab_hint.tscn @@ -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 diff --git a/level/ui/scenes/unit_frame.tscn b/level/ui/scenes/unit_frame.tscn new file mode 100644 index 0000000..6d4ed75 --- /dev/null +++ b/level/ui/scenes/unit_frame.tscn @@ -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 diff --git a/level/ui/scripts/ability_button.gd b/level/ui/scripts/ability_button.gd new file mode 100644 index 0000000..a9987eb --- /dev/null +++ b/level/ui/scripts/ability_button.gd @@ -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() diff --git a/level/ui/scripts/ability_button.gd.uid b/level/ui/scripts/ability_button.gd.uid new file mode 100644 index 0000000..e8bf262 --- /dev/null +++ b/level/ui/scripts/ability_button.gd.uid @@ -0,0 +1 @@ +uid://duqq7n1lsn7md diff --git a/level/ui/scripts/action_bar.gd b/level/ui/scripts/action_bar.gd new file mode 100644 index 0000000..df784c4 --- /dev/null +++ b/level/ui/scripts/action_bar.gd @@ -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() diff --git a/level/ui/scripts/action_bar.gd.uid b/level/ui/scripts/action_bar.gd.uid new file mode 100644 index 0000000..58dc611 --- /dev/null +++ b/level/ui/scripts/action_bar.gd.uid @@ -0,0 +1 @@ +uid://7qld22ugcdsp diff --git a/level/ui/scripts/character_sheet.gd b/level/ui/scripts/character_sheet.gd new file mode 100644 index 0000000..a9b0fe6 --- /dev/null +++ b/level/ui/scripts/character_sheet.gd @@ -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) diff --git a/level/ui/scripts/character_sheet.gd.uid b/level/ui/scripts/character_sheet.gd.uid new file mode 100644 index 0000000..4870115 --- /dev/null +++ b/level/ui/scripts/character_sheet.gd.uid @@ -0,0 +1 @@ +uid://bneg4vj8klvgs diff --git a/level/ui/scripts/hud_manager.gd b/level/ui/scripts/hud_manager.gd new file mode 100644 index 0000000..84c7b20 --- /dev/null +++ b/level/ui/scripts/hud_manager.gd @@ -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() diff --git a/level/ui/scripts/hud_manager.gd.uid b/level/ui/scripts/hud_manager.gd.uid new file mode 100644 index 0000000..320e3dd --- /dev/null +++ b/level/ui/scripts/hud_manager.gd.uid @@ -0,0 +1 @@ +uid://ctm8decmcqnuj diff --git a/level/ui/scripts/resource_bars.gd b/level/ui/scripts/resource_bars.gd new file mode 100644 index 0000000..e37676b --- /dev/null +++ b/level/ui/scripts/resource_bars.gd @@ -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 diff --git a/level/ui/scripts/resource_bars.gd.uid b/level/ui/scripts/resource_bars.gd.uid new file mode 100644 index 0000000..e6f25bb --- /dev/null +++ b/level/ui/scripts/resource_bars.gd.uid @@ -0,0 +1 @@ +uid://qnymjhgipuke diff --git a/level/ui/scripts/unit_frame.gd b/level/ui/scripts/unit_frame.gd new file mode 100644 index 0000000..2c0cc3f --- /dev/null +++ b/level/ui/scripts/unit_frame.gd @@ -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)] diff --git a/level/ui/scripts/unit_frame.gd.uid b/level/ui/scripts/unit_frame.gd.uid new file mode 100644 index 0000000..bc55767 --- /dev/null +++ b/level/ui/scripts/unit_frame.gd.uid @@ -0,0 +1 @@ +uid://dswkn21tmoaev diff --git a/level/ui/theme/wow_style.tres b/level/ui/theme/wow_style.tres new file mode 100644 index 0000000..bfa9523 --- /dev/null +++ b/level/ui/theme/wow_style.tres @@ -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 diff --git a/project.godot b/project.godot index 2a68113..b462b41 100644 --- a/project.godot +++ b/project.godot @@ -20,6 +20,7 @@ config/icon="res://icon.png" [autoload] Network="*res://level/scripts/network.gd" +HUD="*res://level/ui/scripts/hud_manager.gd" [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) ] } +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]