Hithurtboxes
This commit is contained in:
@@ -12,5 +12,7 @@ attack_range = 3.5
|
||||
attack_cooldown = 0.6
|
||||
attack_animation = "Attack_TwoHandSwing"
|
||||
knockback_force = 12.0
|
||||
startup_time = 0.2
|
||||
active_time = 0.15
|
||||
mesh_scene = ExtResource("1_1ytxi")
|
||||
weight = 2.0
|
||||
|
||||
@@ -12,6 +12,8 @@ damage = 8.0
|
||||
attack_range = 2.5
|
||||
attack_cooldown = 0.8
|
||||
knockback_force = 15.0
|
||||
startup_time = 0.25
|
||||
active_time = 0.15
|
||||
can_block = true
|
||||
block_reduction = 0.7
|
||||
mesh_scene = ExtResource("2")
|
||||
|
||||
@@ -12,6 +12,8 @@ attack_range = 3.5
|
||||
attack_cooldown = 0.6
|
||||
knockback_force = 12.0
|
||||
attack_animation = "Attack1"
|
||||
startup_time = 0.12
|
||||
active_time = 0.2
|
||||
mesh_scene = ExtResource("2")
|
||||
pickup_radius = 1.5
|
||||
weight = 2.0
|
||||
|
||||
@@ -11,5 +11,7 @@ damage = 20.0
|
||||
attack_range = 3.5
|
||||
attack_cooldown = 0.6
|
||||
knockback_force = 12.0
|
||||
startup_time = 0.12
|
||||
active_time = 0.2
|
||||
mesh_scene = ExtResource("1_gdc1w")
|
||||
weight = 2.0
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
[gd_scene load_steps=7 format=3 uid="uid://db06e8q8f8bdq"]
|
||||
[gd_scene load_steps=9 format=3 uid="uid://db06e8q8f8bdq"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c2si8gkbnde0c" path="res://level/scripts/player.gd" id="1_player"]
|
||||
[ext_resource type="PackedScene" uid="uid://b22ou40sbkavj" path="res://assets/characters/player/LilguyRigged.glb" id="2_lilguy"]
|
||||
[ext_resource type="Script" uid="uid://cf7jky1bcs560" path="res://level/scripts/lilguy_body.gd" id="3_body"]
|
||||
[ext_resource type="Script" uid="uid://bj7yrijm7bppq" path="res://level/scripts/spring_arm_offset.gd" id="4_spring"]
|
||||
[ext_resource type="Script" path="res://level/scripts/hurt_box.gd" id="5_hurtbox"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_yxyay"]
|
||||
radius = 0.35796
|
||||
height = 1.73092
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hurtbox"]
|
||||
radius = 0.4
|
||||
height = 1.8
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_xbohm"]
|
||||
properties/0/path = NodePath(".:position")
|
||||
properties/0/spawn = true
|
||||
@@ -108,6 +113,16 @@ transform = Transform3D(-17.74905, -295.46814, -48.82108, 21.019196, -50.01525,
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.066, 0.828, 0.01)
|
||||
shape = SubResource("CapsuleShape3D_yxyay")
|
||||
|
||||
[node name="HurtBox" type="Area3D" parent="." node_paths=PackedStringArray("owner_entity")]
|
||||
collision_layer = 16
|
||||
collision_mask = 0
|
||||
script = ExtResource("5_hurtbox")
|
||||
owner_entity = NodePath("..")
|
||||
|
||||
[node name="HurtBoxShape" type="CollisionShape3D" parent="HurtBox"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.066, 0.828, 0.01)
|
||||
shape = SubResource("CapsuleShape3D_hurtbox")
|
||||
|
||||
[node name="SpringArmOffset" type="Node3D" parent="." node_paths=PackedStringArray("_spring_arm")]
|
||||
transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0)
|
||||
script = ExtResource("4_spring")
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cehc5ckhq2byd"]
|
||||
[gd_scene load_steps=4 format=3 uid="uid://cehc5ckhq2byd"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://c3e6e3s2q0uro" path="res://assets/Objects/Applecorer.glb" id="1_yadub"]
|
||||
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_lq7hu"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ei1hh"]
|
||||
size = Vector3(0.19293976, 2.9722443, 0.5605469)
|
||||
|
||||
[node name="TestSwordMesh" type="Node3D"]
|
||||
|
||||
[node name="Applecorer" parent="." instance=ExtResource("1_yadub")]
|
||||
transform = Transform3D(-0.3, 0, -2.6226834e-08, 0, 0.3, 0, 2.6226834e-08, 0, -0.3, 0, 0, 0)
|
||||
|
||||
[node name="HitBox" type="Area3D" parent="."]
|
||||
script = ExtResource("2_lq7hu")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0034065247, 2.15522, 0.0234375)
|
||||
shape = SubResource("BoxShape3D_ei1hh")
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://rkvkbxlweo60"]
|
||||
[gd_scene load_steps=4 format=3 uid="uid://rkvkbxlweo60"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://culurukxpqswh" path="res://assets/Objects/TestSword.glb" id="1_4fdvi"]
|
||||
[ext_resource type="PackedScene" uid="uid://df31n55xyn27i" path="res://assets/Objects/TestSword.glb" id="1_4fdvi"]
|
||||
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_3qhqw"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_5u25i"]
|
||||
size = Vector3(0.19293976, 2.2154922, 0.5605469)
|
||||
|
||||
[node name="TestSwordMesh" type="Node3D"]
|
||||
|
||||
[node name="TestSword" parent="." instance=ExtResource("1_4fdvi")]
|
||||
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0.104662895, 0)
|
||||
|
||||
[node name="HitBox" type="Area3D" parent="."]
|
||||
script = ExtResource("2_3qhqw")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0034065247, 1.3828986, 0.0234375)
|
||||
shape = SubResource("BoxShape3D_5u25i")
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dyjfaq654xne3"]
|
||||
[gd_scene load_steps=4 format=3 uid="uid://dyjfaq654xne3"]
|
||||
|
||||
[ext_resource type="ArrayMesh" uid="uid://cc1kxfbkvpo2d" path="res://assets/Objects/swordlowpoly.obj" id="1"]
|
||||
[ext_resource type="ArrayMesh" uid="uid://1wnuqcx2n4xq" path="res://assets/Objects/swordlowpoly.obj" id="1"]
|
||||
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_wyi6r"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_mhdau"]
|
||||
size = Vector3(0.19293976, 1.0232315, 0.5605469)
|
||||
|
||||
[node name="SwordMesh" type="Node3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.4839063, -2.9492104, -3.4711354)
|
||||
mesh = ExtResource("1")
|
||||
|
||||
[node name="HitBox" type="Area3D" parent="."]
|
||||
script = ExtResource("2_wyi6r")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0034065247, 0.6794083, 0.0234375)
|
||||
shape = SubResource("BoxShape3D_mhdau")
|
||||
|
||||
+109
-42
@@ -3,9 +3,10 @@ class_name BaseWeapon
|
||||
|
||||
## Base class for equipped weapons
|
||||
## Attached to player's hand via BoneAttachment3D
|
||||
## Provides common weapon functionality and stats
|
||||
## Uses HitBox/HurtBox system for damage detection
|
||||
|
||||
signal attack_performed()
|
||||
signal hit_connected(target: Node)
|
||||
|
||||
@export var weapon_data: WeaponData
|
||||
|
||||
@@ -13,10 +14,12 @@ signal attack_performed()
|
||||
var owner_character: Character = null
|
||||
var _mesh_instance: Node3D = null
|
||||
var _attack_timer: float = 0.0
|
||||
var _hitbox: HitBox = null
|
||||
|
||||
func _ready():
|
||||
if weapon_data and weapon_data.mesh_scene:
|
||||
_spawn_mesh()
|
||||
_setup_hitbox()
|
||||
|
||||
func _process(delta):
|
||||
if _attack_timer > 0:
|
||||
@@ -32,6 +35,85 @@ func _spawn_mesh():
|
||||
_mesh_instance = weapon_data.mesh_scene.instantiate()
|
||||
add_child(_mesh_instance)
|
||||
|
||||
# Check if mesh has a HitBox child, use it instead of auto-generated one
|
||||
var mesh_hitbox = _mesh_instance.get_node_or_null("HitBox")
|
||||
if mesh_hitbox and mesh_hitbox is HitBox:
|
||||
# Use the hitbox from the mesh scene
|
||||
print("[BaseWeapon] Found manual HitBox in mesh scene")
|
||||
if _hitbox:
|
||||
_hitbox.queue_free()
|
||||
_hitbox = mesh_hitbox
|
||||
_configure_hitbox()
|
||||
else:
|
||||
print("[BaseWeapon] No manual HitBox found, will auto-generate")
|
||||
|
||||
## Setup the hitbox for this weapon
|
||||
func _setup_hitbox():
|
||||
# Skip if we already have a hitbox (e.g., from mesh scene)
|
||||
if _hitbox:
|
||||
return
|
||||
|
||||
# Create hitbox dynamically based on weapon range
|
||||
_hitbox = HitBox.new()
|
||||
_hitbox.name = "HitBox"
|
||||
add_child(_hitbox)
|
||||
|
||||
# Add collision shape based on attack range - use sphere for consistent detection
|
||||
# regardless of weapon orientation during animations
|
||||
var collision = CollisionShape3D.new()
|
||||
var sphere = SphereShape3D.new()
|
||||
var range_val = weapon_data.attack_range if weapon_data else 1.5
|
||||
sphere.radius = range_val
|
||||
collision.shape = sphere
|
||||
_hitbox.add_child(collision)
|
||||
|
||||
_configure_hitbox()
|
||||
|
||||
## Configure hitbox with weapon stats and owner
|
||||
func _configure_hitbox():
|
||||
if not _hitbox:
|
||||
return
|
||||
|
||||
# Set damage stats from weapon
|
||||
if weapon_data:
|
||||
_hitbox.set_stats(weapon_data.damage, weapon_data.knockback_force)
|
||||
|
||||
# Set owner to prevent self-damage
|
||||
_hitbox.owner_entity = owner_character
|
||||
|
||||
# Connect to hit signal
|
||||
if not _hitbox.hit_landed.is_connected(_on_hitbox_hit):
|
||||
_hitbox.hit_landed.connect(_on_hitbox_hit)
|
||||
|
||||
## Called when hitbox connects with a hurtbox
|
||||
func _on_hitbox_hit(target: Node, damage_amount: float, knockback_amount: float, attacker_pos: Vector3):
|
||||
if not target or not owner_character:
|
||||
return
|
||||
|
||||
hit_connected.emit(target)
|
||||
|
||||
# Route damage through server
|
||||
var attacker_id = multiplayer.get_unique_id()
|
||||
|
||||
if multiplayer.is_server():
|
||||
# We are server, apply directly
|
||||
owner_character._server_apply_damage(
|
||||
target.name,
|
||||
damage_amount,
|
||||
attacker_id,
|
||||
knockback_amount,
|
||||
attacker_pos
|
||||
)
|
||||
else:
|
||||
# Send to server
|
||||
owner_character.rpc_id(1, "_server_apply_damage",
|
||||
target.name,
|
||||
damage_amount,
|
||||
attacker_id,
|
||||
knockback_amount,
|
||||
attacker_pos
|
||||
)
|
||||
|
||||
## Perform an attack with this weapon
|
||||
## Called by the character who owns this weapon
|
||||
func perform_attack() -> bool:
|
||||
@@ -55,57 +137,39 @@ func perform_attack() -> bool:
|
||||
# Sync animation to other clients
|
||||
owner_character._sync_attack_animation.rpc(anim_name)
|
||||
|
||||
# Delay damage until animation hits (roughly 70% through the animation)
|
||||
# This makes the damage apply when the swing actually connects
|
||||
var damage_delay = weapon_data.attack_cooldown * 0.4 # Adjust this multiplier to change when damage happens
|
||||
get_tree().create_timer(damage_delay).timeout.connect(_find_and_damage_targets)
|
||||
# Activate hitbox for the attack duration
|
||||
# Only activate on authority - they detect hits and send to server
|
||||
if owner_character.is_multiplayer_authority():
|
||||
_activate_hitbox()
|
||||
|
||||
attack_performed.emit()
|
||||
return true
|
||||
|
||||
## Find targets in range and apply damage
|
||||
func _find_and_damage_targets():
|
||||
if not owner_character:
|
||||
## Activate the hitbox for attack detection using frame data timing
|
||||
func _activate_hitbox():
|
||||
if not _hitbox:
|
||||
return
|
||||
|
||||
# Check if the owner character has authority (not this node)
|
||||
if not owner_character.is_multiplayer_authority():
|
||||
# Update owner reference in case it changed
|
||||
_hitbox.owner_entity = owner_character
|
||||
|
||||
# STARTUP PHASE - Wait before activating hitbox (wind-up)
|
||||
var startup = weapon_data.startup_time if weapon_data else 0.15
|
||||
if startup > 0:
|
||||
await get_tree().create_timer(startup).timeout
|
||||
|
||||
if not _hitbox or not is_instance_valid(_hitbox):
|
||||
return
|
||||
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsShapeQueryParameters3D.new()
|
||||
var sphere = SphereShape3D.new()
|
||||
sphere.radius = weapon_data.attack_range
|
||||
query.shape = sphere
|
||||
query.transform = global_transform
|
||||
query.collision_mask = 1 # Player layer
|
||||
# ACTIVE PHASE - Hitbox is on, can deal damage
|
||||
_hitbox.activate()
|
||||
|
||||
var results = space_state.intersect_shape(query)
|
||||
var active = weapon_data.active_time if weapon_data else 0.2
|
||||
await get_tree().create_timer(active).timeout
|
||||
|
||||
for result in results:
|
||||
var hit_body = result["collider"]
|
||||
if hit_body != owner_character and hit_body is BaseUnit:
|
||||
var attacker_id = multiplayer.get_unique_id()
|
||||
|
||||
# If we're the server, apply damage directly
|
||||
if multiplayer.is_server():
|
||||
owner_character._server_apply_damage(
|
||||
hit_body.name,
|
||||
weapon_data.damage,
|
||||
attacker_id,
|
||||
weapon_data.knockback_force,
|
||||
owner_character.global_position
|
||||
)
|
||||
else:
|
||||
# Otherwise, request server to apply damage
|
||||
owner_character.rpc_id(1, "_server_apply_damage",
|
||||
hit_body.name,
|
||||
weapon_data.damage,
|
||||
attacker_id,
|
||||
weapon_data.knockback_force,
|
||||
owner_character.global_position
|
||||
)
|
||||
break # Only hit one target per attack
|
||||
# RECOVERY PHASE - Hitbox off (recovery time is remaining cooldown)
|
||||
if _hitbox and is_instance_valid(_hitbox):
|
||||
_hitbox.deactivate()
|
||||
|
||||
## Check if weapon can attack
|
||||
func can_attack() -> bool:
|
||||
@@ -114,6 +178,9 @@ func can_attack() -> bool:
|
||||
## Set the character who owns this weapon
|
||||
func set_owner_character(character: Character):
|
||||
owner_character = character
|
||||
# Update hitbox owner
|
||||
if _hitbox:
|
||||
_hitbox.owner_entity = character
|
||||
|
||||
## Get weapon stats
|
||||
func get_damage() -> float:
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
extends Area3D
|
||||
class_name HitBox
|
||||
|
||||
## A component that deals damage to HurtBoxes
|
||||
## Attach to weapons or attack effects
|
||||
## Uses direct physics queries for reliable hit detection
|
||||
|
||||
signal hit_landed(target: Node, damage: float, knockback: float, attacker_pos: Vector3)
|
||||
|
||||
## Damage dealt on hit
|
||||
@export var damage: float = 10.0
|
||||
## Knockback force applied
|
||||
@export var knockback: float = 5.0
|
||||
## Owner entity (used to prevent self-damage and identify attacker)
|
||||
@export var owner_entity: Node = null
|
||||
|
||||
## Whether hitbox is currently active (only deals damage when active)
|
||||
var is_active: bool = false
|
||||
## Tracks entities hit this attack (prevents multi-hit)
|
||||
var _hits_this_attack: Array[Node] = []
|
||||
## Shape for queries (extracted from child CollisionShape3D)
|
||||
var _query_shape: Shape3D = null
|
||||
|
||||
func _ready():
|
||||
# Find the collision shape for queries
|
||||
for child in get_children():
|
||||
if child is CollisionShape3D and child.shape:
|
||||
_query_shape = child.shape
|
||||
break
|
||||
|
||||
func _physics_process(_delta):
|
||||
if not is_active:
|
||||
return
|
||||
|
||||
_check_hits()
|
||||
|
||||
func _check_hits():
|
||||
if not _query_shape:
|
||||
# Fallback: create a default sphere
|
||||
var sphere = SphereShape3D.new()
|
||||
sphere.radius = 2.0
|
||||
_query_shape = sphere
|
||||
|
||||
# Use physics server for reliable queries
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsShapeQueryParameters3D.new()
|
||||
query.shape = _query_shape
|
||||
query.transform = global_transform
|
||||
query.collision_mask = 16 # Layer 5 (hurtbox)
|
||||
query.collide_with_areas = true
|
||||
query.collide_with_bodies = false
|
||||
|
||||
var results = space_state.intersect_shape(query, 32)
|
||||
|
||||
for result in results:
|
||||
var collider = result["collider"]
|
||||
if collider is HurtBox:
|
||||
_process_hit(collider)
|
||||
|
||||
func _process_hit(hurtbox: HurtBox):
|
||||
# Don't hit our own hurtbox
|
||||
if hurtbox.owner_entity == owner_entity:
|
||||
return
|
||||
|
||||
# Don't hit same entity twice in one attack
|
||||
if hurtbox.owner_entity in _hits_this_attack:
|
||||
return
|
||||
|
||||
# Register this hit
|
||||
var target = hurtbox.owner_entity
|
||||
if target:
|
||||
_hits_this_attack.append(target)
|
||||
|
||||
# Get attacker position for knockback direction
|
||||
var attacker_pos = global_position
|
||||
if owner_entity and owner_entity is Node3D:
|
||||
attacker_pos = owner_entity.global_position
|
||||
|
||||
# Emit signal - let the weapon/owner handle damage routing to server
|
||||
hit_landed.emit(target, damage, knockback, attacker_pos)
|
||||
|
||||
## Activate hitbox (call when attack starts)
|
||||
func activate():
|
||||
is_active = true
|
||||
_hits_this_attack.clear()
|
||||
|
||||
## Deactivate hitbox (call when attack ends)
|
||||
func deactivate():
|
||||
is_active = false
|
||||
_hits_this_attack.clear()
|
||||
|
||||
## Set damage stats (usually from weapon data)
|
||||
func set_stats(new_damage: float, new_knockback: float):
|
||||
damage = new_damage
|
||||
knockback = new_knockback
|
||||
@@ -0,0 +1 @@
|
||||
uid://jyas86y3f0jp
|
||||
@@ -0,0 +1,27 @@
|
||||
extends Area3D
|
||||
class_name HurtBox
|
||||
|
||||
## A component that receives damage from HitBoxes
|
||||
## Attach to any entity that can be damaged (players, enemies, destructibles)
|
||||
## NOTE: This is a passive detection zone - HitBox handles the collision detection
|
||||
|
||||
## The entity that owns this hurtbox (should be a BaseUnit or similar)
|
||||
@export var owner_entity: Node = null
|
||||
|
||||
func _ready():
|
||||
# Auto-find owner if not set (traverse up to find BaseUnit)
|
||||
if owner_entity == null:
|
||||
var parent = get_parent()
|
||||
while parent:
|
||||
if parent is BaseUnit:
|
||||
owner_entity = parent
|
||||
break
|
||||
parent = parent.get_parent()
|
||||
|
||||
# Configure collision - hurtboxes are on layer 5, detect nothing (passive)
|
||||
collision_layer = 16 # Layer 5 (hurtbox)
|
||||
collision_mask = 0 # Don't detect anything - hitboxes detect us
|
||||
|
||||
# Ensure we can be detected but don't detect others
|
||||
monitorable = true
|
||||
monitoring = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://bj3uepduxvgju
|
||||
+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()
|
||||
|
||||
@@ -18,6 +18,14 @@ enum Hand { MAIN_HAND, OFF_HAND, TWO_HAND }
|
||||
@export var attack_animation: String = "Attack1" # Animation to play when attacking
|
||||
@export var knockback_force: float = 8.0 # How much to push the target back
|
||||
|
||||
@export_category("Attack Timing")
|
||||
## Time before hitbox activates (wind-up/anticipation)
|
||||
@export var startup_time: float = 0.15
|
||||
## Duration hitbox stays active (the actual hit window)
|
||||
@export var active_time: float = 0.2
|
||||
## Time after hitbox deactivates (follow-through, can't act)
|
||||
## Note: recovery_time = attack_cooldown - startup_time - active_time (calculated automatically)
|
||||
|
||||
@export_category("Defense Stats")
|
||||
@export var can_block: bool = false
|
||||
@export_range(0.0, 1.0) var block_reduction: float = 0.5 # Percentage of damage blocked (0.5 = 50%)
|
||||
|
||||
@@ -100,3 +100,5 @@ toggle_character_sheet={
|
||||
3d_physics/layer_1="player"
|
||||
3d_physics/layer_2="world"
|
||||
3d_physics/layer_3="weapon"
|
||||
3d_physics/layer_4="hitbox"
|
||||
3d_physics/layer_5="hurtbox"
|
||||
|
||||
Reference in New Issue
Block a user