NPCs/Agent.Targeting.cs
using System.Numerics;
namespace Opium.AI;
// Should probably all be in HostileNPC
partial class Agent
{
/// <summary>
/// Does this agent have line of sight to a specified GameObject?
/// </summary>
private bool HasLineOfSight( GameObject gameObject )
{
// TODO(alex): line of sight cone here
if ( gameObject == null )
return false;
var trace = Scene.Trace
.Ray( CameraObject.WorldPosition, gameObject.WorldPosition )
.IgnoreGameObjectHierarchy( GameObject )
.WithoutTags( "pickup" )
.Run();
if ( !trace.Hit )
return false;
return trace.GameObject == gameObject || gameObject.IsDescendant( trace.GameObject );
}
/// <summary>
/// Find ALL players in the scene
/// </summary>
public IEnumerable<Opium.PlayerController> GetAllPlayers()
{
return Scene.GetAllObjects( true )
.Where( x => x.Components.Get<Opium.PlayerController>( FindMode.EverythingInSelfAndAncestors ) != null )
.Select( x => x.Components.Get<Opium.PlayerController>( FindMode.EverythingInSelfAndAncestors ) );
}
private IEnumerable<Actor> CachedLOS { get; set; } = new List<Actor>();
private TimeSince TimeSinceLOSAcquired = 1f;
private float LOSFrequency => 1f;
[ConVar( "op_dev_ai_los" )]
public static bool LOSDebug { get; set; } = false;
private IEnumerable<Actor> FindActorsInLineOfSight( float range = -1 )
{
var forward = CameraObject.WorldRotation.Forward;
var losSphere = new Sphere( CameraObject.WorldPosition + forward * ( range * 2f ), range * 2f );
if ( LOSDebug )
{
Gizmo.Transform = global::Transform.Zero;
Gizmo.Draw.Color = Color.White.WithAlpha( 0.25f );
Gizmo.Draw.LineSphere( losSphere, 32 );
Gizmo.Draw.Line( CameraObject.WorldPosition, CameraObject.WorldPosition + forward * (range / 2f) );
}
if ( TimeSinceLOSAcquired < LOSFrequency )
{
return CachedLOS;
}
TimeSinceLOSAcquired = 0;
if ( range < 0 )
{
range = DefaultDetectionRange;
}
CachedLOS = Scene.FindInPhysics( losSphere )
.Select( x => x.Components.Get<Actor>( FindMode.EverythingInSelfAndAncestors ) )
.Where( x => HasLineOfSight( x?.GameObject ) )
.Where( x => x.IsAlive );
return CachedLOS;
}
public IEnumerable<Actor> FindEnemiesInLineOfSight( float range = -1 )
{
if ( range < 0 ) range = DefaultDetectionRange;
return FindActorsInLineOfSight( range ).Where( Hates );
}
public Actor FindClosestEnemyInLineOfSight( float range = -1 )
{
if ( range < 0 ) range = DefaultDetectionRange;
return FindActorsInLineOfSight( range ).FirstOrDefault( Hates );
}
/// <summary>
/// Can this agent see a player?
/// </summary>
public bool CanSeeAnyPlayer()
{
var target = FindActorsInLineOfSight();
return (target is not null);
}
/// <summary>
/// Make the agent look at someone.
/// TODO: Make this use proper Actor movement
/// </summary>
public void LookAt( Vector3 position, float smoothing = 5f )
{
var lookRot = Rotation.LookAt( position - CameraObject.WorldPosition );
var lookRotAngles = new Angles( 0, lookRot.Yaw(), 0 );
WorldRotation = Rotation.Slerp( WorldRotation, lookRotAngles.ToRotation(), smoothing * Time.Delta );
}
/// <summary>
/// Make the agent look at someone.
/// TODO: Make this use proper Actor movement
/// </summary>
public void LookAt( Actor target )
{
var lookRot = Rotation.LookAt( target.WorldPosition - CameraObject.WorldPosition );
var lookRotAngles = new Angles( 0, lookRot.Yaw(), 0 );
WorldRotation = lookRotAngles.ToRotation();
}
/// <summary>
/// Gets a path (using the navmesh) between two vectors.
/// </summary>
public List<Vector3> GetPath( Vector3 pointA, Vector3 pointB )
{
var navPath = Scene.NavMesh.GetSimplePath( pointA, pointB );
return navPath;
}
/// <inheritdoc cref="GetPath(Vector3, Vector3)"/>
public List<Vector3> GetPath( Vector3 target )
{
return GetPath( WorldPosition, target );
}
TimeUntil nextStimuli = 0f;
[Property] public float LineOfSightRange { get; set; } = -1;
public void FindStimuli()
{
var enemy = FindClosestEnemyInLineOfSight( LineOfSightRange );
// Can we actually see anyone?
if ( enemy == null )
return;
LastStimulus = new EnemySpottedStimulus( enemy );
if ( enemy.ActiveWeapon is MeleeWeapon melee && nextStimuli )
{
var swinging = melee.MainAttack.State == AttackState.Windup;
var rand = Game.Random.Next( 1, 10 );
if ( swinging && rand >= 3 && nextStimuli )
{
nextStimuli = 1;
Scene.BroadcastStimulus( new AnticipateHitStimulus( enemy.WorldPosition ) );
}
}
}
public DoorComponent FindNearestDoor( float range = -1f )
{
if ( range <= 0 )
range = 128f;
var bbox = new BBox( WorldPosition - range, WorldPosition + range );
var door = Scene.FindInPhysics( bbox )
.Where( x => x.Components.Get<DoorComponent>( FindMode.EverythingInSelfAndAncestors ) != null )
.Select( x => x.Components.Get<DoorComponent>( FindMode.EverythingInSelfAndAncestors ) )
.FirstOrDefault();
Gizmo.Draw.Color = Color.White;
Gizmo.Draw.LineBBox( bbox );
return door;
}
}