|
|
|
|
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
|
|
|
|
|
## Debug mesh for visualization
|
|
|
|
|
var _debug_mesh: MeshInstance3D = null
|
|
|
|
|
var _debug_material: StandardMaterial3D = 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
|
|
|
|
|
_create_debug_visualization(child)
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
func _create_debug_visualization(collision_shape: CollisionShape3D):
|
|
|
|
|
# Create a semi-transparent red mesh to visualize the hitbox
|
|
|
|
|
_debug_mesh = MeshInstance3D.new()
|
|
|
|
|
_debug_material = StandardMaterial3D.new()
|
|
|
|
|
_debug_material.albedo_color = Color(1.0, 0.0, 0.0, 0.4) # Red, semi-transparent
|
|
|
|
|
_debug_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
|
|
|
_debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
|
|
|
_debug_material.cull_mode = BaseMaterial3D.CULL_DISABLED # Visible from both sides
|
|
|
|
|
|
|
|
|
|
# Create mesh matching the collision shape
|
|
|
|
|
var mesh: Mesh = null
|
|
|
|
|
if collision_shape.shape is BoxShape3D:
|
|
|
|
|
var box_mesh = BoxMesh.new()
|
|
|
|
|
box_mesh.size = collision_shape.shape.size
|
|
|
|
|
mesh = box_mesh
|
|
|
|
|
elif collision_shape.shape is SphereShape3D:
|
|
|
|
|
var sphere_mesh = SphereMesh.new()
|
|
|
|
|
sphere_mesh.radius = collision_shape.shape.radius
|
|
|
|
|
sphere_mesh.height = collision_shape.shape.radius * 2
|
|
|
|
|
mesh = sphere_mesh
|
|
|
|
|
elif collision_shape.shape is CapsuleShape3D:
|
|
|
|
|
var capsule_mesh = CapsuleMesh.new()
|
|
|
|
|
capsule_mesh.radius = collision_shape.shape.radius
|
|
|
|
|
capsule_mesh.height = collision_shape.shape.height
|
|
|
|
|
mesh = capsule_mesh
|
|
|
|
|
|
|
|
|
|
if mesh:
|
|
|
|
|
_debug_mesh.mesh = mesh
|
|
|
|
|
_debug_mesh.material_override = _debug_material
|
|
|
|
|
# Don't set transform - it inherits from parent CollisionShape3D
|
|
|
|
|
collision_shape.add_child(_debug_mesh)
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
# Change to yellow when active
|
|
|
|
|
if _debug_material:
|
|
|
|
|
_debug_material.albedo_color = Color(1.0, 1.0, 0.0, 0.5) # Yellow, semi-transparent
|
|
|
|
|
|
|
|
|
|
## Deactivate hitbox (call when attack ends)
|
|
|
|
|
func deactivate():
|
|
|
|
|
is_active = false
|
|
|
|
|
_hits_this_attack.clear()
|
|
|
|
|
# Change back to red when inactive
|
|
|
|
|
if _debug_material:
|
|
|
|
|
_debug_material.albedo_color = Color(1.0, 0.0, 0.0, 0.4) # Red, semi-transparent
|
|
|
|
|
|
|
|
|
|
## Set damage stats (usually from weapon data)
|
|
|
|
|
func set_stats(new_damage: float, new_knockback: float):
|
|
|
|
|
damage = new_damage
|
|
|
|
|
knockback = new_knockback
|