|
|
|
|
@ -42,7 +42,12 @@ var is_blocking: bool = false |
|
|
|
|
@export var attack_damage: float = 10.0 |
|
|
|
|
@export var attack_range: float = 3.0 |
|
|
|
|
@export var attack_cooldown: float = 0.5 |
|
|
|
|
@export var unarmed_knockback: float = 5.0 |
|
|
|
|
@export_category("Unarmed Attack Timing") |
|
|
|
|
@export var unarmed_startup: float = 0.1 # Wind-up before hit |
|
|
|
|
@export var unarmed_active: float = 0.15 # Hit window duration |
|
|
|
|
var _attack_timer: float = 0.0 |
|
|
|
|
var _unarmed_hitbox: HitBox = null |
|
|
|
|
|
|
|
|
|
# Dash system |
|
|
|
|
@export var dash_speed_multiplier: float = 2.0 |
|
|
|
|
@ -64,7 +69,10 @@ func _enter_tree(): |
|
|
|
|
|
|
|
|
|
func _ready(): |
|
|
|
|
super._ready() |
|
|
|
|
set_respawn_point(Vector3(0, 5, 0)) |
|
|
|
|
# Set respawn point to current position (where we spawned) - base_unit._ready already does this |
|
|
|
|
# Don't override with a hardcoded position |
|
|
|
|
|
|
|
|
|
print("[Player ", name, "] _ready called. Authority: ", is_multiplayer_authority(), " Position: ", global_position) |
|
|
|
|
|
|
|
|
|
# Capture mouse for local player |
|
|
|
|
if is_multiplayer_authority(): |
|
|
|
|
@ -72,13 +80,18 @@ func _ready(): |
|
|
|
|
|
|
|
|
|
# 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)!") |
|
|
|
|
# Try specific paths first |
|
|
|
|
if has_node("LilguyRigged/Armature"): |
|
|
|
|
_body = get_node("LilguyRigged/Armature") |
|
|
|
|
print("Auto-found _body: LilguyRigged/Armature") |
|
|
|
|
elif has_node("Armature"): |
|
|
|
|
_body = get_node("Armature") |
|
|
|
|
print("Auto-found _body: Armature") |
|
|
|
|
elif has_node("3DGodotRobot"): |
|
|
|
|
_body = get_node("3DGodotRobot") |
|
|
|
|
print("Auto-found _body: 3DGodotRobot") |
|
|
|
|
else: |
|
|
|
|
push_error("Could not find body node!") |
|
|
|
|
|
|
|
|
|
# Auto-find spring arm offset |
|
|
|
|
if _spring_arm_offset == null: |
|
|
|
|
@ -144,6 +157,9 @@ func _ready(): |
|
|
|
|
# Setup weapon pickup detection area |
|
|
|
|
_setup_weapon_pickup_area() |
|
|
|
|
|
|
|
|
|
# Setup unarmed attack hitbox (deferred to avoid multiplayer timing issues) |
|
|
|
|
call_deferred("_setup_unarmed_hitbox") |
|
|
|
|
|
|
|
|
|
# Auto-find weapon attachment if not set |
|
|
|
|
if _weapon_attachment == null: |
|
|
|
|
var bone_attach = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D") |
|
|
|
|
@ -189,19 +205,19 @@ func _physics_process(delta): |
|
|
|
|
freeze() |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# Apply gravity when not on floor |
|
|
|
|
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 |
|
|
|
|
# Handle jump |
|
|
|
|
if is_on_floor() and Input.is_action_just_pressed("jump"): |
|
|
|
|
velocity.y = JUMP_VELOCITY |
|
|
|
|
|
|
|
|
|
_move() |
|
|
|
|
move_and_slide() |
|
|
|
|
_body.animate(velocity) |
|
|
|
|
|
|
|
|
|
if _body: |
|
|
|
|
_body.animate(velocity) |
|
|
|
|
|
|
|
|
|
func _process(delta): |
|
|
|
|
# Check if multiplayer is ready |
|
|
|
|
@ -269,14 +285,16 @@ func freeze(): |
|
|
|
|
velocity.x = 0 |
|
|
|
|
velocity.z = 0 |
|
|
|
|
_current_speed = 0 |
|
|
|
|
_body.animate(Vector3.ZERO) |
|
|
|
|
if _body: |
|
|
|
|
_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) |
|
|
|
|
if _body: |
|
|
|
|
_body.apply_rotation(velocity) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
var _input_direction: Vector2 = Vector2.ZERO |
|
|
|
|
@ -289,12 +307,14 @@ func _move() -> void: |
|
|
|
|
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 _spring_arm_offset: |
|
|
|
|
_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) |
|
|
|
|
if _body: |
|
|
|
|
_body.apply_rotation(velocity) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
velocity.x = move_toward(velocity.x, 0, _current_speed) |
|
|
|
|
@ -406,29 +426,8 @@ func _perform_attack(): |
|
|
|
|
# Sync animation to other clients |
|
|
|
|
_sync_attack_animation.rpc("Attack_OneHand") |
|
|
|
|
|
|
|
|
|
# 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 |
|
|
|
|
# Activate unarmed hitbox for damage detection |
|
|
|
|
_activate_unarmed_hitbox() |
|
|
|
|
|
|
|
|
|
## Server-side damage application |
|
|
|
|
@rpc("any_peer", "reliable") |
|
|
|
|
@ -464,11 +463,13 @@ 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) |
|
|
|
|
print("[Player ", name, "] _on_died called. Authority: ", is_multiplayer_authority()) |
|
|
|
|
# Only the authority should disable their own processing |
|
|
|
|
if is_multiplayer_authority(): |
|
|
|
|
set_physics_process(false) |
|
|
|
|
set_process(false) |
|
|
|
|
|
|
|
|
|
# Visual feedback - could add death animation here |
|
|
|
|
# Visual feedback - runs on all peers |
|
|
|
|
if _body: |
|
|
|
|
_body.visible = false |
|
|
|
|
|
|
|
|
|
@ -485,18 +486,19 @@ func _on_died(killer_id: int): |
|
|
|
|
get_node("HealthUI/HealthText").text = "DEAD - Respawning..." |
|
|
|
|
|
|
|
|
|
func _on_respawned(): |
|
|
|
|
# Re-enable player |
|
|
|
|
set_physics_process(true) |
|
|
|
|
set_process(true) |
|
|
|
|
print("[Player ", name, "] _on_respawned called. Authority: ", is_multiplayer_authority(), " Position: ", global_position) |
|
|
|
|
# Only the authority should re-enable their own processing |
|
|
|
|
if is_multiplayer_authority(): |
|
|
|
|
set_physics_process(true) |
|
|
|
|
set_process(true) |
|
|
|
|
print("[Player ", name, "] Re-enabled physics processing") |
|
|
|
|
|
|
|
|
|
# Visual feedback - runs on all peers |
|
|
|
|
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(): |
|
|
|
|
@ -585,6 +587,63 @@ func _setup_weapon_pickup_area(): |
|
|
|
|
pickup_area.area_entered.connect(_on_weapon_area_entered) |
|
|
|
|
pickup_area.area_exited.connect(_on_weapon_area_exited) |
|
|
|
|
|
|
|
|
|
func _setup_unarmed_hitbox(): |
|
|
|
|
# Create hitbox for unarmed attacks |
|
|
|
|
_unarmed_hitbox = HitBox.new() |
|
|
|
|
_unarmed_hitbox.name = "UnarmedHitBox" |
|
|
|
|
_unarmed_hitbox.owner_entity = self |
|
|
|
|
_unarmed_hitbox.set_stats(attack_damage, unarmed_knockback) |
|
|
|
|
|
|
|
|
|
# Attach to body so it rotates with player facing direction |
|
|
|
|
if _body: |
|
|
|
|
_body.add_child(_unarmed_hitbox) |
|
|
|
|
else: |
|
|
|
|
add_child(_unarmed_hitbox) |
|
|
|
|
|
|
|
|
|
# Add collision shape - larger sphere in front of player |
|
|
|
|
var collision = CollisionShape3D.new() |
|
|
|
|
var sphere = SphereShape3D.new() |
|
|
|
|
sphere.radius = attack_range # Full attack range as radius |
|
|
|
|
collision.shape = sphere |
|
|
|
|
# Position in front of player (Z is forward for the body) |
|
|
|
|
collision.position = Vector3(0, 0.8, -attack_range * 0.75) |
|
|
|
|
_unarmed_hitbox.add_child(collision) |
|
|
|
|
|
|
|
|
|
# Connect hit signal |
|
|
|
|
_unarmed_hitbox.hit_landed.connect(_on_unarmed_hit) |
|
|
|
|
|
|
|
|
|
func _on_unarmed_hit(target: Node, damage_amount: float, knockback_amount: float, attacker_pos: Vector3): |
|
|
|
|
if not target: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# Route damage through server |
|
|
|
|
var attacker_id = multiplayer.get_unique_id() |
|
|
|
|
|
|
|
|
|
if multiplayer.is_server(): |
|
|
|
|
_server_apply_damage(target.name, damage_amount, attacker_id, knockback_amount, attacker_pos) |
|
|
|
|
else: |
|
|
|
|
rpc_id(1, "_server_apply_damage", target.name, damage_amount, attacker_id, knockback_amount, attacker_pos) |
|
|
|
|
|
|
|
|
|
func _activate_unarmed_hitbox(): |
|
|
|
|
if not _unarmed_hitbox: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# STARTUP PHASE - Wait before activating (wind-up animation) |
|
|
|
|
if unarmed_startup > 0: |
|
|
|
|
await get_tree().create_timer(unarmed_startup).timeout |
|
|
|
|
|
|
|
|
|
if not _unarmed_hitbox or not is_instance_valid(_unarmed_hitbox): |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# ACTIVE PHASE - Hitbox on, can deal damage |
|
|
|
|
_unarmed_hitbox.activate() |
|
|
|
|
|
|
|
|
|
await get_tree().create_timer(unarmed_active).timeout |
|
|
|
|
|
|
|
|
|
# RECOVERY PHASE - Hitbox off |
|
|
|
|
if _unarmed_hitbox and is_instance_valid(_unarmed_hitbox): |
|
|
|
|
_unarmed_hitbox.deactivate() |
|
|
|
|
|
|
|
|
|
func _on_weapon_area_entered(area: Area3D): |
|
|
|
|
# Check if the area belongs to a WorldWeapon |
|
|
|
|
var weapon = area.get_parent() |
|
|
|
|
|