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