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