965 lines
26 KiB
C#
965 lines
26 KiB
C#
|
using System;
|
||
|
using System.Linq;
|
||
|
using EntityStates;
|
||
|
using EntityStates.AI;
|
||
|
using HG;
|
||
|
using JetBrains.Annotations;
|
||
|
using RoR2.Navigation;
|
||
|
using UnityEngine;
|
||
|
using UnityEngine.Networking;
|
||
|
|
||
|
namespace RoR2.CharacterAI;
|
||
|
|
||
|
[RequireComponent(typeof(CharacterMaster))]
|
||
|
public class BaseAI : MonoBehaviour, IManagedMonoBehaviour
|
||
|
{
|
||
|
[Serializable]
|
||
|
public class Target
|
||
|
{
|
||
|
private readonly BaseAI owner;
|
||
|
|
||
|
private bool unset = true;
|
||
|
|
||
|
private GameObject _gameObject;
|
||
|
|
||
|
private float losCheckTimer;
|
||
|
|
||
|
public HurtBox bestHurtBox;
|
||
|
|
||
|
private HurtBoxGroup hurtBoxGroup;
|
||
|
|
||
|
private HurtBox mainHurtBox;
|
||
|
|
||
|
private int bullseyeCount;
|
||
|
|
||
|
private Vector3? lastKnownBullseyePosition;
|
||
|
|
||
|
private Run.FixedTimeStamp lastKnownBullseyePositionTime = Run.FixedTimeStamp.negativeInfinity;
|
||
|
|
||
|
public bool hasLoS;
|
||
|
|
||
|
public GameObject gameObject
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
return _gameObject;
|
||
|
}
|
||
|
set
|
||
|
{
|
||
|
if ((object)value != _gameObject)
|
||
|
{
|
||
|
_gameObject = value;
|
||
|
characterBody = gameObject?.GetComponent<CharacterBody>();
|
||
|
healthComponent = characterBody?.healthComponent;
|
||
|
hurtBoxGroup = characterBody?.hurtBoxGroup;
|
||
|
bullseyeCount = (hurtBoxGroup ? hurtBoxGroup.bullseyeCount : 0);
|
||
|
mainHurtBox = (hurtBoxGroup ? hurtBoxGroup.mainHurtBox : null);
|
||
|
bestHurtBox = mainHurtBox;
|
||
|
hasLoS = false;
|
||
|
unset = !_gameObject;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public CharacterBody characterBody { get; private set; }
|
||
|
|
||
|
public HealthComponent healthComponent { get; private set; }
|
||
|
|
||
|
public Target([NotNull] BaseAI owner)
|
||
|
{
|
||
|
this.owner = owner;
|
||
|
}
|
||
|
|
||
|
public void Update()
|
||
|
{
|
||
|
if ((bool)gameObject)
|
||
|
{
|
||
|
hasLoS = (bool)bestHurtBox && owner.HasLOS(bestHurtBox.transform.position);
|
||
|
if (bullseyeCount > 1 && !hasLoS)
|
||
|
{
|
||
|
bestHurtBox = GetBestHurtBox(out hasLoS);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public bool TestLOSNow()
|
||
|
{
|
||
|
if ((bool)bestHurtBox)
|
||
|
{
|
||
|
return owner.HasLOS(bestHurtBox.transform.position);
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
public bool GetBullseyePosition(out Vector3 position)
|
||
|
{
|
||
|
if ((bool)characterBody && (bool)owner.body && (characterBody.GetVisibilityLevel(owner.body) >= VisibilityLevel.Revealed || lastKnownBullseyePositionTime.timeSince >= 2f))
|
||
|
{
|
||
|
if ((bool)bestHurtBox)
|
||
|
{
|
||
|
lastKnownBullseyePosition = bestHurtBox.transform.position;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
lastKnownBullseyePosition = null;
|
||
|
}
|
||
|
lastKnownBullseyePositionTime = Run.FixedTimeStamp.now;
|
||
|
}
|
||
|
if (lastKnownBullseyePosition.HasValue)
|
||
|
{
|
||
|
position = lastKnownBullseyePosition.Value;
|
||
|
return true;
|
||
|
}
|
||
|
position = (gameObject ? gameObject.transform.position : Vector3.zero);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
private HurtBox GetBestHurtBox(out bool hadLoS)
|
||
|
{
|
||
|
if ((bool)owner && (bool)owner.bodyInputBank && (bool)hurtBoxGroup)
|
||
|
{
|
||
|
Vector3 aimOrigin = owner.bodyInputBank.aimOrigin;
|
||
|
HurtBox hurtBox = null;
|
||
|
float num = float.PositiveInfinity;
|
||
|
HurtBox[] hurtBoxes = hurtBoxGroup.hurtBoxes;
|
||
|
foreach (HurtBox hurtBox2 in hurtBoxes)
|
||
|
{
|
||
|
if (!hurtBox2 || !hurtBox2.transform || !hurtBox2.isBullseye)
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
Vector3 position = hurtBox2.transform.position;
|
||
|
if (CheckLoS(aimOrigin, hurtBox2.transform.position))
|
||
|
{
|
||
|
float sqrMagnitude = (position - aimOrigin).sqrMagnitude;
|
||
|
if (sqrMagnitude < num)
|
||
|
{
|
||
|
num = sqrMagnitude;
|
||
|
hurtBox = hurtBox2;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if ((bool)hurtBox)
|
||
|
{
|
||
|
hadLoS = true;
|
||
|
return hurtBox;
|
||
|
}
|
||
|
}
|
||
|
hadLoS = false;
|
||
|
return mainHurtBox;
|
||
|
}
|
||
|
|
||
|
public void Reset()
|
||
|
{
|
||
|
if (!unset)
|
||
|
{
|
||
|
_gameObject = null;
|
||
|
characterBody = null;
|
||
|
healthComponent = null;
|
||
|
hurtBoxGroup = null;
|
||
|
bullseyeCount = 0;
|
||
|
mainHurtBox = null;
|
||
|
bestHurtBox = mainHurtBox;
|
||
|
hasLoS = false;
|
||
|
unset = true;
|
||
|
losCheckTimer = 0f;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct SkillDriverEvaluation
|
||
|
{
|
||
|
public AISkillDriver dominantSkillDriver;
|
||
|
|
||
|
public Target target;
|
||
|
|
||
|
public Target aimTarget;
|
||
|
|
||
|
public float separationSqrMagnitude;
|
||
|
}
|
||
|
|
||
|
public struct BodyInputs
|
||
|
{
|
||
|
public Vector3 desiredAimDirection;
|
||
|
|
||
|
public Vector3 moveVector;
|
||
|
|
||
|
public bool pressSprint;
|
||
|
|
||
|
public bool pressJump;
|
||
|
|
||
|
public bool pressSkill1;
|
||
|
|
||
|
public bool pressSkill2;
|
||
|
|
||
|
public bool pressSkill3;
|
||
|
|
||
|
public bool pressSkill4;
|
||
|
|
||
|
public bool pressActivateEquipment;
|
||
|
}
|
||
|
|
||
|
public bool showDebugStateChanges;
|
||
|
|
||
|
[Tooltip("If true, this character can spot enemies behind itself.")]
|
||
|
public bool fullVision;
|
||
|
|
||
|
[Tooltip("If true, this AI will not be allowed to retaliate against damage received from a source on its own team.")]
|
||
|
public bool neverRetaliateFriendlies;
|
||
|
|
||
|
public float enemyAttentionDuration = 5f;
|
||
|
|
||
|
public MapNodeGroup.GraphType desiredSpawnNodeGraphType;
|
||
|
|
||
|
[Tooltip("The state machine to run while the body exists.")]
|
||
|
public EntityStateMachine stateMachine;
|
||
|
|
||
|
public SerializableEntityStateType scanState;
|
||
|
|
||
|
public bool isHealer;
|
||
|
|
||
|
public float enemyAttention;
|
||
|
|
||
|
public float aimVectorDampTime = 0.2f;
|
||
|
|
||
|
public float aimVectorMaxSpeed = 6f;
|
||
|
|
||
|
private float aimVelocity;
|
||
|
|
||
|
private float targetRefreshTimer;
|
||
|
|
||
|
private const float targetRefreshInterval = 0.5f;
|
||
|
|
||
|
public LocalNavigator localNavigator = new LocalNavigator();
|
||
|
|
||
|
public string selectedSkilldriverName;
|
||
|
|
||
|
private const float maxVisionDistance = float.PositiveInfinity;
|
||
|
|
||
|
[Tooltip("If true and this AI remains off-screen from players until its first shot, that shot will miss. The desired skill must handle this behaviour itself. If not this will do nothing")]
|
||
|
public bool shouldMissFirstOffScreenShot;
|
||
|
|
||
|
private float offScreenUpdateTimer;
|
||
|
|
||
|
private const float offScreenUpdateInterval = 0.25f;
|
||
|
|
||
|
public static readonly NodeGraphNavigationSystem nodeGraphNavigationSystem = new NodeGraphNavigationSystem();
|
||
|
|
||
|
private BroadNavigationSystem _broadNavigationSystem;
|
||
|
|
||
|
private BroadNavigationSystem.Agent _broadNavigationAgent = BroadNavigationSystem.Agent.invalid;
|
||
|
|
||
|
public bool forceUpdateEveryFrame;
|
||
|
|
||
|
public HurtBox debugEnemyHurtBox;
|
||
|
|
||
|
private BullseyeSearch enemySearch = new BullseyeSearch();
|
||
|
|
||
|
private BullseyeSearch buddySearch = new BullseyeSearch();
|
||
|
|
||
|
private float skillDriverUpdateTimer;
|
||
|
|
||
|
private const float skillDriverMinUpdateInterval = 1f / 6f;
|
||
|
|
||
|
private const float skillDriverMaxUpdateInterval = 0.2f;
|
||
|
|
||
|
public SkillDriverEvaluation skillDriverEvaluation;
|
||
|
|
||
|
protected BodyInputs bodyInputs;
|
||
|
|
||
|
public CharacterMaster master { get; protected set; }
|
||
|
|
||
|
public CharacterBody body { get; protected set; }
|
||
|
|
||
|
public Transform bodyTransform { get; protected set; }
|
||
|
|
||
|
public CharacterDirection bodyCharacterDirection { get; protected set; }
|
||
|
|
||
|
public CharacterMotor bodyCharacterMotor { get; protected set; }
|
||
|
|
||
|
public InputBankTest bodyInputBank { get; protected set; }
|
||
|
|
||
|
public HealthComponent bodyHealthComponent { get; protected set; }
|
||
|
|
||
|
public SkillLocator bodySkillLocator { get; protected set; }
|
||
|
|
||
|
public NetworkIdentity networkIdentity { get; protected set; }
|
||
|
|
||
|
public AISkillDriver[] skillDrivers { get; protected set; }
|
||
|
|
||
|
public bool hasAimConfirmation { get; private set; }
|
||
|
|
||
|
public bool hasAimTarget
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
if (skillDriverEvaluation.aimTarget != null)
|
||
|
{
|
||
|
return skillDriverEvaluation.aimTarget.gameObject;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private BroadNavigationSystem broadNavigationSystem
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
return _broadNavigationSystem;
|
||
|
}
|
||
|
set
|
||
|
{
|
||
|
if (_broadNavigationSystem != value)
|
||
|
{
|
||
|
_broadNavigationSystem?.ReturnAgent(ref _broadNavigationAgent);
|
||
|
_broadNavigationSystem = value;
|
||
|
_broadNavigationSystem?.RequestAgent(ref _broadNavigationAgent);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public BroadNavigationSystem.Agent broadNavigationAgent => _broadNavigationAgent;
|
||
|
|
||
|
public bool AlwaysUpdate => forceUpdateEveryFrame;
|
||
|
|
||
|
public Target currentEnemy { get; private set; }
|
||
|
|
||
|
public Target leader { get; private set; }
|
||
|
|
||
|
private Target buddy { get; set; }
|
||
|
|
||
|
public Target customTarget { get; private set; }
|
||
|
|
||
|
public event Action<CharacterBody> onBodyDiscovered;
|
||
|
|
||
|
public event Action<CharacterBody> onBodyLost;
|
||
|
|
||
|
[ContextMenu("Toggle broad navigation debug drawing")]
|
||
|
private void ToggleBroadNavigationDebugDraw()
|
||
|
{
|
||
|
if (broadNavigationSystem is NodeGraphNavigationSystem)
|
||
|
{
|
||
|
NodeGraphNavigationSystem.Agent agent = (NodeGraphNavigationSystem.Agent)broadNavigationAgent;
|
||
|
agent.drawPath = !agent.drawPath;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void SetBroadNavigationDebugDrawEnabled(bool newBroadNavigationDebugDrawEnabled)
|
||
|
{
|
||
|
if (broadNavigationSystem is NodeGraphNavigationSystem)
|
||
|
{
|
||
|
NodeGraphNavigationSystem.Agent agent = (NodeGraphNavigationSystem.Agent)broadNavigationAgent;
|
||
|
agent.drawPath = newBroadNavigationDebugDrawEnabled;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected void Awake()
|
||
|
{
|
||
|
targetRefreshTimer = 0.5f;
|
||
|
master = GetComponent<CharacterMaster>();
|
||
|
stateMachine = GetComponent<EntityStateMachine>();
|
||
|
stateMachine.enabled = false;
|
||
|
networkIdentity = GetComponent<NetworkIdentity>();
|
||
|
skillDrivers = GetComponents<AISkillDriver>();
|
||
|
currentEnemy = new Target(this);
|
||
|
leader = new Target(this);
|
||
|
buddy = new Target(this);
|
||
|
customTarget = new Target(this);
|
||
|
broadNavigationSystem = nodeGraphNavigationSystem;
|
||
|
}
|
||
|
|
||
|
protected void OnDestroy()
|
||
|
{
|
||
|
broadNavigationSystem = null;
|
||
|
}
|
||
|
|
||
|
protected void Start()
|
||
|
{
|
||
|
if (!Util.HasEffectiveAuthority(networkIdentity))
|
||
|
{
|
||
|
base.enabled = false;
|
||
|
if ((bool)stateMachine)
|
||
|
{
|
||
|
stateMachine.enabled = false;
|
||
|
}
|
||
|
}
|
||
|
if (NetworkServer.active)
|
||
|
{
|
||
|
skillDriverUpdateTimer = UnityEngine.Random.value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected void FixedUpdate()
|
||
|
{
|
||
|
ManagedFixedUpdate(Time.fixedDeltaTime);
|
||
|
}
|
||
|
|
||
|
public void ManagedFixedUpdate(float deltaTime)
|
||
|
{
|
||
|
enemyAttention -= deltaTime;
|
||
|
targetRefreshTimer -= deltaTime;
|
||
|
skillDriverUpdateTimer -= deltaTime;
|
||
|
if ((bool)body)
|
||
|
{
|
||
|
if (shouldMissFirstOffScreenShot)
|
||
|
{
|
||
|
offScreenUpdateTimer -= deltaTime;
|
||
|
if (offScreenUpdateTimer <= 0f)
|
||
|
{
|
||
|
offScreenUpdateTimer = 0.25f;
|
||
|
shouldMissFirstOffScreenShot = OffScreenMissHelper.IsPositionOffScreen(body.transform.position);
|
||
|
}
|
||
|
}
|
||
|
broadNavigationAgent.ConfigureFromBody(body);
|
||
|
if (skillDriverUpdateTimer <= 0f)
|
||
|
{
|
||
|
if ((bool)skillDriverEvaluation.dominantSkillDriver && skillDriverEvaluation.dominantSkillDriver.resetCurrentEnemyOnNextDriverSelection)
|
||
|
{
|
||
|
currentEnemy.Reset();
|
||
|
targetRefreshTimer = 0f;
|
||
|
}
|
||
|
if (!currentEnemy.gameObject && targetRefreshTimer <= 0f)
|
||
|
{
|
||
|
targetRefreshTimer = 0.5f;
|
||
|
HurtBox hurtBox = null;
|
||
|
hurtBox = FindEnemyHurtBox(float.PositiveInfinity, fullVision, filterByLoS: true);
|
||
|
if ((bool)hurtBox && (bool)hurtBox.healthComponent)
|
||
|
{
|
||
|
currentEnemy.gameObject = hurtBox.healthComponent.gameObject;
|
||
|
currentEnemy.bestHurtBox = hurtBox;
|
||
|
}
|
||
|
if ((bool)currentEnemy.gameObject)
|
||
|
{
|
||
|
enemyAttention = enemyAttentionDuration;
|
||
|
}
|
||
|
}
|
||
|
BeginSkillDriver(EvaluateSkillDrivers());
|
||
|
}
|
||
|
}
|
||
|
_broadNavigationAgent.currentPosition = GetNavigationStartPos();
|
||
|
UpdateBodyInputs();
|
||
|
UpdateBodyAim(deltaTime);
|
||
|
debugEnemyHurtBox = currentEnemy.bestHurtBox;
|
||
|
}
|
||
|
|
||
|
private void BeginSkillDriver(SkillDriverEvaluation newSkillDriverEvaluation)
|
||
|
{
|
||
|
if ((bool)skillDriverEvaluation.dominantSkillDriver)
|
||
|
{
|
||
|
_ = skillDriverEvaluation.dominantSkillDriver.customName;
|
||
|
}
|
||
|
skillDriverEvaluation = newSkillDriverEvaluation;
|
||
|
skillDriverUpdateTimer = UnityEngine.Random.Range(1f / 6f, 0.2f);
|
||
|
if ((bool)skillDriverEvaluation.dominantSkillDriver)
|
||
|
{
|
||
|
_ = showDebugStateChanges;
|
||
|
selectedSkilldriverName = skillDriverEvaluation.dominantSkillDriver.customName;
|
||
|
if (skillDriverEvaluation.dominantSkillDriver.driverUpdateTimerOverride >= 0f)
|
||
|
{
|
||
|
skillDriverUpdateTimer = skillDriverEvaluation.dominantSkillDriver.driverUpdateTimerOverride;
|
||
|
}
|
||
|
skillDriverEvaluation.dominantSkillDriver.OnSelected();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
_ = showDebugStateChanges;
|
||
|
selectedSkilldriverName = "";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public virtual void OnBodyStart(CharacterBody newBody)
|
||
|
{
|
||
|
body = newBody;
|
||
|
bodyTransform = newBody.transform;
|
||
|
bodyCharacterDirection = newBody.GetComponent<CharacterDirection>();
|
||
|
bodyCharacterMotor = newBody.GetComponent<CharacterMotor>();
|
||
|
bodyInputBank = newBody.GetComponent<InputBankTest>();
|
||
|
bodyHealthComponent = newBody.GetComponent<HealthComponent>();
|
||
|
bodySkillLocator = newBody.GetComponent<SkillLocator>();
|
||
|
localNavigator.SetBody(newBody);
|
||
|
_broadNavigationAgent.enabled = true;
|
||
|
if ((bool)stateMachine && Util.HasEffectiveAuthority(networkIdentity))
|
||
|
{
|
||
|
stateMachine.enabled = true;
|
||
|
stateMachine.SetNextState(EntityStateCatalog.InstantiateState(ref scanState));
|
||
|
}
|
||
|
base.enabled = true;
|
||
|
if ((bool)bodyCharacterDirection)
|
||
|
{
|
||
|
bodyInputs.desiredAimDirection = bodyCharacterDirection.forward;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
bodyInputs.desiredAimDirection = bodyTransform.forward;
|
||
|
}
|
||
|
if ((bool)bodyInputBank)
|
||
|
{
|
||
|
bodyInputBank.aimDirection = bodyInputs.desiredAimDirection;
|
||
|
}
|
||
|
this.onBodyDiscovered?.Invoke(newBody);
|
||
|
}
|
||
|
|
||
|
public virtual void OnBodyDeath(CharacterBody characterBody)
|
||
|
{
|
||
|
OnBodyLost(characterBody);
|
||
|
}
|
||
|
|
||
|
public virtual void OnBodyDestroyed(CharacterBody characterBody)
|
||
|
{
|
||
|
OnBodyLost(characterBody);
|
||
|
}
|
||
|
|
||
|
public virtual void OnBodyLost(CharacterBody characterBody)
|
||
|
{
|
||
|
if ((object)body != null)
|
||
|
{
|
||
|
base.enabled = false;
|
||
|
body = null;
|
||
|
bodyTransform = null;
|
||
|
bodyCharacterDirection = null;
|
||
|
bodyCharacterMotor = null;
|
||
|
bodyInputBank = null;
|
||
|
bodyHealthComponent = null;
|
||
|
bodySkillLocator = null;
|
||
|
localNavigator.SetBody(null);
|
||
|
_broadNavigationAgent.enabled = false;
|
||
|
if ((bool)stateMachine)
|
||
|
{
|
||
|
stateMachine.enabled = false;
|
||
|
stateMachine.SetNextState(new Idle());
|
||
|
}
|
||
|
this.onBodyLost?.Invoke(characterBody);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public virtual void OnBodyDamaged(DamageReport damageReport)
|
||
|
{
|
||
|
DamageInfo damageInfo = damageReport.damageInfo;
|
||
|
if ((bool)damageInfo.attacker && (bool)body && (!currentEnemy.gameObject || enemyAttention <= 0f) && damageInfo.attacker != body.gameObject && (!neverRetaliateFriendlies || !damageReport.isFriendlyFire))
|
||
|
{
|
||
|
currentEnemy.gameObject = damageInfo.attacker;
|
||
|
enemyAttention = enemyAttentionDuration;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[Obsolete]
|
||
|
public virtual bool HasLOS(Vector3 start, Vector3 end)
|
||
|
{
|
||
|
if (!body)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
Vector3 direction = end - start;
|
||
|
float magnitude = direction.magnitude;
|
||
|
if (body.visionDistance < magnitude)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
Ray ray = default(Ray);
|
||
|
ray.origin = start;
|
||
|
ray.direction = direction;
|
||
|
RaycastHit hitInfo;
|
||
|
return !Physics.Raycast(ray, out hitInfo, magnitude, LayerIndex.world.mask);
|
||
|
}
|
||
|
|
||
|
public virtual bool HasLOS(Vector3 end)
|
||
|
{
|
||
|
if (!body || !bodyInputBank)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
Vector3 aimOrigin = bodyInputBank.aimOrigin;
|
||
|
Vector3 direction = end - aimOrigin;
|
||
|
float magnitude = direction.magnitude;
|
||
|
if (body.visionDistance < magnitude)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
Ray ray = default(Ray);
|
||
|
ray.origin = aimOrigin;
|
||
|
ray.direction = direction;
|
||
|
RaycastHit hitInfo;
|
||
|
return !Physics.Raycast(ray, out hitInfo, magnitude, LayerIndex.world.mask);
|
||
|
}
|
||
|
|
||
|
private Vector3? GetNavigationStartPos()
|
||
|
{
|
||
|
if (!body)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
return body.footPosition;
|
||
|
}
|
||
|
|
||
|
public NodeGraph GetDesiredSpawnNodeGraph()
|
||
|
{
|
||
|
return SceneInfo.instance.GetNodeGraph(desiredSpawnNodeGraphType);
|
||
|
}
|
||
|
|
||
|
public void SetGoalPosition(Vector3? goalPos)
|
||
|
{
|
||
|
BroadNavigationSystem.Agent agent = broadNavigationAgent;
|
||
|
agent.goalPosition = goalPos;
|
||
|
}
|
||
|
|
||
|
public void SetGoalPosition(Target goalTarget)
|
||
|
{
|
||
|
Vector3? goalPosition = null;
|
||
|
if (goalTarget.GetBullseyePosition(out var position))
|
||
|
{
|
||
|
Vector3 value = position;
|
||
|
goalPosition = value;
|
||
|
}
|
||
|
SetGoalPosition(goalPosition);
|
||
|
}
|
||
|
|
||
|
public void DebugDrawPath(Color color, float duration)
|
||
|
{
|
||
|
if (broadNavigationSystem is NodeGraphNavigationSystem)
|
||
|
{
|
||
|
((NodeGraphNavigationSystem.Agent)broadNavigationAgent).DebugDrawPath(color, duration);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static bool CheckLoS(Vector3 start, Vector3 end)
|
||
|
{
|
||
|
Vector3 direction = end - start;
|
||
|
RaycastHit hitInfo;
|
||
|
return !Physics.Raycast(start, direction, out hitInfo, direction.magnitude, LayerIndex.world.mask, QueryTriggerInteraction.Ignore);
|
||
|
}
|
||
|
|
||
|
public HurtBox GetBestHurtBox(GameObject target)
|
||
|
{
|
||
|
HurtBoxGroup hurtBoxGroup = target.GetComponent<CharacterBody>()?.hurtBoxGroup;
|
||
|
if ((bool)hurtBoxGroup && hurtBoxGroup.bullseyeCount > 1 && (bool)bodyInputBank)
|
||
|
{
|
||
|
Vector3 aimOrigin = bodyInputBank.aimOrigin;
|
||
|
HurtBox hurtBox = null;
|
||
|
float num = float.PositiveInfinity;
|
||
|
HurtBox[] hurtBoxes = hurtBoxGroup.hurtBoxes;
|
||
|
foreach (HurtBox hurtBox2 in hurtBoxes)
|
||
|
{
|
||
|
if (!hurtBox2.isBullseye)
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
Vector3 position = hurtBox2.transform.position;
|
||
|
if (CheckLoS(aimOrigin, hurtBox2.transform.position))
|
||
|
{
|
||
|
float sqrMagnitude = (position - aimOrigin).sqrMagnitude;
|
||
|
if (sqrMagnitude < num)
|
||
|
{
|
||
|
num = sqrMagnitude;
|
||
|
hurtBox = hurtBox2;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if ((bool)hurtBox)
|
||
|
{
|
||
|
return hurtBox;
|
||
|
}
|
||
|
}
|
||
|
return Util.FindBodyMainHurtBox(target);
|
||
|
}
|
||
|
|
||
|
public void ForceAcquireNearestEnemyIfNoCurrentEnemy()
|
||
|
{
|
||
|
if ((bool)currentEnemy.gameObject)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
if (!body)
|
||
|
{
|
||
|
Debug.LogErrorFormat("BaseAI.ForceAcquireNearestEnemyIfNoCurrentEnemy for CharacterMaster '{0}' failed: AI has no body to search from.", base.gameObject.name);
|
||
|
return;
|
||
|
}
|
||
|
HurtBox hurtBox = FindEnemyHurtBox(float.PositiveInfinity, full360Vision: true, filterByLoS: false);
|
||
|
if ((bool)hurtBox && (bool)hurtBox.healthComponent)
|
||
|
{
|
||
|
currentEnemy.gameObject = hurtBox.healthComponent.gameObject;
|
||
|
currentEnemy.bestHurtBox = hurtBox;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private HurtBox FindEnemyHurtBox(float maxDistance, bool full360Vision, bool filterByLoS)
|
||
|
{
|
||
|
if (!body)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
enemySearch.viewer = body;
|
||
|
enemySearch.teamMaskFilter = TeamMask.allButNeutral;
|
||
|
enemySearch.teamMaskFilter.RemoveTeam(master.teamIndex);
|
||
|
enemySearch.sortMode = BullseyeSearch.SortMode.Distance;
|
||
|
enemySearch.minDistanceFilter = 0f;
|
||
|
enemySearch.maxDistanceFilter = maxDistance;
|
||
|
enemySearch.searchOrigin = bodyInputBank.aimOrigin;
|
||
|
enemySearch.searchDirection = bodyInputBank.aimDirection;
|
||
|
enemySearch.maxAngleFilter = (full360Vision ? 180f : 90f);
|
||
|
enemySearch.filterByLoS = filterByLoS;
|
||
|
enemySearch.RefreshCandidates();
|
||
|
return enemySearch.GetResults().FirstOrDefault();
|
||
|
}
|
||
|
|
||
|
public bool GameObjectPassesSkillDriverFilters(Target target, AISkillDriver skillDriver, out float separationSqrMagnitude)
|
||
|
{
|
||
|
separationSqrMagnitude = 0f;
|
||
|
if (!target.gameObject)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
float num = 1f;
|
||
|
if ((bool)target.healthComponent)
|
||
|
{
|
||
|
num = target.healthComponent.combinedHealthFraction;
|
||
|
}
|
||
|
if (num < skillDriver.minTargetHealthFraction || num > skillDriver.maxTargetHealthFraction)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
float num2 = 0f;
|
||
|
if ((bool)body)
|
||
|
{
|
||
|
num2 = body.radius;
|
||
|
}
|
||
|
float num3 = 0f;
|
||
|
if ((bool)target.characterBody)
|
||
|
{
|
||
|
num3 = target.characterBody.radius;
|
||
|
}
|
||
|
Vector3 vector = (bodyInputBank ? bodyInputBank.aimOrigin : bodyTransform.position);
|
||
|
target.GetBullseyePosition(out var position);
|
||
|
float sqrMagnitude = (position - vector).sqrMagnitude;
|
||
|
separationSqrMagnitude = sqrMagnitude - num3 * num3 - num2 * num2;
|
||
|
if (separationSqrMagnitude < skillDriver.minDistanceSqr || separationSqrMagnitude > skillDriver.maxDistanceSqr)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
if (skillDriver.selectionRequiresTargetLoS && !target.hasLoS)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private void UpdateTargets()
|
||
|
{
|
||
|
currentEnemy.Update();
|
||
|
leader.Update();
|
||
|
}
|
||
|
|
||
|
protected SkillDriverEvaluation? EvaluateSingleSkillDriver(in SkillDriverEvaluation currentSkillDriverEvaluation, AISkillDriver aiSkillDriver, float myHealthFraction)
|
||
|
{
|
||
|
if (!body || !bodySkillLocator)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
float separationSqrMagnitude = float.PositiveInfinity;
|
||
|
if (aiSkillDriver.noRepeat && currentSkillDriverEvaluation.dominantSkillDriver == aiSkillDriver)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
if (aiSkillDriver.maxTimesSelected >= 0 && aiSkillDriver.timesSelected >= aiSkillDriver.maxTimesSelected)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
Target target = null;
|
||
|
if (aiSkillDriver.requireEquipmentReady && body.equipmentSlot.stock <= 0)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
if (aiSkillDriver.skillSlot != SkillSlot.None)
|
||
|
{
|
||
|
GenericSkill skill = bodySkillLocator.GetSkill(aiSkillDriver.skillSlot);
|
||
|
if (aiSkillDriver.requireSkillReady && (!skill || !skill.IsReady()))
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
if ((bool)aiSkillDriver.requiredSkill && (!skill || !(skill.skillDef == aiSkillDriver.requiredSkill)))
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
if (aiSkillDriver.minUserHealthFraction > myHealthFraction || aiSkillDriver.maxUserHealthFraction < myHealthFraction)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
if ((bool)bodyCharacterMotor && !bodyCharacterMotor.isGrounded && aiSkillDriver.selectionRequiresOnGround)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
switch (aiSkillDriver.moveTargetType)
|
||
|
{
|
||
|
case AISkillDriver.TargetType.CurrentEnemy:
|
||
|
if (GameObjectPassesSkillDriverFilters(currentEnemy, aiSkillDriver, out separationSqrMagnitude))
|
||
|
{
|
||
|
target = currentEnemy;
|
||
|
}
|
||
|
break;
|
||
|
case AISkillDriver.TargetType.NearestFriendlyInSkillRange:
|
||
|
if ((bool)bodyInputBank)
|
||
|
{
|
||
|
buddySearch.teamMaskFilter = TeamMask.none;
|
||
|
buddySearch.teamMaskFilter.AddTeam(master.teamIndex);
|
||
|
buddySearch.sortMode = BullseyeSearch.SortMode.Distance;
|
||
|
buddySearch.minDistanceFilter = aiSkillDriver.minDistanceSqr;
|
||
|
buddySearch.maxDistanceFilter = aiSkillDriver.maxDistance;
|
||
|
buddySearch.searchOrigin = bodyInputBank.aimOrigin;
|
||
|
buddySearch.searchDirection = bodyInputBank.aimDirection;
|
||
|
buddySearch.maxAngleFilter = 180f;
|
||
|
buddySearch.filterByLoS = aiSkillDriver.activationRequiresTargetLoS;
|
||
|
buddySearch.RefreshCandidates();
|
||
|
if ((bool)body)
|
||
|
{
|
||
|
buddySearch.FilterOutGameObject(body.gameObject);
|
||
|
}
|
||
|
buddySearch.FilterCandidatesByHealthFraction(aiSkillDriver.minTargetHealthFraction, aiSkillDriver.maxTargetHealthFraction);
|
||
|
HurtBox hurtBox = buddySearch.GetResults().FirstOrDefault();
|
||
|
if ((bool)hurtBox && (bool)hurtBox.healthComponent)
|
||
|
{
|
||
|
buddy.gameObject = hurtBox.healthComponent.gameObject;
|
||
|
buddy.bestHurtBox = hurtBox;
|
||
|
}
|
||
|
if (GameObjectPassesSkillDriverFilters(buddy, aiSkillDriver, out separationSqrMagnitude))
|
||
|
{
|
||
|
target = buddy;
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case AISkillDriver.TargetType.CurrentLeader:
|
||
|
if (GameObjectPassesSkillDriverFilters(leader, aiSkillDriver, out separationSqrMagnitude))
|
||
|
{
|
||
|
target = leader;
|
||
|
}
|
||
|
break;
|
||
|
case AISkillDriver.TargetType.Custom:
|
||
|
if (GameObjectPassesSkillDriverFilters(customTarget, aiSkillDriver, out separationSqrMagnitude))
|
||
|
{
|
||
|
target = customTarget;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
if (target == null)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
Target target2 = null;
|
||
|
if (aiSkillDriver.aimType != 0)
|
||
|
{
|
||
|
bool flag = aiSkillDriver.selectionRequiresAimTarget;
|
||
|
switch (aiSkillDriver.aimType)
|
||
|
{
|
||
|
case AISkillDriver.AimType.AtMoveTarget:
|
||
|
target2 = target;
|
||
|
break;
|
||
|
case AISkillDriver.AimType.AtCurrentEnemy:
|
||
|
target2 = currentEnemy;
|
||
|
break;
|
||
|
case AISkillDriver.AimType.AtCurrentLeader:
|
||
|
target2 = leader;
|
||
|
break;
|
||
|
default:
|
||
|
flag = false;
|
||
|
break;
|
||
|
}
|
||
|
if (flag && (target2 == null || !target2.gameObject))
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
SkillDriverEvaluation value = default(SkillDriverEvaluation);
|
||
|
value.dominantSkillDriver = aiSkillDriver;
|
||
|
value.target = target;
|
||
|
value.aimTarget = target2;
|
||
|
value.separationSqrMagnitude = separationSqrMagnitude;
|
||
|
return value;
|
||
|
}
|
||
|
|
||
|
public SkillDriverEvaluation EvaluateSkillDrivers()
|
||
|
{
|
||
|
UpdateTargets();
|
||
|
float myHealthFraction = 1f;
|
||
|
if ((bool)bodyHealthComponent)
|
||
|
{
|
||
|
myHealthFraction = bodyHealthComponent.combinedHealthFraction;
|
||
|
}
|
||
|
if ((bool)bodySkillLocator)
|
||
|
{
|
||
|
if ((bool)this.skillDriverEvaluation.dominantSkillDriver && (bool)this.skillDriverEvaluation.dominantSkillDriver.nextHighPriorityOverride)
|
||
|
{
|
||
|
SkillDriverEvaluation? skillDriverEvaluation = EvaluateSingleSkillDriver(in this.skillDriverEvaluation, this.skillDriverEvaluation.dominantSkillDriver.nextHighPriorityOverride, myHealthFraction);
|
||
|
if (skillDriverEvaluation.HasValue)
|
||
|
{
|
||
|
return skillDriverEvaluation.Value;
|
||
|
}
|
||
|
}
|
||
|
for (int i = 0; i < skillDrivers.Length; i++)
|
||
|
{
|
||
|
AISkillDriver aISkillDriver = skillDrivers[i];
|
||
|
if (aISkillDriver.enabled)
|
||
|
{
|
||
|
SkillDriverEvaluation? skillDriverEvaluation2 = EvaluateSingleSkillDriver(in this.skillDriverEvaluation, aISkillDriver, myHealthFraction);
|
||
|
if (skillDriverEvaluation2.HasValue)
|
||
|
{
|
||
|
return skillDriverEvaluation2.Value;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return default(SkillDriverEvaluation);
|
||
|
}
|
||
|
|
||
|
protected void UpdateBodyInputs()
|
||
|
{
|
||
|
if (stateMachine.state is BaseAIState baseAIState)
|
||
|
{
|
||
|
bodyInputs = baseAIState.GenerateBodyInputs(in bodyInputs);
|
||
|
}
|
||
|
if ((bool)bodyInputBank)
|
||
|
{
|
||
|
bodyInputBank.skill1.PushState(bodyInputs.pressSkill1);
|
||
|
bodyInputBank.skill2.PushState(bodyInputs.pressSkill2);
|
||
|
bodyInputBank.skill3.PushState(bodyInputs.pressSkill3);
|
||
|
bodyInputBank.skill4.PushState(bodyInputs.pressSkill4);
|
||
|
bodyInputBank.jump.PushState(bodyInputs.pressJump);
|
||
|
bodyInputBank.sprint.PushState(bodyInputs.pressSprint);
|
||
|
bodyInputBank.activateEquipment.PushState(bodyInputs.pressActivateEquipment);
|
||
|
bodyInputBank.moveVector = bodyInputs.moveVector;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected void UpdateBodyAim(float deltaTime)
|
||
|
{
|
||
|
hasAimConfirmation = false;
|
||
|
if ((bool)bodyInputBank)
|
||
|
{
|
||
|
Vector3 aimDirection = bodyInputBank.aimDirection;
|
||
|
Vector3 desiredAimDirection = bodyInputs.desiredAimDirection;
|
||
|
if (desiredAimDirection != Vector3.zero)
|
||
|
{
|
||
|
Quaternion target = Util.QuaternionSafeLookRotation(desiredAimDirection);
|
||
|
Vector3 vector = Util.SmoothDampQuaternion(Util.QuaternionSafeLookRotation(aimDirection), maxSpeed: aimVectorMaxSpeed, target: target, currentVelocity: ref aimVelocity, smoothTime: aimVectorDampTime, deltaTime: deltaTime) * Vector3.forward;
|
||
|
bodyInputBank.aimDirection = vector;
|
||
|
hasAimConfirmation = Vector3.Dot(vector, desiredAimDirection) >= 0.95f;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[ConCommand(commandName = "ai_draw_path", flags = ConVarFlags.Cheat, helpText = "Enables or disables the drawing of the specified AI's broad navigation path. Format: ai_draw_path <CharacterMaster selector> <0/1>")]
|
||
|
private static void CCAiDrawPath(ConCommandArgs args)
|
||
|
{
|
||
|
CharacterMaster argCharacterMasterInstance = args.GetArgCharacterMasterInstance(0);
|
||
|
args.GetArgBool(1);
|
||
|
if (!argCharacterMasterInstance)
|
||
|
{
|
||
|
throw new ConCommandException("Could not find target.");
|
||
|
}
|
||
|
BaseAI component = argCharacterMasterInstance.GetComponent<BaseAI>();
|
||
|
if (!component)
|
||
|
{
|
||
|
throw new ConCommandException("Target has no AI.");
|
||
|
}
|
||
|
component.ToggleBroadNavigationDebugDraw();
|
||
|
}
|
||
|
}
|