5 using FarseerPhysics.Dynamics;
6 using Microsoft.Xna.Framework;
8 using System.Collections.Generic;
14 public enum AIState {
Idle,
Attack,
Escape,
Eat,
Flee,
Avoid,
Aggressive,
PassiveAggressive,
Protect,
Observe,
Freeze,
Follow,
FleeTo,
Patrol,
PlayDead,
HideTo,
Hiding }
45 get {
return _state; }
48 if (_state == value) {
return; }
67 private readonly
float updateTargetsInterval = 1;
68 private readonly
float updateMemoriesInverval = 1;
69 private readonly
float attackLimbSelectionInterval = 3;
71 private const float minPriority = 10;
76 private float updateTargetsTimer;
77 private float updateMemoriesTimer;
78 private float attackLimbSelectionTimer;
83 private float Sight => GetPerceptionRange(
AIParams.Sight);
84 private float Hearing => GetPerceptionRange(
AIParams.Hearing);
86 private float GetPerceptionRange(
float range)
102 private float FleeHealthThreshold =>
AIParams.FleeHealthThreshold;
103 private bool IsAggressiveBoarder =>
AIParams.AggressiveBoarding;
107 private Limb _attackLimb;
108 private Limb _previousAttackLimb;
111 get {
return _attackLimb; }
114 if (_attackLimb != value)
116 _previousAttackLimb = _attackLimb;
117 if (_previousAttackLimb !=
null)
120 if (_previousAttackLimb.attack.SnapRopeOnNewAttack)
122 _previousAttackLimb.AttachedRope?.Snap();
126 else if (_attackLimb !=
null && _attackLimb.attack.CoolDownTimer <= 0)
129 if (_attackLimb.attack.SnapRopeOnNewAttack)
131 _attackLimb.AttachedRope?.Snap();
136 Reverse = _attackLimb !=
null && _attackLimb.attack.Reverse;
140 private double lastAttackUpdateTime;
142 private Attack _activeAttack;
147 if (_activeAttack ==
null) {
return null; }
148 return lastAttackUpdateTime > Timing.TotalTime - _activeAttack.
Duration ? _activeAttack :
null;
152 _activeAttack = value;
153 lastAttackUpdateTime = Timing.TotalTime;
159 private float targetValue;
162 private Dictionary<AITarget, AITargetMemory> targetMemories;
164 private readonly
int requiredHoleCount;
165 private bool canAttackWalls;
167 private bool canAttackDoors;
168 private bool canAttackItems;
169 private bool canAttackCharacters;
172 private readonly
float priorityFearIncreasement = 2;
173 private readonly
float memoryFadeTime = 0.5f;
175 private float avoidTimer;
176 private float observeTimer;
177 private float sweepTimer;
178 private float circleRotation;
179 private float circleDir;
180 private bool inverseDir;
181 private bool breakCircling;
182 private float circleRotationSpeed;
183 private Vector2 circleOffset;
184 private float circleFallbackDistance;
185 private float strikeTimer;
186 private float aggressionIntensity;
188 private float currentAttackIntensity;
190 private float playDeadTimer;
194 private const float PlayDeadCoolDown = 60;
198 private readonly List<Body> myBodies;
210 return GetTargetParams(Tags.Human).Any(
static tp => tp is { Priority: > 0.0f,
State:
AIState.Attack or
AIState.Aggressive });
218 return GetTargetParams(Tags.Room).Any(
static tp => tp is { Priority: > 0.0f,
State:
AIState.Attack or
AIState.Aggressive });
251 } =
new HashSet<Submarine>();
258 private static bool IsTargetInPlayerTeam(
AITarget target) => target?.Entity?.Submarine !=
null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is
Character { IsOnPlayerTeam:
true };
260 private bool IsAttackingOwner(
Character other) =>
267 private bool reverse;
270 get {
return reverse; }
281 private readonly
float maxSteeringBuffer = 5000;
282 private readonly
float minSteeringBuffer = 500;
283 private readonly
float steeringBufferIncreaseSpeed = 100;
284 private float steeringBuffer;
290 throw new Exception($
"Tried to create an enemy ai controller for human!");
293 targetMemories =
new Dictionary<AITarget, AITargetMemory>();
301 List<XElement> aiElements =
new List<XElement>();
302 List<float> aiCommonness =
new List<float>();
303 foreach (var element
in mainElement.Elements())
305 if (!element.Name.ToString().Equals(
"ai", StringComparison.OrdinalIgnoreCase)) {
continue; }
306 aiElements.Add(element);
307 aiCommonness.Add(element.GetAttributeFloat(
"commonness", 1.0f));
310 if (aiElements.Count == 0)
312 DebugConsole.ThrowError(
"Error in file \"" + c.Params.File.Path +
"\" - no AI element found.",
313 contentPackage: c.Prefab?.ContentPackage);
315 insideSteering =
new IndoorsSteeringManager(
this,
false,
false);
320 MTRandom random =
new MTRandom(ToolBox.StringToInt(seed));
321 XElement aiElement = aiElements.Count == 1 ? aiElements[0] : ToolBox.SelectWeightedRandom(aiElements, aiCommonness, random);
322 foreach (var subElement
in aiElement.Elements())
324 switch (subElement.Name.ToString().ToLowerInvariant())
327 var subElements = subElement.Elements();
328 if (subElements.Any())
330 LoadSubElement(subElements.ToArray().GetRandom(random));
334 LoadSubElement(subElement);
339 void LoadSubElement(XElement subElement)
341 switch (subElement.Name.ToString().ToLowerInvariant())
347 case "swarmbehavior":
362 insideSteering =
new IndoorsSteeringManager(
this,
AIParams.CanOpenDoors, canAttackDoors);
365 requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(
colliderWidth) /
Structure.WallSectionSize);
368 if (
AIParams.PlayDeadProbability > 0)
375 private CharacterParams.AIParams _aiParams;
384 if (_aiParams ==
null)
387 if (_aiParams ==
null)
389 DebugConsole.ThrowError($
"No AI Params defined for {Character.SpeciesName}. AI disabled.",
402 foreach (Identifier tag
in targetingTags)
404 foreach (var tp
in GetTargetParams(tag))
411 private readonly List<Identifier> _targetingTags =
new List<Identifier>();
413 private IEnumerable<Identifier> GetTargetingTags(AITarget aiTarget)
415 _targetingTags.Clear();
416 if (aiTarget?.Entity ==
null) {
return _targetingTags; }
417 if (aiTarget.Entity is Character targetCharacter)
419 if (targetCharacter.IsDead)
421 _targetingTags.Add(Tags.Dead);
423 else if (AIParams.TryGetHighestPriorityTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >=
Character.GetDamageDoneByAttacker(targetCharacter))
425 _targetingTags.Add(tp.Tag);
427 else if (PetBehavior !=
null && aiTarget.Entity == PetBehavior.Owner)
429 _targetingTags.Add(Tags.Owner);
431 else if (PetBehavior !=
null && (!
Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter)))
433 _targetingTags.Add(Tags.Hostile);
435 else if (AIParams.TryGetHighestPriorityTarget(targetCharacter, out CharacterParams.TargetParams tP))
437 _targetingTags.Add(tP.Tag);
439 else if (targetCharacter.AIController is EnemyAIController enemy)
441 if (enemy.PetBehavior !=
null && (PetBehavior !=
null || AIParams.HasTag(Tags.Pet)))
445 _targetingTags.Add(Tags.Pet);
447 else if (targetCharacter.IsHusk && AIParams.HasTag(Tags.Husk))
449 _targetingTags.Add(Tags.Husk);
451 else if (!
Character.IsSameSpeciesOrGroup(targetCharacter))
453 if (enemy.CombatStrength > CombatStrength)
455 _targetingTags.Add(Tags.Stronger);
457 else if (enemy.CombatStrength < CombatStrength)
459 _targetingTags.Add(Tags.Weaker);
463 _targetingTags.Add(Tags.Equal);
468 else if (aiTarget.Entity is
Item targetItem)
470 foreach (var prio
in AIParams.Targets)
472 if (targetItem.HasTag(prio.Tag))
474 _targetingTags.Add(prio.Tag);
477 if (_targetingTags.None())
479 if (targetItem.GetComponent<
Sonar>() !=
null)
481 _targetingTags.Add(Tags.Sonar);
483 if (targetItem.GetComponent<
Door>() !=
null)
485 _targetingTags.Add(Tags.Door);
489 else if (aiTarget.Entity is Structure)
491 _targetingTags.Add(Tags.Wall);
493 else if (aiTarget.Entity is Hull)
495 _targetingTags.Add(Tags.Room);
497 return _targetingTags;
504 SelectedAiTarget = target;
505 currentTargetMemory = GetTargetMemory(target, addIfNotFound:
true);
506 currentTargetMemory.
Priority = priority;
507 ignoredTargets.Remove(target);
510 private float movementMargin;
512 private void ReleaseDragTargets()
514 AttackLimb?.AttachedRope?.Snap();
523 if (probability.HasValue)
525 AIParams.PlayDeadProbability = probability.Value;
530 public override void Update(
float deltaTime)
532 if (DisableEnemyAI) {
return; }
533 base.Update(deltaTime);
534 UpdateTriggers(deltaTime);
536 IsTryingToSteerThroughGap =
false;
543 if (steeringManager == insideSteering)
546 if (currPath !=
null && currPath.CurrentNode !=
null)
553 ignorePlatforms = height < allowedJumpHeight;
566 if (SelectedAiTarget?.
Entity !=
null || EscapeTarget !=
null)
581 stateResetTimer -= deltaTime;
582 if (stateResetTimer <= 0)
584 ResetOriginalState();
588 if (targetIgnoreTimer > 0)
590 targetIgnoreTimer -= deltaTime;
594 ignoredTargets.Clear();
595 targetIgnoreTimer = targetIgnoreTime;
597 avoidTimer -= deltaTime;
602 UpdateCurrentMemoryLocation();
603 if (updateMemoriesTimer > 0)
605 updateMemoriesTimer -= deltaTime;
609 FadeMemories(updateMemoriesInverval);
610 updateMemoriesTimer = updateMemoriesInverval;
615 if (target ==
null && SelectedAiTarget.
Entity is
Item targetItem)
617 target = GetOwner(targetItem);
619 bool shouldFlee =
false;
623 shouldFlee = target.
IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target);
630 SelectedAiTarget =
null;
631 _lastAiTarget =
null;
636 if (TargetingRestrictions != previousTargetingRestrictions)
638 previousTargetingRestrictions = TargetingRestrictions;
640 updateTargetsTimer = 0;
641 SelectedAiTarget =
null;
644 if (updateTargetsTimer > 0)
646 updateTargetsTimer -= deltaTime;
648 else if (avoidTimer <= 0 || activeTriggers.Any() && returnTimer <= 0)
657 bool IsCloseEnoughToTargetSub(
float threshold) => SelectedAiTarget?.Entity?.Submarine is
Submarine sub && sub !=
null && Vector2.DistanceSquared(
Character.
WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
659 if (
Character.
Submarine !=
null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer))
661 if (steeringManager != insideSteering)
663 insideSteering.
Reset();
665 steeringManager = insideSteering;
666 steeringBuffer += steeringBufferIncreaseSpeed * deltaTime;
670 if (steeringManager != outsideSteering)
672 outsideSteering.
Reset();
674 steeringManager = outsideSteering;
675 steeringBuffer = minSteeringBuffer;
677 steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
684 if (steeringManager != insideSteering)
686 insideSteering.
Reset();
688 steeringManager = insideSteering;
692 if (steeringManager != outsideSteering)
694 outsideSteering.
Reset();
696 steeringManager = outsideSteering;
708 UpdateIdle(deltaTime);
714 UpdatePatrol(deltaTime);
717 run = !IsCoolDownRunning || AttackLimb !=
null && AttackLimb.attack.FullSpeedAfterAttack;
718 UpdateAttack(deltaTime);
721 UpdateEating(deltaTime);
729 case AIState.PassiveAggressive:
731 if (SelectedAiTarget?.
Entity ==
null || SelectedAiTarget.
Entity.Removed)
736 float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
737 var attackLimb = AttackLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition);
738 if (attackLimb !=
null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2))
747 UpdateAttack(deltaTime);
752 bool isBeingChased = IsBeingChased;
753 float reactDistance = !isBeingChased && currentTargetingParams is { ReactDistance: > 0 } ? currentTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
754 if (squaredDistance <= Math.Pow(reactDistance, 2))
756 float halfReactDistance = reactDistance / 2;
757 float attackDistance = currentTargetingParams is { AttackDistance: > 0 } ? currentTargetingParams.AttackDistance : halfReactDistance;
758 if (State ==
AIState.Aggressive || State ==
AIState.PassiveAggressive && squaredDistance < Math.Pow(attackDistance, 2))
761 UpdateAttack(deltaTime);
765 run = isBeingChased || squaredDistance < Math.Pow(halfReactDistance, 2);
767 avoidTimer = AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f);
772 UpdateIdle(deltaTime);
781 if (SelectedAiTarget?.
Entity ==
null || SelectedAiTarget.
Entity.Removed)
788 if (SelectedAiTarget.Entity is
Character targetCharacter)
796 if (targetCharacter.IsSameSpeciesOrGroup(c)) {
return false; }
802 return a.Damage >= currentTargetingParams.Threshold;
807 if (attacker?.AiTarget !=
null)
809 ChangeTargetState(attacker,
AIState.Attack, currentTargetingParams.Priority * 2);
812 UpdateWallTarget(requiredHoleCount);
817 float distX = Math.Abs(WorldPosition.X - SelectedAiTarget.WorldPosition.X);
818 float distY = Math.Abs(WorldPosition.Y - SelectedAiTarget.WorldPosition.Y);
824 float dist = distX + distY;
825 float reactDist = GetPerceivingRange(SelectedAiTarget);
826 Vector2 offset = Vector2.Zero;
827 if (currentTargetingParams !=
null)
829 if (currentTargetingParams.ReactDistance > 0)
831 reactDist = currentTargetingParams.ReactDistance;
833 offset = currentTargetingParams.Offset;
835 if (offset != Vector2.Zero)
837 reactDist += offset.Length();
839 if (dist > reactDist + movementMargin)
848 UpdateFollow(deltaTime);
857 movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, reactDist);
866 if (SelectedAiTarget.Entity is
Item item)
868 float rotation = item.Rotation;
880 if (disableTailCoroutine ==
null && SelectedAiTarget.Entity is
Item i && i.
HasTag(Tags.GuardianShelter))
882 if (!CoroutineManager.IsCoroutineRunning(disableTailCoroutine))
884 disableTailCoroutine = CoroutineManager.Invoke(() =>
897 new Vector2(0, -1), footMoveForce: 1);
902 UpdateIdle(deltaTime);
907 if (SelectedAiTarget?.
Entity ==
null || SelectedAiTarget.
Entity.Removed)
913 float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
914 reactDist = currentTargetingParams is { ReactDistance: > 0 } ? currentTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
915 float halfReactDist = reactDist / 2;
916 float attackDist = currentTargetingParams is { AttackDistance: > 0 } ? currentTargetingParams.AttackDistance : halfReactDist;
917 if (sqrDist > Math.Pow(reactDist, 2))
920 UpdateIdle(deltaTime);
922 else if (sqrDist < Math.Pow(attackDist + movementMargin, 2))
924 movementMargin = attackDist;
928 useSteeringLengthAsMovementSpeed =
true;
930 if (sqrDist < Math.Pow(attackDist * 0.75f, 2))
934 useSteeringLengthAsMovementSpeed =
false;
943 FaceTarget(SelectedAiTarget.Entity);
945 observeTimer -= deltaTime;
946 if (observeTimer < 0)
948 IgnoreTarget(SelectedAiTarget);
955 run = sqrDist > Math.Pow(attackDist * 2, 2);
956 movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, attackDist);
957 UpdateFollow(deltaTime);
961 throw new NotImplementedException();
968 IsSteeringThroughGap =
false;
976 SteerInsideLevel(deltaTime);
979 steeringManager.Update(Math.Max(speed, 1.0f));
980 float movementSpeed = useSteeringLengthAsMovementSpeed ?
Steering.Length() : speed;
991 private void UpdateIdle(
float deltaTime,
bool followLastTarget =
true)
995 if (playDeadTimer > 0)
997 playDeadTimer -= deltaTime;
1004 else if (AIParams.PatrolFlooded || AIParams.PatrolDry)
1008 var pathSteering = SteeringManager as IndoorsSteeringManager;
1009 if (pathSteering ==
null)
1011 if (Level.Loaded !=
null && Level.Loaded.GetRealWorldDepth(WorldPosition.Y) >
Character.CharacterHealth.CrushDepth * 0.75f)
1014 SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
1015 SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
1019 if (followLastTarget)
1021 var target = SelectedAiTarget ?? _lastAiTarget;
1022 if (target?.Entity is {
Removed:
false } &&
1024 (_previousAttackLimb?.attack ==
null ||
1025 _previousAttackLimb?.attack is
Attack previousAttack && (previousAttack.AfterAttack !=
AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
1028 var memory = GetTargetMemory(target);
1031 var location = memory.Location;
1032 float dist = Vector2.DistanceSquared(WorldPosition, location);
1033 if (dist < 50 * 50 || !IsPositionInsideAllowedZone(WorldPosition, out _))
1041 SteeringManager.SteeringSeek(
Character.GetRelativeSimPosition(target.Entity, location), 5);
1042 SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
1054 if (pathSteering !=
null && !
Character.AnimController.InWater)
1057 pathSteering.Wander(deltaTime, Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100.0f), stayStillInTightSpace:
false);
1062 steeringManager.SteeringWander(avoidWanderingOutsideLevel:
true);
1065 SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
1071 private readonly List<Hull> targetHulls =
new List<Hull>();
1072 private readonly List<float> hullWeights =
new List<float>();
1074 private Hull patrolTarget;
1075 private float newPatrolTargetTimer;
1076 private float patrolTimerMargin;
1077 private readonly
float newPatrolTargetIntervalMin = 5;
1078 private readonly
float newPatrolTargetIntervalMax = 30;
1079 private bool searchingNewHull;
1081 private void UpdatePatrol(
float deltaTime,
bool followLastTarget =
true)
1083 if (SteeringManager is IndoorsSteeringManager pathSteering)
1085 if (patrolTarget ==
null || IsCurrentPathUnreachable || IsCurrentPathFinished)
1087 newPatrolTargetTimer = Math.Min(newPatrolTargetTimer, newPatrolTargetIntervalMin);
1089 if (newPatrolTargetTimer > 0)
1091 newPatrolTargetTimer -= deltaTime;
1095 if (!searchingNewHull)
1097 searchingNewHull =
true;
1100 else if (targetHulls.Any())
1102 patrolTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
1103 var path = PathSteering.
PathFinder.
FindPath(
Character.SimPosition, patrolTarget.SimPosition,
Character.Submarine, minGapSize: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n));
1104 if (path.Unreachable)
1107 int index = targetHulls.IndexOf(patrolTarget);
1108 targetHulls.RemoveAt(index);
1109 hullWeights.RemoveAt(index);
1110 PathSteering.
Reset();
1111 patrolTarget =
null;
1112 patrolTimerMargin += 0.5f;
1113 patrolTimerMargin = Math.Min(patrolTimerMargin, newPatrolTargetIntervalMin);
1114 newPatrolTargetTimer = Math.Min(newPatrolTargetIntervalMin, patrolTimerMargin);
1118 PathSteering.
SetPath(patrolTarget.SimPosition, path);
1119 patrolTimerMargin = 0;
1120 newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f);
1121 searchingNewHull =
false;
1127 newPatrolTargetTimer = newPatrolTargetIntervalMax;
1128 searchingNewHull =
false;
1131 if (patrolTarget !=
null && pathSteering.CurrentPath !=
null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable)
1133 PathSteering.
SteeringSeek(
Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n));
1138 bool PatrolNodeFilter(PathNode n) =>
1139 AIParams.PatrolFlooded && (
Character.CurrentHull ==
null || n.Waypoint.CurrentHull ==
null || n.Waypoint.CurrentHull.WaterPercentage >= 80) ||
1140 AIParams.PatrolDry &&
Character.CurrentHull !=
null && n.Waypoint.CurrentHull !=
null && n.Waypoint.CurrentHull.WaterPercentage <= 50;
1142 UpdateIdle(deltaTime, followLastTarget);
1145 private void FindTargetHulls()
1147 if (
Character.Submarine ==
null) {
return; }
1148 if (
Character.CurrentHull ==
null) {
return; }
1149 targetHulls.Clear();
1150 hullWeights.Clear();
1151 float hullMinSize = ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth) * 2);
1152 bool checkWaterLevel = !AIParams.PatrolFlooded || !AIParams.PatrolDry;
1153 foreach (var hull
in Hull.HullList)
1155 if (hull.Submarine ==
null) {
continue; }
1156 if (hull.Submarine.TeamID !=
Character.Submarine.TeamID) {
continue; }
1157 if (!
Character.Submarine.IsConnectedTo(hull.Submarine)) {
continue; }
1158 if (hull.RectWidth < hullMinSize || hull.RectHeight < hullMinSize) {
continue; }
1159 if (checkWaterLevel)
1161 if (AIParams.PatrolDry)
1163 if (hull.WaterPercentage > 50) {
continue; }
1165 if (AIParams.PatrolFlooded)
1167 if (hull.WaterPercentage < 80) {
continue; }
1170 if (AIParams.PatrolDry && hull.WaterPercentage < 80)
1172 if (Math.Abs(
Character.CurrentHull.WorldPosition.Y - hull.WorldPosition.Y) >
Character.CurrentHull.CeilingHeight / 2)
1178 if (!targetHulls.Contains(hull))
1180 targetHulls.Add(hull);
1181 float weight = hull.Size.Combine();
1182 float dist = Vector2.Distance(
Character.WorldPosition, hull.WorldPosition);
1183 float optimal = 1000;
1186 float distanceFactor = dist > optimal ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(optimal, max, dist)) : MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, optimal, dist));
1187 float waterFactor = 1;
1188 if (checkWaterLevel)
1190 waterFactor = AIParams.PatrolDry ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage)) : MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 100, hull.WaterPercentage));
1192 weight *= distanceFactor * waterFactor;
1193 hullWeights.Add(weight);
1202 private Vector2 attackWorldPos;
1203 private Vector2 attackSimPos;
1204 private float reachTimer;
1206 private const float reachTimeOut = 10;
1208 private bool IsSameTarget(AITarget target, AITarget otherTarget)
1210 if (target?.Entity == otherTarget?.Entity) {
return true; }
1211 if (IsItemInCharacterInventory(target, otherTarget) || IsItemInCharacterInventory(otherTarget, target)) {
return true; }
1214 bool IsItemInCharacterInventory(AITarget potentialItem, AITarget potentialCharacter)
1216 if (potentialItem?.Entity is
Item item && potentialCharacter?.Entity is Character character)
1218 return item.ParentInventory?.Owner == character;
1224 private void UpdateAttack(
float deltaTime)
1226 if (SelectedAiTarget?.Entity ==
null || SelectedAiTarget.Entity.Removed || currentTargetingParams ==
null)
1232 attackWorldPos = SelectedAiTarget.WorldPosition;
1233 attackSimPos = SelectedAiTarget.SimPosition;
1235 if (SelectedAiTarget.Entity is
Item item)
1249 SelectedAiTarget = owner.AiTarget;
1254 if (wallTarget !=
null)
1256 attackWorldPos = wallTarget.Position;
1257 if (wallTarget.Structure.Submarine !=
null)
1259 attackWorldPos += wallTarget.Structure.Submarine.Position;
1261 attackSimPos =
Character.Submarine == wallTarget.Structure.Submarine ? wallTarget.Position : attackWorldPos;
1262 attackSimPos = ConvertUnits.ToSimUnits(attackSimPos);
1266 attackSimPos =
Character.GetRelativeSimPosition(SelectedAiTarget.Entity);
1271 if (TrySteerThroughGaps(deltaTime))
1276 else if (SelectedAiTarget.Entity is Structure w && wallTarget ==
null)
1278 bool isBroken =
true;
1279 for (
int i = 0; i < w.Sections.Length; i++)
1281 if (!w.SectionBodyDisabled(i))
1284 Vector2 sectionPos = w.SectionPosition(i, world:
true);
1285 attackWorldPos = sectionPos;
1286 attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos);
1292 IgnoreTarget(SelectedAiTarget);
1298 attackLimbSelectionTimer -= deltaTime;
1299 if (AttackLimb ==
null || attackLimbSelectionTimer <= 0)
1301 attackLimbSelectionTimer = attackLimbSelectionInterval * Rand.Range(0.9f, 1.1f);
1302 if (!IsAttackRunning && !IsCoolDownRunning)
1304 AttackLimb = GetAttackLimb(attackWorldPos);
1308 IDamageable damageTarget = wallTarget !=
null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
1310 bool pursue =
false;
1311 if (IsCoolDownRunning && (_previousAttackLimb ==
null || AttackLimb ==
null || AttackLimb.attack.CoolDownTimer > 0))
1313 var currentAttackLimb = AttackLimb ?? _previousAttackLimb;
1314 if (currentAttackLimb.attack.CoolDownTimer >=
1315 currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
1319 currentAttackLimb.attack.AfterAttackTimer += deltaTime;
1321 currentAttackLimb.attack.AfterAttackSecondaryDelay > 0 && currentAttackLimb.attack.AfterAttackTimer > currentAttackLimb.attack.AfterAttackSecondaryDelay ?
1322 currentAttackLimb.attack.AfterAttackSecondary :
1323 currentAttackLimb.attack.AfterAttack;
1324 switch (activeBehavior)
1327 if (currentAttackLimb.IsSevered)
1329 ReleaseEatingTarget();
1333 UpdateEating(deltaTime);
1338 if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
1348 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
true);
1354 if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
1357 if (_previousAiTarget !=
null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
1363 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
true);
1371 var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
1372 if (newLimb !=
null)
1375 AttackLimb = newLimb;
1387 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
true);
1407 if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
1410 UpdateFallBack(attackWorldPos, deltaTime, activeBehavior ==
AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1415 if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
1418 if (_previousAiTarget !=
null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
1420 UpdateFallBack(attackWorldPos, deltaTime, activeBehavior ==
AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1424 var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
1425 if (newLimb !=
null)
1428 AttackLimb = newLimb;
1433 UpdateFallBack(attackWorldPos, deltaTime, activeBehavior ==
AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1440 UpdateFallBack(attackWorldPos, deltaTime, activeBehavior ==
AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1446 if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
1449 UpdateIdle(deltaTime, followLastTarget:
false);
1454 if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
1457 if (_previousAiTarget !=
null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
1459 UpdateIdle(deltaTime, followLastTarget:
false);
1463 var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
1464 if (newLimb !=
null)
1467 AttackLimb = newLimb;
1472 UpdateIdle(deltaTime, followLastTarget:
false);
1479 UpdateIdle(deltaTime, followLastTarget:
false);
1485 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
true);
1488 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
true, avoidObstacles:
false);
1497 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
false);
1503 attackVector =
null;
1508 if (AttackLimb is { IsSevered:
true })
1512 if (AttackLimb ==
null || !IsValidAttack(AttackLimb,
Character.GetAttackContexts(), SelectedAiTarget?.Entity))
1514 AttackLimb = GetAttackLimb(attackWorldPos);
1516 canAttack = AttackLimb !=
null && AttackLimb.attack.CoolDownTimer <= 0;
1519 if (!AIParams.CanOpenDoors)
1521 if (!
Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine !=
null &&
Character.Submarine ==
null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
1523 if (wallTarget ==
null && Vector2.DistanceSquared(
Character.WorldPosition, attackWorldPos) < 2000 * 2000)
1526 Vector2 rayStart = SimPosition;
1529 rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
1531 Vector2 dir = SelectedAiTarget.WorldPosition - WorldPosition;
1532 Vector2 rayEnd = rayStart + dir.ClampLength(
Character.AnimController.Collider.GetLocalFront().Length() * 2);
1533 Body closestBody =
Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs:
true);
1534 if (
Submarine.LastPickedFraction != 1.0f && closestBody !=
null &&
1535 ((!AIParams.TargetOuterWalls || !canAttackWalls) && closestBody.UserData is Structure s && s.Submarine !=
null || !canAttackDoors && closestBody.UserData is
Item i && i.Submarine !=
null && i.GetComponent<
Door>() !=
null))
1539 IgnoreTarget(SelectedAiTarget);
1548 Limb attackTargetLimb =
null;
1551 if (!
Character.AnimController.SimplePhysicsEnabled)
1554 if (wallTarget ==
null && targetCharacter !=
null)
1556 var targetLimbType = AttackLimb.Params.Attack.Attack.TargetLimbType;
1557 attackTargetLimb = GetTargetLimb(AttackLimb, targetCharacter, targetLimbType);
1558 if (attackTargetLimb ==
null)
1561 IgnoreTarget(SelectedAiTarget);
1565 attackWorldPos = attackTargetLimb.WorldPosition;
1566 attackSimPos =
Character.GetRelativeSimPosition(attackTargetLimb);
1569 Vector2 attackLimbPos =
Character.AnimController.SimplePhysicsEnabled ?
Character.WorldPosition : AttackLimb.WorldPosition;
1570 Vector2 toTarget = attackWorldPos - attackLimbPos;
1571 Vector2 toTargetOffset = toTarget;
1573 if (wallTarget !=
null &&
Character.Submarine ==
null)
1575 if (wallTarget.Structure.Submarine !=
null)
1577 Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity);
1578 toTargetOffset += margin;
1581 else if (targetCharacter !=
null)
1583 Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
1584 toTargetOffset += margin;
1586 else if (SelectedAiTarget.Entity is MapEntity e)
1588 if (e.Submarine !=
null)
1590 Vector2 margin = CalculateMargin(e.Submarine.Velocity);
1591 toTargetOffset += margin;
1595 Vector2 CalculateMargin(Vector2 targetVelocity)
1597 if (targetVelocity == Vector2.Zero) {
return Vector2.Zero; }
1598 float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange;
1599 if (diff <= 0 || toTargetOffset.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) {
return Vector2.Zero; }
1600 float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(
Character.AnimController.Collider.LinearVelocity));
1601 if (dot <= 0 || !MathUtils.IsValid(dot)) {
return Vector2.Zero; }
1602 float distanceOffset = diff * AttackLimb.attack.Duration;
1604 return targetVelocity * distanceOffset * dot;
1608 distance = toTargetOffset.Length();
1611 canAttack = distance < AttackLimb.attack.Range;
1617 if (!
Character.IsFacing(attackWorldPos))
1625 if (IsAggressiveBoarder)
1627 if (SelectedAiTarget.Entity is
Item i && i.GetComponent<
Door>() is
Door { CanBeTraversed: true })
1634 else if (currentTargetingParams.AttackPattern ==
AttackPattern.Straight && distance < AttackLimb.attack.Range * 5)
1636 Vector2 targetVelocity = Vector2.Zero;
1637 Submarine targetSub = SelectedAiTarget.Entity.Submarine;
1638 if (targetSub !=
null)
1640 targetVelocity = targetSub.Velocity;
1642 else if (targetCharacter !=
null)
1644 targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity;
1646 else if (SelectedAiTarget.Entity is
Item i && i.body !=
null)
1648 targetVelocity = i.body.LinearVelocity;
1650 float mySpeed =
Character.AnimController.Collider.LinearVelocity.LengthSquared();
1651 float targetSpeed = targetVelocity.LengthSquared();
1652 if (mySpeed < 0.1f || mySpeed > targetSpeed)
1654 reachTimer += deltaTime;
1655 if (reachTimer > reachTimeOut)
1658 IgnoreTarget(SelectedAiTarget);
1667 if (
Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2)
1669 if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range)
1671 humanoidAnimController.Crouch();
1677 if (AttackLimb.attack.Ranged)
1680 float offset = AttackLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
1681 Vector2 forward = VectorExtensions.Forward(AttackLimb.body.TransformedRotation - offset *
Character.AnimController.Dir);
1682 float angle = VectorExtensions.Angle(forward, toTarget);
1683 canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle);
1684 if (canAttack && AttackLimb.attack.AvoidFriendlyFire)
1686 canAttack = !IsBlocked(
Character.GetRelativeSimPosition(SelectedAiTarget.Entity));
1687 bool IsBlocked(Vector2 targetPosition)
1689 foreach (var body
in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
1692 if (body.UserData is Character c)
1696 else if (body.UserData is Limb limb)
1698 hitTarget = limb.character;
1700 if (hitTarget !=
null && !hitTarget.IsDead &&
Character.IsFriendly(hitTarget) && !IsAttackingOwner(hitTarget))
1711 Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb :
null;
1712 bool updateSteering =
true;
1713 if (steeringLimb ==
null)
1718 if (steeringLimb ==
null)
1723 var pathSteering = SteeringManager as IndoorsSteeringManager;
1724 if (AttackLimb !=
null && AttackLimb.attack.Retreat)
1726 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
false);
1730 Vector2 steerPos = attackSimPos;
1731 if (!
Character.AnimController.SimplePhysicsEnabled)
1734 Vector2 offset =
Character.SimPosition - steeringLimb.SimPosition;
1737 if (pathSteering !=
null)
1739 if (pathSteering.CurrentPath !=
null)
1745 if (targetCharacter ==
null || targetCharacter.CurrentHull !=
Character.CurrentHull)
1747 var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
1748 if (door is { CanBeTraversed:
false } && (!
Character.IsInFriendlySub || !door.HasAccess(Character)))
1750 if (door.Item.AiTarget !=
null && SelectedAiTarget != door.Item.AiTarget)
1752 SelectTarget(door.Item.AiTarget, currentTargetMemory.
Priority);
1761 float margin = AttackLimb !=
null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max;
1762 if (!canAttack || distance > margin)
1769 targetCharacter !=
null && VisibleHulls.Contains(targetCharacter.CurrentHull))
1771 Vector2 myPos =
Character.AnimController.SimplePhysicsEnabled ?
Character.SimPosition : steeringLimb.SimPosition;
1772 SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos));
1776 pathSteering.SteeringSeek(steerPos, weight: 2,
1777 minGapWidth: minGapSize,
1778 startNodeFilter: n => (n.Waypoint.CurrentHull ==
null) == (
Character.CurrentHull ==
null),
1779 checkVisiblity:
true);
1781 if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
1784 IgnoreTarget(SelectedAiTarget);
1790 else if (!IsTryingToSteerThroughGap)
1792 if (AttackLimb.attack.Ranged)
1794 float dir =
Character.AnimController.Dir;
1795 if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin)
1797 SteeringManager.Reset();
1802 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
false);
1808 SteeringManager.Reset();
1813 SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition -
Character.WorldPosition));
1818 pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize);
1826 switch (currentTargetingParams.AttackPattern)
1829 if (currentTargetingParams.SweepDistance > 0)
1833 distance = (attackWorldPos - WorldPosition).Length();
1835 float amplitude = MathHelper.Lerp(0, currentTargetingParams.SweepStrength, MathUtils.InverseLerp(currentTargetingParams.SweepDistance, 0, distance));
1838 sweepTimer += deltaTime * currentTargetingParams.SweepSpeed;
1839 float sin = (float)Math.Sin(sweepTimer) * amplitude;
1840 steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, sin);
1844 sweepTimer = Rand.Range(-1000f, 1000f) * currentTargetingParams.SweepSpeed;
1849 if (IsCoolDownRunning) {
break; }
1851 if (currentTargetingParams ==
null) {
break; }
1852 var targetSub = SelectedAiTarget.Entity?.Submarine;
1853 ISpatialEntity spatialTarget = targetSub ?? SelectedAiTarget.Entity;
1854 float targetSize = 0;
1855 if (!currentTargetingParams.IgnoreTargetSize)
1858 targetSub !=
null ? Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2 :
1859 targetCharacter !=
null ? ConvertUnits.ToDisplayUnits(targetCharacter.AnimController.Collider.GetSize().X) : 100;
1861 float sqrDistToTarget = Vector2.DistanceSquared(WorldPosition, spatialTarget.WorldPosition);
1862 bool isProgressive = AIParams.MaxAggression - AIParams.StartAggression > 0;
1866 currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, ClampIntensity(aggressionIntensity));
1868 circleDir = GetDirFromHeadingInRadius();
1871 blockCheckTimer = 0;
1872 breakCircling =
false;
1873 float minFallBackDistance = currentTargetingParams.CircleStartDistance * 0.5f;
1874 float maxFallBackDistance = currentTargetingParams.CircleStartDistance;
1875 float maxRandomOffset = currentTargetingParams.CircleMaxRandomOffset;
1878 float ClampIntensity(
float intensity) => MathHelper.Clamp(intensity * Rand.Range(0.9f, 1.1f), AIParams.StartAggression, AIParams.MaxAggression);
1881 float intensity = ClampIntensity(currentAttackIntensity);
1882 float minRotationSpeed = 0.01f * currentTargetingParams.CircleRotationSpeed;
1883 float maxRotationSpeed = 0.5f * currentTargetingParams.CircleRotationSpeed;
1884 circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, intensity);
1885 circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, intensity);
1886 circleOffset = Rand.Vector(MathHelper.Lerp(maxRandomOffset, 0, intensity));
1890 circleRotationSpeed = currentTargetingParams.CircleRotationSpeed;
1891 circleFallbackDistance = maxFallBackDistance;
1892 circleOffset = Rand.Vector(maxRandomOffset);
1894 circleRotationSpeed *= Rand.Range(1 - currentTargetingParams.CircleRandomRotationFactor, 1 + currentTargetingParams.CircleRandomRotationFactor);
1895 aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression);
1896 DisableAttacksIfLimbNotRanged();
1897 if (targetSub is { Borders.Width: < 1000 } && AttackLimb?.attack is { Ranged:
false })
1899 breakCircling =
true;
1902 else if (sqrDistToTarget > MathUtils.Pow2(targetSize + currentTargetingParams.CircleStartDistance))
1906 else if (sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
1916 Vector2 targetVelocity = GetTargetVelocity();
1917 float targetDistance = currentTargetingParams.IgnoreTargetSize ? currentTargetingParams.CircleStartDistance * 0.9f:
1918 targetSize + currentTargetingParams.CircleStartDistance / 2;
1919 if (AttackLimb !=
null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetVelocity))
1921 strikeTimer = AttackLimb.attack.CoolDown;
1924 else if (!breakCircling && sqrDistToTarget <= MathUtils.Pow2(targetDistance) && targetVelocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
1928 DisableAttacksIfLimbNotRanged();
1931 updateSteering =
false;
1932 bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough:
false, checkBlocking:
true);
1933 if (isBlocked || sqrDistToTarget > MathUtils.Pow2(targetSize + circleFallbackDistance))
1938 DisableAttacksIfLimbNotRanged();
1941 Vector2 targetVel = GetTargetVelocity();
1943 if (breakCircling || targetVel.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()))
1947 else if (sqrDistToTarget > MathUtils.Pow2(targetSize + currentTargetingParams.CircleStartDistance * 1.2f))
1949 if (currentTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100)
1951 circleRotationSpeed *= 1 + deltaTime;
1960 float rotationStep = circleRotationSpeed * deltaTime * circleDir;
1963 circleRotation += rotationStep;
1967 circleRotation = rotationStep;
1969 Vector2 targetPos = attackSimPos + circleOffset;
1970 float targetDist = targetSize;
1971 if (targetDist <= 0)
1973 targetDist = circleFallbackDistance;
1975 if (targetSub !=
null && AttackLimb?.attack is { Ranged:
true })
1977 targetDist += circleFallbackDistance / 2;
1979 if (Vector2.DistanceSquared(SimPosition, targetPos) < ConvertUnits.ToSimUnits(targetDist))
1985 if (canAttack && AttackLimb?.attack is { Ranged:
false } && sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
1988 strikeTimer = AttackLimb.attack.CoolDown;
1996 steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation);
1997 if (IsBlocked(deltaTime, steerPos))
2002 circleDir = -circleDir;
2005 else if (circleRotationSpeed < 1)
2008 circleRotationSpeed *= 1 + deltaTime;
2010 else if (circleOffset.LengthSquared() > 0.1f)
2013 circleOffset = Vector2.Zero;
2018 breakCircling = AttackLimb?.attack is { Ranged:
false };
2026 if (AttackLimb?.attack is { Ranged:
false })
2029 float requiredDistMultiplier = GetStrikeDistanceMultiplier(targetVel);
2030 if (distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
2032 strikeTimer = AttackLimb.attack.CoolDown;
2038 strikeTimer -= deltaTime;
2041 if (strikeTimer <= 0)
2044 aggressionIntensity += AIParams.AggressionCumulation;
2050 bool IsFacing(
float margin)
2052 float offset = steeringLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
2053 Vector2 forward = VectorExtensions.Forward(steeringLimb.body.TransformedRotation - offset *
Character.AnimController.Dir);
2054 return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin;
2057 float GetStrikeDistanceMultiplier(Vector2 targetVelocity)
2059 if (currentTargetingParams.CircleStrikeDistanceMultiplier < 1) {
return 0; }
2060 float requiredDistMultiplier = 2;
2061 bool isHeading = Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(
Steering)) > 0.9f;
2064 requiredDistMultiplier = currentTargetingParams.CircleStrikeDistanceMultiplier;
2065 float targetVelocityHorizontal = Math.Abs(targetVelocity.X);
2066 if (targetVelocityHorizontal > 1)
2069 requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(currentTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1));
2070 if (requiredDistMultiplier < 2)
2072 requiredDistMultiplier = 2;
2076 return requiredDistMultiplier;
2079 float GetDirFromHeadingInRadius()
2081 Vector2 heading = VectorExtensions.Forward(
Character.AnimController.Collider.Rotation);
2082 float angle = MathUtils.VectorToAngle(heading);
2083 return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1;
2086 Vector2 GetTargetVelocity()
2088 if (targetSub !=
null)
2090 return targetSub.Velocity;
2092 else if (targetCharacter !=
null)
2094 return targetCharacter.AnimController.Collider.LinearVelocity;
2096 return Vector2.Zero;
2099 float GetTargetMaxSpeed() =>
Character.ApplyTemporarySpeedLimits(
Character.AnimController.SwimFastParams.MovementSpeed * (targetSub !=
null ? 0.3f : 0.5f));
2104 if (currentTargetingParams.AttackPattern ==
AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
2106 bool advance = !canAttack &&
Character.CurrentHull ==
null || distance > attackLimb.attack.Range * 0.9f;
2107 bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
2111 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
false);
2115 SteeringManager.SteeringSeek(steerPos, 10);
2119 if (
Character.CurrentHull ==
null && !canAttack)
2121 SteeringManager.SteeringWander(avoidWanderingOutsideLevel:
true);
2122 SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
2126 SteeringManager.Reset();
2127 FaceTarget(SelectedAiTarget.Entity);
2131 else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
2133 if (pathSteering !=
null)
2135 pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
2139 SteeringManager.SteeringSeek(steerPos, 10);
2142 if (
Character.CurrentHull ==
null && (SelectedAiTarget?.Entity is Character c && c.Submarine ==
null ||
2144 distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2) ||
2145 AttackLimb !=
null && AttackLimb.attack.Ranged))
2147 SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
2152 Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
2153 if (AttackLimb?.attack is Attack { Ranged:
true } attack)
2155 AimRangedAttack(attack, attackTargetLimb as ISpatialEntity ?? targetEntity);
2159 if (!UpdateLimbAttack(deltaTime, attackSimPos, damageTarget, distance, attackTargetLimb))
2161 IgnoreTarget(SelectedAiTarget);
2164 else if (IsAttackRunning)
2166 AttackLimb.attack.ResetAttackTimer();
2169 void DisableAttacksIfLimbNotRanged()
2171 if (AttackLimb?.attack is { Ranged:
false })
2180 if (attack is not { Ranged:
true }) {
return; }
2181 if (targetEntity is
Entity { Removed:
true }) {
return; }
2184 Limb limb = GetLimbToRotate(attack);
2188 float offset = limb.
Params.GetSpriteOrientation() - MathHelper.PiOver2;
2190 float angle = MathUtils.VectorToAngle(toTarget);
2196 private bool IsValidAttack(
Limb attackingLimb, IEnumerable<AttackContext> currentContexts,
Entity target)
2198 if (attackingLimb ==
null) {
return false; }
2199 if (target ==
null) {
return false; }
2200 var attack = attackingLimb.
attack;
2201 if (attack ==
null) {
return false; }
2202 if (attack.CoolDownTimer > 0) {
return false; }
2203 if (!attack.IsValidContext(currentContexts)) {
return false; }
2204 if (!attack.IsValidTarget(target)) {
return false; }
2216 if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) {
return false; }
2218 if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) {
return false; }
2223 Vector2 toTarget = attackWorldPos - attackLimbPos;
2224 if (attack.MinRange > 0 && toTarget.LengthSquared() < MathUtils.Pow2(attack.MinRange)) {
return false; }
2225 float offset = attackingLimb.
Params.GetSpriteOrientation() - MathHelper.PiOver2;
2227 float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
2228 if (angle > attack.RequiredAngle) {
return false; }
2230 if (attack.RootForceWorldEnd.LengthSquared() > 1)
2235 case Character targetCharacter when
Character.CurrentHull != targetCharacter.CurrentHull || targetCharacter.IsKnockedDown:
2236 case Item targetItem when
Character.CurrentHull != targetItem.CurrentHull:
2239 float verticalDistance = Math.Abs(attackWorldPos.Y -
Character.WorldPosition.Y);
2240 if (verticalDistance > 50)
2248 private readonly List<Limb> attackLimbs =
new List<Limb>();
2249 private readonly List<float> weights =
new List<float>();
2250 private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb =
null)
2252 var currentContexts =
Character.GetAttackContexts();
2253 Entity target = wallTarget !=
null ? wallTarget.Structure : SelectedAiTarget?.
Entity;
2254 if (target ==
null) {
return null; }
2255 Limb selectedLimb =
null;
2256 float currentPriority = -1;
2257 foreach (Limb limb
in Character.AnimController.Limbs)
2259 if (limb == ignoredLimb) {
continue; }
2260 if (limb.IsSevered || limb.IsStuck) {
continue; }
2261 if (!IsValidAttack(limb, currentContexts, target)) {
continue; }
2262 if (AIParams.RandomAttack)
2264 attackLimbs.Add(limb);
2265 weights.Add(limb.attack.Priority);
2269 float priority = CalculatePriority(limb, attackWorldPos);
2270 if (priority > currentPriority)
2272 currentPriority = priority;
2273 selectedLimb = limb;
2277 if (AIParams.RandomAttack)
2279 selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Unsynced);
2280 attackLimbs.Clear();
2283 return selectedLimb;
2285 float CalculatePriority(Limb limb, Vector2 attackPos)
2287 float prio = 1 + limb.attack.Priority;
2288 if (
Character.AnimController.SimplePhysicsEnabled) {
return prio; }
2289 float dist = Vector2.Distance(limb.WorldPosition, attackPos);
2290 float distanceFactor = 1;
2291 if (limb.attack.Ranged)
2294 distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist));
2300 distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
2302 return prio * distanceFactor;
2308 float reactionTime = Rand.Range(0.1f, 0.3f);
2309 updateTargetsTimer = Math.Min(updateTargetsTimer, reactionTime);
2310 bool wasLatched = IsLatchedOnSub;
2312 if (attackResult.
Damage > 0)
2316 if (attacker?.AiTarget ==
null || attacker.
Removed || attacker.
IsDead) {
return; }
2319 if (attackResult.
Damage >= AIParams.DamageThreshold)
2321 ReleaseDragTargets();
2327 avoidTimer = AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f);
2343 if (!isFriendly && attackResult.
Damage > 0.0f)
2350 case AIState.PlayDead when Rand.Value() < 0.5f:
2360 if (AIParams.AttackWhenProvoked && canAttack)
2362 if (ignoredTargets.Contains(attacker.
AiTarget))
2364 ignoredTargets.Remove(attacker.
AiTarget);
2368 ChangeTargetState(Tags.Husk,
AIState.Attack, 100);
2372 ChangeTargetState(attacker,
AIState.Attack, 100);
2379 ChangeTargetState(Tags.Husk, canAttack ?
AIState.Attack :
AIState.Escape, 100);
2383 if (enemyAI.CombatStrength > CombatStrength)
2385 if (!AIParams.HasTag(
"stronger"))
2387 ChangeTargetState(attacker, canAttack ?
AIState.Attack :
AIState.Escape, 100);
2390 else if (enemyAI.CombatStrength < CombatStrength)
2392 if (!AIParams.HasTag(
"weaker"))
2394 ChangeTargetState(attacker, canAttack ?
AIState.Attack :
AIState.Escape, 100);
2397 else if (!AIParams.HasTag(
"equal"))
2399 ChangeTargetState(attacker, canAttack ?
AIState.Attack :
AIState.Escape, 100);
2404 ChangeTargetState(attacker, canAttack ?
AIState.Attack :
AIState.Escape, 100);
2407 else if (canAttack && attacker.
IsHuman &&
2408 AIParams.TryGetTargets(attacker, out IEnumerable<CharacterParams.TargetParams> targetingParams))
2412 tempParamsList.Clear();
2413 tempParamsList.AddRange(targetingParams);
2414 foreach (var tp
in tempParamsList)
2418 ChangeTargetState(attacker,
AIState.Attack, 100);
2428 if (State ==
AIState.Attack && (IsAttackRunning || IsCoolDownRunning))
2431 if (IsAttackRunning)
2433 avoidGunFire =
false;
2441 if (limb.attack !=
null)
2443 limb.attack.CoolDownTimer *= reactionTime;
2447 else if (avoidGunFire && attackResult.
Damage >= AIParams.DamageThreshold)
2450 avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
2456 avoidTimer = AIParams.MinFleeTime * Rand.Range(0.75f, 1.25f);
2461 private Item GetEquippedItem(
Limb limb)
2465 return limb.
type switch
2473 var slot = GetInvSlotForLimb();
2476 return Character.Inventory.GetItemInLimbSlot(slot);
2482 private static float GetRelativeDamage(
float dmg,
float vitality) => dmg / Math.Max(vitality, 1.0f);
2484 private bool UpdateLimbAttack(
float deltaTime, Vector2 attackSimPos, IDamageable damageTarget,
float distance = -1, Limb targetLimb =
null)
2486 if (SelectedAiTarget?.Entity ==
null) {
return false; }
2487 if (AttackLimb?.attack ==
null) {
return false; }
2488 ISpatialEntity spatialTarget = wallTarget !=
null ? wallTarget.Structure : SelectedAiTarget.Entity as ISpatialEntity;
2489 if (spatialTarget ==
null) {
return false; }
2490 ActiveAttack = AttackLimb.attack;
2491 if (wallTarget !=
null)
2494 var aiTarget = wallTarget.Structure.AiTarget;
2495 if (aiTarget !=
null && SelectedAiTarget != aiTarget)
2497 SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound:
true).Priority);
2502 if (damageTarget ==
null) {
return false; }
2503 ActiveAttack = AttackLimb.attack;
2504 if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
2506 Limb referenceLimb = GetLimbToRotate(ActiveAttack);
2507 if (referenceLimb !=
null)
2509 Vector2 toTarget = attackWorldPos - referenceLimb.WorldPosition;
2510 float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
2511 Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir);
2512 float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
2513 if (angle > ActiveAttack.RequiredAngleToShoot)
2522 Item item = GetEquippedItem(AttackLimb);
2525 if (item.RequireAimToUse)
2527 if (!
Aim(deltaTime, spatialTarget, item))
2533 if (damageTarget !=
null)
2536 item.Use(deltaTime, user: Character);
2540 if (damageTarget ==
null) {
return true; }
2543 if (!ActiveAttack.IsRunning)
2546 GameMain.NetworkMember.CreateEntityEvent(Character,
new Character.SetAttackTargetEventData(
2552 Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
2555 if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
2557 if (ActiveAttack.CoolDownTimer > 0)
2559 SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f));
2561 if (LatchOntoAI !=
null && SelectedAiTarget.Entity is Character targetCharacter)
2563 LatchOntoAI.SetAttachTarget(targetCharacter);
2565 if (!ActiveAttack.Ranged)
2567 if (damageTarget.Health > 0 && attackResult.Damage > 0)
2570 float greed = AIParams.AggressionGreed;
2576 currentTargetMemory.
Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
2580 currentTargetMemory.
Priority -= Math.Max(currentTargetMemory.
Priority / 2, 1);
2581 return currentTargetMemory.
Priority > 1;
2588 private float aimTimer;
2589 private float visibilityCheckTimer;
2590 private bool canSeeTarget;
2591 private float sinTime;
2592 private bool Aim(
float deltaTime, ISpatialEntity target,
Item weapon)
2594 if (target ==
null || weapon ==
null) {
return false; }
2595 if (AttackLimb ==
null) {
return false; }
2596 Vector2 toTarget = target.WorldPosition - weapon.WorldPosition;
2597 float dist = toTarget.Length();
2598 Character.CursorPosition = target.WorldPosition;
2599 if (AttackLimb.attack.SwayAmount > 0)
2601 sinTime += deltaTime * AttackLimb.attack.SwayFrequency;
2602 Character.CursorPosition += VectorExtensions.Forward(weapon.body.TransformedRotation + (
float)Math.Sin(sinTime) / 2, dist / 2 * AttackLimb.attack.SwayAmount);
2608 visibilityCheckTimer -= deltaTime;
2609 if (visibilityCheckTimer <= 0.0f)
2611 canSeeTarget =
Character.CanSeeTarget(target);
2612 visibilityCheckTimer = 0.2f;
2622 aimTimer -= deltaTime;
2625 float angle = VectorExtensions.Angle(VectorExtensions.Forward(weapon.body.TransformedRotation), toTarget);
2626 float minDistance = 300;
2627 float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(minDistance, 1000, dist));
2628 float margin = MathHelper.PiOver4 * distanceFactor;
2629 if (angle < margin || dist < minDistance)
2631 var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
2632 var pickedBody =
Submarine.PickBody(weapon.SimPosition,
Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture:
true);
2633 if (pickedBody !=
null)
2635 if (target is MapEntity)
2637 if (pickedBody.UserData is Submarine sub && sub == target.Submarine)
2641 else if (target == pickedBody.UserData)
2648 if (pickedBody.UserData is Character c)
2652 else if (pickedBody.UserData is Limb limb)
2656 if (t !=
null && (t == target || (!
Character.IsFriendly(t) || IsAttackingOwner(t))))
2665 private void SetAimTimer(
float timer = 1.5f) => aimTimer = timer * Rand.Range(0.75f, 1.25f);
2667 private readonly
float blockCheckInterval = 0.1f;
2668 private float blockCheckTimer;
2669 private bool isBlocked;
2670 private bool IsBlocked(
float deltaTime, Vector2 steerPos, Category collisionCategory = Physics.CollisionLevel)
2672 blockCheckTimer -= deltaTime;
2673 if (blockCheckTimer <= 0)
2675 blockCheckTimer = blockCheckInterval;
2676 isBlocked =
Submarine.PickBodies(SimPosition, steerPos, collisionCategory: collisionCategory).Any();
2681 private Vector2? attackVector =
null;
2682 private bool UpdateFallBack(Vector2 attackWorldPos,
float deltaTime,
bool followThrough,
bool checkBlocking =
false,
bool avoidObstacles =
true)
2684 if (attackVector ==
null)
2686 attackVector = attackWorldPos - WorldPosition;
2688 Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
2689 if (!MathUtils.IsValid(dir))
2691 dir = Vector2.UnitY;
2693 steeringManager.SteeringManual(deltaTime, dir);
2694 if (
Character.AnimController.InWater && !Reverse && avoidObstacles)
2696 SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
2700 return !IsBlocked(deltaTime, SimPosition + dir * (avoidLookAheadDistance / 2));
2705 private Limb GetLimbToRotate(Attack attack)
2707 Limb limb = AttackLimb;
2708 if (attack.RotationLimbIndex > -1 && attack.RotationLimbIndex <
Character.AnimController.Limbs.Length)
2710 limb =
Character.AnimController.Limbs[attack.RotationLimbIndex];
2719 private void UpdateEating(
float deltaTime)
2721 if (SelectedAiTarget?.Entity ==
null || SelectedAiTarget.Entity.Removed)
2723 ReleaseEatingTarget();
2729 if (mouthLimb ==
null)
2731 DebugConsole.ThrowError(
"Character \"" +
Character.SpeciesName +
"\" failed to eat a target (No head limb found)",
2732 contentPackage:
Character.Prefab.ContentPackage);
2733 IgnoreTarget(SelectedAiTarget);
2734 ReleaseEatingTarget();
2738 Vector2 mouthPos =
Character.AnimController.SimplePhysicsEnabled ? SimPosition :
Character.AnimController.GetMouthPosition() ?? Vector2.Zero;
2739 Vector2 attackSimPosition =
Character.GetRelativeSimPosition(SelectedAiTarget.Entity);
2740 Vector2 limbDiff = attackSimPosition - mouthPos;
2741 float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2);
2742 bool tooFar =
Character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent;
2745 steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2);
2748 SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
2753 if (SelectedAiTarget.Entity is Character targetCharacter)
2755 Character.SelectCharacter(targetCharacter);
2757 else if (SelectedAiTarget.Entity is
Item item)
2759 if (!item.Removed && item.body !=
null)
2761 float itemBodyExtent = item.body.GetMaxExtent() * 2;
2762 if (Math.Abs(limbDiff.X) < itemBodyExtent &&
2763 Math.Abs(limbDiff.Y) <
Character.AnimController.Collider.GetMaxExtent() +
Character.AnimController.ColliderHeightFromFloor)
2765 Vector2 velocity = limbDiff;
2766 if (limbDiff.LengthSquared() > 0.01f) { velocity = Vector2.Normalize(velocity); }
2767 item.body.LinearVelocity *= 0.9f;
2768 item.body.LinearVelocity -= velocity * 0.25f;
2769 bool wasBroken = item.Condition <= 0.0f;
2770 item.LastEatenTime = (float)Timing.TotalTimeUnpaused;
2771 item.AddDamage(Character,
2773 new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f *
Character.Params.EatingSpeed),
2774 impulseDirection: Vector2.Zero,
2777 if (item.Condition <= 0.0f)
2779 if (!wasBroken) { PetBehavior?.OnEat(item); }
2780 Entity.Spawner.AddItemToRemoveQueue(item);
2785 steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3);
2786 Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos);
2791 IgnoreTarget(SelectedAiTarget);
2792 ReleaseEatingTarget();
2797 private void ReleaseEatingTarget()
2805 private void UpdateFollow(
float deltaTime)
2807 if (SelectedAiTarget ==
null || SelectedAiTarget.Entity ==
null || SelectedAiTarget.Entity.Removed)
2812 if (
Character.CurrentHull !=
null && steeringManager == insideSteering)
2817 SelectedAiTarget.Entity is Character c && VisibleHulls.Contains(c.CurrentHull))
2820 SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition -
Character.WorldPosition));
2825 PathSteering.
SteeringSeek(
Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2, minGapWidth: minGapSize);
2831 SteeringManager.SteeringSeek(
Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 5);
2833 if (steeringManager is IndoorsSteeringManager pathSteering)
2835 if (!pathSteering.IsPathDirty && pathSteering.CurrentPath !=
null && pathSteering.CurrentPath.Unreachable)
2839 IgnoreTarget(SelectedAiTarget);
2842 else if (
Character.AnimController.InWater)
2844 SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
2853 return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter == character;
2862 return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter !=
null && enemyAI.LatchOntoAI.TargetCharacter != character;
2878 bool isAnyTargetClose =
false;
2879 bool isBeingChased = IsBeingChased;
2880 const float priorityValueMaxModifier = 5;
2882 bool tryToGetInside =
2890 if (ignoredTargets.Contains(aiTarget)) {
continue; }
2898 if (targetCharacter ==
Character) {
continue; }
2901 if (targetCharacter is { IsPlayer:
true }) {
continue; }
2910 var targetingTags = GetTargetingTags(aiTarget);
2912 #region Filter out targets by entity type, based on contextual information.
2914 if (targetCharacter !=
null)
2917 if (targetCharacter.HasAbilityFlag(
AbilityFlags.IgnoredByEnemyAI)) {
continue; }
2946 if (hull.Submarine ==
null) {
continue; }
2947 if (hull.Submarine.Info.IsRuin) {
continue; }
2951 door = item.GetComponent<
Door>();
2952 bool targetingFromOutsideToInside = item.CurrentHull !=
null && !isCharacterInside;
2953 if (targetingFromOutsideToInside)
2955 if (door !=
null && (!canAttackDoors && !AIParams.CanOpenDoors) || !canAttackWalls)
2961 if (door ==
null && targetingFromOutsideToInside)
2963 if (item.Submarine?.Info is { IsRuin:
true })
2969 else if (targetingTags.Contains(Tags.Nasonov))
2971 if ((item.Submarine ==
null || !item.Submarine.Info.IsPlayer) && item.ParentInventory ==
null)
2990 if (s.IsPlatform) {
continue; }
2991 if (s.Submarine ==
null) {
continue; }
2992 if (s.Submarine.Info.IsRuin) {
continue; }
2993 bool isInnerWall = s.Prefab.Tags.Contains(
"inner");
2994 if (isInnerWall && !isCharacterInside)
2999 if (!tryToGetInside && IsWallDisabled(s))
3007 bool isOutdoor = door.
LinkedGap is { FlowTargetHull: not
null, IsRoomToRoom:
false };
3013 if (!canAttackDoors) {
continue; }
3030 #region Choose valid targeting params.
3031 if (targetingTags.None()) {
continue; }
3032 CharacterParams.TargetParams matchingTargetParams =
null;
3033 foreach (var targetParams
in GetTargetParams(targetingTags))
3035 if (matchingTargetParams !=
null)
3037 if (matchingTargetParams.Priority > targetParams.Priority)
3043 if (targetParams.IgnoreInside &&
Character.CurrentHull !=
null) {
continue; }
3044 if (targetParams.IgnoreOutside &&
Character.CurrentHull ==
null) {
continue; }
3045 if (targetParams.IgnoreIncapacitated && targetCharacter is { IsIncapacitated:
true }) {
continue; }
3046 if (targetParams.IgnoreTargetInside && aiTarget.Entity.Submarine !=
null) {
continue; }
3047 if (targetParams.IgnoreTargetOutside && aiTarget.Entity.Submarine ==
null) {
continue; }
3048 if (aiTarget.Entity is ISerializableEntity se)
3050 if (targetParams.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) {
continue; }
3052 if (targetParams.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) {
continue; }
3053 if (targetParams.IgnoreIfNotInSameSub)
3055 if (aiTarget.Entity.Submarine !=
Character.Submarine) {
continue; }
3056 var targetHull = targetCharacter !=
null ? targetCharacter.CurrentHull : aiTarget.Entity is
Item it ? it.CurrentHull :
null;
3057 if (targetHull ==
null != (
Character.CurrentHull ==
null)) {
continue; }
3061 if (targetCharacter !=
null && targetCharacter.Submarine !=
Character.Submarine)
3067 if (aiTarget.Entity is
Item targetItem)
3069 if (targetParams.IgnoreContained && targetItem.ParentInventory !=
null) {
continue; }
3071 switch (targetParams.State)
3075 float healthThreshold = targetParams.Threshold;
3076 if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0)
3081 healthThreshold = currentTargetingParams == targetParams && State ==
AIState.FleeTo ? targetParams.ThresholdMax : targetParams.ThresholdMin;
3083 if (
Character.HealthPercentage > healthThreshold)
3090 if (!canAttackItems)
3096 if (targetItem.HasTag(Tags.GuardianShelter))
3099 bool ignore =
false;
3100 foreach (Character otherCharacter
in Character.CharacterList)
3102 if (otherCharacter == Character) {
continue; }
3103 if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) {
continue; }
3104 if (!
Character.IsFriendly(otherCharacter)) {
continue; }
3108 if (ignore) {
continue; }
3111 matchingTargetParams = targetParams;
3113 if (matchingTargetParams ==
null) {
continue; }
3116 #region Modify the priority dynamically
3117 float valueModifier = 1.0f;
3118 if (targetCharacter !=
null)
3120 if (targetCharacter.AIController is EnemyAIController)
3122 if (matchingTargetParams.Tag == Tags.Stronger && State is
AIState.Avoid or
AIState.Escape or
AIState.Flee)
3124 if (SelectedAiTarget == aiTarget)
3129 if (IsBeingChasedBy(targetCharacter))
3133 if (
Character.CurrentHull !=
null && !VisibleHulls.Contains(targetCharacter.CurrentHull))
3143 if (aiTarget.Entity is Structure s)
3145 bool isInnerWall = s.Prefab.Tags.Contains(
"inner");
3147 valueModifier = 200f / s.MaxHealth;
3148 for (
int i = 0; i < s.Sections.Length; i++)
3150 var section = s.Sections[i];
3151 if (section.gap ==
null) {
continue; }
3152 bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull !=
null;
3155 if (!isCharacterInside)
3157 if (CanPassThroughHole(s, i))
3159 valueModifier *= leadsInside ? (IsAggressiveBoarder ? priorityValueMaxModifier : 1) : 0;
3161 else if (IsAggressiveBoarder && leadsInside && canAttackWalls)
3164 valueModifier *= 1 + section.gap.Open;
3170 if (IsAggressiveBoarder)
3178 else if (CanPassThroughHole(s, i))
3180 valueModifier *= isInnerWall ? 0.5f : 0;
3182 else if (!canAttackWalls)
3189 valueModifier = 0.1f;
3194 if (!canAttackWalls)
3201 valueModifier *= 1 - section.gap.Open * 0.25f;
3202 valueModifier = Math.Max(valueModifier, 0.1f);
3209 if (isInnerWall || !canAttackWalls)
3215 else if (IsAggressiveBoarder)
3219 valueModifier *= 1 + section.gap.Open;
3222 valueModifier = Math.Clamp(valueModifier, 0, priorityValueMaxModifier);
3227 if (IsAggressiveBoarder)
3232 if (door.CanBeTraversed)
3234 valueModifier = priorityValueMaxModifier;
3236 else if (door.LinkedGap !=
null)
3238 valueModifier = 1 + door.LinkedGap.Open * (priorityValueMaxModifier - 1);
3244 bool isOpen = door.CanBeTraversed;
3245 bool isOutdoor = door.LinkedGap is { FlowTargetHull: not
null, IsRoomToRoom:
false };
3246 valueModifier = isOpen || isOutdoor ? 0 : 1;
3250 else if (aiTarget.Entity is IDamageable { Health: <= 0.0f })
3256 if (matchingTargetParams.State ==
AIState.Eat &&
Character.Params.Health.HealthRegenerationWhenEating > 0 && !
Character.IsPet)
3258 valueModifier *= MathHelper.Lerp(1f, 0.1f,
Character.HealthPercentage / 100f);
3260 valueModifier *= matchingTargetParams.Priority;
3261 if (valueModifier == 0.0f) {
continue; }
3262 if (matchingTargetParams.Tag != Tags.Decoy)
3264 if (SwarmBehavior !=
null && SwarmBehavior.Members.Any())
3267 foreach (AICharacter otherCharacter
in SwarmBehavior.Members)
3269 if (otherCharacter == Character) {
continue; }
3270 if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) {
continue; }
3277 foreach (Character otherCharacter
in Character.CharacterList)
3279 if (otherCharacter == Character) {
continue; }
3280 if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) {
continue; }
3281 if (!
Character.IsFriendly(otherCharacter)) {
continue; }
3288 if (!aiTarget.IsWithinSector(WorldPosition)) {
continue; }
3289 Vector2 toTarget = aiTarget.WorldPosition -
Character.WorldPosition;
3290 float dist = toTarget.Length();
3291 float nonModifiedDist = dist;
3293 if (targetMemories.ContainsKey(aiTarget))
3297 if (matchingTargetParams.PerceptionDistanceMultiplier > 0.0f)
3299 dist /= matchingTargetParams.PerceptionDistanceMultiplier;
3302 if (matchingTargetParams.MaxPerceptionDistance > 0.0f &&
3303 dist * dist > matchingTargetParams.MaxPerceptionDistance * matchingTargetParams.MaxPerceptionDistance)
3308 if (State is
AIState.PlayDead && targetCharacter ==
null)
3313 else if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget || State is
AIState.PlayDead or
AIState.Hiding))
3318 if (SelectedAiTarget == aiTarget)
3320 if (
Character.Submarine ==
null && aiTarget.Entity is ISpatialEntity { Submarine: not
null } spatialEntity)
3322 if (matchingTargetParams.Tag == Tags.Door || matchingTargetParams.Tag == Tags.Wall)
3324 Vector2 rayStart =
Character.SimPosition;
3325 Vector2 rayEnd = aiTarget.SimPosition + spatialEntity.Submarine.SimPosition;
3326 Body closestBody =
Submarine.PickBody(rayStart, rayEnd, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture:
true);
3327 if (closestBody is { UserData: ISpatialEntity hit })
3329 Vector2 hitPos = hit.SimPosition;
3330 if (closestBody.UserData is Submarine)
3334 else if (hit.Submarine !=
null)
3336 hitPos += hit.Submarine.SimPosition;
3338 float subHalfWidth = spatialEntity.Submarine.Borders.Width / 2f;
3339 float subHalfHeight = spatialEntity.Submarine.Borders.Height / 2f;
3340 Vector2 diff = ConvertUnits.ToDisplayUnits(rayEnd - hitPos);
3341 bool isOtherSideOfTheSub = Math.Abs(diff.X) > subHalfWidth || Math.Abs(diff.Y) > subHalfHeight;
3342 if (isOtherSideOfTheSub)
3344 IgnoreTarget(aiTarget);
3352 valueModifier *= 1.1f;
3358 float reactDistance = matchingTargetParams.ReactDistance;
3359 if (reactDistance > 0 && reactDistance < dist)
3369 dist = Math.Max(dist, 100.0f);
3370 targetMemory = GetTargetMemory(aiTarget, addIfNotFound:
true, keepAlive: SelectedAiTarget != aiTarget);
3373 float diff = Math.Abs(toTarget.Y) -
Character.CurrentHull.Size.Y;
3377 dist *= MathHelper.Clamp(diff / 100, 2, 3);
3381 if (
Character.Submarine ==
null && aiTarget.Entity?.Submarine !=
null && targetCharacter ==
null)
3383 if (matchingTargetParams.PrioritizeSubCenter || matchingTargetParams.AttackPattern is
AttackPattern.Circle or
AttackPattern.Sweep)
3385 if (!isAnyTargetClose)
3387 if (
Submarine.MainSubs.Contains(aiTarget.Entity.Submarine))
3390 float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X);
3391 dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter));
3393 else if (matchingTargetParams.AttackPattern ==
AttackPattern.Circle)
3401 if (targetCharacter !=
null &&
Character.CurrentHull !=
null &&
Character.CurrentHull == targetCharacter.CurrentHull)
3408 switch (matchingTargetParams.State)
3414 if (matchingTargetParams.State ==
AIState.Attack)
3417 if (State == matchingTargetParams.State && SelectedAiTarget == aiTarget) {
break; }
3419 bool insideSameSub = aiTarget?.Entity?.Submarine !=
null && aiTarget.Entity.Submarine ==
Character.Submarine;
3420 if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
3423 bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget);
3424 if (
Character.LastAttackers.None(a => a.Damage > 0 && a.Character !=
null && (a.Character == aiTarget.Entity || a.Character.IsOnPlayerTeam && isTargetInPlayerTeam)))
3433 targetMemory.Priority /
3437 if (valueModifier > targetValue)
3439 if (aiTarget.Entity is
Item i)
3442 if (owner == Character) {
continue; }
3445 if (owner.AiTarget !=
null && ignoredTargets.Contains(owner.AiTarget)) {
continue; }
3451 if (owner.HasAbilityFlag(
AbilityFlags.IgnoredByEnemyAI))
3457 if (GetTargetParams(GetTargetingTags(owner.AiTarget)).Any(t => t.State ==
AIState.Idle)) {
continue; }
3460 if (targetCharacter !=
null)
3462 if (
Character.CurrentHull !=
null && targetCharacter.CurrentHull !=
Character.CurrentHull)
3464 if (matchingTargetParams.State is
AIState.Observe or
AIState.Eat ||
3467 if (!VisibleHulls.Contains(targetCharacter.CurrentHull))
3474 if (targetCharacter.Submarine !=
Character.Submarine || (targetCharacter.CurrentHull ==
null) != (
Character.CurrentHull ==
null))
3476 if (targetCharacter.Submarine !=
null)
3479 valueModifier *= 0.5f;
3489 if (SelectedAiTarget?.Entity != targetCharacter)
3495 else if (targetCharacter.Submarine ==
null &&
Character.Submarine ==
null)
3498 if (dist > Math.Clamp(ConvertUnits.ToDisplayUnits(colliderLength) * 10, 1000, 5000))
3500 if (
Submarine.PickBodies(SimPosition, targetCharacter.SimPosition, collisionCategory: Physics.CollisionLevel).Any())
3507 newTarget = aiTarget;
3508 selectedTargetParams = matchingTargetParams;
3509 targetValue = valueModifier;
3510 if (!isAnyTargetClose)
3512 isAnyTargetClose = ConvertUnits.ToDisplayUnits(colliderLength) > nonModifiedDist;
3517 currentTargetingParams = selectedTargetParams;
3518 currentTargetMemory = targetMemory;
3519 State = currentTargetingParams?.State ??
AIState.Idle;
3520 SelectedAiTarget = newTarget;
3521 if ((LatchOntoAI is not { IsAttached:
true } || wallTarget !=
null) && State is
AIState.Attack or
AIState.Aggressive or
AIState.PassiveAggressive)
3523 UpdateWallTarget(requiredHoleCount);
3525 updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
3530 public Vector2 Position;
3532 public int SectionIndex;
3534 public WallTarget(Vector2 position, Structure structure =
null,
int sectionIndex = -1)
3536 Position = position;
3538 SectionIndex = sectionIndex;
3542 private WallTarget wallTarget;
3543 private readonly List<(Body, int, Vector2)> wallHits =
new List<(Body,
int, Vector2)>(3);
3544 private void UpdateWallTarget(
int requiredHoleCount)
3547 if (SelectedAiTarget ==
null) {
return; }
3548 if (SelectedAiTarget.Entity ==
null) {
return; }
3549 if (!canAttackWalls) {
return; }
3550 if (HasValidPath(requireNonDirty:
true)) {
return; }
3553 Vector2 refPos = AttackLimb !=
null ? AttackLimb.SimPosition : SimPosition;
3556 Vector2 rayStart = refPos;
3557 Vector2 rayEnd = SelectedAiTarget.SimPosition;
3558 if (SelectedAiTarget.Entity.Submarine !=
null &&
Character.Submarine ==
null)
3560 rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
3562 else if (SelectedAiTarget.Entity.Submarine ==
null &&
Character.Submarine !=
null)
3564 rayEnd -=
Character.Submarine.SimPosition;
3566 DoRayCast(rayStart, rayEnd);
3570 Vector2 rayStart = refPos;
3571 Vector2 rayEnd = rayStart + VectorExtensions.Forward(
Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5);
3572 if (SelectedAiTarget.Entity.Submarine !=
null &&
Character.Submarine ==
null)
3574 rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
3575 rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition;
3577 else if (SelectedAiTarget.Entity.Submarine ==
null &&
Character.Submarine !=
null)
3579 rayStart -=
Character.Submarine.SimPosition;
3580 rayEnd -=
Character.Submarine.SimPosition;
3582 DoRayCast(rayStart, rayEnd);
3586 Vector2 rayStart = refPos;
3587 Vector2 rayEnd = rayStart +
Steering * 5;
3588 if (SelectedAiTarget.Entity.Submarine !=
null &&
Character.Submarine ==
null)
3590 rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
3591 rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition;
3593 else if (SelectedAiTarget.Entity.Submarine ==
null &&
Character.Submarine !=
null)
3595 rayStart -=
Character.Submarine.SimPosition;
3596 rayEnd -=
Character.Submarine.SimPosition;
3598 DoRayCast(rayStart, rayEnd);
3602 Vector2 targetdiff = ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition - (AttackLimb !=
null ? AttackLimb.WorldPosition : WorldPosition));
3603 float targetDistance = targetdiff.LengthSquared();
3604 Body closestBody =
null;
3605 float closestDistance = 0;
3606 int sectionIndex = -1;
3607 Vector2 sectionPos = Vector2.Zero;
3608 foreach ((Body body,
int index, Vector2 sectionPosition) in wallHits)
3611 float distance = Vector2.DistanceSquared(
3613 Submarine.GetRelativeSimPosition(ConvertUnits.ToSimUnits(sectionPosition),
Character.Submarine, structure.Submarine));
3615 if (distance > targetDistance) {
continue; }
3616 if (closestBody ==
null || closestDistance == 0 || distance < closestDistance)
3619 closestDistance = distance;
3621 sectionPos = sectionPosition;
3622 sectionIndex = index;
3625 if (closestBody ==
null || sectionIndex == -1) {
return; }
3626 Vector2 attachTargetNormal;
3627 if (wall.IsHorizontal)
3629 attachTargetNormal =
new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y));
3630 sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y;
3634 attachTargetNormal =
new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f);
3635 sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
3637 LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
3639 !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
3643 bool isTargetingDoor = SelectedAiTarget.Entity is
Item i && i.GetComponent<
Door>() !=
null;
3645 if (!isTargetingDoor)
3650 IgnoreTarget(SelectedAiTarget);
3656 wallTarget =
new WallTarget(sectionPos, wall, sectionIndex);
3662 IgnoreTarget(SelectedAiTarget);
3667 void DoRayCast(Vector2 rayStart, Vector2 rayEnd)
3669 Body hitTarget =
Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs:
true,
3672 if (hitTarget !=
null && IsValid(hitTarget, out wall))
3674 int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(
Submarine.LastPickedPosition));
3675 if (sectionIndex >= 0)
3677 wallHits.Add((hitTarget, sectionIndex, GetSectionPosition(wall, sectionIndex)));
3682 Vector2 GetSectionPosition(Structure wall,
int sectionIndex)
3684 float sectionDamage = wall.SectionDamage(sectionIndex);
3685 for (
int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
3687 if (wall.SectionBodyDisabled(i))
3690 CanPassThroughHole(wall, i, requiredHoleCount))
3701 if (wall.SectionDamage(i) > sectionDamage)
3706 return wall.SectionPosition(sectionIndex, world:
false);
3709 bool IsValid(Body hit, out Structure wall)
3712 if (
Submarine.LastPickedFraction == 1.0f) {
return false; }
3713 if (hit.UserData is not Structure w) {
return false; }
3714 if (w.Submarine ==
null) {
return false; }
3715 if (w.Submarine != SelectedAiTarget.Entity.Submarine) {
return false; }
3718 if (w.Prefab.Tags.Contains(
"inner"))
3722 else if (!AIParams.TargetOuterWalls)
3732 private bool TrySteerThroughGaps(
float deltaTime)
3734 if (wallTarget !=
null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex, requiredHoleCount))
3736 WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex);
3737 Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, world:
true);
3738 return section?.gap !=
null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime);
3740 else if (SelectedAiTarget !=
null)
3742 if (SelectedAiTarget.Entity is Structure wall)
3744 for (
int i = 0; i < wall.Sections.Length; i++)
3746 WallSection section = wall.Sections[i];
3747 if (CanPassThroughHole(wall, i, requiredHoleCount) && section?.gap !=
null)
3749 return SteerThroughGap(wall, section, wall.SectionPosition(i,
true), deltaTime);
3753 else if (SelectedAiTarget.Entity is
Item i)
3755 var door = i.GetComponent<
Door>();
3757 if (door?.LinkedGap?.FlowTargetHull !=
null && !door.LinkedGap.IsRoomToRoom && door.CanBeTraversed)
3759 if (
Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25)
3761 if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
3763 float maxDistance = Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100);
3764 return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: maxDistance);
3773 private AITargetMemory GetTargetMemory(AITarget target,
bool addIfNotFound =
false,
bool keepAlive =
false)
3775 if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
3779 memory =
new AITargetMemory(target, minPriority);
3780 targetMemories.Add(target, memory);
3785 memory.Priority = Math.Max(memory.Priority, minPriority);
3790 private void UpdateCurrentMemoryLocation()
3792 if (_selectedAiTarget !=
null)
3794 if (_selectedAiTarget.Entity ==
null || _selectedAiTarget.Entity.Removed)
3796 _selectedAiTarget =
null;
3798 else if (CanPerceive(_selectedAiTarget, checkVisibility:
false))
3800 var memory = GetTargetMemory(_selectedAiTarget);
3803 memory.Location = _selectedAiTarget.WorldPosition;
3809 private readonly List<AITarget> removals =
new List<AITarget>();
3810 private void FadeMemories(
float deltaTime)
3813 foreach (var kvp
in targetMemories)
3815 var target = kvp.Key;
3816 var memory = kvp.Value;
3818 float fadeTime = memoryFadeTime;
3819 if (target == SelectedAiTarget)
3824 else if (target == _lastAiTarget)
3829 memory.Priority -= fadeTime * deltaTime;
3831 if (memory.Priority <= 1 || target.Entity ==
null || target.Entity.Removed || !AITarget.List.Contains(target))
3833 removals.Add(target);
3836 removals.ForEach(r => targetMemories.Remove(r));
3839 private readonly
float targetIgnoreTime = 10;
3840 private float targetIgnoreTimer;
3841 private readonly HashSet<AITarget> ignoredTargets =
new HashSet<AITarget>();
3844 if (target ==
null) {
return; }
3845 ignoredTargets.Add(target);
3846 targetIgnoreTimer = targetIgnoreTime * Rand.Range(0.75f, 1.25f);
3850 #region State switching
3855 private readonly
float stateResetCooldown = 10;
3856 private float stateResetTimer;
3857 private bool isStateChanged;
3863 if (trigger.IsTriggered) {
return; }
3864 if (activeTriggers.ContainsKey(trigger)) {
return; }
3865 if (activeTriggers.ContainsValue(currentTargetingParams))
3867 if (!trigger.AllowToOverride) {
return; }
3868 var existingTrigger = activeTriggers.FirstOrDefault(kvp => kvp.Value == currentTargetingParams && kvp.Key.AllowToBeOverridden);
3869 if (existingTrigger.Key ==
null) {
return; }
3870 activeTriggers.Remove(existingTrigger.Key);
3873 activeTriggers.Add(trigger, currentTargetingParams);
3874 ChangeParams(currentTargetingParams, trigger.State);
3877 private void UpdateTriggers(
float deltaTime)
3879 foreach (var triggerObject
in activeTriggers)
3882 if (trigger.IsPermanent) {
continue; }
3884 if (!trigger.IsActive)
3887 ResetParams(triggerObject.Value);
3888 inactiveTriggers.Add(trigger);
3891 foreach (StatusEffect.AITrigger trigger in inactiveTriggers)
3893 activeTriggers.Remove(trigger);
3895 inactiveTriggers.Clear();
3901 private bool TryResetOriginalState(Identifier tag)
3903 if (!modifiedParams.ContainsKey(tag)) {
return false; }
3904 if (AIParams.TryGetTargets(tag, out IEnumerable<CharacterParams.TargetParams> matchingParams))
3906 foreach (var targetParams
in matchingParams)
3908 modifiedParams.Remove(tag);
3909 if (tempParams.ContainsKey(tag))
3911 tempParams.Values.ForEach(t => AIParams.RemoveTarget(t));
3912 tempParams.Remove(tag);
3914 ResetParams(targetParams);
3921 private readonly Dictionary<Identifier, IEnumerable<CharacterParams.TargetParams>> modifiedParams =
new Dictionary<Identifier, IEnumerable<CharacterParams.TargetParams>>();
3922 private readonly Dictionary<Identifier, CharacterParams.TargetParams> tempParams =
new Dictionary<Identifier, CharacterParams.TargetParams>();
3923 private readonly List<CharacterParams.TargetParams> tempParamsList =
new List<CharacterParams.TargetParams>();
3925 private void ChangeParams(CharacterParams.TargetParams targetParams, AIState state,
float? priority =
null)
3927 if (targetParams ==
null) {
return; }
3928 if (priority.HasValue)
3930 targetParams.Priority = priority.Value;
3932 targetParams.State = state;
3935 private void ResetParams(CharacterParams.TargetParams targetParams)
3937 targetParams?.Reset();
3938 if (currentTargetingParams == targetParams || State is
AIState.Idle or
AIState.Patrol)
3946 private void ChangeParams(Identifier tag, AIState state,
float? priority =
null,
bool onlyExisting =
false,
bool ignoreAttacksIfNotInSameSub =
false)
3948 var existingTargetParams = GetTargetParams(tag);
3949 if (existingTargetParams.None())
3951 if (!onlyExisting && !tempParams.ContainsKey(tag))
3953 if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out CharacterParams.TargetParams targetParams))
3958 targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub;
3960 tempParams.Add(tag, targetParams);
3966 foreach (var targetParams
in existingTargetParams)
3968 if (priority.HasValue)
3970 targetParams.Priority = Math.Max(targetParams.Priority, priority.Value);
3972 targetParams.State = state;
3974 modifiedParams.TryAdd(tag, existingTargetParams);
3978 private void ChangeTargetState(Identifier tag, AIState state,
float? priority =
null)
3980 isStateChanged =
true;
3981 SetStateResetTimer();
3982 ChangeParams(tag, state, priority);
3989 private void ChangeTargetState(Character target, AIState state,
float? priority =
null)
3991 isStateChanged =
true;
3992 SetStateResetTimer();
3993 if (!
Character.IsPet || !target.IsHuman)
3996 ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
4000 if (AIParams.TryGetHighestPriorityTarget(Tags.Human, out CharacterParams.TargetParams targetParams))
4002 priority = targetParams.Priority;
4007 ChangeParams(Tags.Weapon, state, priority);
4008 ChangeParams(Tags.ToolItem, state, priority);
4014 if (target.Submarine !=
null &&
Character.Submarine ==
null && (canAttackDoors || canAttackWalls))
4016 ChangeParams(Tags.Room, state, priority / 2);
4019 ChangeParams(Tags.Wall, state, priority / 2);
4021 if (canAttackDoors && IsAggressiveBoarder)
4023 ChangeParams(Tags.Door, state, priority / 2);
4026 ChangeParams(Tags.Provocative, state, priority, onlyExisting:
true);
4031 private void ResetOriginalState()
4033 isStateChanged =
false;
4034 modifiedParams.Keys.ForEachMod(tag => TryResetOriginalState(tag));
4040 base.OnTargetChanged(previousTarget, newTarget);
4041 if ((newTarget !=
null || wallTarget !=
null) && IsLatchedOnSub)
4045 wall = wallTarget?.Structure;
4051 for (
int i = 0; i < wall.Sections.Length; i++)
4053 if (CanPassThroughHole(wall, i))
4055 releaseTarget =
true;
4069 if (newTarget ==
null) {
return; }
4070 if (currentTargetingParams !=
null)
4072 observeTimer = currentTargetingParams.Timer * Rand.Range(0.75f, 1.25f);
4085 if (disableTailCoroutine !=
null)
4088 if (!isInTransition)
4090 CoroutineManager.StopCoroutines(disableTailCoroutine);
4092 disableTailCoroutine =
null;
4097 ReleaseDragTargets();
4103 if (isStateChanged && to ==
AIState.Idle && from != to)
4105 SetStateResetTimer();
4107 blockCheckTimer = 0;
4116 playDeadTimer = PlayDeadCoolDown;
4121 Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
4126 private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f);
4128 private float GetPerceivingRange(
AITarget target)
4131 if (AIParams.MaxPerceptionDistance >= 0 && maxSightOrSoundRange > AIParams.MaxPerceptionDistance) {
return AIParams.
MaxPerceptionDistance; }
4132 return maxSightOrSoundRange;
4135 private bool CanPerceive(AITarget target,
float dist = -1,
float distSquared = -1,
bool checkVisibility =
false)
4137 if (target?.Entity ==
null) {
return false; }
4138 bool insideSightRange;
4139 bool insideSoundRange;
4140 if (checkVisibility)
4143 Submarine targetSub = target.Entity.Submarine;
4148 (
Character.IsPet && (mySub !=
null || targetSub !=
null)) ||
4149 (mySub !=
null && (targetSub ==
null || (targetSub == mySub && !targetSub.Info.IsPlayer)));
4153 if (AIParams.MaxPerceptionDistance >= 0 && dist > AIParams.MaxPerceptionDistance) {
return false; }
4154 insideSightRange = IsInRange(dist, target.SightRange, Sight);
4155 if (!checkVisibility && insideSightRange) {
return true; }
4156 insideSoundRange = IsInRange(dist, target.SoundRange, Hearing);
4160 if (distSquared < 0)
4162 distSquared = Vector2.DistanceSquared(
Character.WorldPosition, target.WorldPosition);
4164 if (AIParams.MaxPerceptionDistance >= 0 && distSquared > AIParams.MaxPerceptionDistance * AIParams.MaxPerceptionDistance) {
return false; }
4165 insideSightRange = IsInRangeSqr(distSquared, target.SightRange, Sight);
4166 if (!checkVisibility && insideSightRange) {
return true; }
4167 insideSoundRange = IsInRangeSqr(distSquared, target.SoundRange, Hearing);
4169 if (!checkVisibility)
4171 return insideSightRange || insideSoundRange;
4175 if (!insideSightRange && !insideSoundRange) {
return false; }
4177 if (target.Entity is Character c && VisibleHulls.Contains(c.CurrentHull) || target.Entity is
Item i && VisibleHulls.Contains(i.CurrentHull))
4179 return insideSightRange || insideSoundRange;
4186 return IsInRange(dist, target.SoundRange, Hearing / 2);
4190 if (distSquared < 0)
4192 distSquared = Vector2.DistanceSquared(
Character.WorldPosition, target.WorldPosition);
4194 return IsInRangeSqr(distSquared, target.SoundRange, Hearing / 2);
4199 bool IsInRange(
float dist,
float range,
float perception) => dist <= range * perception;
4200 bool IsInRangeSqr(
float distSquared,
float range,
float perception) => distSquared <= MathUtils.Pow2(range * perception);
4205 canAttackWalls =
LatchOntoAI is { AttachToSub:
true };
4206 canAttackDoors =
false;
4207 canAttackCharacters =
false;
4208 canAttackItems =
false;
4211 if (limb.IsSevered) {
continue; }
4212 if (limb.Disabled) {
continue; }
4213 if (limb.attack ==
null) {
continue; }
4214 if (!canAttackWalls)
4216 canAttackWalls = (limb.attack.StructureDamage > 0 || limb.attack.Ranged && limb.attack.IsValidTarget(
AttackTarget.Structure));
4218 if (!canAttackDoors)
4221 canAttackDoors = (limb.attack.ItemDamage > 0 || limb.attack.Ranged) && limb.attack.IsValidTarget(
AttackTarget.Structure);
4223 if (!canAttackItems)
4226 canAttackItems = canAttackDoors || (limb.attack.ItemDamage > 0 || limb.attack.Ranged) && limb.attack.IsValidTarget(
AttackTarget.Structure |
AttackTarget.Item);
4228 if (!canAttackCharacters)
4230 canAttackCharacters = limb.attack.IsValidTarget(
AttackTarget.Character);
4233 if (PathSteering !=
null)
4239 private bool IsPositionInsideAllowedZone(Vector2 pos, out Vector2 targetDir)
4241 targetDir = Vector2.Zero;
4243 if (Level.Loaded.LevelData.Biome.IsEndBiome) {
return true; }
4244 if (AIParams.AvoidAbyss)
4246 if (pos.Y < Level.Loaded.AbyssStart)
4249 targetDir = Vector2.UnitY;
4252 else if (AIParams.StayInAbyss)
4254 if (pos.Y > Level.Loaded.AbyssStart)
4257 targetDir = -Vector2.UnitY;
4259 else if (pos.Y < Level.Loaded.AbyssEnd)
4262 targetDir = Vector2.UnitY;
4265 float margin = Level.OutsideBoundsCurrentMargin;
4266 if (pos.X < -margin)
4269 targetDir = Vector2.UnitX;
4271 else if (pos.X > Level.Loaded.Size.X + margin)
4274 targetDir = -Vector2.UnitX;
4276 return targetDir == Vector2.Zero;
4279 private Vector2 returnDir;
4280 private float returnTimer;
4281 private void SteerInsideLevel(
float deltaTime)
4283 if (SteeringManager is IndoorsSteeringManager) {
return; }
4284 if (Level.Loaded ==
null) {
return; }
4285 if (State ==
AIState.Attack && returnTimer <= 0) {
return; }
4286 float returnTime = 5;
4287 if (!IsPositionInsideAllowedZone(WorldPosition, out Vector2 targetDir))
4289 returnDir = targetDir;
4290 returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
4292 if (returnTimer > 0)
4294 returnTimer -= deltaTime;
4295 SteeringManager.Reset();
4296 SteeringManager.SteeringManual(deltaTime, returnDir * 10);
4297 SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, 15);
4303 IsTryingToSteerThroughGap =
true;
4307 bool success = base.SteerThroughGap(wall, section, targetWorldPos, deltaTime);
4314 IsSteeringThroughGap = success;
4318 public override bool SteerThroughGap(
Gap gap, Vector2 targetWorldPos,
float deltaTime,
float maxDistance = -1)
4320 bool success = base.SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance);
4328 IsSteeringThroughGap = success;
4336 if (SelectedAiTarget !=
null && (SelectedAiTarget.Entity ==
null || SelectedAiTarget.Entity.Removed))
4343 targetMemory.Priority += deltaTime * PriorityFearIncrement;
4345 bool isSteeringThroughGap = UpdateEscape(deltaTime, canAttackDoors);
4346 if (!isSteeringThroughGap)
4350 SteerAwayFromTheEnemy();
4352 else if (canAttackDoors && HasValidPath())
4355 if (door is { CanBeTraversed:
false } && !door.HasAccess(
Character))
4357 if (SelectedAiTarget != door.Item.AiTarget || State !=
AIState.Attack)
4359 SelectTarget(door.Item.AiTarget, CurrentTargetMemory.
Priority);
4366 if (EscapeTarget ==
null)
4370 SteerAwayFromTheEnemy();
4381 return isSteeringThroughGap;
4383 void SteerAwayFromTheEnemy()
4385 if (SelectedAiTarget ==
null) {
return; }
4386 Vector2 escapeDir = Vector2.Normalize(WorldPosition - SelectedAiTarget.WorldPosition);
4387 if (!MathUtils.IsValid(escapeDir))
4389 escapeDir = Vector2.UnitY;
4394 escapeDir =
new Vector2(Math.Sign(escapeDir.X), 0);
4401 private readonly List<Limb> targetLimbs =
new List<Limb>();
4404 targetLimbs.Clear();
4405 foreach (var limb
in target.AnimController.Limbs)
4407 if (limb.type == targetLimbType || targetLimbType ==
LimbType.None)
4409 targetLimbs.Add(limb);
4412 if (targetLimbs.None())
4415 targetLimbs.AddRange(target.AnimController.Limbs);
4417 float closestDist =
float.MaxValue;
4418 Limb targetLimb =
null;
4419 foreach (
Limb limb
in targetLimbs)
4421 if (limb.IsSevered) {
continue; }
4422 if (limb.Hidden) {
continue; }
4423 float dist = Vector2.DistanceSquared(limb.WorldPosition, attackLimb.WorldPosition) / Math.Max(limb.AttackPriority, 0.1f);
4424 if (dist < closestDist)
4435 var pickable = item.GetComponent<
Pickable>();
4436 if (pickable !=
null)
4461 private float priority;
4463 public float Priority
4465 get {
return priority; }
4466 set { priority = MathHelper.Clamp(value, 1.0f, 100.0f); }
4473 this.priority = priority;
readonly Character Character
AITarget SelectedAiTarget
SteeringManager SteeringManager
readonly float colliderLength
SteeringManager steeringManager
readonly float colliderWidth
AIObjective CurrentObjective
Includes orders.
static List< AITarget > List
bool ShouldBeIgnored()
Is some condition met (e.g. entity null, indetectable, outside level) that prevents anyone from detec...
AITargetMemory(AITarget target, float priority)
float GetCurrentSpeed(bool useMaxSpeed)
abstract SwimParams SwimFastParams
void ApplyPose(Vector2 leftHandPos, Vector2 rightHandPos, Vector2 leftFootPos, Vector2 rightFootPos, float footMoveForce=10)
Attacks are used to deal damage to characters, structures and items. They can be defined in the weapo...
readonly CharacterParams Params
Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo=null, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, RagdollParams ragdollParams=null, bool spawnInitialItems=true)
void SetInput(InputType inputType, bool hit, bool held)
float??? MaxPerceptionDistance
Character?? SelectedCharacter
virtual AIController AIController
override Vector2? SimPosition
void EvaluatePlayDeadProbability(float? probability=null)
bool IsFriendly(Character other)
CharacterInventory Inventory
bool IsSameSpeciesOrGroup(Character other)
readonly CharacterPrefab Prefab
static Character Controlled
IEnumerable< Attacker > LastAttackers
static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
readonly AnimController AnimController
IEnumerable< Item >?? HeldItems
Items the character has in their hand slots. Doesn't return nulls and only returns items held in both...
Contains character data that should be editable in the character editor.
AIParams AI
Parameters for EnemyAIController. Not used by HumanAIController.
ContentXElement? FirstElement()
ContentXElement OriginalElement
static EnemyTargetingRestrictions TargetingRestrictions
override void OnTargetChanged(AITarget previousTarget, AITarget newTarget)
override void OnStateChanged(AIState from, AIState to)
override void Update(float deltaTime)
static bool DisableEnemyAI
static bool IsTargetBeingChasedBy(Character target, Character character)
override bool Escape(float deltaTime)
bool CanPassThroughHole(Structure wall, int sectionIndex)
void EvaluatePlayDeadProbability(float? probability=null)
static bool IsLatchedToSomeoneElse(Character target, Character character)
override void SelectTarget(AITarget target)
CharacterParams.AIParams AIParams
Shorthand for Character.Params.AI with null checking.
bool IsBeingChasedBy(Character c)
void IgnoreTarget(AITarget target)
EnemyAIController(Character c, string seed)
void LaunchTrigger(StatusEffect.AITrigger trigger)
AITargetMemory CurrentTargetMemory
float PriorityFearIncrement
override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
CharacterParams.TargetParams CurrentTargetingParams
void SelectTarget(AITarget target, float priority)
bool TargetOutposts
Enable the character to attack the outposts and the characters inside them. Disabled by default in no...
override bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance=-1)
SwarmBehavior SwarmBehavior
HashSet< Submarine > UnattackableSubmarines
The monster won't try to damage these submarines
void AimRangedAttack(Attack attack, ISpatialEntity targetEntity)
static bool IsLatchedTo(Character target, Character character)
Limb GetTargetLimb(Limb attackLimb, Character target, LimbType targetLimbType=LimbType.None)
override void OnAttacked(Character attacker, AttackResult attackResult)
virtual Vector2 WorldPosition
Entity(Submarine submarine, ushort id)
static NetworkMember NetworkMember
static GameSession GameSession
AIObjectiveManager ObjectiveManager
void SteeringSeek(Vector2 target, float weight, float minGapWidth=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisiblity=true)
bool IsNextLadderSameAsCurrent
void SetPath(Vector2 targetPos, SteeringPath path)
bool HasTag(Identifier tag)
Inventory FindParentInventory(Func< Inventory, bool > predicate)
Entity GetRootInventoryOwner()
void DeattachFromBody(bool reset, float cooldown=0)
List< Joint > AttachJoints
void Update(EnemyAIController enemyAI, float deltaTime)
readonly LimbParams Params
int ForceToEndLocationIndex
SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub=null, string errorMsgStr=null, float minGapSize=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisibility=true)
bool SuppressSmoothRotationCalls
Ignore rotation calls for the rest of this and the next update. Automatically disabled after that....
void MoveToPos(Vector2 simPosition, float force, Vector2? pullPos=null)
float TransformedRotation
Takes flipping (Dir) into account.
void SmoothRotate(float targetRotation, float force=10.0f, bool wrapAngle=true)
Rotate the body towards the target rotation in the "shortest direction", taking into account the curr...
ContentPackage? ContentPackage
CanEnterSubmarine CanEnterSubmarine
Vector2 GetColliderBottom()
void RestoreTemporarilyDisabled()
bool? SimplePhysicsEnabled
void HideAndDisable(LimbType limbType, float duration=0, bool ignoreCollisions=true)
Can be used to trigger a behavior change of some kind on an AI character. Only applicable for enemy c...
void UpdateTimer(float deltaTime)
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
void SteeringManual(float deltaTime, Vector2 velocity)
void SteeringWander(float weight=1, bool avoidWanderingOutsideLevel=false)
void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight=1)
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
OutpostGenerationParams OutpostGenerationParams
void UpdateSteering(float deltaTime)
AbilityFlags
AbilityFlags are a set of toggleable flags that can be applied to characters.
ActionType
ActionTypes define when a StatusEffect is executed.
@ Character
Characters only
@ Structure
Structures and hulls, but also items (for backwards support)!
EnemyTargetingRestrictions