Code/AI/Default/TacticalState.cs
using Sandbox.Sboku;
using Sandbox.Shared;
using System.Collections.Generic;
using System.Linq;

namespace Sandbox.AI.Default;
internal class TacticalState : StateBase, IActionState
{
    public TacticalState(SbokuBase bot) : base(bot)
    {
    }

    public override void Think()
    {
        if (!Bot.IsNavigating)
        {
            // Repeat
            Bot.SetActionState<TacticalState>();
            return;
        }
    }

    public override void OnSet()
    {
        FindCover();
    }

    private void FindCover()
    {
        Vector3 tarPos = Target.GameObject.WorldPosition;
        Vector3 botPos = Bot.WorldPosition;
        
        var startAngle = Game.Random.Next(0, 360 + 1);
        for (float angle = startAngle; angle < startAngle + 360; angle += Settings.CoverScanAngle)
        {
            Vector3 direction = Rotation.FromYaw(angle).Forward;
            var h = Bot.Character.Height / 2;
            var trace = Scene.Trace.Ray(botPos + h, botPos + h + direction * Bot.MaxFightRange)
                                   .IgnoreGameObjectHierarchy(Bot.GameObject)
                                   .IgnoreGameObjectHierarchy(Target.GameObject)
                                   .Run();
            Scene.GetAllComponents<ISbokuBot>();
            if (Settings.ShowDebugOverlay)
                Bot.Scene.DebugOverlay.Line(trace.StartPosition, trace.EndPosition, Color.Magenta, 3);

            if (trace.Hit && trace.GameObject != null && !PathCrossesFire(botPos, trace.EndPosition))
            {
                var thru = Scene.Trace.Ray(trace.EndPosition, trace.EndPosition + direction * 50).IgnoreGameObjectHierarchy(Scene).Run();
                var pos = Scene.NavMesh.GetClosestPoint(thru.EndPosition);
                if (pos is not Vector3 vect)
                    continue;

                var potentialPath = Bot.Scene.NavMesh.GetSimplePathSafe(Bot.WorldPosition, vect);
                if (potentialPath.Any())
                {
                    if (Settings.ShowDebugOverlay)
                    {
                        Scene.DebugOverlay.Sphere(new Sphere(trace.EndPosition, 15), Color.Orange, 3);
                        Scene.DebugOverlay.Sphere(new Sphere(vect, 15), Color.Red, 3);
                    }
                    var path = Scene.NavMesh.GetSimplePathSafe(Bot.WorldPosition, vect);
                    if (path.Any())
                    {
                        Bot.MoveTo(path);
                        return;
                    }
                }
            }
        }

        var rand = Scene.NavMesh.GetRandomPoint(Target.GameObject.WorldPosition, Bot.MaxFightRange);
        // If not, we'll try again on the next think
        if (rand is Vector3 point)
        {
            Bot.MoveTo(point);
        }
    }

    private bool PathCrossesFire(Vector3 startPos, Vector3 endPos)
    {
        foreach (var otherBot in Scene.GetAllComponents<SbokuBase>().Where(x => x.IsValid && x.IsActiveCombatState<ShootState>()))
        {
            if (otherBot == Bot)
                continue; 

            Vector3 botToTargetStart = otherBot.WorldPosition;
            Vector3 botToTargetEnd = otherBot.Target.GameObject.WorldPosition;

            var fireLine = Scene.Trace.Ray(botToTargetStart, botToTargetEnd)
                                      .IgnoreGameObjectHierarchy(otherBot.GameObject)
                                      .IgnoreGameObjectHierarchy(otherBot.Target.GameObject)
                                      .Run();

            if (!fireLine.Hit)
                continue;

            if (PathsIntersect(startPos.x, startPos.y, endPos.x, endPos.y,
                               botToTargetStart.x, botToTargetStart.y, botToTargetEnd.x, botToTargetEnd.y))
            {
                return true;
            }
        }
        return false;
    }

    private bool PathsIntersect(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4)
    {
        float d = (x4 - x3) * (y2 - y1) - (y4 - y3) * (x2 - x1);
        if (d == 0) return false;

        float uA = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / d;
        float uB = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / d;

        return uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1;
    }
}