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
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user