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