extends BaseUnit
class_name Character
const NORMAL_SPEED = 6.0
const SPRINT_SPEED = 10.0
const JUMP_VELOCITY = 10
enum SkinColor { BLUE , YELLOW , GREEN , RED }
@ onready var nickname : Label3D = $ PlayerNick / Nickname
@ onready var health_label : Label3D = null # Optional 3D health label
@ export_category ( " Objects " )
@ export var _body : Node3D = null
@ export var _spring_arm_offset : Node3D = null
@ export var _weapon_attachment : BoneAttachment3D = null # WeaponPoint bone attachment (main hand)
@ export var _weapon_container : Node3D = null # Container node for main hand weapons
@ export var _offhand_attachment : BoneAttachment3D = null # OffHandPoint bone attachment
@ export var _offhand_container : Node3D = null # Container node for off-hand weapons
@ export_category ( " Skin Colors " )
@ export var blue_texture : CompressedTexture2D
@ export var yellow_texture : CompressedTexture2D
@ export var green_texture : CompressedTexture2D
@ export var red_texture : CompressedTexture2D
@ onready var _bottom_mesh : MeshInstance3D = get_node_or_null ( " 3DGodotRobot/RobotArmature/Skeleton3D/Bottom " )
@ onready var _chest_mesh : MeshInstance3D = get_node_or_null ( " 3DGodotRobot/RobotArmature/Skeleton3D/Chest " )
@ onready var _face_mesh : MeshInstance3D = get_node_or_null ( " 3DGodotRobot/RobotArmature/Skeleton3D/Face " )
@ onready var _limbs_head_mesh : MeshInstance3D = get_node_or_null ( " 3DGodotRobot/RobotArmature/Skeleton3D/Llimbs and head " )
var _current_speed : float
var gravity = ProjectSettings . get_setting ( " physics/3d/default_gravity " )
# Weapon system
var equipped_weapon : BaseWeapon = null # Main hand weapon
var equipped_offhand : BaseWeapon = null # Off-hand weapon
var _nearby_weapons : Array [ WorldWeapon ] = [ ]
var is_blocking : bool = false
# Attack system
@ export var attack_damage : float = 10.0
@ export var attack_range : float = 3.0
@ export var attack_cooldown : float = 0.5
var _attack_timer : float = 0.0
# Dash system
@ export var dash_speed_multiplier : float = 2.0
@ export var dash_duration : float = 0.25
@ export var dash_cooldown : float = 4.0
var _dash_timer : float = 0.0
var _dash_cooldown_timer : float = 0.0
var _is_dashing : bool = false
var _dash_direction : Vector3 = Vector3 . ZERO
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
3 weeks ago
# 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 ( )
func _ready ( ) :
super . _ready ( )
set_respawn_point ( Vector3 ( 0 , 5 , 0 ) )
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
3 weeks ago
# 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 ( ) :
if child . name == " Armature " or child . name == " 3DGodotRobot " :
_body = child
print ( " Auto-found _body: " , child . name )
break
if _body == null :
push_error ( " Could not find body node (Armature or 3DGodotRobot)! " )
# Auto-find spring arm offset
if _spring_arm_offset == null :
if has_node ( " SpringArmOffset " ) :
_spring_arm_offset = get_node ( " SpringArmOffset " )
print ( " Auto-found _spring_arm_offset " )
else :
push_error ( " Could not find SpringArmOffset! " )
# Auto-find weapon attachments if not set
if _weapon_attachment == null :
if has_node ( " Armature/Skeleton3D/WeaponPoint " ) :
_weapon_attachment = get_node ( " Armature/Skeleton3D/WeaponPoint " )
print ( " Auto-found _weapon_attachment " )
elif has_node ( " 3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D " ) :
_weapon_attachment = get_node ( " 3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D " )
print ( " Auto-found _weapon_attachment (robot) " )
else :
print ( " WARNING: WeaponPoint not found! Check if you ' ve added BoneAttachment3D nodes. " )
if has_node ( " Armature/Skeleton3D " ) :
print ( " Skeleton3D children: " )
var skeleton = get_node ( " Armature/Skeleton3D " )
for child in skeleton . get_children ( ) :
print ( " - " , child . name , " ( " , child . get_class ( ) , " ) " )
# Auto-find weapon container
if _weapon_container == null and _weapon_attachment :
var container = _weapon_attachment . get_node_or_null ( " WeaponContainer " )
if container :
_weapon_container = container
print ( " Auto-found _weapon_container " )
# Auto-find offhand attachment
if _offhand_attachment == null :
if has_node ( " Armature/Skeleton3D/OffhandPoint " ) :
_offhand_attachment = get_node ( " Armature/Skeleton3D/OffhandPoint " )
print ( " Auto-found _offhand_attachment " )
elif has_node ( " 3DGodotRobot/RobotArmature/Skeleton3D/OffHandPoint " ) :
_offhand_attachment = get_node ( " 3DGodotRobot/RobotArmature/Skeleton3D/OffHandPoint " )
print ( " Auto-found _offhand_attachment (robot) " )
else :
print ( " WARNING: OffhandPoint not found! " )
# Auto-find offhand container
if _offhand_container == null and _offhand_attachment :
var container = _offhand_attachment . get_node_or_null ( " OffhandContainer " )
if container :
_offhand_container = container
print ( " Auto-found _offhand_container " )
# Try to get optional health label
if has_node ( " PlayerNick/HealthLabel " ) :
health_label = get_node ( " PlayerNick/HealthLabel " )
# Connect health signals
health_changed . connect ( _on_health_changed )
died . connect ( _on_died )
respawned . connect ( _on_respawned )
# Update health display
_update_health_display ( )
# Setup weapon pickup detection area
_setup_weapon_pickup_area ( )
# Auto-find weapon attachment if not set
if _weapon_attachment == null :
var bone_attach = get_node_or_null ( " 3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D " )
if bone_attach :
_weapon_attachment = bone_attach
print ( " Auto-found weapon attachment point " )
else :
push_warning ( " Could not find BoneAttachment3D for weapons! " )
# Auto-find weapon container if not set
if _weapon_container == null and _weapon_attachment :
var container = _weapon_attachment . get_node_or_null ( " WeaponContainer " )
if container :
_weapon_container = container
print ( " Auto-found main hand weapon container " )
else :
push_warning ( " Could not find WeaponContainer! Weapons will attach directly to BoneAttachment3D. " )
# Auto-find off-hand attachment if not set
if _offhand_attachment == null :
var offhand_attach = get_node_or_null ( " 3DGodotRobot/RobotArmature/Skeleton3D/OffHandPoint " )
if offhand_attach :
_offhand_attachment = offhand_attach
print ( " Auto-found off-hand attachment point " )
# Auto-find off-hand container if not set
if _offhand_container == null and _offhand_attachment :
var container = _offhand_attachment . get_node_or_null ( " OffHandContainer " )
if container :
_offhand_container = container
print ( " Auto-found off-hand weapon container " )
func _physics_process ( delta ) :
# Check if multiplayer is ready
if multiplayer . multiplayer_peer == null :
return
if not is_multiplayer_authority ( ) :
return
var current_scene = get_tree ( ) . get_current_scene ( )
if current_scene and current_scene . has_method ( " is_chat_visible " ) and current_scene . is_chat_visible ( ) and is_on_floor ( ) :
freeze ( )
return
if not is_on_floor ( ) :
velocity . y -= gravity * delta
_body . animate ( velocity )
if is_on_floor ( ) :
if Input . is_action_just_pressed ( " jump " ) :
velocity . y = JUMP_VELOCITY
else :
velocity . y -= gravity * delta
_move ( )
move_and_slide ( )
_body . animate ( velocity )
func _process ( delta ) :
# Check if multiplayer is ready
if multiplayer . multiplayer_peer == null :
return
if not is_multiplayer_authority ( ) :
return
_check_fall_and_respawn ( )
# Update attack cooldown
if _attack_timer > 0 :
_attack_timer -= delta
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
3 weeks ago
attack_cooldown_updated . emit ( _attack_timer , attack_cooldown )
# Update dash timers
if _dash_timer > 0 :
_dash_timer -= delta
if _dash_timer < = 0 :
_is_dashing = false
if _dash_cooldown_timer > 0 :
_dash_cooldown_timer -= delta
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
3 weeks ago
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 ( ) :
_perform_dash ( )
# Handle block input
if Input . is_action_pressed ( " block " ) and not is_dead :
_try_block ( )
else :
is_blocking = false
# Handle attack input
if Input . is_action_just_pressed ( " attack " ) and not is_dead and not _is_dashing and not is_blocking :
_perform_attack ( )
# Handle weapon pickup/drop
if Input . is_action_just_pressed ( " pickup " ) and not is_dead :
print ( " Pickup pressed! Main: " , equipped_weapon != null , " OffHand: " , equipped_offhand != null , " Nearby: " , _nearby_weapons . size ( ) )
# Try to pickup nearby weapon
if _nearby_weapons . size ( ) > 0 :
_pickup_nearest_weapon ( )
# If no nearby weapons, drop what we're holding (main hand first, then off-hand)
elif equipped_weapon :
# Tell server to drop (server will handle spawning)
if multiplayer . is_server ( ) :
drop_weapon ( false ) # main hand
else :
rpc_id ( 1 , " drop_weapon " , false ) # Only send to server
elif equipped_offhand :
# Drop off-hand
if multiplayer . is_server ( ) :
drop_weapon ( true ) # off-hand
else :
rpc_id ( 1 , " drop_weapon " , true )
else :
print ( " No weapons nearby to pick up " )
func freeze ( ) :
velocity . x = 0
velocity . z = 0
_current_speed = 0
_body . animate ( Vector3 . ZERO )
func _move ( ) - > void :
# If dashing, use dash movement
if _is_dashing :
velocity . x = _dash_direction . x * _current_speed * dash_speed_multiplier
velocity . z = _dash_direction . z * _current_speed * dash_speed_multiplier
_body . apply_rotation ( velocity )
return
var _input_direction : Vector2 = Vector2 . ZERO
if is_multiplayer_authority ( ) :
_input_direction = Input . get_vector (
" move_left " , " move_right " ,
" move_forward " , " move_backward "
)
var _direction : Vector3 = transform . basis * Vector3 ( _input_direction . x , 0 , _input_direction . y ) . normalized ( )
is_running ( )
_direction = _direction . rotated ( Vector3 . UP , _spring_arm_offset . rotation . y )
if _direction :
velocity . x = _direction . x * _current_speed
velocity . z = _direction . z * _current_speed
_body . apply_rotation ( velocity )
return
velocity . x = move_toward ( velocity . x , 0 , _current_speed )
velocity . z = move_toward ( velocity . z , 0 , _current_speed )
func is_running ( ) - > bool :
if Input . is_action_pressed ( " shift " ) :
_current_speed = SPRINT_SPEED
return true
else :
_current_speed = NORMAL_SPEED
return false
func _check_fall_and_respawn ( ) :
if global_transform . origin . y < - 15.0 and multiplayer . is_server ( ) :
# Use BaseUnit's respawn system
_respawn ( )
@ rpc ( " any_peer " , " reliable " )
func change_nick ( new_nick : String ) :
if nickname :
nickname . text = new_nick
func get_texture_from_name ( skin_color : SkinColor ) - > CompressedTexture2D :
match skin_color :
SkinColor . BLUE : return blue_texture
SkinColor . GREEN : return green_texture
SkinColor . RED : return red_texture
SkinColor . YELLOW : return yellow_texture
_ : return blue_texture
@ rpc ( " any_peer " , " reliable " )
func set_player_skin ( skin_name : SkinColor ) - > void :
var texture = get_texture_from_name ( skin_name )
set_mesh_texture ( _bottom_mesh , texture )
set_mesh_texture ( _chest_mesh , texture )
set_mesh_texture ( _face_mesh , texture )
set_mesh_texture ( _limbs_head_mesh , texture )
func set_mesh_texture ( mesh_instance : MeshInstance3D , texture : CompressedTexture2D ) - > void :
if mesh_instance :
var material : = mesh_instance . get_surface_override_material ( 0 )
if material and material is StandardMaterial3D :
var new_material : = material
new_material . albedo_texture = texture
mesh_instance . set_surface_override_material ( 0 , new_material )
## Block system
func _try_block ( ) :
# Check if we have a weapon that can block
var can_block_weapon = false
if equipped_weapon and equipped_weapon . can_block ( ) :
can_block_weapon = true
elif equipped_offhand and equipped_offhand . can_block ( ) :
can_block_weapon = true
is_blocking = can_block_weapon
# Could add blocking animation here
# if is_blocking and _body:
# _body.play_block_animation()
## Get block reduction amount (0.0 to 1.0)
func get_block_reduction ( ) - > float :
if not is_blocking :
return 0.0
var reduction = 0.0
# Use the highest block reduction from equipped weapons
if equipped_weapon and equipped_weapon . can_block ( ) :
reduction = max ( reduction , equipped_weapon . get_block_reduction ( ) )
if equipped_offhand and equipped_offhand . can_block ( ) :
reduction = max ( reduction , equipped_offhand . get_block_reduction ( ) )
return reduction
## Attack system
func _perform_attack ( ) :
if not is_multiplayer_authority ( ) or is_dead :
return
# Use main hand weapon if available
if equipped_weapon and equipped_weapon . can_attack ( ) :
equipped_weapon . perform_attack ( )
return
# Or use off-hand weapon if available
if equipped_offhand and equipped_offhand . can_attack ( ) :
equipped_offhand . perform_attack ( )
return
# Fallback to default unarmed attack
# Don't attack if already attacking
if _body and _body . animation_player and _body . animation_player . current_animation == " Attack1 " :
return
if _attack_timer > 0 :
return
_attack_timer = attack_cooldown
# Play attack animation once
if _body :
_body . play_attack ( )
# Find nearest enemy in range
var space_state = get_world_3d ( ) . direct_space_state
var query = PhysicsShapeQueryParameters3D . new ( )
var sphere = SphereShape3D . new ( )
sphere . radius = attack_range
query . shape = sphere
query . transform = global_transform
query . collision_mask = 1 # Player layer
var results = space_state . intersect_shape ( query )
for result in results :
var hit_body = result [ " collider " ]
if hit_body != self and hit_body is BaseUnit :
var attacker_id = multiplayer . get_unique_id ( )
# If we're the server, apply damage directly (default unarmed knockback)
if multiplayer . is_server ( ) :
_server_apply_damage ( hit_body . name , attack_damage , attacker_id , 5.0 , global_position )
else :
# Otherwise, request server to apply damage
rpc_id ( 1 , " _server_apply_damage " , hit_body . name , attack_damage , attacker_id , 5.0 , global_position )
break # Only hit one target per attack
## Server-side damage application
@ rpc ( " any_peer " , " reliable " )
func _server_apply_damage ( target_name : String , damage : float , attacker_id : int , knockback : float = 0.0 , attacker_pos : Vector3 = Vector3 . ZERO ) :
if not multiplayer . is_server ( ) :
return
# Get the target from the players container
var level = get_tree ( ) . get_current_scene ( )
if not level or not level . has_node ( " PlayersContainer " ) :
return
var players_container = level . get_node ( " PlayersContainer " )
if not players_container . has_node ( target_name ) :
return
var target = players_container . get_node ( target_name )
if target and target is BaseUnit :
target . take_damage ( damage , attacker_id , knockback , attacker_pos )
## Health display and callbacks
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
3 weeks ago
# Old 2D health UI - now handled by HUD Manager
# Kept for reference, but no longer used
func _update_health_display ( ) :
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
3 weeks ago
# 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 ) ]
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
3 weeks ago
# 2D UI is now handled by HUD Manager autoload
func _on_health_changed ( _old_health : float , _new_health : float ) :
_update_health_display ( )
func _on_died ( killer_id : int ) :
# Disable player when dead
set_physics_process ( false )
set_process ( false )
# Visual feedback - could add death animation here
if _body :
_body . visible = false
# Print death message
var killer_name = " Unknown "
if Network . players . has ( killer_id ) :
killer_name = Network . players [ killer_id ] [ " nick " ]
if is_multiplayer_authority ( ) :
print ( " You were killed by " , killer_name )
# Show death message on UI
if has_node ( " HealthUI/HealthText " ) :
get_node ( " HealthUI/HealthText " ) . text = " DEAD - Respawning... "
func _on_respawned ( ) :
# Re-enable player
set_physics_process ( true )
set_process ( true )
if _body :
_body . visible = true
_update_health_display ( )
if is_multiplayer_authority ( ) :
print ( " You respawned! " )
## Dash system
func _perform_dash ( ) :
if not is_multiplayer_authority ( ) or is_dead or not is_on_floor ( ) :
return
# Get current movement direction or use forward if no input
var _input_direction : Vector2 = Input . get_vector (
" move_left " , " move_right " ,
" move_forward " , " move_backward "
)
var _direction : Vector3
if _input_direction . length ( ) > 0 :
# Dash in input direction
_direction = transform . basis * Vector3 ( _input_direction . x , 0 , _input_direction . y ) . normalized ( )
_direction = _direction . rotated ( Vector3 . UP , _spring_arm_offset . rotation . y )
else :
# Dash forward if no input
_direction = - transform . basis . z
_direction = _direction . rotated ( Vector3 . UP , _spring_arm_offset . rotation . y )
# Set dash parameters
_dash_direction = _direction
_is_dashing = true
_dash_timer = dash_duration
_dash_cooldown_timer = dash_cooldown
# Animation is handled by the Body's animate function (Jump animation plays during dash)
## Override hurt animation from BaseUnit
func _play_hurt_animation ( ) :
if _body and _body . animation_player :
# Try to play a hurt/hit animation if it exists
if _body . animation_player . has_animation ( " Hurt " ) :
_body . animation_player . play ( " Hurt " )
elif _body . animation_player . has_animation ( " Hit " ) :
_body . animation_player . play ( " Hit " )
elif _body . animation_player . has_animation ( " TakeDamage " ) :
_body . animation_player . play ( " TakeDamage " )
else :
# Fallback: briefly flash the character red
_flash_hurt ( )
## Flash effect when no hurt animation exists
func _flash_hurt ( ) :
if not _body :
return
# Only works if _body has a modulate property (CanvasItem or some Node3D with visual children)
if " modulate " in _body :
# Store original modulate
var original_modulate = _body . modulate
# Flash red
_body . modulate = Color ( 1.5 , 0.5 , 0.5 , 1.0 )
# Return to normal after a brief moment
await get_tree ( ) . create_timer ( 0.15 ) . timeout
if _body :
_body . modulate = original_modulate
# If no modulate, just skip the visual effect
## Weapon System
func _setup_weapon_pickup_area ( ) :
# Create an Area3D to detect nearby weapons
var pickup_area = Area3D . new ( )
pickup_area . name = " WeaponPickupArea "
pickup_area . collision_layer = 0
pickup_area . collision_mask = 4 # Will detect WorldWeapons (we'll use layer 3)
add_child ( pickup_area )
# Create collision shape for pickup range
var pickup_collision = CollisionShape3D . new ( )
var sphere = SphereShape3D . new ( )
sphere . radius = 2.0 # Pickup range
pickup_collision . shape = sphere
pickup_area . add_child ( pickup_collision )
# Connect signals to track nearby weapons
pickup_area . area_entered . connect ( _on_weapon_area_entered )
pickup_area . area_exited . connect ( _on_weapon_area_exited )
func _on_weapon_area_entered ( area : Area3D ) :
# Check if the area belongs to a WorldWeapon
var weapon = area . get_parent ( )
if weapon is WorldWeapon and weapon not in _nearby_weapons :
_nearby_weapons . append ( weapon )
if is_multiplayer_authority ( ) :
print ( " Weapon nearby: " , weapon . weapon_data . weapon_name if weapon . weapon_data else " Unknown " )
func _on_weapon_area_exited ( area : Area3D ) :
# Remove weapon from nearby list
var weapon = area . get_parent ( )
if weapon is WorldWeapon and weapon in _nearby_weapons :
_nearby_weapons . erase ( weapon )
if is_multiplayer_authority ( ) :
print ( " Weapon left range: " , weapon . weapon_data . weapon_name if weapon . weapon_data else " Unknown " )
## Equip a weapon from WorldWeapon data (receives resource path)
@ rpc ( " any_peer " , " call_local " , " reliable " )
func equip_weapon_from_world ( weapon_data_path : String ) :
print ( " [Client " , multiplayer . get_unique_id ( ) , " ] equip_weapon_from_world called for: " , weapon_data_path )
var data = load ( weapon_data_path ) as WeaponData
if data :
equip_weapon ( data )
print ( " [Client " , multiplayer . get_unique_id ( ) , " ] Equipped weapon: " , data . weapon_name )
else :
push_error ( " Failed to load weapon data from: " + weapon_data_path )
## Equip a weapon with given data
func equip_weapon ( data : WeaponData ) :
# Determine which hand based on weapon type
var is_offhand = ( data . hand_type == WeaponData . Hand . OFF_HAND )
# Unequip current weapon in that hand first
if is_offhand :
if equipped_offhand :
unequip_weapon ( true )
else :
if equipped_weapon :
unequip_weapon ( false )
# Determine where to attach the weapon
var attach_point : Node3D
if is_offhand :
attach_point = _offhand_container if _offhand_container else _offhand_attachment
else :
attach_point = _weapon_container if _weapon_container else _weapon_attachment
if not attach_point :
push_error ( " No weapon attachment point found for " , " off-hand " if is_offhand else " main hand " )
return
# Create new weapon instance
var weapon = BaseWeapon . new ( )
weapon . weapon_data = data
weapon . name = " EquippedOffHand " if is_offhand else " EquippedWeapon "
weapon . set_owner_character ( self )
# Attach to weapon container (or bone attachment if no container)
attach_point . add_child ( weapon )
# Store reference
if is_offhand :
equipped_offhand = weapon
else :
equipped_weapon = weapon
if is_multiplayer_authority ( ) :
print ( " Equipped: " , data . weapon_name , " to " , attach_point . name )
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
3 weeks ago
weapon_equipped_changed . emit ( )
## Unequip current weapon (local only)
func unequip_weapon ( is_offhand : bool = false ) :
if is_offhand :
if equipped_offhand :
equipped_offhand . queue_free ( )
equipped_offhand = null
else :
if equipped_weapon :
equipped_weapon . queue_free ( )
equipped_weapon = null
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
3 weeks ago
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 ) :
unequip_weapon ( is_offhand )
## Drop currently equipped weapon
@ rpc ( " any_peer " , " reliable " )
func drop_weapon ( is_offhand : bool = false ) :
var has_weapon = equipped_offhand if is_offhand else equipped_weapon
print ( " drop_weapon called on peer " , multiplayer . get_unique_id ( ) , " is_server: " , multiplayer . is_server ( ) , " has weapon: " , has_weapon != null , " offhand: " , is_offhand )
if not has_weapon :
print ( " No weapon equipped in " , " off-hand " if is_offhand else " main hand " , " , cannot drop " )
return
# Only server spawns the world weapon
if multiplayer . is_server ( ) :
print ( " Server spawning dropped weapon " )
_spawn_world_weapon ( has_weapon . weapon_data )
# Tell all clients to unequip
rpc ( " _unequip_weapon_sync " , is_offhand )
# Unequip locally
unequip_weapon ( is_offhand )
if is_multiplayer_authority ( ) :
print ( " Dropped weapon from " , " off-hand " if is_offhand else " main hand " )
## Spawn a weapon in the world (server only)
func _spawn_world_weapon ( data : WeaponData ) :
if not multiplayer . is_server ( ) :
return
# Get the resource path
var resource_path = data . resource_path
if resource_path == " " :
push_error ( " WeaponData has no resource path! Make sure to save it as a .tres file " )
return
# Position in front of player
var spawn_pos = global_position + ( - transform . basis . z * 2.0 )
spawn_pos . y += 1.0 # Spawn at chest height
# Calculate forward velocity
var velocity = - transform . basis . z * 3.0
# Tell level to spawn weapon on all clients
var level = get_tree ( ) . get_current_scene ( )
if level and level . has_method ( " spawn_world_weapon " ) :
# Increment the level's weapon counter
level . _weapon_spawn_counter += 1
level . rpc ( " spawn_world_weapon " , resource_path , spawn_pos , velocity , level . _weapon_spawn_counter )
## Pick up nearest weapon
func _pickup_nearest_weapon ( ) :
if _nearby_weapons . size ( ) == 0 :
return
# Find closest weapon
var closest_weapon : WorldWeapon = null
var closest_distance : float = INF
for weapon in _nearby_weapons :
if not is_instance_valid ( weapon ) :
continue
var distance = global_position . distance_to ( weapon . global_position )
if distance < closest_distance :
closest_distance = distance
closest_weapon = weapon
if closest_weapon :
# Check what hand this weapon goes in
var weapon_hand_type = closest_weapon . weapon_data . hand_type if closest_weapon . weapon_data else WeaponData . Hand . MAIN_HAND
var is_offhand = ( weapon_hand_type == WeaponData . Hand . OFF_HAND )
var slot_occupied = equipped_offhand if is_offhand else equipped_weapon
# If the slot is occupied, drop it first
if slot_occupied :
print ( " Slot occupied, dropping current weapon before picking up " )
if multiplayer . is_server ( ) :
drop_weapon ( is_offhand )
else :
rpc_id ( 1 , " drop_weapon " , is_offhand )
# Wait a tiny bit for the drop to complete
await get_tree ( ) . create_timer ( 0.1 ) . timeout
# Request server to pickup (server validates)
if multiplayer . is_server ( ) :
closest_weapon . try_pickup ( multiplayer . get_unique_id ( ) )
else :
closest_weapon . rpc_id ( 1 , " try_pickup " , multiplayer . get_unique_id ( ) )