NextbotAI.cs

A Nextbot AI component for NPCs. It searches for the closest player or push-ability object, moves toward them using either simple kinematic movement or a NavMeshAgent path, plays proximity/chase and attack sounds, jumps over obstacles, and performs a melee-style attack that downs the target and applies knockback.

NetworkingFile Access
using Sandbox;
using System;
public sealed class NextbotAI : Component
{
    [Property] public float MoveSpeed { get; set; } = 420f;
    [Property] public float Acceleration { get; set; } = 8f;
    [Property] public float AttackDistance { get; set; } = 70f;
    [Property] public float Damage { get; set; } = 100f;
    [Property] public float KnockbackForce { get; set; } = 900f;
    [Property] public string AttackSound { get; set; } = "music/buttonhover.sound";
    [Property] public string ChaseSound { get; set; } = "music/ssstik.io_1790169556772 (mp3cut.net).sound";
    [Property] public float ProximityMaxDistance { get; set; } = 2200f;
    [Property] public bool BillboardFacing { get; set; } = true;
    [Property] public float JumpForce { get; set; } = 280f;
    [Property] public float TargetRetargetInterval { get; set; } = 0.2f;
    [Property] public float InitialGracePeriod { get; set; } = 6f;

    private GameObject _targetPlayer;
    private PlayerHealth _targetHealth;
    private CharacterController _targetController;
    private Rigidbody _targetRigidbody;
    private NavMeshAgent _navMeshAgent;
    private bool _hasNavMeshPath;
    private Vector3 _currentVelocity;
    private float _verticalVelocity = 0f;
    private SoundHandle _proximitySound;
    [Sync] private TimeSince _timeSinceSpawned { get; set; } = 0f;
    [Sync] private TimeSince _lastAttack { get; set; } = 2f;
    [Sync] private TimeSince _lastTargetSearch { get; set; } = 1f;

    public static float GetClosestBotDistance( Vector3 fromPosition )
    {
        float minDistance = float.MaxValue;
        var bots = Game.ActiveScene?.GetAllComponents<NextbotAI>();
        if ( bots == null ) return minDistance;

        foreach ( var bot in bots )
        {
            if ( !bot.IsValid() || !bot.GameObject.IsValid() || !bot.GameObject.Enabled ) continue;
            float d = Vector3.DistanceBetween( fromPosition, bot.WorldPosition );
            if ( d < minDistance ) minDistance = d;
        }

        return minDistance;
    }

    protected override void OnStart()
    {
        _timeSinceSpawned = 0;
        _navMeshAgent = Components.Get<NavMeshAgent>();
        if ( !_navMeshAgent.IsValid() )
        {
            _navMeshAgent = Components.Create<NavMeshAgent>();
        }

        if ( _navMeshAgent.IsValid() )
        {
            _navMeshAgent.UpdatePosition = true;
            _navMeshAgent.UpdateRotation = false;
            _navMeshAgent.Acceleration = MoveSpeed * Acceleration;
        }

        if ( !string.IsNullOrEmpty( ChaseSound ) )
        {
            _proximitySound = Sound.Play( ChaseSound, WorldPosition );
        }
    }

    protected override void OnUpdate()
    {
        if ( IsProxy ) return;

        if ( _proximitySound != null )
        {
            _proximitySound.Position = WorldPosition;
            var listenerPos = Scene.Camera.IsValid() ? Scene.Camera.WorldPosition :
                              (PlayerHealth.LocalInstance.IsValid() ? PlayerHealth.LocalInstance.WorldPosition : WorldPosition);
            float dist = Vector3.DistanceBetween( WorldPosition, listenerPos );
            float vol = MathX.Clamp( 1.0f - (dist / ProximityMaxDistance), 0f, 1f );
            _proximitySound.Volume = vol;
        }

        if ( NextbotSpawner.IsGracePeriodActive || _timeSinceSpawned < InitialGracePeriod )
        {
            _hasNavMeshPath = false;
            if ( _navMeshAgent.IsValid() ) _navMeshAgent.UpdatePosition = false;
            return;
        }

        if ( _lastTargetSearch > TargetRetargetInterval )
        {
            _lastTargetSearch = 0;
            FindClosestTarget();
        }

        if ( !_targetPlayer.IsValid() || ( _targetHealth.IsValid() && ( _targetHealth.IsDead || _targetHealth.IsDowned ) ) )
        {
            _hasNavMeshPath = false;
            if ( _navMeshAgent.IsValid() ) _navMeshAgent.UpdatePosition = false;
            return;
        }

        Vector3 targetPos = _targetPlayer.WorldPosition;
        Vector3 myPos = WorldPosition;
        Vector3 dirHorizontal = (targetPos - myPos).WithZ( 0 );
        float distance = dirHorizontal.Length;

        if ( dirHorizontal.LengthSquared > 0.01f )
        {
            if ( BillboardFacing && Scene.Camera.IsValid() )
            {
                WorldRotation = Rotation.LookAt( (Scene.Camera.WorldPosition - WorldPosition).WithZ( 0 ) );
            }
            else
            {
                WorldRotation = Rotation.LookAt( dirHorizontal.Normal );
            }
        }

        if ( distance > AttackDistance * 0.6f )
        {
            if ( !_hasNavMeshPath )
            {
                Vector3 desiredVelocity = dirHorizontal.Normal * MoveSpeed;
                _currentVelocity = Vector3.Lerp( _currentVelocity, desiredVelocity, Time.Delta * Acceleration );

                var forwardTr = Scene.Trace.Ray( myPos + Vector3.Up * 15f, myPos + Vector3.Up * 15f + dirHorizontal.Normal * 45f )
                    .IgnoreGameObjectHierarchy( GameObject )
                    .Run();

                if ( forwardTr.Hit && _verticalVelocity <= 0f )
                {
                    _verticalVelocity = JumpForce;
                }

                _verticalVelocity -= 800f * Time.Delta;

                var groundTr = Scene.Trace.Ray( myPos + Vector3.Up * 25f, myPos + Vector3.Down * 35f )
                    .IgnoreGameObjectHierarchy( GameObject )
                    .Run();

                float nextZ = myPos.z + _verticalVelocity * Time.Delta;
                if ( groundTr.Hit && nextZ <= groundTr.EndPosition.z )
                {
                    nextZ = groundTr.EndPosition.z;
                    _verticalVelocity = 0f;
                }

                Vector3 nextPos = myPos + _currentVelocity * Time.Delta;
                nextPos.z = nextZ;
                WorldPosition = nextPos;
            }
        }
        if ( distance <= AttackDistance && _lastAttack > 0.8f )
        {
            ExecuteAttack();
        }
    }

    private void FindClosestTarget()
    {
        float minDistance = float.MaxValue;
        GameObject closest = null;
        PlayerHealth closestHealth = null;
        CharacterController closestController = null;
        Rigidbody closestRigidbody = null;
        Vector3 myPos = WorldPosition;

        var players = Scene.GetAllComponents<PlayerController>();
        foreach ( var p in players )
        {
            if ( !p.IsValid() || !p.GameObject.IsValid() || !p.GameObject.Enabled ) continue;
            if ( p.GameObject == GameObject ) continue;

            var health = p.GameObject.Components.Get<PlayerHealth>() ??
                         p.GameObject.Components.GetInChildren<PlayerHealth>();
            if ( health.IsValid() && ( health.IsDead || health.IsDowned ) ) continue;

            float dist = Vector3.DistanceBetween( myPos, p.WorldPosition );
            if ( dist < minDistance )
            {
                minDistance = dist;
                closest = p.GameObject;
                closestHealth = health;
                closestController = p.GameObject.Components.Get<CharacterController>() ??
                                    p.GameObject.Components.GetInChildren<CharacterController>();
                closestRigidbody = p.GameObject.Components.Get<Rigidbody>() ??
                                   p.GameObject.Components.GetInChildren<Rigidbody>();
            }
        }

        if ( closest == null )
        {
            var pushAbilities = Scene.GetAllComponents<PushAbility>();
            foreach ( var push in pushAbilities )
            {
                if ( !push.IsValid() || !push.GameObject.IsValid() || !push.GameObject.Enabled || push.GameObject == GameObject ) continue;

                var health = push.GameObject.Components.Get<PlayerHealth>() ??
                             push.GameObject.Components.GetInChildren<PlayerHealth>();
                if ( health.IsValid() && ( health.IsDead || health.IsDowned ) ) continue;

                float dist = Vector3.DistanceBetween( myPos, push.WorldPosition );
                if ( dist < minDistance )
                {
                    minDistance = dist;
                    closest = push.GameObject;
                    closestHealth = health;
                    closestController = push.GameObject.Components.Get<CharacterController>() ??
                                        push.GameObject.Components.GetInChildren<CharacterController>();
                    closestRigidbody = push.GameObject.Components.Get<Rigidbody>() ??
                                       push.GameObject.Components.GetInChildren<Rigidbody>();
                }
            }
        }

        _targetPlayer = closest;
        _targetHealth = closestHealth;
        _targetController = closestController;
        _targetRigidbody = closestRigidbody;
        _hasNavMeshPath = _targetPlayer.IsValid() && TrySetNavigationPath( _targetPlayer.WorldPosition );
    }

    private bool TrySetNavigationPath( Vector3 targetPosition )
    {
        if ( !_navMeshAgent.IsValid() || Scene.NavMesh == null || !Scene.NavMesh.IsEnabled || Scene.NavMesh.IsGenerating )
        {
            if ( _navMeshAgent.IsValid() ) _navMeshAgent.UpdatePosition = false;
            return false;
        }

        var path = Scene.NavMesh.CalculatePath( new()
        {
            Start = WorldPosition,
            Target = targetPosition,
            Agent = _navMeshAgent
        } );

        if ( !path.IsValid || path.Points.Count == 0 )
        {
            _navMeshAgent.UpdatePosition = false;
            return false;
        }

        _navMeshAgent.SetAgentPosition( WorldPosition );
        _navMeshAgent.UpdatePosition = true;
        _navMeshAgent.SetPath( path );
        return true;
    }

    private void ExecuteAttack()
    {
        _lastAttack = 0;

        if ( !string.IsNullOrEmpty( AttackSound ) )
        {
            Sound.Play( AttackSound, WorldPosition );
        }

        CameraJuice.Instance?.Shake( 26f, 7f );

        if ( _targetHealth.IsValid() )
        {
            _targetHealth.EnterDowned();
        }

        Vector3 pushDir = (_targetPlayer.WorldPosition - WorldPosition).Normal + Vector3.Up * 0.45f;
        Vector3 knockback = pushDir * KnockbackForce;

        if ( _targetController.IsValid() )
        {
            _targetController.Velocity += knockback;
        }
        else if ( _targetRigidbody.IsValid() )
        {
            _targetRigidbody.ApplyImpulse( knockback );
        }

        if ( Killfeed.Instance != null )
        {
            Killfeed.Instance.AddEntry( "NEXTBOT", _targetPlayer.Name, "DOWNED" );
        }
    }

    protected override void OnDestroy()
    {
        _proximitySound?.Stop();
        _proximitySound = null;
        base.OnDestroy();
    }
}