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:
2025-11-16 22:48:50 +00:00
parent d475c0abf9
commit 15865dd15f
24 changed files with 1386 additions and 58 deletions
+30 -58
View File
@@ -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):