class_name FollowerThinker extends NPCRunnerThinker @export var target: Node3D @export var target_distance: float = 0.0 var raycast: PhysicsRayQueryParameters3D func _ready() -> void: raycast = PhysicsRayQueryParameters3D.new() func impetus() -> Vector3: var displacement := ( target.global_position - runner.global_position ).slide(runner.up_normal) if displacement.length() <= target_distance: return Vector3.ZERO else: return displacement.limit_length() func should_jump() -> bool: if ( runner.state == &'jump' or runner.state == &'wall_jump' or runner.state == &'ability' ): return false var displacement := ( target.global_position - runner.global_position ) var vertical_displacement := displacement.project(runner.up_normal) var horizontal_displacement := displacement - vertical_displacement if vertical_displacement.normalized().dot(runner.up_normal) <= 0.0: vertical_displacement = Vector3.ZERO if displacement.length() <= target_distance: return false if vertical_displacement.length() > target_distance: return true if runner.state == &'wall_slide': return ( runner.wall_normal.dot( horizontal_displacement.normalized() ) > 0.25 or runner.linear_velocity.normalized().dot(Vector3.UP) < -0.25 ) if not runner.wall_normal.is_zero_approx(): return true if target is PhysicsBody3D: raycast.exclude = [self, target] else: raycast.exclude = [self] raycast.from = runner.global_position raycast.to = runner.global_position + horizontal_displacement var raycast_result := ( runner.get_world_3d().direct_space_state.intersect_ray(raycast) ) if raycast_result.is_empty(): return false if ( raycast_result[&'normal'].dot(-horizontal_displacement.normalized()) > 0.25 ): return true return false func should_do_ability() -> bool: return runner.state == &'ability' and ( target.global_position - runner.global_position ).normalized().dot(runner.up_normal) < -0.25