Server LuaCsForBarotrauma
EnemyAIController.cs
4 using FarseerPhysics;
5 using FarseerPhysics.Dynamics;
6 using Microsoft.Xna.Framework;
7 using System;
8 using System.Collections.Generic;
9 using System.Xml.Linq;
10 using System.Linq;
11 
12 namespace Barotrauma
13 {
15 
16  public enum AttackPattern { Straight, Sweep, Circle }
17 
19 
20  public enum WallTargetingMethod
21  {
22  Target = 0x1,
23  Heading = 0x2,
24  Steering = 0x4
25  }
26 
27  [Flags]
29  {
30  None = 0x0,
31  PlayerCharacters = 0x1,
32  PlayerSubmarines = 0x2
33  }
34 
36  {
37  public static bool DisableEnemyAI;
38 
40  private EnemyTargetingRestrictions previousTargetingRestrictions;
41 
42  private AIState _state;
43  public AIState State
44  {
45  get { return _state; }
46  set
47  {
48  if (_state == value) { return; }
49  if (_state == AIState.PlayDead && value == AIState.Idle)
50  {
51  // Don't allow to switch to Idle from PlayDead.
52  return;
53  }
54  PreviousState = _state;
55  OnStateChanged(_state, value);
56  _state = value;
57  }
58  }
59 
60  public AIState PreviousState { get; private set; }
61 
65  public bool TargetOutposts;
66 
67  private readonly float updateTargetsInterval = 1;
68  private readonly float updateMemoriesInverval = 1;
69  private readonly float attackLimbSelectionInterval = 3;
70  // Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target.
71  private const float minPriority = 10;
72 
73  private IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
74  private SteeringManager outsideSteering, insideSteering;
75 
76  private float updateTargetsTimer;
77  private float updateMemoriesTimer;
78  private float attackLimbSelectionTimer;
79 
80  private bool IsAttackRunning => AttackLimb != null && AttackLimb.attack.IsRunning;
81  private bool IsCoolDownRunning => AttackLimb != null && AttackLimb.attack.CoolDownTimer > 0 || _previousAttackLimb != null && _previousAttackLimb.attack.CoolDownTimer > 0;
82  public float CombatStrength => AIParams.CombatStrength;
83  private float Sight => GetPerceptionRange(AIParams.Sight);
84  private float Hearing => GetPerceptionRange(AIParams.Hearing);
85 
86  private float GetPerceptionRange(float range)
87  {
88  // TODO: make adjustable
89  if (State is AIState.PlayDead or AIState.Hiding)
90  {
91  // Intentionally constant
92  return 0.2f;
93  }
94  if (PreviousState is AIState.PlayDead or AIState.Hiding)
95  {
96  // Significantly buffed
97  return range * 1.5f;
98  }
99  return range;
100  }
101 
102  private float FleeHealthThreshold => AIParams.FleeHealthThreshold;
103  private bool IsAggressiveBoarder => AIParams.AggressiveBoarding;
104 
105  private FishAnimController FishAnimController => Character.AnimController as FishAnimController;
106 
107  private Limb _attackLimb;
108  private Limb _previousAttackLimb;
110  {
111  get { return _attackLimb; }
112  private set
113  {
114  if (_attackLimb != value)
115  {
116  _previousAttackLimb = _attackLimb;
117  if (_previousAttackLimb != null)
118  {
120  if (_previousAttackLimb.attack.SnapRopeOnNewAttack)
121  {
122  _previousAttackLimb.AttachedRope?.Snap();
123  }
124  }
125  }
126  else if (_attackLimb != null && _attackLimb.attack.CoolDownTimer <= 0)
127  {
129  if (_attackLimb.attack.SnapRopeOnNewAttack)
130  {
131  _attackLimb.AttachedRope?.Snap();
132  }
133  }
134  _attackLimb = value;
135  attackVector = null;
136  Reverse = _attackLimb != null && _attackLimb.attack.Reverse;
137  }
138  }
139 
140  private double lastAttackUpdateTime;
141 
142  private Attack _activeAttack;
144  {
145  get
146  {
147  if (_activeAttack == null) { return null; }
148  return lastAttackUpdateTime > Timing.TotalTime - _activeAttack.Duration ? _activeAttack : null;
149  }
150  private set
151  {
152  _activeAttack = value;
153  lastAttackUpdateTime = Timing.TotalTime;
154  }
155  }
156 
157  public AITargetMemory CurrentTargetMemory => currentTargetMemory;
158  private AITargetMemory currentTargetMemory;
159  private float targetValue;
160  private CharacterParams.TargetParams currentTargetingParams;
161 
162  private Dictionary<AITarget, AITargetMemory> targetMemories;
163 
164  private readonly int requiredHoleCount;
165  private bool canAttackWalls;
166  public bool CanAttackDoors => canAttackDoors;
167  private bool canAttackDoors;
168  private bool canAttackItems;
169  private bool canAttackCharacters;
170 
171  public float PriorityFearIncrement => priorityFearIncreasement;
172  private readonly float priorityFearIncreasement = 2;
173  private readonly float memoryFadeTime = 0.5f;
174 
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;
187  private CirclePhase CirclePhase;
188  private float currentAttackIntensity;
189 
190  private float playDeadTimer;
194  private const float PlayDeadCoolDown = 60;
195 
196  private CoroutineHandle disableTailCoroutine;
197 
198  private readonly List<Body> myBodies;
199 
200  public LatchOntoAI LatchOntoAI { get; private set; }
201  public SwarmBehavior SwarmBehavior { get; private set; }
202  public PetBehavior PetBehavior { get; private set; }
203 
204  public CharacterParams.TargetParams CurrentTargetingParams => currentTargetingParams;
205 
206  public bool AttackHumans
207  {
208  get
209  {
210  return GetTargetParams(Tags.Human).Any(static tp => tp is { Priority: > 0.0f, State: AIState.Attack or AIState.Aggressive });
211  }
212  }
213 
214  public bool AttackRooms
215  {
216  get
217  {
218  return GetTargetParams(Tags.Room).Any(static tp => tp is { Priority: > 0.0f, State: AIState.Attack or AIState.Aggressive });
219  }
220  }
221 
223  {
224  get
225  {
226  //can't enter a submarine when attached to one
227  if (LatchOntoAI is { IsAttachedToSub: true }) { return CanEnterSubmarine.False; }
229  }
230  }
231 
232  public override bool CanFlip
233  {
234  get
235  {
236  //can't flip when attached to something, when eating, or reversing or in a (relatively) small room
237  return !Reverse &&
238  (State != AIState.Eat || Character.SelectedCharacter == null) &&
239  (LatchOntoAI == null || !LatchOntoAI.IsAttachedToSub) &&
240  (Character.CurrentHull == null || !Character.AnimController.InWater || Math.Min(Character.CurrentHull.Size.X, Character.CurrentHull.Size.Y) > ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth)));
241  }
242  }
243 
247  public HashSet<Submarine> UnattackableSubmarines
248  {
249  get;
250  private set;
251  } = new HashSet<Submarine>();
252 
253  public static bool IsTargetBeingChasedBy(Character target, Character character)
254  => character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && enemyAI.State is AIState.Attack or AIState.Aggressive;
256  private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character);
257 
258  private static bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character { IsOnPlayerTeam: true };
259 
260  private bool IsAttackingOwner(Character other) =>
261  PetBehavior != null && PetBehavior.Owner != null &&
262  !other.IsUnconscious && !other.IsHandcuffed &&
263  other.AIController is HumanAIController humanAI &&
265  combat.Enemy != null && combat.Enemy == PetBehavior.Owner;
266 
267  private bool reverse;
268  public bool Reverse
269  {
270  get { return reverse; }
271  private set
272  {
273  reverse = value;
274  if (FishAnimController != null)
275  {
276  FishAnimController.Reverse = reverse;
277  }
278  }
279  }
280 
281  private readonly float maxSteeringBuffer = 5000;
282  private readonly float minSteeringBuffer = 500;
283  private readonly float steeringBufferIncreaseSpeed = 100;
284  private float steeringBuffer;
285 
286  public EnemyAIController(Character c, string seed) : base(c)
287  {
288  if (c.IsHuman)
289  {
290  throw new Exception($"Tried to create an enemy ai controller for human!");
291  }
293  targetMemories = new Dictionary<AITarget, AITargetMemory>();
294  steeringManager = outsideSteering;
295  //allow targeting outposts and outpost NPCs in outpost levels
296  TargetOutposts =
297  (Level.Loaded != null && Level.Loaded.Type == LevelData.LevelType.Outpost) ||
298  //the main sub can be an outpost in the editor
299  Submarine.MainSub is { Info.Type: SubmarineType.Outpost };
300 
301  List<XElement> aiElements = new List<XElement>();
302  List<float> aiCommonness = new List<float>();
303  foreach (var element in mainElement.Elements())
304  {
305  if (!element.Name.ToString().Equals("ai", StringComparison.OrdinalIgnoreCase)) { continue; }
306  aiElements.Add(element);
307  aiCommonness.Add(element.GetAttributeFloat("commonness", 1.0f));
308  }
309 
310  if (aiElements.Count == 0)
311  {
312  DebugConsole.ThrowError("Error in file \"" + c.Params.File.Path + "\" - no AI element found.",
313  contentPackage: c.Prefab?.ContentPackage);
314  outsideSteering = new SteeringManager(this);
315  insideSteering = new IndoorsSteeringManager(this, false, false);
316  return;
317  }
318 
319  //choose a random ai element
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())
323  {
324  switch (subElement.Name.ToString().ToLowerInvariant())
325  {
326  case "chooserandom":
327  var subElements = subElement.Elements();
328  if (subElements.Any())
329  {
330  LoadSubElement(subElements.ToArray().GetRandom(random));
331  }
332  break;
333  default:
334  LoadSubElement(subElement);
335  break;
336  }
337  }
338 
339  void LoadSubElement(XElement subElement)
340  {
341  switch (subElement.Name.ToString().ToLowerInvariant())
342  {
343  case "latchonto":
344  LatchOntoAI = new LatchOntoAI(subElement, this);
345  break;
346  case "swarm":
347  case "swarmbehavior":
348  SwarmBehavior = new SwarmBehavior(subElement, this);
349  break;
350  case "petbehavior":
351  PetBehavior = new PetBehavior(subElement, this);
352  break;
353  }
354  }
355  //pets are friendly!
356  if (PetBehavior != null || Character.Group == "human")
357  {
358  Character.TeamID = CharacterTeamType.FriendlyNPC;
359  }
361  outsideSteering = new SteeringManager(this);
362  insideSteering = new IndoorsSteeringManager(this, AIParams.CanOpenDoors, canAttackDoors);
363  steeringManager = outsideSteering;
364  State = AIState.Idle;
365  requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
366  myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
368  if (AIParams.PlayDeadProbability > 0)
369  {
371  }
372  CreatureMetrics.UnlockInEditor(Character.SpeciesName);
373  }
374 
375  private CharacterParams.AIParams _aiParams;
380  public CharacterParams.AIParams AIParams
381  {
382  get
383  {
384  if (_aiParams == null)
385  {
386  _aiParams = Character.Params.AI;
387  if (_aiParams == null)
388  {
389  DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.",
390  contentPackage: Character.Prefab.ContentPackage);
391  Enabled = false;
392  _aiParams = new CharacterParams.AIParams(null, Character.Params);
393  }
394  }
395  return _aiParams;
396  }
397  }
398  private IEnumerable<CharacterParams.TargetParams> GetTargetParams(Identifier targetTag) => AIParams.GetTargets(targetTag);
399 
400  private IEnumerable<CharacterParams.TargetParams> GetTargetParams(IEnumerable<Identifier> targetingTags)
401  {
402  foreach (Identifier tag in targetingTags)
403  {
404  foreach (var tp in GetTargetParams(tag))
405  {
406  yield return tp;
407  }
408  }
409  }
410 
411  private readonly List<Identifier> _targetingTags = new List<Identifier>();
412 
413  private IEnumerable<Identifier> GetTargetingTags(AITarget aiTarget)
414  {
415  _targetingTags.Clear();
416  if (aiTarget?.Entity == null) { return _targetingTags; }
417  if (aiTarget.Entity is Character targetCharacter)
418  {
419  if (targetCharacter.IsDead)
420  {
421  _targetingTags.Add(Tags.Dead);
422  }
423  else if (AIParams.TryGetHighestPriorityTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >= Character.GetDamageDoneByAttacker(targetCharacter))
424  {
425  _targetingTags.Add(tp.Tag);
426  }
427  else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner)
428  {
429  _targetingTags.Add(Tags.Owner);
430  }
431  else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter)))
432  {
433  _targetingTags.Add(Tags.Hostile);
434  }
435  else if (AIParams.TryGetHighestPriorityTarget(targetCharacter, out CharacterParams.TargetParams tP))
436  {
437  _targetingTags.Add(tP.Tag);
438  }
439  else if (targetCharacter.AIController is EnemyAIController enemy)
440  {
441  if (enemy.PetBehavior != null && (PetBehavior != null || AIParams.HasTag(Tags.Pet)))
442  {
443  // Pets see other pets as pets by default.
444  // Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below.
445  _targetingTags.Add(Tags.Pet);
446  }
447  else if (targetCharacter.IsHusk && AIParams.HasTag(Tags.Husk))
448  {
449  _targetingTags.Add(Tags.Husk);
450  }
451  else if (!Character.IsSameSpeciesOrGroup(targetCharacter))
452  {
453  if (enemy.CombatStrength > CombatStrength)
454  {
455  _targetingTags.Add(Tags.Stronger);
456  }
457  else if (enemy.CombatStrength < CombatStrength)
458  {
459  _targetingTags.Add(Tags.Weaker);
460  }
461  else
462  {
463  _targetingTags.Add(Tags.Equal);
464  }
465  }
466  }
467  }
468  else if (aiTarget.Entity is Item targetItem)
469  {
470  foreach (var prio in AIParams.Targets)
471  {
472  if (targetItem.HasTag(prio.Tag))
473  {
474  _targetingTags.Add(prio.Tag);
475  }
476  }
477  if (_targetingTags.None())
478  {
479  if (targetItem.GetComponent<Sonar>() != null)
480  {
481  _targetingTags.Add(Tags.Sonar);
482  }
483  if (targetItem.GetComponent<Door>() != null)
484  {
485  _targetingTags.Add(Tags.Door);
486  }
487  }
488  }
489  else if (aiTarget.Entity is Structure)
490  {
491  _targetingTags.Add(Tags.Wall);
492  }
493  else if (aiTarget.Entity is Hull)
494  {
495  _targetingTags.Add(Tags.Room);
496  }
497  return _targetingTags;
498  }
499 
500  public override void SelectTarget(AITarget target) => SelectTarget(target, 100);
501 
502  public void SelectTarget(AITarget target, float priority)
503  {
504  SelectedAiTarget = target;
505  currentTargetMemory = GetTargetMemory(target, addIfNotFound: true);
506  currentTargetMemory.Priority = priority;
507  ignoredTargets.Remove(target);
508  }
509 
510  private float movementMargin;
511 
512  private void ReleaseDragTargets()
513  {
514  AttackLimb?.AttachedRope?.Snap();
516  {
517  Character.HeldItems.ForEach(i => i.GetComponent<Holdable>()?.GetRope()?.Snap());
518  }
519  }
520 
521  public void EvaluatePlayDeadProbability(float? probability = null)
522  {
523  if (probability.HasValue)
524  {
525  AIParams.PlayDeadProbability = probability.Value;
526  }
527  Character.AllowPlayDead = Rand.Value() <= AIParams.PlayDeadProbability;
528  }
529 
530  public override void Update(float deltaTime)
531  {
532  if (DisableEnemyAI) { return; }
533  base.Update(deltaTime);
534  UpdateTriggers(deltaTime);
536  IsTryingToSteerThroughGap = false;
537  Reverse = false;
538 
539  //doesn't do anything usually, but events may sometimes change monsters' (or pets' that use enemy AI) teams
541 
543  if (steeringManager == insideSteering)
544  {
545  var currPath = PathSteering.CurrentPath;
546  if (currPath != null && currPath.CurrentNode != null)
547  {
548  if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
549  {
550  // Don't allow to jump from too high.
551  float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2;
552  float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y);
553  ignorePlatforms = height < allowedJumpHeight;
554  }
555  }
556  if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent)
557  {
559  }
560  }
561  Character.AnimController.IgnorePlatforms = ignorePlatforms;
562 
563  if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater &&
565  {
566  if (SelectedAiTarget?.Entity != null || EscapeTarget != null)
567  {
568  Entity t = SelectedAiTarget?.Entity ?? EscapeTarget;
569  float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath() ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
570  Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left;
571  }
572  else
573  {
575  }
576  }
577  if (isStateChanged)
578  {
579  if (State == AIState.Idle || State == AIState.Patrol)
580  {
581  stateResetTimer -= deltaTime;
582  if (stateResetTimer <= 0)
583  {
584  ResetOriginalState();
585  }
586  }
587  }
588  if (targetIgnoreTimer > 0)
589  {
590  targetIgnoreTimer -= deltaTime;
591  }
592  else
593  {
594  ignoredTargets.Clear();
595  targetIgnoreTimer = targetIgnoreTime;
596  }
597  avoidTimer -= deltaTime;
598  if (avoidTimer < 0)
599  {
600  avoidTimer = 0;
601  }
602  UpdateCurrentMemoryLocation();
603  if (updateMemoriesTimer > 0)
604  {
605  updateMemoriesTimer -= deltaTime;
606  }
607  else
608  {
609  FadeMemories(updateMemoriesInverval);
610  updateMemoriesTimer = updateMemoriesInverval;
611  }
612  if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null)
613  {
614  Character target = SelectedAiTarget.Entity as Character;
615  if (target == null && SelectedAiTarget.Entity is Item targetItem)
616  {
617  target = GetOwner(targetItem);
618  }
619  bool shouldFlee = false;
620  if (target != null)
621  {
622  // Keep fleeing if being chased or if we see a human target (that don't have enemy ai).
623  shouldFlee = target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target);
624  }
625  // If we should not flee, just idle. Don't allow any other AI state when below the health threshold.
626  State = shouldFlee ? AIState.Flee : AIState.Idle;
627  wallTarget = null;
628  if (State != AIState.Flee)
629  {
630  SelectedAiTarget = null;
631  _lastAiTarget = null;
632  }
633  }
634  else
635  {
636  if (TargetingRestrictions != previousTargetingRestrictions)
637  {
638  previousTargetingRestrictions = TargetingRestrictions;
639  // update targeting instantly when there's a change in targeting restrictions
640  updateTargetsTimer = 0;
641  SelectedAiTarget = null;
642  }
643 
644  if (updateTargetsTimer > 0)
645  {
646  updateTargetsTimer -= deltaTime;
647  }
648  else if (avoidTimer <= 0 || activeTriggers.Any() && returnTimer <= 0)
649  {
650  UpdateTargets();
651  }
652  }
653 
654  if (Character.Params.UsePathFinding && AIParams.UsePathFindingToGetInside && AIParams.CanOpenDoors)
655  {
656  // Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk).
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);
658 
659  if (Character.Submarine != null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer))
660  {
661  if (steeringManager != insideSteering)
662  {
663  insideSteering.Reset();
664  }
665  steeringManager = insideSteering;
666  steeringBuffer += steeringBufferIncreaseSpeed * deltaTime;
667  }
668  else
669  {
670  if (steeringManager != outsideSteering)
671  {
672  outsideSteering.Reset();
673  }
674  steeringManager = outsideSteering;
675  steeringBuffer = minSteeringBuffer;
676  }
677  steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
678  }
679  else
680  {
681  // Normally the monsters only use pathing inside submarines, not outside.
683  {
684  if (steeringManager != insideSteering)
685  {
686  insideSteering.Reset();
687  }
688  steeringManager = insideSteering;
689  }
690  else
691  {
692  if (steeringManager != outsideSteering)
693  {
694  outsideSteering.Reset();
695  }
696  steeringManager = outsideSteering;
697  }
698  }
699 
700  bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater;
701  bool run = false;
702  switch (State)
703  {
704  case AIState.Freeze:
706  break;
707  case AIState.Idle:
708  UpdateIdle(deltaTime);
709  break;
710  case AIState.PlayDead:
711  Character.IsRagdolled = true;
712  break;
713  case AIState.Patrol:
714  UpdatePatrol(deltaTime);
715  break;
716  case AIState.Attack:
717  run = !IsCoolDownRunning || AttackLimb != null && AttackLimb.attack.FullSpeedAfterAttack;
718  UpdateAttack(deltaTime);
719  break;
720  case AIState.Eat:
721  UpdateEating(deltaTime);
722  break;
723  case AIState.Escape:
724  case AIState.Flee:
725  run = true;
726  Escape(deltaTime);
727  break;
728  case AIState.Avoid:
729  case AIState.PassiveAggressive:
730  case AIState.Aggressive:
731  if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity.Removed)
732  {
733  State = AIState.Idle;
734  return;
735  }
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))
739  {
740  run = true;
741  if (State == AIState.Avoid)
742  {
743  Escape(deltaTime);
744  }
745  else
746  {
747  UpdateAttack(deltaTime);
748  }
749  }
750  else
751  {
752  bool isBeingChased = IsBeingChased;
753  float reactDistance = !isBeingChased && currentTargetingParams is { ReactDistance: > 0 } ? currentTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
754  if (squaredDistance <= Math.Pow(reactDistance, 2))
755  {
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))
759  {
760  run = true;
761  UpdateAttack(deltaTime);
762  }
763  else
764  {
765  run = isBeingChased || squaredDistance < Math.Pow(halfReactDistance, 2);
766  State = AIState.Escape;
767  avoidTimer = AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f);
768  }
769  }
770  else
771  {
772  UpdateIdle(deltaTime);
773  }
774  }
775  break;
776  case AIState.Protect:
777  case AIState.Follow:
778  case AIState.FleeTo:
779  case AIState.HideTo:
780  case AIState.Hiding:
781  if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity.Removed)
782  {
783  State = AIState.Idle;
784  return;
785  }
786  if (State == AIState.Protect)
787  {
788  if (SelectedAiTarget.Entity is Character targetCharacter)
789  {
790  bool ShouldRetaliate(Character.Attacker a)
791  {
792  Character c = a.Character;
793  if (c == null || c.IsUnconscious || c.Removed) { return false; }
794  // Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group.
795  if (Character.IsSameSpeciesOrGroup(c)) { return false; }
796  if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; }
797  //don't try to attack targets in a sub that belongs to a different team
798  //(for example, targets in an outpost if we're in the main sub)
799  if (c.Submarine?.TeamID != Character.Submarine?.TeamID) { return false; }
800  if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
801  {
802  return a.Damage >= currentTargetingParams.Threshold;
803  }
804  return true;
805  }
806  Character attacker = targetCharacter.LastAttackers.LastOrDefault(ShouldRetaliate)?.Character;
807  if (attacker?.AiTarget != null)
808  {
809  ChangeTargetState(attacker, AIState.Attack, currentTargetingParams.Priority * 2);
810  SelectTarget(attacker.AiTarget);
811  State = AIState.Attack;
812  UpdateWallTarget(requiredHoleCount);
813  return;
814  }
815  }
816  }
817  float distX = Math.Abs(WorldPosition.X - SelectedAiTarget.WorldPosition.X);
818  float distY = Math.Abs(WorldPosition.Y - SelectedAiTarget.WorldPosition.Y);
819  if (Character.Submarine != null && distY > 50 && SelectedAiTarget.Entity is Character targetC && !VisibleHulls.Contains(targetC.CurrentHull))
820  {
821  // Target not visible, and possibly on a different floor.
822  distY *= 3;
823  }
824  float dist = distX + distY;
825  float reactDist = GetPerceivingRange(SelectedAiTarget);
826  Vector2 offset = Vector2.Zero;
827  if (currentTargetingParams != null)
828  {
829  if (currentTargetingParams.ReactDistance > 0)
830  {
831  reactDist = currentTargetingParams.ReactDistance;
832  }
833  offset = currentTargetingParams.Offset;
834  }
835  if (offset != Vector2.Zero)
836  {
837  reactDist += offset.Length();
838  }
839  if (dist > reactDist + movementMargin)
840  {
841  movementMargin = State is AIState.FleeTo or AIState.HideTo or AIState.Hiding ? 0 : reactDist;
842  if (State == AIState.Hiding)
843  {
844  // Too far to hide.
845  State = AIState.HideTo;
846  }
847  run = true;
848  UpdateFollow(deltaTime);
849  }
850  else
851  {
852  if (State == AIState.HideTo)
853  {
854  // Close enough to hide.
855  State = AIState.Hiding;
856  }
857  movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, reactDist);
858  if (State is AIState.FleeTo or AIState.Hiding)
859  {
861  Character.AnimController.TargetMovement = Vector2.Zero;
863  {
864  float force = Character.AnimController.Collider.Mass / 10;
865  Character.AnimController.Collider.MoveToPos(SelectedAiTarget.Entity.SimPosition + ConvertUnits.ToSimUnits(offset), force);
866  if (SelectedAiTarget.Entity is Item item)
867  {
868  float rotation = item.Rotation;
870  var mainLimb = Character.AnimController.MainLimb;
871  if (mainLimb.type == LimbType.Head)
872  {
874  }
875  else
876  {
877  mainLimb.body.SmoothRotate(rotation, Character.AnimController.SwimFastParams.TorsoTorque);
878  }
879  }
880  if (disableTailCoroutine == null && SelectedAiTarget.Entity is Item i && i.HasTag(Tags.GuardianShelter))
881  {
882  if (!CoroutineManager.IsCoroutineRunning(disableTailCoroutine))
883  {
884  disableTailCoroutine = CoroutineManager.Invoke(() =>
885  {
886  if (Character is { Removed: false })
887  {
888  Character.AnimController.HideAndDisable(LimbType.Tail, ignoreCollisions: false);
889  }
890  }, 1f);
891  }
892  }
894  new Vector2(0, -1),
895  new Vector2(0, -1),
896  new Vector2(0, -1),
897  new Vector2(0, -1), footMoveForce: 1);
898  }
899  }
900  else
901  {
902  UpdateIdle(deltaTime);
903  }
904  }
905  break;
906  case AIState.Observe:
907  if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity.Removed)
908  {
909  State = AIState.Idle;
910  return;
911  }
912  run = false;
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))
918  {
919  // Too far to react
920  UpdateIdle(deltaTime);
921  }
922  else if (sqrDist < Math.Pow(attackDist + movementMargin, 2))
923  {
924  movementMargin = attackDist;
927  {
928  useSteeringLengthAsMovementSpeed = true;
929  Vector2 dir = Vector2.Normalize(SelectedAiTarget.WorldPosition - Character.WorldPosition);
930  if (sqrDist < Math.Pow(attackDist * 0.75f, 2))
931  {
932  // Keep the distance, if too close
933  dir = -dir;
934  useSteeringLengthAsMovementSpeed = false;
935  Reverse = true;
936  run = true;
937  }
938  SteeringManager.SteeringManual(deltaTime, dir * 0.2f);
939  }
940  else
941  {
942  // TODO: doesn't work right here
943  FaceTarget(SelectedAiTarget.Entity);
944  }
945  observeTimer -= deltaTime;
946  if (observeTimer < 0)
947  {
948  IgnoreTarget(SelectedAiTarget);
949  State = AIState.Idle;
950  ResetAITarget();
951  }
952  }
953  else
954  {
955  run = sqrDist > Math.Pow(attackDist * 2, 2);
956  movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, attackDist);
957  UpdateFollow(deltaTime);
958  }
959  break;
960  default:
961  throw new NotImplementedException();
962  }
963 
965  {
966  LatchOntoAI?.Update(this, deltaTime);
967  }
968  IsSteeringThroughGap = false;
969  if (SwarmBehavior != null)
970  {
973  SwarmBehavior.UpdateSteering(deltaTime);
974  }
975  // Ensure that the creature keeps inside the level
976  SteerInsideLevel(deltaTime);
978  // Doesn't work if less than 1, when we use steering length as movement speed.
979  steeringManager.Update(Math.Max(speed, 1.0f));
980  float movementSpeed = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed;
983  {
984  // Limit the swimming speed inside the sub.
986  }
987  }
988 
989  #region Idle
990 
991  private void UpdateIdle(float deltaTime, bool followLastTarget = true)
992  {
994  {
995  if (playDeadTimer > 0)
996  {
997  playDeadTimer -= deltaTime;
998  }
999  else
1000  {
1001  State = AIState.PlayDead;
1002  }
1003  }
1004  else if (AIParams.PatrolFlooded || AIParams.PatrolDry)
1005  {
1006  State = AIState.Patrol;
1007  }
1008  var pathSteering = SteeringManager as IndoorsSteeringManager;
1009  if (pathSteering == null)
1010  {
1011  if (Level.Loaded != null && Level.Loaded.GetRealWorldDepth(WorldPosition.Y) > Character.CharacterHealth.CrushDepth * 0.75f)
1012  {
1013  // Steer straight up if very deep
1014  SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
1015  SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
1016  return;
1017  }
1018  }
1019  if (followLastTarget)
1020  {
1021  var target = SelectedAiTarget ?? _lastAiTarget;
1022  if (target?.Entity is { Removed: false } &&
1023  PreviousState == AIState.Attack && Character.CurrentHull == null &&
1024  (_previousAttackLimb?.attack == null ||
1025  _previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
1026  {
1027  // Keep heading to the last known position of the target
1028  var memory = GetTargetMemory(target);
1029  if (memory != null)
1030  {
1031  var location = memory.Location;
1032  float dist = Vector2.DistanceSquared(WorldPosition, location);
1033  if (dist < 50 * 50 || !IsPositionInsideAllowedZone(WorldPosition, out _))
1034  {
1035  // Target is gone
1036  ResetAITarget();
1037  }
1038  else
1039  {
1040  // Steer towards the target
1041  SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(target.Entity, location), 5);
1042  SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
1043  return;
1044  }
1045  }
1046  else
1047  {
1048  ResetAITarget();
1049  }
1050  }
1051  }
1052  if (!Character.IsClimbing)
1053  {
1054  if (pathSteering != null && !Character.AnimController.InWater)
1055  {
1056  // Wander around inside
1057  pathSteering.Wander(deltaTime, Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100.0f), stayStillInTightSpace: false);
1058  }
1059  else
1060  {
1061  // Wander around outside or swimming
1062  steeringManager.SteeringWander(avoidWanderingOutsideLevel: true);
1063  if (Character.AnimController.InWater)
1064  {
1065  SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
1066  }
1067  }
1068  }
1069  }
1070 
1071  private readonly List<Hull> targetHulls = new List<Hull>();
1072  private readonly List<float> hullWeights = new List<float>();
1073 
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;
1080 
1081  private void UpdatePatrol(float deltaTime, bool followLastTarget = true)
1082  {
1083  if (SteeringManager is IndoorsSteeringManager pathSteering)
1084  {
1085  if (patrolTarget == null || IsCurrentPathUnreachable || IsCurrentPathFinished)
1086  {
1087  newPatrolTargetTimer = Math.Min(newPatrolTargetTimer, newPatrolTargetIntervalMin);
1088  }
1089  if (newPatrolTargetTimer > 0)
1090  {
1091  newPatrolTargetTimer -= deltaTime;
1092  }
1093  else
1094  {
1095  if (!searchingNewHull)
1096  {
1097  searchingNewHull = true;
1098  FindTargetHulls();
1099  }
1100  else if (targetHulls.Any())
1101  {
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)
1105  {
1106  //can't go to this room, remove it from the list and try another room
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);
1115  }
1116  else
1117  {
1118  PathSteering.SetPath(patrolTarget.SimPosition, path);
1119  patrolTimerMargin = 0;
1120  newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f);
1121  searchingNewHull = false;
1122  }
1123  }
1124  else
1125  {
1126  // Couldn't find a valid hull
1127  newPatrolTargetTimer = newPatrolTargetIntervalMax;
1128  searchingNewHull = false;
1129  }
1130  }
1131  if (patrolTarget != null && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable)
1132  {
1133  PathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n));
1134  return;
1135  }
1136  }
1137 
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;
1141 
1142  UpdateIdle(deltaTime, followLastTarget);
1143  }
1144 
1145  private void FindTargetHulls()
1146  {
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)
1154  {
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)
1160  {
1161  if (AIParams.PatrolDry)
1162  {
1163  if (hull.WaterPercentage > 50) { continue; }
1164  }
1165  if (AIParams.PatrolFlooded)
1166  {
1167  if (hull.WaterPercentage < 80) { continue; }
1168  }
1169  }
1170  if (AIParams.PatrolDry && hull.WaterPercentage < 80)
1171  {
1172  if (Math.Abs(Character.CurrentHull.WorldPosition.Y - hull.WorldPosition.Y) > Character.CurrentHull.CeilingHeight / 2)
1173  {
1174  // Ignore dry hulls that are on a different level
1175  continue;
1176  }
1177  }
1178  if (!targetHulls.Contains(hull))
1179  {
1180  targetHulls.Add(hull);
1181  float weight = hull.Size.Combine();
1182  float dist = Vector2.Distance(Character.WorldPosition, hull.WorldPosition);
1183  float optimal = 1000;
1184  float max = 3000;
1185  // Prefer rooms that are far but not too far.
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)
1189  {
1190  waterFactor = AIParams.PatrolDry ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage)) : MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 100, hull.WaterPercentage));
1191  }
1192  weight *= distanceFactor * waterFactor;
1193  hullWeights.Add(weight);
1194  }
1195  }
1196  }
1197 
1198  #endregion
1199 
1200  #region Attack
1201 
1202  private Vector2 attackWorldPos;
1203  private Vector2 attackSimPos;
1204  private float reachTimer;
1205  // How long the monster tries to reach out for the target when it's close to it before ignoring it.
1206  private const float reachTimeOut = 10;
1207 
1208  private bool IsSameTarget(AITarget target, AITarget otherTarget)
1209  {
1210  if (target?.Entity == otherTarget?.Entity) { return true; }
1211  if (IsItemInCharacterInventory(target, otherTarget) || IsItemInCharacterInventory(otherTarget, target)) { return true; }
1212  return false;
1213 
1214  bool IsItemInCharacterInventory(AITarget potentialItem, AITarget potentialCharacter)
1215  {
1216  if (potentialItem?.Entity is Item item && potentialCharacter?.Entity is Character character)
1217  {
1218  return item.ParentInventory?.Owner == character;
1219  }
1220  return false;
1221  }
1222  }
1223 
1224  private void UpdateAttack(float deltaTime)
1225  {
1226  if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity.Removed || currentTargetingParams == null)
1227  {
1228  State = AIState.Idle;
1229  return;
1230  }
1231 
1232  attackWorldPos = SelectedAiTarget.WorldPosition;
1233  attackSimPos = SelectedAiTarget.SimPosition;
1234 
1235  if (SelectedAiTarget.Entity is Item item)
1236  {
1237  // If the item is held by a character, attack the character instead.
1238  Character owner = GetOwner(item);
1239  if (owner != null)
1240  {
1241  if (Character.IsFriendly(owner) || owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
1242  {
1243  ResetAITarget();
1244  State = AIState.Idle;
1245  return;
1246  }
1247  else
1248  {
1249  SelectedAiTarget = owner.AiTarget;
1250  }
1251  }
1252  }
1253 
1254  if (wallTarget != null)
1255  {
1256  attackWorldPos = wallTarget.Position;
1257  if (wallTarget.Structure.Submarine != null)
1258  {
1259  attackWorldPos += wallTarget.Structure.Submarine.Position;
1260  }
1261  attackSimPos = Character.Submarine == wallTarget.Structure.Submarine ? wallTarget.Position : attackWorldPos;
1262  attackSimPos = ConvertUnits.ToSimUnits(attackSimPos);
1263  }
1264  else
1265  {
1266  attackSimPos = Character.GetRelativeSimPosition(SelectedAiTarget.Entity);
1267  }
1268 
1269  if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True)
1270  {
1271  if (TrySteerThroughGaps(deltaTime))
1272  {
1273  return;
1274  }
1275  }
1276  else if (SelectedAiTarget.Entity is Structure w && wallTarget == null)
1277  {
1278  bool isBroken = true;
1279  for (int i = 0; i < w.Sections.Length; i++)
1280  {
1281  if (!w.SectionBodyDisabled(i))
1282  {
1283  isBroken = false;
1284  Vector2 sectionPos = w.SectionPosition(i, world: true);
1285  attackWorldPos = sectionPos;
1286  attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos);
1287  break;
1288  }
1289  }
1290  if (isBroken)
1291  {
1292  IgnoreTarget(SelectedAiTarget);
1293  State = AIState.Idle;
1294  ResetAITarget();
1295  return;
1296  }
1297  }
1298  attackLimbSelectionTimer -= deltaTime;
1299  if (AttackLimb == null || attackLimbSelectionTimer <= 0)
1300  {
1301  attackLimbSelectionTimer = attackLimbSelectionInterval * Rand.Range(0.9f, 1.1f);
1302  if (!IsAttackRunning && !IsCoolDownRunning)
1303  {
1304  AttackLimb = GetAttackLimb(attackWorldPos);
1305  }
1306  }
1307  Character targetCharacter = SelectedAiTarget.Entity as Character;
1308  IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
1309  bool canAttack = !Character.IsClimbing;
1310  bool pursue = false;
1311  if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0))
1312  {
1313  var currentAttackLimb = AttackLimb ?? _previousAttackLimb;
1314  if (currentAttackLimb.attack.CoolDownTimer >=
1315  currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
1316  {
1317  return;
1318  }
1319  currentAttackLimb.attack.AfterAttackTimer += deltaTime;
1320  AIBehaviorAfterAttack activeBehavior =
1321  currentAttackLimb.attack.AfterAttackSecondaryDelay > 0 && currentAttackLimb.attack.AfterAttackTimer > currentAttackLimb.attack.AfterAttackSecondaryDelay ?
1322  currentAttackLimb.attack.AfterAttackSecondary :
1323  currentAttackLimb.attack.AfterAttack;
1324  switch (activeBehavior)
1325  {
1326  case AIBehaviorAfterAttack.Eat:
1327  if (currentAttackLimb.IsSevered)
1328  {
1329  ReleaseEatingTarget();
1330  }
1331  else
1332  {
1333  UpdateEating(deltaTime);
1334  }
1335  return;
1336  case AIBehaviorAfterAttack.Pursue:
1337  case AIBehaviorAfterAttack.PursueIfCanAttack:
1338  if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
1339  {
1340  // No (valid) secondary cooldown defined.
1341  if (activeBehavior == AIBehaviorAfterAttack.Pursue)
1342  {
1343  canAttack = false;
1344  pursue = true;
1345  }
1346  else
1347  {
1348  UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
1349  return;
1350  }
1351  }
1352  else
1353  {
1354  if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
1355  {
1356  // Don't allow attacking when the attack target has just changed.
1357  if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
1358  {
1359  canAttack = false;
1360  if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack)
1361  {
1362  // Fall back if cannot attack.
1363  UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
1364  return;
1365  }
1366  AttackLimb = null;
1367  }
1368  else
1369  {
1370  // If the secondary cooldown is defined and expired, check if we can switch the attack
1371  var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
1372  if (newLimb != null)
1373  {
1374  // Attack with the new limb
1375  AttackLimb = newLimb;
1376  }
1377  else
1378  {
1379  // No new limb was found.
1380  if (activeBehavior == AIBehaviorAfterAttack.Pursue)
1381  {
1382  canAttack = false;
1383  pursue = true;
1384  }
1385  else
1386  {
1387  UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
1388  return;
1389  }
1390  }
1391  }
1392  }
1393  else
1394  {
1395  // Cooldown not yet expired, cannot attack -> steer towards the target
1396  canAttack = false;
1397  }
1398  }
1399  break;
1400  case AIBehaviorAfterAttack.FallBackUntilCanAttack:
1401  case AIBehaviorAfterAttack.FollowThroughUntilCanAttack:
1402  case AIBehaviorAfterAttack.ReverseUntilCanAttack:
1403  if (activeBehavior == AIBehaviorAfterAttack.ReverseUntilCanAttack)
1404  {
1405  Reverse = true;
1406  }
1407  if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
1408  {
1409  // No (valid) secondary cooldown defined.
1410  UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1411  return;
1412  }
1413  else
1414  {
1415  if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
1416  {
1417  // Don't allow attacking when the attack target has just changed.
1418  if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
1419  {
1420  UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1421  return;
1422  }
1423  // If the secondary cooldown is defined and expired, check if we can switch the attack
1424  var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
1425  if (newLimb != null)
1426  {
1427  // Attack with the new limb
1428  AttackLimb = newLimb;
1429  }
1430  else
1431  {
1432  // No new limb was found.
1433  UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1434  return;
1435  }
1436  }
1437  else
1438  {
1439  // Cooldown not yet expired -> steer away from the target
1440  UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1441  return;
1442  }
1443  }
1444  break;
1445  case AIBehaviorAfterAttack.IdleUntilCanAttack:
1446  if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
1447  {
1448  // No (valid) secondary cooldown defined.
1449  UpdateIdle(deltaTime, followLastTarget: false);
1450  return;
1451  }
1452  else
1453  {
1454  if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
1455  {
1456  // Don't allow attacking when the attack target has just changed.
1457  if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget))
1458  {
1459  UpdateIdle(deltaTime, followLastTarget: false);
1460  return;
1461  }
1462  // If the secondary cooldown is defined and expired, check if we can switch the attack
1463  var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
1464  if (newLimb != null)
1465  {
1466  // Attack with the new limb
1467  AttackLimb = newLimb;
1468  }
1469  else
1470  {
1471  // No new limb was found.
1472  UpdateIdle(deltaTime, followLastTarget: false);
1473  return;
1474  }
1475  }
1476  else
1477  {
1478  // Cooldown not yet expired
1479  UpdateIdle(deltaTime, followLastTarget: false);
1480  return;
1481  }
1482  }
1483  break;
1484  case AIBehaviorAfterAttack.FollowThrough:
1485  UpdateFallBack(attackWorldPos, deltaTime, followThrough: true);
1486  return;
1487  case AIBehaviorAfterAttack.FollowThroughWithoutObstacleAvoidance:
1488  UpdateFallBack(attackWorldPos, deltaTime, followThrough: true, avoidObstacles: false);
1489  return;
1490  case AIBehaviorAfterAttack.FallBack:
1491  case AIBehaviorAfterAttack.Reverse:
1492  default:
1493  if (activeBehavior == AIBehaviorAfterAttack.Reverse)
1494  {
1495  Reverse = true;
1496  }
1497  UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
1498  return;
1499  }
1500  }
1501  else
1502  {
1503  attackVector = null;
1504  }
1505 
1506  if (canAttack)
1507  {
1508  if (AttackLimb is { IsSevered: true })
1509  {
1510  AttackLimb = null;
1511  }
1512  if (AttackLimb == null || !IsValidAttack(AttackLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity))
1513  {
1514  AttackLimb = GetAttackLimb(attackWorldPos);
1515  }
1516  canAttack = AttackLimb != null && AttackLimb.attack.CoolDownTimer <= 0;
1517  }
1518 
1519  if (!AIParams.CanOpenDoors)
1520  {
1521  if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
1522  {
1523  if (wallTarget == null && Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
1524  {
1525  // Check that we are not bumping into a door or a wall
1526  Vector2 rayStart = SimPosition;
1527  if (Character.Submarine == null)
1528  {
1529  rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
1530  }
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))
1536  {
1537  // Target is unreachable, there's a door or wall ahead
1538  State = AIState.Idle;
1539  IgnoreTarget(SelectedAiTarget);
1540  ResetAITarget();
1541  return;
1542  }
1543  }
1544  }
1545  }
1546 
1547  float distance = 0;
1548  Limb attackTargetLimb = null;
1549  if (canAttack)
1550  {
1551  if (!Character.AnimController.SimplePhysicsEnabled)
1552  {
1553  // Target a specific limb instead of the target center position
1554  if (wallTarget == null && targetCharacter != null)
1555  {
1556  var targetLimbType = AttackLimb.Params.Attack.Attack.TargetLimbType;
1557  attackTargetLimb = GetTargetLimb(AttackLimb, targetCharacter, targetLimbType);
1558  if (attackTargetLimb == null)
1559  {
1560  State = AIState.Idle;
1561  IgnoreTarget(SelectedAiTarget);
1562  ResetAITarget();
1563  return;
1564  }
1565  attackWorldPos = attackTargetLimb.WorldPosition;
1566  attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb);
1567  }
1568  }
1569  Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition;
1570  Vector2 toTarget = attackWorldPos - attackLimbPos;
1571  Vector2 toTargetOffset = toTarget;
1572  // Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute
1573  if (wallTarget != null && Character.Submarine == null)
1574  {
1575  if (wallTarget.Structure.Submarine != null)
1576  {
1577  Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity);
1578  toTargetOffset += margin;
1579  }
1580  }
1581  else if (targetCharacter != null)
1582  {
1583  Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
1584  toTargetOffset += margin;
1585  }
1586  else if (SelectedAiTarget.Entity is MapEntity e)
1587  {
1588  if (e.Submarine != null)
1589  {
1590  Vector2 margin = CalculateMargin(e.Submarine.Velocity);
1591  toTargetOffset += margin;
1592  }
1593  }
1594 
1595  Vector2 CalculateMargin(Vector2 targetVelocity)
1596  {
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;
1603  // Intentionally omit the unit conversion because we use distanceOffset as a multiplier.
1604  return targetVelocity * distanceOffset * dot;
1605  }
1606 
1607  // Check that we can reach the target
1608  distance = toTargetOffset.Length();
1609  if (canAttack)
1610  {
1611  canAttack = distance < AttackLimb.attack.Range;
1612  }
1613  if (canAttack && !Character.InWater && Character.AnimController.CanWalk)
1614  {
1615  // On ground, ensure that the monster is facing the target, so that they don't hit the target while standing with their back towards it.
1616  // In water, we don't want such checks, because it's ok for the monsters to attack targets on their sides and even behind them.
1617  if (!Character.IsFacing(attackWorldPos))
1618  {
1619  canAttack = false;
1620  }
1621  }
1622  if (canAttack)
1623  {
1624  reachTimer = 0;
1625  if (IsAggressiveBoarder)
1626  {
1627  if (SelectedAiTarget.Entity is Item i && i.GetComponent<Door>() is Door { CanBeTraversed: true })
1628  {
1629  // Don't attack open doors, just steer through them.
1630  canAttack = false;
1631  }
1632  }
1633  }
1634  else if (currentTargetingParams.AttackPattern == AttackPattern.Straight && distance < AttackLimb.attack.Range * 5)
1635  {
1636  Vector2 targetVelocity = Vector2.Zero;
1637  Submarine targetSub = SelectedAiTarget.Entity.Submarine;
1638  if (targetSub != null)
1639  {
1640  targetVelocity = targetSub.Velocity;
1641  }
1642  else if (targetCharacter != null)
1643  {
1644  targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity;
1645  }
1646  else if (SelectedAiTarget.Entity is Item i && i.body != null)
1647  {
1648  targetVelocity = i.body.LinearVelocity;
1649  }
1650  float mySpeed = Character.AnimController.Collider.LinearVelocity.LengthSquared();
1651  float targetSpeed = targetVelocity.LengthSquared();
1652  if (mySpeed < 0.1f || mySpeed > targetSpeed)
1653  {
1654  reachTimer += deltaTime;
1655  if (reachTimer > reachTimeOut)
1656  {
1657  reachTimer = 0;
1658  IgnoreTarget(SelectedAiTarget);
1659  State = AIState.Idle;
1660  ResetAITarget();
1661  return;
1662  }
1663  }
1664  }
1665 
1666  // Crouch if the target is down (only humanoids), so that we can reach it.
1667  if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2)
1668  {
1669  if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range)
1670  {
1671  humanoidAnimController.Crouch();
1672  }
1673  }
1674 
1675  if (canAttack)
1676  {
1677  if (AttackLimb.attack.Ranged)
1678  {
1679  // Check that is facing the target
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)
1685  {
1686  canAttack = !IsBlocked(Character.GetRelativeSimPosition(SelectedAiTarget.Entity));
1687  bool IsBlocked(Vector2 targetPosition)
1688  {
1689  foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
1690  {
1691  Character hitTarget = null;
1692  if (body.UserData is Character c)
1693  {
1694  hitTarget = c;
1695  }
1696  else if (body.UserData is Limb limb)
1697  {
1698  hitTarget = limb.character;
1699  }
1700  if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget) && !IsAttackingOwner(hitTarget))
1701  {
1702  return true;
1703  }
1704  }
1705  return false;
1706  }
1707  }
1708  }
1709  }
1710  }
1711  Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null;
1712  bool updateSteering = true;
1713  if (steeringLimb == null)
1714  {
1715  // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target.
1716  steeringLimb = Character.AnimController.GetLimb(LimbType.Head) ?? Character.AnimController.GetLimb(LimbType.Torso);
1717  }
1718  if (steeringLimb == null)
1719  {
1720  State = AIState.Idle;
1721  return;
1722  }
1723  var pathSteering = SteeringManager as IndoorsSteeringManager;
1724  if (AttackLimb != null && AttackLimb.attack.Retreat)
1725  {
1726  UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
1727  }
1728  else
1729  {
1730  Vector2 steerPos = attackSimPos;
1731  if (!Character.AnimController.SimplePhysicsEnabled)
1732  {
1733  // Offset so that we don't overshoot the movement
1734  Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
1735  steerPos += offset;
1736  }
1737  if (pathSteering != null)
1738  {
1739  if (pathSteering.CurrentPath != null)
1740  {
1741  // Attack doors
1742  if (canAttackDoors)
1743  {
1744  // If the target is in the same hull, there shouldn't be any doors blocking the path
1745  if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
1746  {
1747  var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
1748  if (door is { CanBeTraversed: false } && (!Character.IsInFriendlySub || !door.HasAccess(Character)))
1749  {
1750  if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
1751  {
1752  SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority);
1753  State = AIState.Attack;
1754  return;
1755  }
1756  }
1757  }
1758  }
1759  // When pursuing, we don't want to pursue too close
1760  float max = 300;
1761  float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max;
1762  if (!canAttack || distance > margin)
1763  {
1764  // Steer towards the target if in the same room and swimming
1765  // Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering.
1766  if (Character.CurrentHull != null &&
1767  Character.Submarine != null && !Character.Submarine.Info.IsRuin &&
1768  (Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
1769  targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))
1770  {
1771  Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
1772  SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos));
1773  }
1774  else
1775  {
1776  pathSteering.SteeringSeek(steerPos, weight: 2,
1777  minGapWidth: minGapSize,
1778  startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null),
1779  checkVisiblity: true);
1780 
1781  if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
1782  {
1783  State = AIState.Idle;
1784  IgnoreTarget(SelectedAiTarget);
1785  ResetAITarget();
1786  return;
1787  }
1788  }
1789  }
1790  else if (!IsTryingToSteerThroughGap)
1791  {
1792  if (AttackLimb.attack.Ranged)
1793  {
1794  float dir = Character.AnimController.Dir;
1795  if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin)
1796  {
1797  SteeringManager.Reset();
1798  }
1799  else
1800  {
1801  // Too close
1802  UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
1803  }
1804  }
1805  else
1806  {
1807  // Close enough
1808  SteeringManager.Reset();
1809  }
1810  }
1811  else
1812  {
1813  SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
1814  }
1815  }
1816  else
1817  {
1818  pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize);
1819  }
1820  }
1821  else
1822  {
1823  // Sweeping and circling doesn't work well inside
1824  if (Character.CurrentHull == null)
1825  {
1826  switch (currentTargetingParams.AttackPattern)
1827  {
1828  case AttackPattern.Sweep:
1829  if (currentTargetingParams.SweepDistance > 0)
1830  {
1831  if (distance <= 0)
1832  {
1833  distance = (attackWorldPos - WorldPosition).Length();
1834  }
1835  float amplitude = MathHelper.Lerp(0, currentTargetingParams.SweepStrength, MathUtils.InverseLerp(currentTargetingParams.SweepDistance, 0, distance));
1836  if (amplitude > 0)
1837  {
1838  sweepTimer += deltaTime * currentTargetingParams.SweepSpeed;
1839  float sin = (float)Math.Sin(sweepTimer) * amplitude;
1840  steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, sin);
1841  }
1842  else
1843  {
1844  sweepTimer = Rand.Range(-1000f, 1000f) * currentTargetingParams.SweepSpeed;
1845  }
1846  }
1847  break;
1848  case AttackPattern.Circle:
1849  if (IsCoolDownRunning) { break; }
1850  if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { 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)
1856  {
1857  targetSize =
1858  targetSub != null ? Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2 :
1859  targetCharacter != null ? ConvertUnits.ToDisplayUnits(targetCharacter.AnimController.Collider.GetSize().X) : 100;
1860  }
1861  float sqrDistToTarget = Vector2.DistanceSquared(WorldPosition, spatialTarget.WorldPosition);
1862  bool isProgressive = AIParams.MaxAggression - AIParams.StartAggression > 0;
1863  switch (CirclePhase)
1864  {
1865  case CirclePhase.Start:
1866  currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, ClampIntensity(aggressionIntensity));
1867  inverseDir = false;
1868  circleDir = GetDirFromHeadingInRadius();
1869  circleRotation = 0;
1870  strikeTimer = 0;
1871  blockCheckTimer = 0;
1872  breakCircling = false;
1873  float minFallBackDistance = currentTargetingParams.CircleStartDistance * 0.5f;
1874  float maxFallBackDistance = currentTargetingParams.CircleStartDistance;
1875  float maxRandomOffset = currentTargetingParams.CircleMaxRandomOffset;
1876  // The lower the rotation speed, the slower the progression. Also the distance to the target stays longer.
1877  // So basically if the value is higher, the creature will strike the sub more quickly and with more precision.
1878  float ClampIntensity(float intensity) => MathHelper.Clamp(intensity * Rand.Range(0.9f, 1.1f), AIParams.StartAggression, AIParams.MaxAggression);
1879  if (isProgressive)
1880  {
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));
1887  }
1888  else
1889  {
1890  circleRotationSpeed = currentTargetingParams.CircleRotationSpeed;
1891  circleFallbackDistance = maxFallBackDistance;
1892  circleOffset = Rand.Vector(maxRandomOffset);
1893  }
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 })
1898  {
1899  breakCircling = true;
1900  CirclePhase = CirclePhase.CloseIn;
1901  }
1902  else if (sqrDistToTarget > MathUtils.Pow2(targetSize + currentTargetingParams.CircleStartDistance))
1903  {
1904  CirclePhase = CirclePhase.CloseIn;
1905  }
1906  else if (sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
1907  {
1908  CirclePhase = CirclePhase.FallBack;
1909  }
1910  else
1911  {
1912  CirclePhase = CirclePhase.Advance;
1913  }
1914  break;
1915  case CirclePhase.CloseIn:
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))
1920  {
1921  strikeTimer = AttackLimb.attack.CoolDown;
1922  CirclePhase = CirclePhase.Strike;
1923  }
1924  else if (!breakCircling && sqrDistToTarget <= MathUtils.Pow2(targetDistance) && targetVelocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
1925  {
1926  CirclePhase = CirclePhase.Advance;
1927  }
1928  DisableAttacksIfLimbNotRanged();
1929  break;
1930  case CirclePhase.FallBack:
1931  updateSteering = false;
1932  bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true);
1933  if (isBlocked || sqrDistToTarget > MathUtils.Pow2(targetSize + circleFallbackDistance))
1934  {
1935  CirclePhase = CirclePhase.Advance;
1936  break;
1937  }
1938  DisableAttacksIfLimbNotRanged();
1939  break;
1940  case CirclePhase.Advance:
1941  Vector2 targetVel = GetTargetVelocity();
1942  // If the target is moving fast, just steer towards the target
1943  if (breakCircling || targetVel.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()))
1944  {
1945  CirclePhase = CirclePhase.CloseIn;
1946  }
1947  else if (sqrDistToTarget > MathUtils.Pow2(targetSize + currentTargetingParams.CircleStartDistance * 1.2f))
1948  {
1949  if (currentTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100)
1950  {
1951  circleRotationSpeed *= 1 + deltaTime;
1952  }
1953  else
1954  {
1955  CirclePhase = CirclePhase.CloseIn;
1956  }
1957  }
1958  else
1959  {
1960  float rotationStep = circleRotationSpeed * deltaTime * circleDir;
1961  if (isProgressive)
1962  {
1963  circleRotation += rotationStep;
1964  }
1965  else
1966  {
1967  circleRotation = rotationStep;
1968  }
1969  Vector2 targetPos = attackSimPos + circleOffset;
1970  float targetDist = targetSize;
1971  if (targetDist <= 0)
1972  {
1973  targetDist = circleFallbackDistance;
1974  }
1975  if (targetSub != null && AttackLimb?.attack is { Ranged: true })
1976  {
1977  targetDist += circleFallbackDistance / 2;
1978  }
1979  if (Vector2.DistanceSquared(SimPosition, targetPos) < ConvertUnits.ToSimUnits(targetDist))
1980  {
1981  // Too close to the target point
1982  // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point,
1983  // which makes it continue circling around the point (as supposed)
1984  // But when there is some offset and the offset is too near, this is not what we want.
1985  if (canAttack && AttackLimb?.attack is { Ranged: false } && sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
1986  {
1987  CirclePhase = CirclePhase.Strike;
1988  strikeTimer = AttackLimb.attack.CoolDown;
1989  }
1990  else
1991  {
1992  CirclePhase = CirclePhase.Start;
1993  }
1994  break;
1995  }
1996  steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation);
1997  if (IsBlocked(deltaTime, steerPos))
1998  {
1999  if (!inverseDir)
2000  {
2001  // First try changing the direction
2002  circleDir = -circleDir;
2003  inverseDir = true;
2004  }
2005  else if (circleRotationSpeed < 1)
2006  {
2007  // Then try increasing the rotation speed to change the movement curve
2008  circleRotationSpeed *= 1 + deltaTime;
2009  }
2010  else if (circleOffset.LengthSquared() > 0.1f)
2011  {
2012  // Then try removing the offset
2013  circleOffset = Vector2.Zero;
2014  }
2015  else
2016  {
2017  // If we still fail, just steer towards the target
2018  breakCircling = AttackLimb?.attack is { Ranged: false };
2019  if (!breakCircling)
2020  {
2021  CirclePhase = CirclePhase.FallBack;
2022  }
2023  }
2024  }
2025  }
2026  if (AttackLimb?.attack is { Ranged: false })
2027  {
2028  canAttack = false;
2029  float requiredDistMultiplier = GetStrikeDistanceMultiplier(targetVel);
2030  if (distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
2031  {
2032  strikeTimer = AttackLimb.attack.CoolDown;
2033  CirclePhase = CirclePhase.Strike;
2034  }
2035  }
2036  break;
2037  case CirclePhase.Strike:
2038  strikeTimer -= deltaTime;
2039  // just continue the movement forward to make it possible to evade the attack
2040  steerPos = SimPosition + Steering;
2041  if (strikeTimer <= 0)
2042  {
2043  CirclePhase = CirclePhase.Start;
2044  aggressionIntensity += AIParams.AggressionCumulation;
2045  }
2046  break;
2047  }
2048  break;
2049 
2050  bool IsFacing(float margin)
2051  {
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;
2055  }
2056 
2057  float GetStrikeDistanceMultiplier(Vector2 targetVelocity)
2058  {
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;
2062  if (isHeading)
2063  {
2064  requiredDistMultiplier = currentTargetingParams.CircleStrikeDistanceMultiplier;
2065  float targetVelocityHorizontal = Math.Abs(targetVelocity.X);
2066  if (targetVelocityHorizontal > 1)
2067  {
2068  // Reduce the required distance if the target is moving.
2069  requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(currentTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1));
2070  if (requiredDistMultiplier < 2)
2071  {
2072  requiredDistMultiplier = 2;
2073  }
2074  }
2075  }
2076  return requiredDistMultiplier;
2077  }
2078 
2079  float GetDirFromHeadingInRadius()
2080  {
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;
2084  }
2085 
2086  Vector2 GetTargetVelocity()
2087  {
2088  if (targetSub != null)
2089  {
2090  return targetSub.Velocity;
2091  }
2092  else if (targetCharacter != null)
2093  {
2094  return targetCharacter.AnimController.Collider.LinearVelocity;
2095  }
2096  return Vector2.Zero;
2097  }
2098 
2099  float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.SwimFastParams.MovementSpeed * (targetSub != null ? 0.3f : 0.5f));
2100  }
2101  }
2102  if (updateSteering)
2103  {
2104  if (currentTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
2105  {
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);
2108  if (fallBack)
2109  {
2110  Reverse = true;
2111  UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
2112  }
2113  else if (advance)
2114  {
2115  SteeringManager.SteeringSeek(steerPos, 10);
2116  }
2117  else
2118  {
2119  if (Character.CurrentHull == null && !canAttack)
2120  {
2121  SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true);
2122  SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
2123  }
2124  else
2125  {
2126  SteeringManager.Reset();
2127  FaceTarget(SelectedAiTarget.Entity);
2128  }
2129  }
2130  }
2131  else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
2132  {
2133  if (pathSteering != null)
2134  {
2135  pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
2136  }
2137  else
2138  {
2139  SteeringManager.SteeringSeek(steerPos, 10);
2140  }
2141  }
2142  if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null ||
2143  distance == 0 ||
2144  distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2) ||
2145  AttackLimb != null && AttackLimb.attack.Ranged))
2146  {
2147  SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
2148  }
2149  }
2150  }
2151  }
2152  Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
2153  if (AttackLimb?.attack is Attack { Ranged: true } attack)
2154  {
2155  AimRangedAttack(attack, attackTargetLimb as ISpatialEntity ?? targetEntity);
2156  }
2157  if (canAttack)
2158  {
2159  if (!UpdateLimbAttack(deltaTime, attackSimPos, damageTarget, distance, attackTargetLimb))
2160  {
2161  IgnoreTarget(SelectedAiTarget);
2162  }
2163  }
2164  else if (IsAttackRunning)
2165  {
2166  AttackLimb.attack.ResetAttackTimer();
2167  }
2168 
2169  void DisableAttacksIfLimbNotRanged()
2170  {
2171  if (AttackLimb?.attack is { Ranged: false })
2172  {
2173  canAttack = false;
2174  }
2175  }
2176  }
2177 
2178  public void AimRangedAttack(Attack attack, ISpatialEntity targetEntity)
2179  {
2180  if (attack is not { Ranged: true }) { return; }
2181  if (targetEntity is Entity { Removed: true }) { return; }
2182  Character.SetInput(InputType.Aim, false, true);
2183  if (attack.AimRotationTorque <= 0) { return; }
2184  Limb limb = GetLimbToRotate(attack);
2185  if (limb != null)
2186  {
2187  Vector2 toTarget = targetEntity.WorldPosition - limb.WorldPosition;
2188  float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
2189  limb.body.SuppressSmoothRotationCalls = false;
2190  float angle = MathUtils.VectorToAngle(toTarget);
2191  limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque);
2192  limb.body.SuppressSmoothRotationCalls = true;
2193  }
2194  }
2195 
2196  private bool IsValidAttack(Limb attackingLimb, IEnumerable<AttackContext> currentContexts, Entity target)
2197  {
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; }
2205  if (!attackingLimb.attack.Ranged)
2206  {
2207  switch (target)
2208  {
2209  case Item when attackingLimb.attack.ItemDamage <= 0:
2210  case Structure when attackingLimb.attack.StructureDamage <= 0:
2211  return false;
2212  }
2213  }
2214  if (target is ISerializableEntity se and Barotrauma.Character)
2215  {
2216  if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; }
2217  }
2218  if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { return false; }
2219  if (attack.Ranged)
2220  {
2221  // Check that is approximately facing the target
2222  Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : attackingLimb.WorldPosition;
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;
2226  Vector2 forward = VectorExtensions.Forward(attackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir);
2227  float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
2228  if (angle > attack.RequiredAngle) { return false; }
2229  }
2230  if (attack.RootForceWorldEnd.LengthSquared() > 1)
2231  {
2232  // Don't allow root motion attacks, if we are not on the same level with the target, because it can cause warping.
2233  switch (target)
2234  {
2235  case Character targetCharacter when Character.CurrentHull != targetCharacter.CurrentHull || targetCharacter.IsKnockedDown:
2236  case Item targetItem when Character.CurrentHull != targetItem.CurrentHull:
2237  return false;
2238  }
2239  float verticalDistance = Math.Abs(attackWorldPos.Y - Character.WorldPosition.Y);
2240  if (verticalDistance > 50)
2241  {
2242  return false;
2243  }
2244  }
2245  return true;
2246  }
2247 
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)
2251  {
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)
2258  {
2259  if (limb == ignoredLimb) { continue; }
2260  if (limb.IsSevered || limb.IsStuck) { continue; }
2261  if (!IsValidAttack(limb, currentContexts, target)) { continue; }
2262  if (AIParams.RandomAttack)
2263  {
2264  attackLimbs.Add(limb);
2265  weights.Add(limb.attack.Priority);
2266  }
2267  else
2268  {
2269  float priority = CalculatePriority(limb, attackWorldPos);
2270  if (priority > currentPriority)
2271  {
2272  currentPriority = priority;
2273  selectedLimb = limb;
2274  }
2275  }
2276  }
2277  if (AIParams.RandomAttack)
2278  {
2279  selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Unsynced);
2280  attackLimbs.Clear();
2281  weights.Clear();
2282  }
2283  return selectedLimb;
2284 
2285  float CalculatePriority(Limb limb, Vector2 attackPos)
2286  {
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)
2292  {
2293  float min = 100;
2294  distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist));
2295  }
2296  else
2297  {
2298  // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
2299  // We also need a max value that is more than the actual range.
2300  distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
2301  }
2302  return prio * distanceFactor;
2303  }
2304  }
2305 
2306  public override void OnAttacked(Character attacker, AttackResult attackResult)
2307  {
2308  float reactionTime = Rand.Range(0.1f, 0.3f);
2309  updateTargetsTimer = Math.Min(updateTargetsTimer, reactionTime);
2310  bool wasLatched = IsLatchedOnSub;
2312  if (attackResult.Damage > 0)
2313  {
2314  LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1);
2315  }
2316  if (attacker?.AiTarget == null || attacker.Removed || attacker.IsDead) { return; }
2317  AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true, keepAlive: true);
2318  targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt;
2319  if (attackResult.Damage >= AIParams.DamageThreshold)
2320  {
2321  ReleaseDragTargets();
2322  }
2323  bool isFriendly = Character.IsFriendly(attacker);
2324  if (wasLatched)
2325  {
2326  State = AIState.Escape;
2327  avoidTimer = AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f);
2328  if (!isFriendly)
2329  {
2330  SelectTarget(attacker.AiTarget);
2331  }
2332  return;
2333  }
2334  if (State == AIState.Flee)
2335  {
2336  if (!isFriendly)
2337  {
2338  SelectTarget(attacker.AiTarget);
2339  }
2340  return;
2341  }
2342 
2343  if (!isFriendly && attackResult.Damage > 0.0f)
2344  {
2345  bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls;
2346  if (canAttack)
2347  {
2348  switch (State)
2349  {
2350  case AIState.PlayDead when Rand.Value() < 0.5f:
2351  // 50% chance of not reacting when playing dead.
2352  return;
2353  case AIState.PlayDead:
2354  case AIState.Hiding:
2355  SelectTarget(attacker.AiTarget);
2356  State = AIState.Attack;
2357  break;
2358  }
2359  }
2360  if (AIParams.AttackWhenProvoked && canAttack)
2361  {
2362  if (ignoredTargets.Contains(attacker.AiTarget))
2363  {
2364  ignoredTargets.Remove(attacker.AiTarget);
2365  }
2366  if (attacker.IsHusk)
2367  {
2368  ChangeTargetState(Tags.Husk, AIState.Attack, 100);
2369  }
2370  else
2371  {
2372  ChangeTargetState(attacker, AIState.Attack, 100);
2373  }
2374  }
2375  else if (!AIParams.HasTag(attacker.SpeciesName))
2376  {
2377  if (attacker.IsHusk)
2378  {
2379  ChangeTargetState(Tags.Husk, canAttack ? AIState.Attack : AIState.Escape, 100);
2380  }
2381  else if (attacker.AIController is EnemyAIController enemyAI)
2382  {
2383  if (enemyAI.CombatStrength > CombatStrength)
2384  {
2385  if (!AIParams.HasTag("stronger"))
2386  {
2387  ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
2388  }
2389  }
2390  else if (enemyAI.CombatStrength < CombatStrength)
2391  {
2392  if (!AIParams.HasTag("weaker"))
2393  {
2394  ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
2395  }
2396  }
2397  else if (!AIParams.HasTag("equal"))
2398  {
2399  ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
2400  }
2401  }
2402  else
2403  {
2404  ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
2405  }
2406  }
2407  else if (canAttack && attacker.IsHuman &&
2408  AIParams.TryGetTargets(attacker, out IEnumerable<CharacterParams.TargetParams> targetingParams))
2409  {
2410  //use a temporary list, because changing the state may change the targetingParams returned by TryGetTargets,
2411  //which would cause a "collection was modified" exception
2412  tempParamsList.Clear();
2413  tempParamsList.AddRange(targetingParams);
2414  foreach (var tp in tempParamsList)
2415  {
2416  if (tp.State is AIState.Aggressive or AIState.PassiveAggressive)
2417  {
2418  ChangeTargetState(attacker, AIState.Attack, 100);
2419  }
2420  }
2421  }
2422  }
2423 
2424  // Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
2425  bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine;
2426  bool avoidGunFire = AIParams.AvoidGunfire && attacker.Submarine != Character.Submarine;
2427 
2428  if (State == AIState.Attack && (IsAttackRunning || IsCoolDownRunning))
2429  {
2430  retaliate = false;
2431  if (IsAttackRunning)
2432  {
2433  avoidGunFire = false;
2434  }
2435  }
2436  if (retaliate)
2437  {
2438  // Reduce the cooldown so that the character can react
2439  foreach (var limb in Character.AnimController.Limbs)
2440  {
2441  if (limb.attack != null)
2442  {
2443  limb.attack.CoolDownTimer *= reactionTime;
2444  }
2445  }
2446  }
2447  else if (avoidGunFire && attackResult.Damage >= AIParams.DamageThreshold)
2448  {
2449  State = AIState.Escape;
2450  avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
2451  SelectTarget(attacker.AiTarget);
2452  }
2453  if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold)
2454  {
2455  State = AIState.Flee;
2456  avoidTimer = AIParams.MinFleeTime * Rand.Range(0.75f, 1.25f);
2457  SelectTarget(attacker.AiTarget);
2458  }
2459  }
2460 
2461  private Item GetEquippedItem(Limb limb)
2462  {
2463  InvSlotType GetInvSlotForLimb()
2464  {
2465  return limb.type switch
2466  {
2467  LimbType.RightHand => InvSlotType.RightHand,
2468  LimbType.LeftHand => InvSlotType.LeftHand,
2469  LimbType.Head => InvSlotType.Head,
2470  _ => InvSlotType.None,
2471  };
2472  }
2473  var slot = GetInvSlotForLimb();
2474  if (slot != InvSlotType.None)
2475  {
2476  return Character.Inventory.GetItemInLimbSlot(slot);
2477  }
2478  return null;
2479  }
2480 
2481  // 10 dmg, 100 health -> 0.1
2482  private static float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f);
2483 
2484  private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, float distance = -1, Limb targetLimb = null)
2485  {
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)
2492  {
2493  // If the selected target is not the wall target, make the wall target the selected target.
2494  var aiTarget = wallTarget.Structure.AiTarget;
2495  if (aiTarget != null && SelectedAiTarget != aiTarget)
2496  {
2497  SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority);
2498  State = AIState.Attack;
2499  return true;
2500  }
2501  }
2502  if (damageTarget == null) { return false; }
2503  ActiveAttack = AttackLimb.attack;
2504  if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
2505  {
2506  Limb referenceLimb = GetLimbToRotate(ActiveAttack);
2507  if (referenceLimb != null)
2508  {
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)
2514  {
2515  return true;
2516  }
2517  }
2518  }
2519  if (Character.Params.CanInteract && Character.Inventory != null)
2520  {
2521  // Use equipped items (weapons)
2522  Item item = GetEquippedItem(AttackLimb);
2523  if (item != null)
2524  {
2525  if (item.RequireAimToUse)
2526  {
2527  if (!Aim(deltaTime, spatialTarget, item))
2528  {
2529  // Valid target, but can't shoot -> return true so that it will not be ignored.
2530  return true;
2531  }
2532  }
2533  if (damageTarget != null)
2534  {
2535  Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
2536  item.Use(deltaTime, user: Character);
2537  }
2538  }
2539  }
2540  if (damageTarget == null) { return true; }
2541  //simulate attack input to get the character to attack client-side
2542  Character.SetInput(InputType.Attack, true, true);
2543  if (!ActiveAttack.IsRunning)
2544  {
2545 #if SERVER
2546  GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
2547  AttackLimb,
2548  damageTarget,
2549  targetLimb,
2550  SimPosition));
2551 #else
2552  Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
2553 #endif
2554  }
2555  if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
2556  {
2557  if (ActiveAttack.CoolDownTimer > 0)
2558  {
2559  SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f));
2560  }
2561  if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter)
2562  {
2563  LatchOntoAI.SetAttachTarget(targetCharacter);
2564  }
2565  if (!ActiveAttack.Ranged)
2566  {
2567  if (damageTarget.Health > 0 && attackResult.Damage > 0)
2568  {
2569  // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
2570  float greed = AIParams.AggressionGreed;
2571  if (damageTarget is not Barotrauma.Character)
2572  {
2573  // Halve the greed for attacking non-characters.
2574  greed /= 2;
2575  }
2576  currentTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
2577  }
2578  else
2579  {
2580  currentTargetMemory.Priority -= Math.Max(currentTargetMemory.Priority / 2, 1);
2581  return currentTargetMemory.Priority > 1;
2582  }
2583  }
2584  }
2585  return true;
2586  }
2587 
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)
2593  {
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)
2600  {
2601  sinTime += deltaTime * AttackLimb.attack.SwayFrequency;
2602  Character.CursorPosition += VectorExtensions.Forward(weapon.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2 * AttackLimb.attack.SwayAmount);
2603  }
2604  if (Character.Submarine != null)
2605  {
2606  Character.CursorPosition -= Character.Submarine.Position;
2607  }
2608  visibilityCheckTimer -= deltaTime;
2609  if (visibilityCheckTimer <= 0.0f)
2610  {
2611  canSeeTarget = Character.CanSeeTarget(target);
2612  visibilityCheckTimer = 0.2f;
2613  }
2614  if (!canSeeTarget)
2615  {
2616  SetAimTimer();
2617  return false;
2618  }
2619  Character.SetInput(InputType.Aim, false, true);
2620  if (aimTimer > 0)
2621  {
2622  aimTimer -= deltaTime;
2623  return false;
2624  }
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)
2630  {
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)
2634  {
2635  if (target is MapEntity)
2636  {
2637  if (pickedBody.UserData is Submarine sub && sub == target.Submarine)
2638  {
2639  return true;
2640  }
2641  else if (target == pickedBody.UserData)
2642  {
2643  return true;
2644  }
2645  }
2646 
2647  Character t = null;
2648  if (pickedBody.UserData is Character c)
2649  {
2650  t = c;
2651  }
2652  else if (pickedBody.UserData is Limb limb)
2653  {
2654  t = limb.character;
2655  }
2656  if (t != null && (t == target || (!Character.IsFriendly(t) || IsAttackingOwner(t))))
2657  {
2658  return true;
2659  }
2660  }
2661  }
2662  return false;
2663  }
2664 
2665  private void SetAimTimer(float timer = 1.5f) => aimTimer = timer * Rand.Range(0.75f, 1.25f);
2666 
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)
2671  {
2672  blockCheckTimer -= deltaTime;
2673  if (blockCheckTimer <= 0)
2674  {
2675  blockCheckTimer = blockCheckInterval;
2676  isBlocked = Submarine.PickBodies(SimPosition, steerPos, collisionCategory: collisionCategory).Any();
2677  }
2678  return isBlocked;
2679  }
2680 
2681  private Vector2? attackVector = null;
2682  private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false, bool avoidObstacles = true)
2683  {
2684  if (attackVector == null)
2685  {
2686  attackVector = attackWorldPos - WorldPosition;
2687  }
2688  Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
2689  if (!MathUtils.IsValid(dir))
2690  {
2691  dir = Vector2.UnitY;
2692  }
2693  steeringManager.SteeringManual(deltaTime, dir);
2694  if (Character.AnimController.InWater && !Reverse && avoidObstacles)
2695  {
2696  SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
2697  }
2698  if (checkBlocking)
2699  {
2700  return !IsBlocked(deltaTime, SimPosition + dir * (avoidLookAheadDistance / 2));
2701  }
2702  return true;
2703  }
2704 
2705  private Limb GetLimbToRotate(Attack attack)
2706  {
2707  Limb limb = AttackLimb;
2708  if (attack.RotationLimbIndex > -1 && attack.RotationLimbIndex < Character.AnimController.Limbs.Length)
2709  {
2710  limb = Character.AnimController.Limbs[attack.RotationLimbIndex];
2711  }
2712  return limb;
2713  }
2714 
2715  #endregion
2716 
2717  #region Eat
2718 
2719  private void UpdateEating(float deltaTime)
2720  {
2721  if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity.Removed)
2722  {
2723  ReleaseEatingTarget();
2724  return;
2725  }
2726  if (SelectedAiTarget.Entity is Barotrauma.Character or Item)
2727  {
2728  Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head);
2729  if (mouthLimb == null)
2730  {
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();
2735  ResetAITarget();
2736  return;
2737  }
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;
2743  if (tooFar)
2744  {
2745  steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2);
2746  if (Character.InWater)
2747  {
2748  SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
2749  }
2750  }
2751  else
2752  {
2753  if (SelectedAiTarget.Entity is Character targetCharacter)
2754  {
2755  Character.SelectCharacter(targetCharacter);
2756  }
2757  else if (SelectedAiTarget.Entity is Item item)
2758  {
2759  if (!item.Removed && item.body != null)
2760  {
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)
2764  {
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,
2772  item.WorldPosition,
2773  new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed),
2774  impulseDirection: Vector2.Zero,
2775  deltaTime);
2776  Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
2777  if (item.Condition <= 0.0f)
2778  {
2779  if (!wasBroken) { PetBehavior?.OnEat(item); }
2780  Entity.Spawner.AddItemToRemoveQueue(item);
2781  }
2782  }
2783  }
2784  }
2785  steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3);
2786  Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos);
2787  }
2788  }
2789  else
2790  {
2791  IgnoreTarget(SelectedAiTarget);
2792  ReleaseEatingTarget();
2793  ResetAITarget();
2794  }
2795  }
2796 
2797  private void ReleaseEatingTarget()
2798  {
2799  State = AIState.Idle;
2800  Character.DeselectCharacter();
2801  }
2802 
2803  #endregion
2804 
2805  private void UpdateFollow(float deltaTime)
2806  {
2807  if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
2808  {
2809  State = AIState.Idle;
2810  return;
2811  }
2812  if (Character.CurrentHull != null && steeringManager == insideSteering)
2813  {
2814  // Inside, but not inside ruins
2815  if ((Character.AnimController.InWater || !Character.AnimController.CanWalk) &&
2816  Character.Submarine != null && !Character.Submarine.Info.IsRuin &&
2817  SelectedAiTarget.Entity is Character c && VisibleHulls.Contains(c.CurrentHull))
2818  {
2819  // Steer towards the target if in the same room and swimming
2820  SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
2821  }
2822  else
2823  {
2824  // Use path finding
2825  PathSteering.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2, minGapWidth: minGapSize);
2826  }
2827  }
2828  else
2829  {
2830  // Outside
2831  SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 5);
2832  }
2833  if (steeringManager is IndoorsSteeringManager pathSteering)
2834  {
2835  if (!pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable)
2836  {
2837  // Can't reach
2838  State = AIState.Idle;
2839  IgnoreTarget(SelectedAiTarget);
2840  }
2841  }
2842  else if (Character.AnimController.InWater)
2843  {
2844  SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
2845  }
2846  }
2847 
2848  #region Targeting
2849  public static bool IsLatchedTo(Character target, Character character)
2850  {
2851  if (target.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null)
2852  {
2853  return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter == character;
2854  }
2855  return false;
2856  }
2857 
2858  public static bool IsLatchedToSomeoneElse(Character target, Character character)
2859  {
2860  if (target.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null)
2861  {
2862  return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter != null && enemyAI.LatchOntoAI.TargetCharacter != character;
2863  }
2864  return false;
2865  }
2866 
2867  private bool IsLatchedOnSub => LatchOntoAI != null && LatchOntoAI.IsAttachedToSub;
2868 
2869  //goes through all the AItargets, evaluates how preferable it is to attack the target,
2870  //whether the Character can see/hear the target and chooses the most preferable target within
2871  //sight/hearing range
2872  public void UpdateTargets()
2873  {
2874  targetValue = 0;
2875  AITarget newTarget = null;
2876  CharacterParams.TargetParams selectedTargetParams = null;
2877  AITargetMemory targetMemory = null;
2878  bool isAnyTargetClose = false;
2879  bool isBeingChased = IsBeingChased;
2880  const float priorityValueMaxModifier = 5;
2881  bool isCharacterInside = Character.CurrentHull != null;
2882  bool tryToGetInside =
2884  //characters that are aggressive boarders can partially enter the sub can attempt to push through holes
2885  (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial && IsAggressiveBoarder);
2886 
2887  foreach (AITarget aiTarget in AITarget.List)
2888  {
2889  if (aiTarget.ShouldBeIgnored()) { continue; }
2890  if (ignoredTargets.Contains(aiTarget)) { continue; }
2891  if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; }
2892  if (!TargetOutposts && GameMain.GameSession.GameMode is not TestGameMode)
2893  {
2894  if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsOutpost) { continue; }
2895  }
2896  Character targetCharacter = aiTarget.Entity as Character;
2897  //ignore the aitarget if it is the Character itself
2898  if (targetCharacter == Character) { continue; }
2899  if (TargetingRestrictions.HasFlag(EnemyTargetingRestrictions.PlayerCharacters))
2900  {
2901  if (targetCharacter is { IsPlayer: true }) { continue; }
2902 
2903  // monsters can ignore a player character, but attack a diving suit equipped by the character
2904  if (aiTarget.Entity is Item item && item.GetRootInventoryOwner() is Character { IsPlayer: true}) { continue; }
2905  }
2906  if (TargetingRestrictions.HasFlag(EnemyTargetingRestrictions.PlayerSubmarines))
2907  {
2908  if (aiTarget.Entity.Submarine?.Info is { IsPlayer: true }) { continue; }
2909  }
2910  var targetingTags = GetTargetingTags(aiTarget);
2911 
2912  #region Filter out targets by entity type, based on contextual information.
2913  Door door = null;
2914  if (targetCharacter != null)
2915  {
2916  // ignore if target is tagged to be explicitly ignored (Feign Death)
2917  if (targetCharacter.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { continue; }
2918  if (AIParams.Targets.None() && Character.IsFriendly(targetCharacter))
2919  {
2920  continue;
2921  }
2922  }
2923  else
2924  {
2925  // Ignore all structures, items, and hulls inside these subs.
2926  if (aiTarget.Entity.Submarine != null)
2927  {
2928  if (aiTarget.Entity.Submarine.Info.IsWreck ||
2929  aiTarget.Entity.Submarine.Info.IsBeacon ||
2930  UnattackableSubmarines.Contains(aiTarget.Entity.Submarine))
2931  {
2932  continue;
2933  }
2934  //ignore the megaruin in end levels
2935  if (aiTarget.Entity.Submarine.Info.OutpostGenerationParams != null &&
2937  {
2938  continue;
2939  }
2940  }
2941  if (aiTarget.Entity is Hull hull)
2942  {
2943  // Ignore the target if it's a room and the character is already inside a sub
2944  if (Character.CurrentHull != null) { continue; }
2945  // Ignore ruins
2946  if (hull.Submarine == null) { continue; }
2947  if (hull.Submarine.Info.IsRuin) { continue; }
2948  }
2949  else if (aiTarget.Entity is Item item)
2950  {
2951  door = item.GetComponent<Door>();
2952  bool targetingFromOutsideToInside = item.CurrentHull != null && !isCharacterInside;
2953  if (targetingFromOutsideToInside)
2954  {
2955  if (door != null && (!canAttackDoors && !AIParams.CanOpenDoors) || !canAttackWalls)
2956  {
2957  // Can't reach
2958  continue;
2959  }
2960  }
2961  if (door == null && targetingFromOutsideToInside)
2962  {
2963  if (item.Submarine?.Info is { IsRuin: true })
2964  {
2965  // Ignore ruin items when the creature is outside.
2966  continue;
2967  }
2968  }
2969  else if (targetingTags.Contains(Tags.Nasonov))
2970  {
2971  if ((item.Submarine == null || !item.Submarine.Info.IsPlayer) && item.ParentInventory == null)
2972  {
2973  // Only target nasonovartifacts when they are held be a player or inside the playersub
2974  continue;
2975  }
2976  }
2977  // Ignore the target if it's a decoy and the character is already inside a sub
2978  if (Character.CurrentHull != null && targetingTags.Contains(Tags.Decoy))
2979  {
2980  continue;
2981  }
2982  }
2983  else if (aiTarget.Entity is Structure s)
2984  {
2985  if (!s.HasBody)
2986  {
2987  // Ignore structures that doesn't have a body (not walls)
2988  continue;
2989  }
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)
2995  {
2996  // Ignore inner walls when outside (walltargets still work)
2997  continue;
2998  }
2999  if (!tryToGetInside && IsWallDisabled(s))
3000  {
3001  continue;
3002  }
3003  }
3004  if (door != null)
3005  {
3006  if (door.Item.Submarine == null) { continue; }
3007  bool isOutdoor = door.LinkedGap is { FlowTargetHull: not null, IsRoomToRoom: false };
3008  // Ignore inner doors when outside
3009  if (Character.CurrentHull == null && !isOutdoor) { continue; }
3010  bool isOpen = door.CanBeTraversed;
3011  if (!isOpen)
3012  {
3013  if (!canAttackDoors) { continue; }
3014  }
3016  {
3017  // Ignore broken and open doors, if cannot enter submarine
3018  // Also ignore them if the monster can only partially enter the sub:
3019  // these monsters tend to be too large to get through doors anyway.
3020  continue;
3021  }
3022  }
3023  else if (aiTarget.Entity is IDamageable { Health: <= 0.0f })
3024  {
3025  continue;
3026  }
3027  }
3028  #endregion
3029 
3030  #region Choose valid targeting params.
3031  if (targetingTags.None()) { continue; }
3032  CharacterParams.TargetParams matchingTargetParams = null;
3033  foreach (var targetParams in GetTargetParams(targetingTags))
3034  {
3035  if (matchingTargetParams != null)
3036  {
3037  if (matchingTargetParams.Priority > targetParams.Priority)
3038  {
3039  // Valid higher priority params already found.
3040  continue;
3041  }
3042  }
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)
3049  {
3050  if (targetParams.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { continue; }
3051  }
3052  if (targetParams.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; }
3053  if (targetParams.IgnoreIfNotInSameSub)
3054  {
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; }
3058  }
3059  if (targetParams.State is AIState.Observe or AIState.Eat)
3060  {
3061  if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine)
3062  {
3063  // Never allow observing or eating characters that are inside a different submarine / outside when we are inside.
3064  continue;
3065  }
3066  }
3067  if (aiTarget.Entity is Item targetItem)
3068  {
3069  if (targetParams.IgnoreContained && targetItem.ParentInventory != null) { continue; }
3070 
3071  switch (targetParams.State)
3072  {
3073  case AIState.FleeTo:
3074  {
3075  float healthThreshold = targetParams.Threshold;
3076  if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0)
3077  {
3078  // If both min and max thresholds are defined, use ThresholdMax when currently fleeing to the target and ThresholdMin when doing something else.
3079  // This is used to make the fractal guardians target the guardian repair pods with a different health threshold: when they are already targeting the pod, they will use ThresholdMax to keep them generating until (nearly) full health.
3080  // And when the guardians are outside the pod, they use ThresholdMin, so that they don't target the pod before falling under certain level of health.
3081  healthThreshold = currentTargetingParams == targetParams && State == AIState.FleeTo ? targetParams.ThresholdMax : targetParams.ThresholdMin;
3082  }
3083  if (Character.HealthPercentage > healthThreshold)
3084  {
3085  continue;
3086  }
3087  break;
3088  }
3089  case AIState.Attack or AIState.Aggressive:
3090  if (!canAttackItems)
3091  {
3092  continue;
3093  }
3094  break;
3095  }
3096  if (targetItem.HasTag(Tags.GuardianShelter))
3097  {
3098  // Ignore guardian pods completely, if they are targeted by someone else (is or will be occupied).
3099  bool ignore = false;
3100  foreach (Character otherCharacter in Character.CharacterList)
3101  {
3102  if (otherCharacter == Character) { continue; }
3103  if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
3104  if (!Character.IsFriendly(otherCharacter)) { continue; }
3105  ignore = true;
3106  break;
3107  }
3108  if (ignore) { continue; }
3109  }
3110  }
3111  matchingTargetParams = targetParams;
3112  }
3113  if (matchingTargetParams == null) { continue; }
3114  #endregion
3115 
3116  #region Modify the priority dynamically
3117  float valueModifier = 1.0f;
3118  if (targetCharacter != null)
3119  {
3120  if (targetCharacter.AIController is EnemyAIController)
3121  {
3122  if (matchingTargetParams.Tag == Tags.Stronger && State is AIState.Avoid or AIState.Escape or AIState.Flee)
3123  {
3124  if (SelectedAiTarget == aiTarget)
3125  {
3126  // Frightened -> hold on to the target
3127  valueModifier *= 2;
3128  }
3129  if (IsBeingChasedBy(targetCharacter))
3130  {
3131  valueModifier *= 2;
3132  }
3133  if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull))
3134  {
3135  // Inside but in a different room
3136  valueModifier /= 2;
3137  }
3138  }
3139  }
3140  }
3141  else
3142  {
3143  if (aiTarget.Entity is Structure s)
3144  {
3145  bool isInnerWall = s.Prefab.Tags.Contains("inner");
3146  // Prefer weaker walls (200 is the default for normal hull walls)
3147  valueModifier = 200f / s.MaxHealth;
3148  for (int i = 0; i < s.Sections.Length; i++)
3149  {
3150  var section = s.Sections[i];
3151  if (section.gap == null) { continue; }
3152  bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null;
3153  if (tryToGetInside)
3154  {
3155  if (!isCharacterInside)
3156  {
3157  if (CanPassThroughHole(s, i))
3158  {
3159  valueModifier *= leadsInside ? (IsAggressiveBoarder ? priorityValueMaxModifier : 1) : 0;
3160  }
3161  else if (IsAggressiveBoarder && leadsInside && canAttackWalls)
3162  {
3163  // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
3164  valueModifier *= 1 + section.gap.Open;
3165  }
3166  }
3167  else
3168  {
3169  // Inside
3170  if (IsAggressiveBoarder)
3171  {
3172  if (!isInnerWall)
3173  {
3174  // Only interested in getting inside (aggressive boarder) -> don't target outer walls when already inside
3175  valueModifier = 0;
3176  break;
3177  }
3178  else if (CanPassThroughHole(s, i))
3179  {
3180  valueModifier *= isInnerWall ? 0.5f : 0;
3181  }
3182  else if (!canAttackWalls)
3183  {
3184  valueModifier = 0;
3185  break;
3186  }
3187  else
3188  {
3189  valueModifier = 0.1f;
3190  }
3191  }
3192  else
3193  {
3194  if (!canAttackWalls)
3195  {
3196  valueModifier = 0;
3197  break;
3198  }
3199  // We are actually interested in breaking things -> reduce the priority when the wall is already broken
3200  // (Terminalcells)
3201  valueModifier *= 1 - section.gap.Open * 0.25f;
3202  valueModifier = Math.Max(valueModifier, 0.1f);
3203  }
3204  }
3205  }
3206  else
3207  {
3208  // Cannot enter
3209  if (isInnerWall || !canAttackWalls)
3210  {
3211  // Ignore inner walls and all walls if cannot do damage on walls.
3212  valueModifier = 0;
3213  break;
3214  }
3215  else if (IsAggressiveBoarder)
3216  {
3217  // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
3218  // (Bonethreshers)
3219  valueModifier *= 1 + section.gap.Open;
3220  }
3221  }
3222  valueModifier = Math.Clamp(valueModifier, 0, priorityValueMaxModifier);
3223  }
3224  }
3225  if (door != null)
3226  {
3227  if (IsAggressiveBoarder)
3228  {
3229  if (Character.CurrentHull == null)
3230  {
3231  // Increase the priority if the character is outside and the door is from outside to inside
3232  if (door.CanBeTraversed)
3233  {
3234  valueModifier = priorityValueMaxModifier;
3235  }
3236  else if (door.LinkedGap != null)
3237  {
3238  valueModifier = 1 + door.LinkedGap.Open * (priorityValueMaxModifier - 1);
3239  }
3240  }
3241  else
3242  {
3243  // Inside -> ignore open doors and outer doors
3244  bool isOpen = door.CanBeTraversed;
3245  bool isOutdoor = door.LinkedGap is { FlowTargetHull: not null, IsRoomToRoom: false };
3246  valueModifier = isOpen || isOutdoor ? 0 : 1;
3247  }
3248  }
3249  }
3250  else if (aiTarget.Entity is IDamageable { Health: <= 0.0f })
3251  {
3252  continue;
3253  }
3254  }
3255  //no need to eat if the character is already in full health (except if it's a pet - pets actually need to eat to stay alive, not just to regain health)
3256  if (matchingTargetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0 && !Character.IsPet)
3257  {
3258  valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f);
3259  }
3260  valueModifier *= matchingTargetParams.Priority;
3261  if (valueModifier == 0.0f) { continue; }
3262  if (matchingTargetParams.Tag != Tags.Decoy)
3263  {
3264  if (SwarmBehavior != null && SwarmBehavior.Members.Any())
3265  {
3266  // Halve the priority for each swarm mate targeting the same target -> reduces stacking
3267  foreach (AICharacter otherCharacter in SwarmBehavior.Members)
3268  {
3269  if (otherCharacter == Character) { continue; }
3270  if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
3271  valueModifier /= 2;
3272  }
3273  }
3274  else
3275  {
3276  // The same as above, but using all the friendly characters in the level.
3277  foreach (Character otherCharacter in Character.CharacterList)
3278  {
3279  if (otherCharacter == Character) { continue; }
3280  if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
3281  if (!Character.IsFriendly(otherCharacter)) { continue; }
3282  valueModifier /= 2;
3283  }
3284  }
3285  }
3286  #endregion
3287 
3288  if (!aiTarget.IsWithinSector(WorldPosition)) { continue; }
3289  Vector2 toTarget = aiTarget.WorldPosition - Character.WorldPosition;
3290  float dist = toTarget.Length();
3291  float nonModifiedDist = dist;
3292  //if the target has been within range earlier, the character will notice it more easily
3293  if (targetMemories.ContainsKey(aiTarget))
3294  {
3295  dist *= 0.9f;
3296  }
3297  if (matchingTargetParams.PerceptionDistanceMultiplier > 0.0f)
3298  {
3299  dist /= matchingTargetParams.PerceptionDistanceMultiplier;
3300  }
3301 
3302  if (matchingTargetParams.MaxPerceptionDistance > 0.0f &&
3303  dist * dist > matchingTargetParams.MaxPerceptionDistance * matchingTargetParams.MaxPerceptionDistance)
3304  {
3305  continue;
3306  }
3307 
3308  if (State is AIState.PlayDead && targetCharacter == null)
3309  {
3310  // Only react to characters, when playing dead.
3311  continue;
3312  }
3313  else if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget || State is AIState.PlayDead or AIState.Hiding))
3314  {
3315  continue;
3316  }
3317 
3318  if (SelectedAiTarget == aiTarget)
3319  {
3320  if (Character.Submarine == null && aiTarget.Entity is ISpatialEntity { Submarine: not null } spatialEntity)
3321  {
3322  if (matchingTargetParams.Tag == Tags.Door || matchingTargetParams.Tag == Tags.Wall)
3323  {
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 })
3328  {
3329  Vector2 hitPos = hit.SimPosition;
3330  if (closestBody.UserData is Submarine)
3331  {
3332  hitPos = Submarine.LastPickedPosition;
3333  }
3334  else if (hit.Submarine != null)
3335  {
3336  hitPos += hit.Submarine.SimPosition;
3337  }
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)
3343  {
3344  IgnoreTarget(aiTarget);
3345  ResetAITarget();
3346  continue;
3347  }
3348  }
3349  }
3350  }
3351  // Stick to the current target
3352  valueModifier *= 1.1f;
3353  }
3354  if (!isBeingChased)
3355  {
3356  if (matchingTargetParams.State is AIState.Avoid or AIState.PassiveAggressive or AIState.Aggressive)
3357  {
3358  float reactDistance = matchingTargetParams.ReactDistance;
3359  if (reactDistance > 0 && reactDistance < dist)
3360  {
3361  // The target is too far and should be ignored.
3362  continue;
3363  }
3364  }
3365  }
3366 
3367  //if the target is very close, the distance doesn't make much difference
3368  // -> just ignore the distance and target whatever has the highest priority
3369  dist = Math.Max(dist, 100.0f);
3370  targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true, keepAlive: SelectedAiTarget != aiTarget);
3371  if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null)
3372  {
3373  float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y;
3374  if (diff > 0)
3375  {
3376  // Inside the sub, treat objects that are up or down, as they were farther away.
3377  dist *= MathHelper.Clamp(diff / 100, 2, 3);
3378  }
3379  }
3380 
3381  if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && targetCharacter == null)
3382  {
3383  if (matchingTargetParams.PrioritizeSubCenter || matchingTargetParams.AttackPattern is AttackPattern.Circle or AttackPattern.Sweep)
3384  {
3385  if (!isAnyTargetClose)
3386  {
3387  if (Submarine.MainSubs.Contains(aiTarget.Entity.Submarine))
3388  {
3389  // Prioritize targets that are near the horizontal center of the sub, but only when none of the targets is reachable.
3390  float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X);
3391  dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter));
3392  }
3393  else if (matchingTargetParams.AttackPattern == AttackPattern.Circle)
3394  {
3395  dist *= 5;
3396  }
3397  }
3398  }
3399  }
3400 
3401  if (targetCharacter != null && Character.CurrentHull != null && Character.CurrentHull == targetCharacter.CurrentHull)
3402  {
3403  // In the same room with the target character
3404  dist /= 2;
3405  }
3406 
3407  // Don't target characters that are outside of the allowed zone, unless chasing or escaping.
3408  switch (matchingTargetParams.State)
3409  {
3410  case AIState.Escape:
3411  case AIState.Avoid:
3412  break;
3413  default:
3414  if (matchingTargetParams.State == AIState.Attack)
3415  {
3416  // In the attack state allow going into non-allowed zone only when chasing a target.
3417  if (State == matchingTargetParams.State && SelectedAiTarget == aiTarget) { break; }
3418  }
3419  bool insideSameSub = aiTarget?.Entity?.Submarine != null && aiTarget.Entity.Submarine == Character.Submarine;
3420  if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
3421  {
3422  // If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state.
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)))
3425  {
3426  continue;
3427  }
3428  }
3429  break;
3430  }
3431 
3432  valueModifier *=
3433  targetMemory.Priority /
3434  //sqrt = the further the target is, the less the distance matters
3435  MathF.Sqrt(dist);
3436 
3437  if (valueModifier > targetValue)
3438  {
3439  if (aiTarget.Entity is Item i)
3440  {
3441  Character owner = GetOwner(i);
3442  if (owner == Character) { continue; }
3443  if (owner != null)
3444  {
3445  if (owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)) { continue; }
3446  if (Character.IsFriendly(owner))
3447  {
3448  // Don't target items that we own. This is a rare case, and almost entirely related to Humanhusks (in the vanilla game).
3449  continue;
3450  }
3451  if (owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
3452  {
3453  // ignore if owner is tagged to be explicitly ignored (Feign Death)
3454  continue;
3455  }
3456  // if the enemy is configured to ignore the target character, ignore the provocative item they're holding/wearing too
3457  if (GetTargetParams(GetTargetingTags(owner.AiTarget)).Any(t => t.State == AIState.Idle)) { continue; }
3458  }
3459  }
3460  if (targetCharacter != null)
3461  {
3462  if (Character.CurrentHull != null && targetCharacter.CurrentHull != Character.CurrentHull)
3463  {
3464  if (matchingTargetParams.State is AIState.Observe or AIState.Eat ||
3465  (matchingTargetParams.State is AIState.Follow or AIState.Protect && (!Character.CanClimb || !Character.CanInteract || !AIParams.CanOpenDoors || !Character.Params.UsePathFinding)))
3466  {
3467  if (!VisibleHulls.Contains(targetCharacter.CurrentHull))
3468  {
3469  // Probably can't get to the target -> ignore.
3470  continue;
3471  }
3472  }
3473  }
3474  if (targetCharacter.Submarine != Character.Submarine || (targetCharacter.CurrentHull == null) != (Character.CurrentHull == null))
3475  {
3476  if (targetCharacter.Submarine != null)
3477  {
3478  // Target is inside -> reduce the priority
3479  valueModifier *= 0.5f;
3480  if (Character.Submarine != null)
3481  {
3482  // Both inside different submarines -> can ignore safely
3483  continue;
3484  }
3485  }
3486  else if (Character.CurrentHull != null)
3487  {
3488  // Target outside, but we are inside -> Ignore the target but allow to keep target that is currently selected.
3489  if (SelectedAiTarget?.Entity != targetCharacter)
3490  {
3491  continue;
3492  }
3493  }
3494  }
3495  else if (targetCharacter.Submarine == null && Character.Submarine == null)
3496  {
3497  // Ignore the target when it's far enough and blocked by the level geometry, because the steering avoidance probably can't get us to the target.
3498  if (dist > Math.Clamp(ConvertUnits.ToDisplayUnits(colliderLength) * 10, 1000, 5000))
3499  {
3500  if (Submarine.PickBodies(SimPosition, targetCharacter.SimPosition, collisionCategory: Physics.CollisionLevel).Any())
3501  {
3502  continue;
3503  }
3504  }
3505  }
3506  }
3507  newTarget = aiTarget;
3508  selectedTargetParams = matchingTargetParams;
3509  targetValue = valueModifier;
3510  if (!isAnyTargetClose)
3511  {
3512  isAnyTargetClose = ConvertUnits.ToDisplayUnits(colliderLength) > nonModifiedDist;
3513  }
3514  }
3515  }
3516 
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)
3522  {
3523  UpdateWallTarget(requiredHoleCount);
3524  }
3525  updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
3526  }
3527 
3528  class WallTarget
3529  {
3530  public Vector2 Position;
3531  public Structure Structure;
3532  public int SectionIndex;
3533 
3534  public WallTarget(Vector2 position, Structure structure = null, int sectionIndex = -1)
3535  {
3536  Position = position;
3537  Structure = structure;
3538  SectionIndex = sectionIndex;
3539  }
3540  }
3541 
3542  private WallTarget wallTarget;
3543  private readonly List<(Body, int, Vector2)> wallHits = new List<(Body, int, Vector2)>(3);
3544  private void UpdateWallTarget(int requiredHoleCount)
3545  {
3546  wallTarget = null;
3547  if (SelectedAiTarget == null) { return; }
3548  if (SelectedAiTarget.Entity == null) { return; }
3549  if (!canAttackWalls) { return; }
3550  if (HasValidPath(requireNonDirty: true)) { return; }
3551  wallHits.Clear();
3552  Structure wall = null;
3553  Vector2 refPos = AttackLimb != null ? AttackLimb.SimPosition : SimPosition;
3554  if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target))
3555  {
3556  Vector2 rayStart = refPos;
3557  Vector2 rayEnd = SelectedAiTarget.SimPosition;
3558  if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
3559  {
3560  rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
3561  }
3562  else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
3563  {
3564  rayEnd -= Character.Submarine.SimPosition;
3565  }
3566  DoRayCast(rayStart, rayEnd);
3567  }
3568  if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading))
3569  {
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)
3573  {
3574  rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
3575  rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition;
3576  }
3577  else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
3578  {
3579  rayStart -= Character.Submarine.SimPosition;
3580  rayEnd -= Character.Submarine.SimPosition;
3581  }
3582  DoRayCast(rayStart, rayEnd);
3583  }
3584  if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering))
3585  {
3586  Vector2 rayStart = refPos;
3587  Vector2 rayEnd = rayStart + Steering * 5;
3588  if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
3589  {
3590  rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
3591  rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition;
3592  }
3593  else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
3594  {
3595  rayStart -= Character.Submarine.SimPosition;
3596  rayEnd -= Character.Submarine.SimPosition;
3597  }
3598  DoRayCast(rayStart, rayEnd);
3599  }
3600  if (wallHits.Any())
3601  {
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)
3609  {
3610  Structure structure = body.UserData as Structure;
3611  float distance = Vector2.DistanceSquared(
3612  refPos,
3613  Submarine.GetRelativeSimPosition(ConvertUnits.ToSimUnits(sectionPosition), Character.Submarine, structure.Submarine));
3614  //if the wall is further than the target (e.g. at the other side of the sub?), we shouldn't be targeting it
3615  if (distance > targetDistance) { continue; }
3616  if (closestBody == null || closestDistance == 0 || distance < closestDistance)
3617  {
3618  closestBody = body;
3619  closestDistance = distance;
3620  wall = structure;
3621  sectionPos = sectionPosition;
3622  sectionIndex = index;
3623  }
3624  }
3625  if (closestBody == null || sectionIndex == -1) { return; }
3626  Vector2 attachTargetNormal;
3627  if (wall.IsHorizontal)
3628  {
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;
3631  }
3632  else
3633  {
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;
3636  }
3637  LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
3638  if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True ||
3639  !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
3640  {
3641  if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True)
3642  {
3643  bool isTargetingDoor = SelectedAiTarget.Entity is Item i && i.GetComponent<Door>() != null;
3644  // Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces.
3645  if (!isTargetingDoor)
3646  {
3647  //TODO: this might cause problems: many wall pieces (like smaller shuttle pieces
3648  //and small decorative wall structures are currently marked as having no AI target,
3649  //which can mean a monster very frequently ignores targets inside because they're blocked by those structures
3650  IgnoreTarget(SelectedAiTarget);
3651  ResetAITarget();
3652  }
3653  }
3654  else
3655  {
3656  wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
3657  }
3658  }
3659  else
3660  {
3661  // Blocked by a disabled wall.
3662  IgnoreTarget(SelectedAiTarget);
3663  ResetAITarget();
3664  }
3665  }
3666 
3667  void DoRayCast(Vector2 rayStart, Vector2 rayEnd)
3668  {
3669  Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true,
3670  ignoreSensors: CanEnterSubmarine != CanEnterSubmarine.False,
3671  ignoreDisabledWalls: CanEnterSubmarine != CanEnterSubmarine.False);
3672  if (hitTarget != null && IsValid(hitTarget, out wall))
3673  {
3674  int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
3675  if (sectionIndex >= 0)
3676  {
3677  wallHits.Add((hitTarget, sectionIndex, GetSectionPosition(wall, sectionIndex)));
3678  }
3679  }
3680  }
3681 
3682  Vector2 GetSectionPosition(Structure wall, int sectionIndex)
3683  {
3684  float sectionDamage = wall.SectionDamage(sectionIndex);
3685  for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
3686  {
3687  if (wall.SectionBodyDisabled(i))
3688  {
3689  if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.False &&
3690  CanPassThroughHole(wall, i, requiredHoleCount))
3691  {
3692  sectionIndex = i;
3693  break;
3694  }
3695  else
3696  {
3697  // Ignore and keep breaking other sections
3698  continue;
3699  }
3700  }
3701  if (wall.SectionDamage(i) > sectionDamage)
3702  {
3703  sectionIndex = i;
3704  }
3705  }
3706  return wall.SectionPosition(sectionIndex, world: false);
3707  }
3708 
3709  bool IsValid(Body hit, out Structure wall)
3710  {
3711  wall = null;
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; }
3716  if (Character.Submarine == null)
3717  {
3718  if (w.Prefab.Tags.Contains("inner"))
3719  {
3720  if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.False) { return false; }
3721  }
3722  else if (!AIParams.TargetOuterWalls)
3723  {
3724  return false;
3725  }
3726  }
3727  wall = w;
3728  return true;
3729  }
3730  }
3731 
3732  private bool TrySteerThroughGaps(float deltaTime)
3733  {
3734  if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex, requiredHoleCount))
3735  {
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);
3739  }
3740  else if (SelectedAiTarget != null)
3741  {
3742  if (SelectedAiTarget.Entity is Structure wall)
3743  {
3744  for (int i = 0; i < wall.Sections.Length; i++)
3745  {
3746  WallSection section = wall.Sections[i];
3747  if (CanPassThroughHole(wall, i, requiredHoleCount) && section?.gap != null)
3748  {
3749  return SteerThroughGap(wall, section, wall.SectionPosition(i, true), deltaTime);
3750  }
3751  }
3752  }
3753  else if (SelectedAiTarget.Entity is Item i)
3754  {
3755  var door = i.GetComponent<Door>();
3756  // Don't try to enter dry hulls if cannot walk or if the gap is too narrow
3757  if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && door.CanBeTraversed)
3758  {
3759  if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25)
3760  {
3761  if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
3762  {
3763  float maxDistance = Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100);
3764  return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: maxDistance);
3765  }
3766  }
3767  }
3768  }
3769  }
3770  return false;
3771  }
3772 
3773  private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound = false, bool keepAlive = false)
3774  {
3775  if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
3776  {
3777  if (addIfNotFound)
3778  {
3779  memory = new AITargetMemory(target, minPriority);
3780  targetMemories.Add(target, memory);
3781  }
3782  }
3783  if (keepAlive)
3784  {
3785  memory.Priority = Math.Max(memory.Priority, minPriority);
3786  }
3787  return memory;
3788  }
3789 
3790  private void UpdateCurrentMemoryLocation()
3791  {
3792  if (_selectedAiTarget != null)
3793  {
3794  if (_selectedAiTarget.Entity == null || _selectedAiTarget.Entity.Removed)
3795  {
3796  _selectedAiTarget = null;
3797  }
3798  else if (CanPerceive(_selectedAiTarget, checkVisibility: false))
3799  {
3800  var memory = GetTargetMemory(_selectedAiTarget);
3801  if (memory != null)
3802  {
3803  memory.Location = _selectedAiTarget.WorldPosition;
3804  }
3805  }
3806  }
3807  }
3808 
3809  private readonly List<AITarget> removals = new List<AITarget>();
3810  private void FadeMemories(float deltaTime)
3811  {
3812  removals.Clear();
3813  foreach (var kvp in targetMemories)
3814  {
3815  var target = kvp.Key;
3816  var memory = kvp.Value;
3817  // Slowly decrease all memories
3818  float fadeTime = memoryFadeTime;
3819  if (target == SelectedAiTarget)
3820  {
3821  // Don't decrease the current memory
3822  fadeTime = 0;
3823  }
3824  else if (target == _lastAiTarget)
3825  {
3826  // Halve the latest memory fading.
3827  fadeTime /= 2;
3828  }
3829  memory.Priority -= fadeTime * deltaTime;
3830  // Remove targets that have no priority or have been removed
3831  if (memory.Priority <= 1 || target.Entity == null || target.Entity.Removed || !AITarget.List.Contains(target))
3832  {
3833  removals.Add(target);
3834  }
3835  }
3836  removals.ForEach(r => targetMemories.Remove(r));
3837  }
3838 
3839  private readonly float targetIgnoreTime = 10;
3840  private float targetIgnoreTimer;
3841  private readonly HashSet<AITarget> ignoredTargets = new HashSet<AITarget>();
3842  public void IgnoreTarget(AITarget target)
3843  {
3844  if (target == null) { return; }
3845  ignoredTargets.Add(target);
3846  targetIgnoreTimer = targetIgnoreTime * Rand.Range(0.75f, 1.25f);
3847  }
3848  #endregion
3849 
3850  #region State switching
3855  private readonly float stateResetCooldown = 10;
3856  private float stateResetTimer;
3857  private bool isStateChanged;
3858  private readonly Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams>();
3859  private readonly HashSet<StatusEffect.AITrigger> inactiveTriggers = new HashSet<StatusEffect.AITrigger>();
3860 
3861  public void LaunchTrigger(StatusEffect.AITrigger trigger)
3862  {
3863  if (trigger.IsTriggered) { return; }
3864  if (activeTriggers.ContainsKey(trigger)) { return; }
3865  if (activeTriggers.ContainsValue(currentTargetingParams))
3866  {
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);
3871  }
3872  trigger.Launch();
3873  activeTriggers.Add(trigger, currentTargetingParams);
3874  ChangeParams(currentTargetingParams, trigger.State);
3875  }
3876 
3877  private void UpdateTriggers(float deltaTime)
3878  {
3879  foreach (var triggerObject in activeTriggers)
3880  {
3881  StatusEffect.AITrigger trigger = triggerObject.Key;
3882  if (trigger.IsPermanent) { continue; }
3883  trigger.UpdateTimer(deltaTime);
3884  if (!trigger.IsActive)
3885  {
3886  trigger.Reset();
3887  ResetParams(triggerObject.Value);
3888  inactiveTriggers.Add(trigger);
3889  }
3890  }
3891  foreach (StatusEffect.AITrigger trigger in inactiveTriggers)
3892  {
3893  activeTriggers.Remove(trigger);
3894  }
3895  inactiveTriggers.Clear();
3896  }
3897 
3901  private bool TryResetOriginalState(Identifier tag)
3902  {
3903  if (!modifiedParams.ContainsKey(tag)) { return false; }
3904  if (AIParams.TryGetTargets(tag, out IEnumerable<CharacterParams.TargetParams> matchingParams))
3905  {
3906  foreach (var targetParams in matchingParams)
3907  {
3908  modifiedParams.Remove(tag);
3909  if (tempParams.ContainsKey(tag))
3910  {
3911  tempParams.Values.ForEach(t => AIParams.RemoveTarget(t));
3912  tempParams.Remove(tag);
3913  }
3914  ResetParams(targetParams);
3915  return true;
3916  }
3917  }
3918  return false;
3919  }
3920 
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>();
3924 
3925  private void ChangeParams(CharacterParams.TargetParams targetParams, AIState state, float? priority = null)
3926  {
3927  if (targetParams == null) { return; }
3928  if (priority.HasValue)
3929  {
3930  targetParams.Priority = priority.Value;
3931  }
3932  targetParams.State = state;
3933  }
3934 
3935  private void ResetParams(CharacterParams.TargetParams targetParams)
3936  {
3937  targetParams?.Reset();
3938  if (currentTargetingParams == targetParams || State is AIState.Idle or AIState.Patrol)
3939  {
3940  ResetAITarget();
3941  State = AIState.Idle;
3942  PreviousState = AIState.Idle;
3943  }
3944  }
3945 
3946  private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false, bool ignoreAttacksIfNotInSameSub = false)
3947  {
3948  var existingTargetParams = GetTargetParams(tag);
3949  if (existingTargetParams.None())
3950  {
3951  if (!onlyExisting && !tempParams.ContainsKey(tag))
3952  {
3953  if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out CharacterParams.TargetParams targetParams))
3954  {
3955  if (state == AIState.Attack)
3956  {
3957  // Only applies to new temp target params. Shouldn't affect any existing definitions (handled below).
3958  targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub;
3959  }
3960  tempParams.Add(tag, targetParams);
3961  }
3962  }
3963  }
3964  else
3965  {
3966  foreach (var targetParams in existingTargetParams)
3967  {
3968  if (priority.HasValue)
3969  {
3970  targetParams.Priority = Math.Max(targetParams.Priority, priority.Value);
3971  }
3972  targetParams.State = state;
3973  }
3974  modifiedParams.TryAdd(tag, existingTargetParams);
3975  }
3976  }
3977 
3978  private void ChangeTargetState(Identifier tag, AIState state, float? priority = null)
3979  {
3980  isStateChanged = true;
3981  SetStateResetTimer();
3982  ChangeParams(tag, state, priority);
3983  }
3984 
3989  private void ChangeTargetState(Character target, AIState state, float? priority = null)
3990  {
3991  isStateChanged = true;
3992  SetStateResetTimer();
3993  if (!Character.IsPet || !target.IsHuman)
3994  {
3995  //don't turn pets hostile to all humans when attacked by one
3996  ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
3997  }
3998  if (target.IsHuman)
3999  {
4000  if (AIParams.TryGetHighestPriorityTarget(Tags.Human, out CharacterParams.TargetParams targetParams))
4001  {
4002  priority = targetParams.Priority;
4003  }
4004  // Target also items, because if we are blind and the target doesn't move, we can only perceive the target when it uses items
4005  if (state is AIState.Attack or AIState.Escape)
4006  {
4007  ChangeParams(Tags.Weapon, state, priority);
4008  ChangeParams(Tags.ToolItem, state, priority);
4009  }
4010  if (state == AIState.Attack)
4011  {
4012  // If the target is shooting from the submarine, we might not perceive it because it doesn't move.
4013  // --> Target the submarine too.
4014  if (target.Submarine != null && Character.Submarine == null && (canAttackDoors || canAttackWalls))
4015  {
4016  ChangeParams(Tags.Room, state, priority / 2);
4017  if (canAttackWalls)
4018  {
4019  ChangeParams(Tags.Wall, state, priority / 2);
4020  }
4021  if (canAttackDoors && IsAggressiveBoarder)
4022  {
4023  ChangeParams(Tags.Door, state, priority / 2);
4024  }
4025  }
4026  ChangeParams(Tags.Provocative, state, priority, onlyExisting: true);
4027  }
4028  }
4029  }
4030 
4031  private void ResetOriginalState()
4032  {
4033  isStateChanged = false;
4034  modifiedParams.Keys.ForEachMod(tag => TryResetOriginalState(tag));
4035  }
4036  #endregion
4037 
4038  protected override void OnTargetChanged(AITarget previousTarget, AITarget newTarget)
4039  {
4040  base.OnTargetChanged(previousTarget, newTarget);
4041  if ((newTarget != null || wallTarget != null) && IsLatchedOnSub)
4042  {
4043  if (newTarget?.Entity is not Structure wall)
4044  {
4045  wall = wallTarget?.Structure;
4046  }
4047  // The target is not a wall or it's not the same as we are attached to -> release
4048  bool releaseTarget = wall?.Bodies == null || (!wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB) && wall.Submarine?.PhysicsBody?.FarseerBody != LatchOntoAI.AttachJoints[0].BodyB);
4049  if (!releaseTarget)
4050  {
4051  for (int i = 0; i < wall.Sections.Length; i++)
4052  {
4053  if (CanPassThroughHole(wall, i))
4054  {
4055  releaseTarget = true;
4056  }
4057  }
4058  }
4059  if (releaseTarget)
4060  {
4061  wallTarget = null;
4062  LatchOntoAI.DeattachFromBody(reset: true, cooldown: 1);
4063  }
4064  }
4065  else
4066  {
4067  wallTarget = null;
4068  }
4069  if (newTarget == null) { return; }
4070  if (currentTargetingParams != null)
4071  {
4072  observeTimer = currentTargetingParams.Timer * Rand.Range(0.75f, 1.25f);
4073  }
4074  reachTimer = 0;
4075  sinTime = 0;
4076  if (breakCircling && strikeTimer <= 0 && CirclePhase != CirclePhase.CloseIn)
4077  {
4078  CirclePhase = CirclePhase.Start;
4079  }
4080  }
4081 
4082  protected override void OnStateChanged(AIState from, AIState to)
4083  {
4084  LatchOntoAI?.DeattachFromBody(reset: true);
4085  if (disableTailCoroutine != null)
4086  {
4087  bool isInTransition = from is AIState.HideTo or AIState.Hiding && to is AIState.HideTo or AIState.Hiding;
4088  if (!isInTransition)
4089  {
4090  CoroutineManager.StopCoroutines(disableTailCoroutine);
4092  disableTailCoroutine = null;
4093  }
4094  }
4095  if (to is AIState.Hiding)
4096  {
4097  ReleaseDragTargets();
4098  }
4100  AttackLimb = null;
4101  movementMargin = 0;
4102  ResetEscape();
4103  if (isStateChanged && to == AIState.Idle && from != to)
4104  {
4105  SetStateResetTimer();
4106  }
4107  blockCheckTimer = 0;
4108  reachTimer = 0;
4109  sinTime = 0;
4110  if (breakCircling && strikeTimer <= 0 && CirclePhase != CirclePhase.CloseIn)
4111  {
4112  CirclePhase = CirclePhase.Start;
4113  }
4114  if (to != AIState.Idle)
4115  {
4116  playDeadTimer = PlayDeadCoolDown;
4117  }
4118 #if CLIENT
4119  if (to == AIState.Attack)
4120  {
4121  Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
4122  }
4123 #endif
4124  }
4125 
4126  private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f);
4127 
4128  private float GetPerceivingRange(AITarget target)
4129  {
4130  float maxSightOrSoundRange = Math.Max(target.SightRange * Sight, target.SoundRange * Hearing);
4131  if (AIParams.MaxPerceptionDistance >= 0 && maxSightOrSoundRange > AIParams.MaxPerceptionDistance) { return AIParams.MaxPerceptionDistance; }
4132  return maxSightOrSoundRange;
4133  }
4134 
4135  private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false)
4136  {
4137  if (target?.Entity == null) { return false; }
4138  bool insideSightRange;
4139  bool insideSoundRange;
4140  if (checkVisibility)
4141  {
4142  Submarine mySub = Character.Submarine;
4143  Submarine targetSub = target.Entity.Submarine;
4144  // We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible.
4145  // When the monsters attack the player sub, they wall hack so that they can be more aggressive.
4146  // Pets should always check the visibility, unless the pet and the target are both outside the submarine -> shouldn't target when they can't perceive (= no wall hack)
4147  checkVisibility =
4148  (Character.IsPet && (mySub != null || targetSub != null)) ||
4149  (mySub != null && (targetSub == null || (targetSub == mySub && !targetSub.Info.IsPlayer)));
4150  }
4151  if (dist > 0)
4152  {
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);
4157  }
4158  else
4159  {
4160  if (distSquared < 0)
4161  {
4162  distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition);
4163  }
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);
4168  }
4169  if (!checkVisibility)
4170  {
4171  return insideSightRange || insideSoundRange;
4172  }
4173  else
4174  {
4175  if (!insideSightRange && !insideSoundRange) { return false; }
4176  // Inside the same submarine -> check whether the target is behind a wall
4177  if (target.Entity is Character c && VisibleHulls.Contains(c.CurrentHull) || target.Entity is Item i && VisibleHulls.Contains(i.CurrentHull))
4178  {
4179  return insideSightRange || insideSoundRange;
4180  }
4181  else
4182  {
4183  // No line of sight to the target -> Ignore sight and use only half of the sound range
4184  if (dist > 0)
4185  {
4186  return IsInRange(dist, target.SoundRange, Hearing / 2);
4187  }
4188  else
4189  {
4190  if (distSquared < 0)
4191  {
4192  distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition);
4193  }
4194  return IsInRangeSqr(distSquared, target.SoundRange, Hearing / 2);
4195  }
4196  }
4197  }
4198 
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);
4201  }
4202 
4203  public void ReevaluateAttacks()
4204  {
4205  canAttackWalls = LatchOntoAI is { AttachToSub: true };
4206  canAttackDoors = false;
4207  canAttackCharacters = false;
4208  canAttackItems = false;
4209  foreach (var limb in Character.AnimController.Limbs)
4210  {
4211  if (limb.IsSevered) { continue; }
4212  if (limb.Disabled) { continue; }
4213  if (limb.attack == null) { continue; }
4214  if (!canAttackWalls)
4215  {
4216  canAttackWalls = (limb.attack.StructureDamage > 0 || limb.attack.Ranged && limb.attack.IsValidTarget(AttackTarget.Structure));
4217  }
4218  if (!canAttackDoors)
4219  {
4220  // Doors are technically items, but intentionally treated as structures here.
4221  canAttackDoors = (limb.attack.ItemDamage > 0 || limb.attack.Ranged) && limb.attack.IsValidTarget(AttackTarget.Structure);
4222  }
4223  if (!canAttackItems)
4224  {
4225  // AttackTarget.Structure is also accepted for backwards support.
4226  canAttackItems = canAttackDoors || (limb.attack.ItemDamage > 0 || limb.attack.Ranged) && limb.attack.IsValidTarget(AttackTarget.Structure | AttackTarget.Item);
4227  }
4228  if (!canAttackCharacters)
4229  {
4230  canAttackCharacters = limb.attack.IsValidTarget(AttackTarget.Character);
4231  }
4232  }
4233  if (PathSteering != null)
4234  {
4235  PathSteering.CanBreakDoors = canAttackDoors;
4236  }
4237  }
4238 
4239  private bool IsPositionInsideAllowedZone(Vector2 pos, out Vector2 targetDir)
4240  {
4241  targetDir = Vector2.Zero;
4242  if (Level.Loaded == null) { return true; }
4243  if (Level.Loaded.LevelData.Biome.IsEndBiome) { return true; }
4244  if (AIParams.AvoidAbyss)
4245  {
4246  if (pos.Y < Level.Loaded.AbyssStart)
4247  {
4248  // Too far down
4249  targetDir = Vector2.UnitY;
4250  }
4251  }
4252  else if (AIParams.StayInAbyss)
4253  {
4254  if (pos.Y > Level.Loaded.AbyssStart)
4255  {
4256  // Too far up
4257  targetDir = -Vector2.UnitY;
4258  }
4259  else if (pos.Y < Level.Loaded.AbyssEnd)
4260  {
4261  // Too far down
4262  targetDir = Vector2.UnitY;
4263  }
4264  }
4265  float margin = Level.OutsideBoundsCurrentMargin;
4266  if (pos.X < -margin)
4267  {
4268  // Too far left
4269  targetDir = Vector2.UnitX;
4270  }
4271  else if (pos.X > Level.Loaded.Size.X + margin)
4272  {
4273  // Too far right
4274  targetDir = -Vector2.UnitX;
4275  }
4276  return targetDir == Vector2.Zero;
4277  }
4278 
4279  private Vector2 returnDir;
4280  private float returnTimer;
4281  private void SteerInsideLevel(float deltaTime)
4282  {
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))
4288  {
4289  returnDir = targetDir;
4290  returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
4291  }
4292  if (returnTimer > 0)
4293  {
4294  returnTimer -= deltaTime;
4295  SteeringManager.Reset();
4296  SteeringManager.SteeringManual(deltaTime, returnDir * 10);
4297  SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, 15);
4298  }
4299  }
4300 
4301  public override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
4302  {
4303  IsTryingToSteerThroughGap = true;
4304  wallTarget = null;
4305  LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
4307  bool success = base.SteerThroughGap(wall, section, targetWorldPos, deltaTime);
4308  if (success)
4309  {
4310  // If already inside, target the hull, else target the wall.
4311  SelectedAiTarget = Character.CurrentHull != null ? section.gap.AiTarget : wall.AiTarget;
4312  SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
4313  }
4314  IsSteeringThroughGap = success;
4315  return success;
4316  }
4317 
4318  public override bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1)
4319  {
4320  bool success = base.SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance);
4321  if (success)
4322  {
4323  wallTarget = null;
4324  LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
4326  SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
4327  }
4328  IsSteeringThroughGap = success;
4329  return success;
4330  }
4331 
4332  public bool CanPassThroughHole(Structure wall, int sectionIndex) => CanPassThroughHole(wall, sectionIndex, requiredHoleCount);
4333 
4334  public override bool Escape(float deltaTime)
4335  {
4336  if (SelectedAiTarget != null && (SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed))
4337  {
4338  State = AIState.Idle;
4339  return false;
4340  }
4341  else if (CurrentTargetMemory is AITargetMemory targetMemory && SelectedAiTarget?.Entity is Character)
4342  {
4343  targetMemory.Priority += deltaTime * PriorityFearIncrement;
4344  }
4345  bool isSteeringThroughGap = UpdateEscape(deltaTime, canAttackDoors);
4346  if (!isSteeringThroughGap)
4347  {
4348  if (SelectedAiTarget?.Entity is Character targetCharacter && targetCharacter.CurrentHull == Character.CurrentHull)
4349  {
4350  SteerAwayFromTheEnemy();
4351  }
4352  else if (canAttackDoors && HasValidPath())
4353  {
4354  var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
4355  if (door is { CanBeTraversed: false } && !door.HasAccess(Character))
4356  {
4357  if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack)
4358  {
4359  SelectTarget(door.Item.AiTarget, CurrentTargetMemory.Priority);
4360  State = AIState.Attack;
4361  return false;
4362  }
4363  }
4364  }
4365  }
4366  if (EscapeTarget == null)
4367  {
4368  if (SelectedAiTarget?.Entity is Character)
4369  {
4370  SteerAwayFromTheEnemy();
4371  }
4372  else
4373  {
4374  SteeringManager.SteeringWander(avoidWanderingOutsideLevel: Character.CurrentHull == null);
4375  if (Character.CurrentHull == null)
4376  {
4377  SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
4378  }
4379  }
4380  }
4381  return isSteeringThroughGap;
4382 
4383  void SteerAwayFromTheEnemy()
4384  {
4385  if (SelectedAiTarget == null) { return; }
4386  Vector2 escapeDir = Vector2.Normalize(WorldPosition - SelectedAiTarget.WorldPosition);
4387  if (!MathUtils.IsValid(escapeDir))
4388  {
4389  escapeDir = Vector2.UnitY;
4390  }
4392  {
4393  // Inside
4394  escapeDir = new Vector2(Math.Sign(escapeDir.X), 0);
4395  }
4397  SteeringManager.SteeringManual(deltaTime, escapeDir);
4398  }
4399  }
4400 
4401  private readonly List<Limb> targetLimbs = new List<Limb>();
4402  public Limb GetTargetLimb(Limb attackLimb, Character target, LimbType targetLimbType = LimbType.None)
4403  {
4404  targetLimbs.Clear();
4405  foreach (var limb in target.AnimController.Limbs)
4406  {
4407  if (limb.type == targetLimbType || targetLimbType == LimbType.None)
4408  {
4409  targetLimbs.Add(limb);
4410  }
4411  }
4412  if (targetLimbs.None())
4413  {
4414  // If no limbs of given type was found, accept any limb.
4415  targetLimbs.AddRange(target.AnimController.Limbs);
4416  }
4417  float closestDist = float.MaxValue;
4418  Limb targetLimb = null;
4419  foreach (Limb limb in targetLimbs)
4420  {
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)
4425  {
4426  closestDist = dist;
4427  targetLimb = limb;
4428  }
4429  }
4430  return targetLimb;
4431  }
4432 
4433  private static Character GetOwner(Item item)
4434  {
4435  var pickable = item.GetComponent<Pickable>();
4436  if (pickable != null)
4437  {
4438  Character owner = pickable.Picker ?? item.FindParentInventory(i => i.Owner is Character)?.Owner as Character;
4439  if (owner != null)
4440  {
4441  var target = owner.AiTarget;
4442  if (target?.Entity != null && !target.Entity.Removed)
4443  {
4444  return owner;
4445  }
4446  }
4447  }
4448  return null;
4449  }
4450  }
4451 
4452  //the "memory" of the Character
4453  //keeps track of how preferable it is to attack a specific target
4454  //(if the Character can't inflict much damage the target, the priority decreases
4455  //and if the target attacks the Character, the priority increases)
4457  {
4458  public readonly AITarget Target;
4459  public Vector2 Location { get; set; }
4460 
4461  private float priority;
4462 
4463  public float Priority
4464  {
4465  get { return priority; }
4466  set { priority = MathHelper.Clamp(value, 1.0f, 100.0f); }
4467  }
4468 
4469  public AITargetMemory(AITarget target, float priority)
4470  {
4471  Target = target;
4472  Location = target.WorldPosition;
4473  this.priority = priority;
4474  }
4475  }
4476 }
readonly Character Character
Definition: AIController.cs:15
SteeringManager SteeringManager
Definition: AIController.cs:54
readonly float colliderLength
SteeringManager steeringManager
Definition: AIController.cs:51
readonly float colliderWidth
AIObjective CurrentObjective
Includes orders.
static List< AITarget > List
Definition: AITarget.cs:10
bool ShouldBeIgnored()
Is some condition met (e.g. entity null, indetectable, outside level) that prevents anyone from detec...
Definition: AITarget.cs:190
TargetType Type
Definition: AITarget.cs:140
Vector2 WorldPosition
Definition: AITarget.cs:150
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...
Definition: Attack.cs:102
float AimRotationTorque
Definition: Attack.cs:228
float Duration
Definition: Attack.cs:157
float StructureDamage
Definition: Attack.cs:175
float CoolDownTimer
Definition: Attack.cs:732
float ItemDamage
Definition: Attack.cs:189
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)
bool IsFriendly(Character other)
bool IsSameSpeciesOrGroup(Character other)
static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
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 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)
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)
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)
AITarget AiTarget
Definition: Entity.cs:55
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Entity(Submarine submarine, ushort id)
Definition: Entity.cs:90
Submarine Submarine
Definition: Entity.cs:53
static NetworkMember NetworkMember
Definition: GameMain.cs:41
static GameSession GameSession
Definition: GameMain.cs:45
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)
void SetPath(Vector2 targetPos, SteeringPath path)
Inventory FindParentInventory(Func< Inventory, bool > predicate)
void DeattachFromBody(bool reset, float cooldown=0)
Definition: LatchOntoAI.cs:461
List< Joint > AttachJoints
Definition: LatchOntoAI.cs:51
void Update(EnemyAIController enemyAI, float deltaTime)
Definition: LatchOntoAI.cs:137
readonly Attack attack
Definition: Limb.cs:270
readonly LimbParams Params
Definition: Limb.cs:214
readonly LimbType type
Definition: Limb.cs:227
PhysicsBody body
Definition: Limb.cs:217
Vector2?? WorldPosition
Definition: Limb.cs:401
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)
Definition: PathFinder.cs:173
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
Definition: Prefab.cs:37
float?? ImpactTolerance
Definition: Ragdoll.cs:318
CanEnterSubmarine CanEnterSubmarine
Definition: Ragdoll.cs:339
Vector2? TargetMovement
Definition: Ragdoll.cs:298
PhysicsBody? Collider
Definition: Ragdoll.cs:145
Vector2 GetColliderBottom()
Definition: Ragdoll.cs:2183
void RestoreTemporarilyDisabled()
bool? SimplePhysicsEnabled
Definition: Ragdoll.cs:257
bool IgnorePlatforms
Definition: Ragdoll.cs:373
void ReleaseStuckLimbs()
Definition: Ragdoll.cs:2225
Direction TargetDir
Definition: Ragdoll.cs:135
Vector2 movement
Definition: Ragdoll.cs:106
void HideAndDisable(LimbType limbType, float duration=0, bool ignoreCollisions=true)
Definition: Ragdoll.cs:2231
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 ...
Definition: StatusEffect.cs:72
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)
LimbType
Definition: Limb.cs:19
AbilityFlags
AbilityFlags are a set of toggleable flags that can be applied to characters.
Definition: Enums.cs:641
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:26
AttackTarget
Definition: Attack.cs:30
@ Character
Characters only
@ Structure
Structures and hulls, but also items (for backwards support)!
AIBehaviorAfterAttack
Definition: Attack.cs:47