Hithurtboxes
This commit is contained in:
+111
-52
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user