BotAI.cs

AI component for a bot character. It finds the nearest other CharacterController, moves toward it, faces the movement direction, uses a PushAbility when within attack range, and occasionally dashes while airborne; when no target it wanders by choosing random directions every 2–4 seconds.

Networking
using Sandbox;
using System;
using System.Linq;

public sealed class BotAI : Component
{
    [Property] public float Speed { get; set; } = 220f;
    [Property] public float AttackRange { get; set; } = 220f;

    private CharacterController _cc;
    private PushAbility _push;
    private ManeuverAbility _maneuver;
    private TimeUntil _nextDecision;
    private Vector3 _moveDir;

    protected override void OnAwake()
    {
        _cc = Components.Get<CharacterController>();
        _push = Components.Get<PushAbility>();
        _maneuver = Components.Get<ManeuverAbility>();
    }

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

        var target = FindTarget();

        if ( target.IsValid() )
        {
            _moveDir = (target.WorldPosition - WorldPosition).WithZ( 0 ).Normal;

            if ( _cc.IsValid() )
            {
                _cc.Velocity = _moveDir * Speed;
                WorldRotation = Rotation.LookAt( _moveDir );
            }

            float dist = Vector3.DistanceBetween( WorldPosition, target.WorldPosition );
            if ( dist <= AttackRange && _push.IsValid() && _push.IsReady )
            {
                _push.TryExecuteBotPush();
            }
        }
        else
        {
            if ( _nextDecision )
            {
                _nextDecision = Game.Random.Float( 2f, 4f );
                float angle = Game.Random.Float( 0f, MathF.PI * 2f );
                _moveDir = new Vector3( MathF.Cos( angle ), MathF.Sin( angle ), 0f );
            }

            if ( _cc.IsValid() )
            {
                _cc.Velocity = _moveDir * (Speed * 0.6f);
            }
        }

        if ( _cc.IsValid() && !_cc.IsOnGround && _maneuver.IsValid() && _maneuver.IsDashReady )
        {
            _maneuver.TryExecuteBotDash( -_cc.Velocity.Normal );
        }
    }

    private GameObject FindTarget()
    {
        return Scene.GetAllComponents<CharacterController>()
            .Where( c => c.GameObject != GameObject && c.GameObject.IsValid() )
            .OrderBy( c => Vector3.DistanceBetween( WorldPosition, c.WorldPosition ) )
            .FirstOrDefault()?.GameObject;
    }
}