Client LuaCsForBarotrauma
AIObjectiveCombat.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using FarseerPhysics.Dynamics;
9 using System.Collections.Immutable;
10 
11 namespace Barotrauma
12 {
14  {
15  public override Identifier Identifier { get; set; } = "combat".ToIdentifier();
16 
17  public override bool KeepDivingGearOn => true;
18  public override bool IgnoreUnsafeHulls => true;
19  protected override bool AllowOutsideSubmarine => true;
20  protected override bool AllowInAnySub => true;
21 
22  private readonly CombatMode initialMode;
23 
24  private float checkWeaponsTimer;
25  private const float CheckWeaponsInterval = 1;
26  private float ignoreWeaponTimer;
27  private const float IgnoredWeaponsClearTime = 10;
28 
29  private const float GoodWeaponPriority = 30;
30 
31  private float holdFireTimer;
32  private bool hasAimed;
33  private bool isLethalWeapon;
34  private bool AllowCoolDown => allowCooldown || !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID;
35  private bool allowCooldown;
36 
37  public Character Enemy { get; private set; }
38  public bool HoldPosition { get; set; }
39 
40  private Item _weapon;
41  private Item Weapon
42  {
43  get { return _weapon; }
44  set
45  {
46  _weapon = value;
47  _weaponComponent = null;
48  }
49  }
50  private ItemComponent _weaponComponent;
51  private ItemComponent WeaponComponent
52  {
53  get
54  {
55  if (Weapon == null) { return null; }
56  return _weaponComponent ?? GetWeaponComponent(Weapon);
57  }
58  }
59 
60  protected override bool ConcurrentObjectives => true;
61  public override bool AbandonWhenCannotCompleteSubObjectives => false;
62 
63  private readonly AIObjectiveFindSafety findSafety;
64  private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
65  private readonly HashSet<Item> ignoredWeapons = new HashSet<Item>();
66 
67  private AIObjectiveContainItem seekAmmunitionObjective;
68  private AIObjectiveGoTo retreatObjective;
69  private AIObjectiveGoTo followTargetObjective;
70  private AIObjectiveGetItem seekWeaponObjective;
71 
72  private Hull retreatTarget;
73  private float coolDownTimer;
74  private float pathBackTimer;
75  private const float DefaultCoolDown = 10.0f;
76  private const float PathBackCheckTime = 1.0f;
77  private IEnumerable<Body> myBodies;
78  private float aimTimer;
79  private float reloadTimer;
80  private float spreadTimer;
81 
82  private bool canSeeTarget;
83  private float visibilityCheckTimer;
84  private const float VisibilityCheckInterval = 0.2f;
85 
86  private float sqrDistance;
87  private const float MaxDistance = 2000;
88  private const float DistanceCheckInterval = 0.2f;
89  private float distanceTimer;
90 
91  private const float CloseDistanceThreshold = 300;
92  private const float FloorHeightApproximate = 100;
93 
94  public bool AllowHoldFire;
95  public bool SpeakWarnings;
96  private bool firstWarningTriggered;
97  private bool lastWarningTriggered;
98 
99  public float ArrestHoldFireTime { get; init; } = 10;
100 
101  private const float ArrestTargetDistance = 100;
102  private bool arrestingRegistered;
103 
107  public Func<bool> holdFireCondition;
108 
109  public enum CombatMode
110  {
114  Defensive,
118  Offensive,
122  Arrest,
126  Retreat,
130  None
131  }
132 
133  public CombatMode Mode { get; private set; }
134 
135  private bool IsOffensiveOrArrest => initialMode is CombatMode.Offensive or CombatMode.Arrest;
136  private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f) || (!character.IsInstigator && Enemy.IsHandcuffed && Enemy.IsKnockedDown);
137  private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
138 
139  private float AimSpeed => HumanAIController.AimSpeed;
140  private float AimAccuracy => HumanAIController.AimAccuracy;
141 
147  private bool IsEnemyClose(float margin)
148  {
149  if (Enemy == null) { return false; }
150  Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition;
152  {
153  // Inside, not in the same hull with the enemy
154  if (Math.Abs(toEnemy.Y) > FloorHeightApproximate)
155  {
156  // Different floor
157  return false;
158  }
160  {
161  // Potentially visible and on the same floor -> use only the horizontal distance.
162  return Math.Abs(toEnemy.X) < margin;
163  }
164  }
165  // Outside or inside in the same hull -> use the normal distance check.
166  return Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin;
167  }
168 
169  public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = DefaultCoolDown)
170  : base(character, objectiveManager, priorityModifier)
171  {
172  if (mode == CombatMode.None)
173  {
174 #if DEBUG
175  DebugConsole.ThrowError("Combat mode == None");
176 #endif
177  return;
178  }
179  Enemy = enemy;
180  coolDownTimer = coolDown;
181  findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
182  if (findSafety != null)
183  {
184  findSafety.Priority = 0;
186  }
187  Mode = mode;
188  initialMode = Mode;
189  if (Enemy == null)
190  {
191  Mode = CombatMode.Retreat;
192  }
193  spreadTimer = Rand.Range(-10f, 10f);
194  SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
196  }
197 
198  protected override float GetPriority()
199  {
200  if (TargetEliminated)
201  {
202  Priority = 0;
203  return Priority;
204  }
205  // 91-100
206  const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
207  const float maxPriority = AIObjectiveManager.MaxObjectivePriority;
208  const float priorityScale = maxPriority - minPriority;
209  float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
210  float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
212  {
213  xDist /= 2;
214  yDist /= 2;
215  }
216  float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
217  float devotion = CumulatedDevotion / 100;
218  float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
219  Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
220  if (Priority > 0)
221  {
223  {
224  Priority = 0;
225  }
226  }
227  return Priority;
228  }
229 
230  public override void Update(float deltaTime)
231  {
232  base.Update(deltaTime);
233  ignoreWeaponTimer -= deltaTime;
234  checkWeaponsTimer -= deltaTime;
235  if (reloadTimer > 0)
236  {
237  reloadTimer -= deltaTime;
238  }
239  if (ignoreWeaponTimer < 0)
240  {
241  ignoredWeapons.Clear();
242  ignoreWeaponTimer = IgnoredWeaponsClearTime;
243  }
244  bool isFightingIntruders = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
245  if (findSafety != null && isFightingIntruders)
246  {
247  findSafety.Priority = 0;
248  }
249  if (!AllowCoolDown && !character.IsOnPlayerTeam && !isFightingIntruders)
250  {
251  distanceTimer -= deltaTime;
252  if (distanceTimer < 0)
253  {
254  distanceTimer = DistanceCheckInterval;
255  sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
256  }
257  }
258  }
259 
260  protected override bool CheckObjectiveState()
261  {
262  if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
263  {
264  // Target still in the outpost
265  if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsSecurity)
266  {
267  // Outpost guards shouldn't lose the target in friendly outposts,
268  // However, if we are not a guard, let's ensure that we allow the cooldown.
269  allowCooldown = true;
270  }
271  }
272  else
273  {
274  if ((Enemy.Submarine == null && character.Submarine != null) || sqrDistance > MaxDistance * MaxDistance)
275  {
276  // The target escaped from us.
277  Abandon = true;
278  if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest)
279  {
280  Enemy.IsCriminal = true;
281  }
282  return false;
283  }
284  if (Enemy.Submarine != null && character.Submarine != null && character.TeamID == CharacterTeamType.FriendlyNPC)
285  {
287  {
288  allowCooldown = true;
289  // Target not in the outpost anymore.
291  {
292  allowCooldown = false;
293  coolDownTimer = DefaultCoolDown;
294  }
295  else if (pathBackTimer <= 0)
296  {
297  // Check once per sec during the cooldown whether we can find a path back to the docking port
298  pathBackTimer = PathBackCheckTime;
299  foreach ((Submarine sub, DockingPort dockingPort) in character.Submarine.ConnectedDockingPorts)
300  {
301  if (sub.TeamID != character.TeamID) { continue; }
302  var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(dockingPort.Item), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
303  if (path.Unreachable)
304  {
305  allowCooldown = false;
306  coolDownTimer = DefaultCoolDown;
307  }
308  }
309  }
310  if (IsOffensiveOrArrest)
311  {
312  Enemy.IsCriminal = true;
313  }
314  }
315  }
316  }
317  return TargetEliminated || (AllowCoolDown && coolDownTimer <= 0);
318  }
319 
320  protected override void Act(float deltaTime)
321  {
322  if (IsEnemyDisabled)
323  {
324  IsCompleted = true;
325  return;
326  }
327  if (AllowCoolDown)
328  {
329  coolDownTimer -= deltaTime;
330  if (pathBackTimer > 0)
331  {
332  pathBackTimer -= deltaTime;
333  }
334  }
335  if (seekAmmunitionObjective == null && seekWeaponObjective == null)
336  {
337  if (Mode != CombatMode.Retreat && TryArm())
338  {
339  OperateWeapon(deltaTime);
340  }
341  if (HoldPosition)
342  {
344  }
345  else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
346  {
347  Move(deltaTime);
348  }
349  }
350  }
351 
352  private void Move(float deltaTime)
353  {
354  switch (Mode)
355  {
356  case CombatMode.Offensive:
357  case CombatMode.Arrest:
358  Engage(deltaTime);
359  break;
360  case CombatMode.Defensive:
362  {
363  if ((character.CurrentHull == null || character.CurrentHull == Enemy.CurrentHull) && sqrDistance < 200 * 200)
364  {
365  Engage(deltaTime);
366  }
367  else
368  {
369  // Keep following the goto target
370  var gotoObjective = objectiveManager.GetOrder<AIObjectiveGoTo>();
371  if (gotoObjective != null)
372  {
373  gotoObjective.ForceAct(deltaTime);
375  {
377  ForceWalk = true;
379  }
380  }
381  else
382  {
384  }
385  }
386  }
387  else
388  {
389  Retreat(deltaTime);
390  }
391  break;
392  case CombatMode.Retreat:
393  Retreat(deltaTime);
394  break;
395  default:
396  throw new NotImplementedException();
397  }
398  }
399 
400  private bool TryArm()
401  {
402  if (character.LockHands || Enemy == null)
403  {
404  Weapon = null;
405  RemoveSubObjective(ref seekAmmunitionObjective);
406  return false;
407  }
408  bool isAllowedToSeekWeapons = character.IsHostileEscortee || character.IsPrisoner || // Prisoners and terrorists etc are always allowed to seek new weapons.
409  (character.IsInFriendlySub // Other characters need to be on a friendly sub in order to "know" where the weapons are. This also prevents NPCs "stealing" player items.
410  && IsOffensiveOrArrest // = Defensive or retreating AI shouldn't seek new weapons.
411  && !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
412  if (checkWeaponsTimer < 0)
413  {
414  checkWeaponsTimer = CheckWeaponsInterval;
415  // First go through all weapons and try to reload without seeking ammunition
416  HashSet<ItemComponent> allWeapons = FindWeaponsFromInventory();
417  while (allWeapons.Any())
418  {
419  Weapon = GetWeapon(allWeapons, out _weaponComponent);
420  if (Weapon == null)
421  {
422  // No weapons
423  break;
424  }
425  if (!character.Inventory.Contains(Weapon) || WeaponComponent == null)
426  {
427  // Not in the inventory anymore or cannot find the weapon component
428  allWeapons.Remove(WeaponComponent);
429  Weapon = null;
430  continue;
431  }
432  if (!WeaponComponent.IsEmpty(character))
433  {
434  // All good, the weapon is loaded
435  break;
436  }
437  bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(CloseDistanceThreshold);
438  if (Reload(seekAmmo: seekAmmo))
439  {
440  // All good, we can use the weapon.
441  break;
442  }
443  else if (seekAmmunitionObjective != null)
444  {
445  // Seeking ammo.
446  break;
447  }
448  else
449  {
450  // No ammo and should not try to seek ammo.
451  allWeapons.Remove(WeaponComponent);
452  Weapon = null;
453  }
454  }
455  if (Weapon == null)
456  {
457  // No weapon found with the conditions above. Try again, now let's try to seek ammunition too
458  Weapon = FindWeapon(out _weaponComponent);
459  if (Weapon != null)
460  {
461  if (!CheckWeapon(seekAmmo: true))
462  {
463  if (seekAmmunitionObjective != null)
464  {
465  // No loaded weapon, but we are trying to seek ammunition.
466  return false;
467  }
468  else
469  {
470  Weapon = null;
471  }
472  }
473  }
474  }
475  if (!isAllowedToSeekWeapons)
476  {
477  if (WeaponComponent == null)
478  {
479  SpeakNoWeapons();
480  Mode = CombatMode.Retreat;
481  }
482  }
483  else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistanceThreshold))))
484  {
485  // No weapon or only a poor weapon equipped -> try to find better.
486  RemoveSubObjective(ref retreatObjective);
487  RemoveSubObjective(ref followTargetObjective);
488  TryAddSubObjective(ref seekWeaponObjective,
489  constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false)
490  {
491  AllowStealing = HumanAIController.IsMentallyUnstable,
492  AbortCondition = obj => IsEnemyClose(200),
493  EvaluateCombatPriority = false, // Use a custom formula instead
494  GetItemPriority = i =>
495  {
496  if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
497  if (i.IsOwnedBy(character)) { return 0; }
498  float priority = 0;
499  if (GetWeaponComponent(i) is ItemComponent ic)
500  {
501  priority = GetWeaponPriority(ic, prioritizeMelee: false, canSeekAmmo: true, out _) / 100;
502  }
503  if (priority <= 0) { return 0; }
504  // Check that we are not running directly towards the enemy.
505  Vector2 toItem = i.WorldPosition - character.WorldPosition;
506  float range = HumanAIController.FindWeaponsRange;
507  if (range is > 0 and < float.PositiveInfinity)
508  {
509  // Y distance is irrelevant when we are on the same floor. If we are on a different floor, let's double it.
510  float yDiff = Math.Abs(toItem.Y) > FloorHeightApproximate ? toItem.Y * 2 : 0;
511  Vector2 adjustedDiff = new Vector2(toItem.X, yDiff);
512  if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range))
513  {
514  // Too far -> not allowed to seek.
515  return 0;
516  }
517  }
518  Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition;
519  if (Math.Sign(toItem.X) == Math.Sign(toEnemy.X))
520  {
521  // Going towards the enemy -> reduce the priority.
522  priority *= 0.5f;
523  }
524  if (i.CurrentHull != null && !HumanAIController.VisibleHulls.Contains(i.CurrentHull))
525  {
526  if (Math.Abs(toItem.Y) > FloorHeightApproximate && Math.Abs(toEnemy.Y) > FloorHeightApproximate)
527  {
528  if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y))
529  {
530  // Different floor, at the direction of the enemy -> reduce the priority.
531  priority *= 0.75f;
532  }
533  }
534  }
535  return priority;
536  }
537  },
538  onCompleted: () => RemoveSubObjective(ref seekWeaponObjective),
539  onAbandon: () =>
540  {
541  RemoveSubObjective(ref seekWeaponObjective);
542  if (Weapon == null)
543  {
544  SpeakNoWeapons();
545  Mode = CombatMode.Retreat;
546  }
547  else if (!objectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>())
548  {
549  // Poor weapon equipped
550  Mode = CombatMode.Defensive;
551  }
552  });
553  }
554  }
555  else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
556  {
557  if (!CheckWeapon(seekAmmo: false))
558  {
559  Weapon = null;
560  }
561  }
562  return Weapon != null;
563 
564  bool CheckWeapon(bool seekAmmo)
565  {
566  if (!character.Inventory.Contains(Weapon) || WeaponComponent == null)
567  {
568  // Not in the inventory anymore or cannot find the weapon component
569  return false;
570  }
571  if (WeaponComponent.IsEmpty(character))
572  {
573  // Try reloading (and seek ammo)
574  if (!Reload(seekAmmo))
575  {
576  return false;
577  }
578  }
579  return true;
580  };
581  }
582 
583  private void OperateWeapon(float deltaTime)
584  {
585  switch (Mode)
586  {
587  case CombatMode.Offensive:
588  case CombatMode.Defensive:
589  case CombatMode.Arrest:
590  if (Equip())
591  {
592  Attack(deltaTime);
593  }
594  break;
595  case CombatMode.Retreat:
596  break;
597  default:
598  throw new NotImplementedException();
599  }
600  }
601 
602  private Item FindWeapon(out ItemComponent weaponComponent) => GetWeapon(FindWeaponsFromInventory(), out weaponComponent);
603 
604  private static ItemComponent GetWeaponComponent(Item item) =>
605  item.GetComponent<MeleeWeapon>() ??
606  item.GetComponent<RangedWeapon>() ??
607  item.GetComponent<RepairTool>() ??
608  item.GetComponent<Holdable>() as ItemComponent;
609 
613  private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool canSeekAmmo, out float lethalDmg)
614  {
615  lethalDmg = -1;
616  float priority = weapon.CombatPriority;
617  if (priority <= 0) { return 0; }
618  if (weapon is RepairTool repairTool)
619  {
620  switch (repairTool.UsableIn)
621  {
622  case RepairTool.UseEnvironment.Air:
623  if (character.InWater) { return 0; }
624  break;
625  case RepairTool.UseEnvironment.Water:
626  if (!character.InWater) { return 0; }
627  break;
628  case RepairTool.UseEnvironment.None:
629  return 0;
630  case RepairTool.UseEnvironment.Both:
631  default:
632  break;
633  }
634  }
635  if (prioritizeMelee && weapon is MeleeWeapon)
636  {
637  priority *= 5;
638  }
639  if (weapon.IsEmpty(character))
640  {
641  if (weapon is RangedWeapon && !canSeekAmmo)
642  {
643  // Ignore weapons that don't have any ammunition, when we are not allowed to seek more ammo.
644  return 0;
645  }
646  else
647  {
648  // Reduce the priority for weapons that don't have proper ammunition loaded.
649  if (character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType))
650  {
651  // Yet prefer the equipped weapon.
652  priority *= 0.75f;
653  }
654  else
655  {
656  priority *= 0.5f;
657  }
658  }
659  }
661  {
662  if (weapon.Item.HasTag(Tags.StunnerItem))
663  {
664  priority /= 2;
665  }
666  }
667  else if (Enemy.IsKnockedDown && Mode != CombatMode.Arrest)
668  {
669  // Enemy is stunned, reduce the priority of stunner weapons.
670  Attack attack = GetAttackDefinition(weapon);
671  if (attack != null)
672  {
673  lethalDmg = attack.GetTotalDamage();
674  float max = lethalDmg + 1;
675  if (weapon.Item.HasTag(Tags.StunnerItem))
676  {
677  priority = max;
678  }
679  else
680  {
681  float stunDmg = ApproximateStunDamage(weapon, attack);
682  float diff = stunDmg - lethalDmg;
683  priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max);
684  }
685  }
686  }
687  else if (Mode == CombatMode.Arrest)
688  {
689  // Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons.
690  if (weapon.Item.HasTag(Tags.StunnerItem))
691  {
692  priority *= 5;
693  }
694  else
695  {
696  Attack attack = GetAttackDefinition(weapon);
697  if (attack != null)
698  {
699  lethalDmg = attack.GetTotalDamage();
700  float stunDmg = ApproximateStunDamage(weapon, attack);
701  float diff = stunDmg - lethalDmg;
702  if (diff < 0)
703  {
704  priority /= 2;
705  }
706  }
707  }
708  }
709  else if (weapon is MeleeWeapon && weapon.Item.HasTag(Tags.StunnerItem) && (Enemy.Params.Health.StunImmunity || !CanMeleeStunnerStun(weapon)))
710  {
711  // Cannot do stun damage -> use the melee damage to determine the priority.
712  Attack attack = GetAttackDefinition(weapon);
713  priority = attack?.GetTotalDamage() ?? priority / 2;
714  }
715  // Reduce the priority of the weapon, if we don't have requires skills to use it.
716  float startPriority = priority;
717  var skillRequirementHints = weapon.Item.Prefab.SkillRequirementHints;
718  if (skillRequirementHints != null)
719  {
720  // If there are any skill requirement hints defined, let's use them.
721  // This should be the most accurate (manually defined) representation of the requirements (taking into account property conditionals etc).
722  foreach (SkillRequirementHint hint in skillRequirementHints)
723  {
724  float skillLevel = character.GetSkillLevel(hint.Skill);
725  float targetLevel = hint.Level;
726  priority = ReducePriority(priority, skillLevel, targetLevel);
727  }
728  }
729  else
730  {
731  // If no skill requirement hints are defined, let's rely on the required skill definition.
732  // This can be inaccurate in some cases (hmg, rifle), but in those cases there should be a skill requirement hint defined for the weapon.
733  foreach (Skill skill in weapon.RequiredSkills)
734  {
735  float skillLevel = character.GetSkillLevel(skill.Identifier);
736  // Skill multiplier is currently always 1, so it's not really needed, but that could change(?)
737  float targetLevel = skill.Level * weapon.GetSkillMultiplier();
738  priority = ReducePriority(priority, skillLevel, targetLevel);
739  }
740  }
741  // Don't allow to reduce more than half, because an assault rifle is still an assault rifle, even in untrained hands.
742  priority = Math.Max(priority, startPriority / 2);
743  return priority;
744 
745  float ReducePriority(float prio, float skillLevel, float targetLevel)
746  {
747  float diff = targetLevel - skillLevel;
748  if (diff > 0)
749  {
750  prio -= diff;
751  }
752  return prio;
753  }
754  }
755 
756  private float ApproximateStunDamage(ItemComponent weapon, Attack attack)
757  {
758  // Try to reduce the priority using the actual damage values and status effects.
759  // This is an approximation, because we can't check the status effect conditions here.
760  // The result might be incorrect if there is a high stun effect that's only applied in certain conditions.
761  var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character));
762  if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List<StatusEffect> hitEffects))
763  {
764  statusEffects = statusEffects.Concat(hitEffects);
765  }
766  float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0);
767  float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
768  {
769  float stunAmount = 0;
770  var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType);
771  if (stunAffliction != null)
772  {
773  stunAmount = stunAffliction.Strength;
774  }
775  return stunAmount;
776  });
777  return attack.Stun + afflictionsStun + effectsStun;
778  }
779 
780  private static bool CanMeleeStunnerStun(ItemComponent weapon)
781  {
782  // If there's an item container that takes a battery,
783  // assume that it's required for the stun effect
784  // as we can't check the status effect conditions here.
785  Identifier mobileBatteryTag = Tags.MobileBattery;
786  var containers = weapon.Item.Components.Where(ic =>
787  ic is ItemContainer container &&
788  container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
789  // If there's no such container, assume that the melee weapon can stun without a battery.
790  return containers.None() || containers.Any(container =>
791  (container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
792  }
793 
794  private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
795  {
796  weaponComponent = null;
797  float bestPriority = 0;
798  float lethalDmg = -1;
799  bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
800  bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistanceThreshold);
801  foreach (var weapon in weaponList)
802  {
803  float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
804  if (priority > bestPriority)
805  {
806  weaponComponent = weapon;
807  bestPriority = priority;
808  }
809  }
810  if (weaponComponent == null) { return null; }
811  if (bestPriority < 1) { return null; }
812  if (Mode == CombatMode.Arrest)
813  {
814  if (weaponComponent.Item.HasTag(Tags.StunnerItem))
815  {
816  isLethalWeapon = false;
817  }
818  else
819  {
820  if (lethalDmg < 0)
821  {
822  lethalDmg = GetLethalDamage(weaponComponent);
823  }
824  isLethalWeapon = lethalDmg > 1;
825  }
826  if (AllowHoldFire)
827  {
828  if (!hasAimed && holdFireTimer <= 0)
829  {
830  holdFireTimer = ArrestHoldFireTime * Rand.Range(0.9f, 1.1f);
831  }
832  else
833  {
834  if (SpeakWarnings)
835  {
836  if (!lastWarningTriggered && holdFireTimer < ArrestHoldFireTime * 0.3f)
837  {
838  FriendlyGuardSpeak("dialogarrest.lastwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f);
839  lastWarningTriggered = true;
840  }
841  else if (!firstWarningTriggered && holdFireTimer < ArrestHoldFireTime * 0.8f)
842  {
843  FriendlyGuardSpeak("dialogarrest.firstwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f);
844  firstWarningTriggered = true;
845  }
846  }
847  }
848  }
849  }
850  return weaponComponent.Item;
851  }
852 
853  public static float GetLethalDamage(ItemComponent weapon)
854  {
855  float lethalDmg = 0;
856  Attack attack = GetAttackDefinition(weapon);
857  if (attack != null)
858  {
859  lethalDmg = attack.GetTotalDamage();
860  }
861  return lethalDmg;
862  }
863 
864  private static Attack GetAttackDefinition(ItemComponent weapon)
865  {
866  Attack attack = weapon switch
867  {
868  MeleeWeapon meleeWeapon => meleeWeapon.Attack,
869  RangedWeapon rangedWeapon => rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack,
870  _ => null
871  };
872  return attack;
873  }
874 
875  private HashSet<ItemComponent> FindWeaponsFromInventory()
876  {
877  weapons.Clear();
878  foreach (var item in character.Inventory.AllItems)
879  {
880  if (ignoredWeapons.Contains(item)) { continue; }
881  GetWeapons(item, weapons);
882  if (item.OwnInventory != null)
883  {
884  item.OwnInventory.AllItems.ForEach(i => GetWeapons(i, weapons));
885  }
886  }
887  return weapons;
888  }
889 
890  private static void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
891  {
892  if (item == null) { return; }
893  foreach (var component in item.Components)
894  {
895  if (component.CombatPriority > 0)
896  {
897  weaponList.Add(component);
898  }
899  }
900  }
901 
902  private void UnequipWeapon()
903  {
904  if (Weapon == null) { return; }
905  if (character.LockHands) { return; }
906  if (character.HeldItems.Contains(Weapon)) { return; }
907  character.Unequip(Weapon);
908  }
909 
910  private bool Equip()
911  {
912  if (character.LockHands) { return false; }
913  if (WeaponComponent.IsEmpty(character))
914  {
915  return false;
916  }
917  if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType))
918  {
919  ClearInputs();
920  Weapon.TryInteract(character, forceSelectKey: true);
921  var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType);
922  bool successfullyEquipped = character.TryPutItem(Weapon, slots);
923  if (!successfullyEquipped && character.HasHandsFull(out (Item leftHandItem, Item rightHandItem) items))
924  {
925  // Unequip and try again.
926  character.Unequip(items.leftHandItem);
927  character.Unequip(items.rightHandItem);
928  successfullyEquipped = character.TryPutItem(Weapon, slots);
929  }
930  if (successfullyEquipped)
931  {
932  SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
933  SetReloadTime(WeaponComponent);
934  }
935  else
936  {
937  SpeakNoWeapons();
938  Weapon = null;
939  Mode = CombatMode.Retreat;
940  return false;
941  }
942  }
943  return true;
944  }
945 
946  private float findHullTimer;
947  private const float findHullInterval = 1.0f;
948 
949  private void Retreat(float deltaTime)
950  {
952  {
953  // Only relevant when we are retreating from monsters and are not inside a friendly sub.
954  PlayerCrewSpeak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDurationBetweenSimilar: 20);
955  }
956  RemoveFollowTarget();
957  RemoveSubObjective(ref seekAmmunitionObjective);
958  if (retreatTarget != null)
959  {
961  {
962  // In the same hull with the enemy
963  if (retreatTarget == character.CurrentHull)
964  {
965  // Go elsewhere
966  retreatTarget = null;
967  }
968  }
969  }
970  if (retreatObjective != null && retreatObjective.Target != retreatTarget)
971  {
972  RemoveSubObjective(ref retreatObjective);
973  }
974  if (character.Submarine == null && sqrDistance < MathUtils.Pow2(MaxDistance))
975  {
976  // Swim away
979  SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition));
980  SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
981  return;
982  }
983  if (retreatTarget == null || retreatObjective is { CanBeCompleted: false })
984  {
985  if (findHullTimer > 0)
986  {
987  findHullTimer -= deltaTime;
988  }
989  else
990  {
991  HullSearchStatus hullSearchStatus = findSafety.FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
992  if (hullSearchStatus != HullSearchStatus.Finished)
993  {
994  findSafety.UpdateSimpleEscape(deltaTime);
995  return;
996  }
997  retreatTarget = potentialSafeHull;
998  findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
999  }
1000  }
1001  if (retreatTarget != null && character.CurrentHull != retreatTarget)
1002  {
1003  TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager)
1004  {
1005  UsePathingOutside = false,
1006  SpeakIfFails = false
1007  },
1008  onAbandon: () =>
1009  {
1010  if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
1011  {
1012  // If in the same room with an enemy -> don't try to escape because we'd want to fight it
1014  RemoveSubObjective(ref retreatObjective);
1015  }
1016  else
1017  {
1018  // else abandon and fall back to find safety mode
1019  Abandon = true;
1020  }
1021  },
1022  onCompleted: () => RemoveSubObjective(ref retreatObjective));
1023  }
1024  }
1025 
1026  private void Engage(float deltaTime)
1027  {
1028  if (WeaponComponent == null)
1029  {
1030  RemoveFollowTarget();
1032  return;
1033  }
1034  if (character.LockHands || Enemy == null)
1035  {
1036  Mode = CombatMode.Retreat;
1038  return;
1039  }
1040  retreatTarget = null;
1041  RemoveSubObjective(ref retreatObjective);
1042  RemoveSubObjective(ref seekAmmunitionObjective);
1043  RemoveSubObjective(ref seekWeaponObjective);
1044  if (character.Submarine == null && WeaponComponent is MeleeWeapon meleeWeapon)
1045  {
1046  if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
1047  {
1049  // Swim towards the target
1052  SteeringManager.SteeringAvoid(deltaTime, 5, weight: 15);
1053  }
1054  else
1055  {
1057  }
1058  return;
1059  }
1061  {
1062  // An outpost guard following the target (possibly a player) to another sub -> don't go further, unless can see the enemy.
1064  {
1066  RemoveFollowTarget();
1067  return;
1068  }
1069  }
1070  if (followTargetObjective != null && followTargetObjective.Target != Enemy)
1071  {
1072  RemoveFollowTarget();
1073  }
1074  TryAddSubObjective(ref followTargetObjective,
1075  constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true, closeEnough: 50)
1076  {
1077  UsePathingOutside = false,
1078  IgnoreIfTargetDead = true,
1079  TargetName = Enemy.DisplayName,
1080  AlwaysUseEuclideanDistance = false,
1081  SpeakIfFails = false
1082  },
1083  onAbandon: () =>
1084  {
1085  if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
1086  {
1087  // If in the same room with an enemy -> don't try to escape because we'd want to fight it
1089  RemoveSubObjective(ref followTargetObjective);
1090  }
1091  else
1092  {
1093  // else abandon and fall back to find safety mode
1094  Abandon = true;
1095  }
1096  });
1097  if (followTargetObjective == null) { return; }
1098  if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown && !arrestingRegistered)
1099  {
1100  bool hasHandCuffs = HumanAIController.HasItem(character, Tags.HandLockerItem, out _);
1101  if (!hasHandCuffs && character.TeamID == CharacterTeamType.FriendlyNPC)
1102  {
1103  // Spawn handcuffs
1104  ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier());
1105  if (prefab != null)
1106  {
1107  Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: i =>
1108  {
1109  i.SpawnedInCurrentOutpost = true;
1110  i.AllowStealing = false;
1111  });
1112  }
1113  }
1114  arrestingRegistered = true;
1115  followTargetObjective.Completed += OnArrestTargetReached;
1116  followTargetObjective.CloseEnough = ArrestTargetDistance;
1117  }
1118  if (!arrestingRegistered)
1119  {
1120  followTargetObjective.CloseEnough =
1121  WeaponComponent switch
1122  {
1123  RangedWeapon => 1000,
1124  MeleeWeapon mw => mw.Range,
1125  RepairTool rt => rt.Range,
1126  _ => 50
1127  };
1128  }
1129  }
1130 
1131  private void RemoveFollowTarget()
1132  {
1133  if (followTargetObjective != null)
1134  {
1135  if (arrestingRegistered)
1136  {
1137  followTargetObjective.Completed -= OnArrestTargetReached;
1138  }
1139  RemoveSubObjective(ref followTargetObjective);
1140  }
1141  arrestingRegistered = false;
1142  }
1143 
1144  private void OnArrestTargetReached()
1145  {
1146  if (!Enemy.IsKnockedDown)
1147  {
1148  RemoveFollowTarget();
1149  return;
1150  }
1151  if (character.TeamID == CharacterTeamType.FriendlyNPC)
1152  {
1153  // Confiscate stolen goods and all weapons
1154  foreach (var item in Enemy.Inventory.AllItemsMod)
1155  {
1156  // Ignore handcuffs already on the target.
1157  if (item.HasTag(Tags.HandLockerItem) && Enemy.HasEquippedItem(item)) { continue; }
1158  if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
1159  {
1160  item.Drop(character);
1161  character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
1162  }
1163  }
1164  }
1165 
1166  //prefer using handcuffs already on the enemy's inventory
1167  if (!HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out IEnumerable<Item> matchingItems))
1168  {
1169  HumanAIController.HasItem(character, Tags.HandLockerItem, out matchingItems);
1170  }
1171 
1172  if (matchingItems.Any() &&
1173  !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
1174  {
1175  var handCuffs = matchingItems.First();
1176  if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
1177  {
1178 #if DEBUG
1179  DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
1180 #endif
1181  if (objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
1182  {
1183  Abandon = true;
1184  return;
1185  }
1186  }
1187  character.Speak(TextManager.Get("DialogTargetArrested").Value, null, 3.0f, "targetarrested".ToIdentifier(), 30.0f);
1188  }
1189  if (!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
1190  {
1191  IsCompleted = true;
1192  }
1193  }
1194 
1198  private void SeekAmmunition(ImmutableHashSet<Identifier> ammunitionIdentifiers)
1199  {
1200  retreatTarget = null;
1201  RemoveSubObjective(ref retreatObjective);
1202  RemoveSubObjective(ref seekWeaponObjective);
1203  RemoveFollowTarget();
1204  var itemContainer = Weapon.GetComponent<ItemContainer>();
1205  TryAddSubObjective(ref seekAmmunitionObjective,
1206  constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
1207  {
1208  ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
1209  checkInventory = false,
1210  MoveWholeStack = true
1211  },
1212  onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective),
1213  onAbandon: () =>
1214  {
1215  SteeringManager.Reset();
1216  RemoveSubObjective(ref seekAmmunitionObjective);
1217  ignoredWeapons.Add(Weapon);
1218  Weapon = null;
1219  });
1220  }
1221 
1226  private bool Reload(bool seekAmmo)
1227  {
1228  if (WeaponComponent == null) { return false; }
1229  if (Weapon.OwnInventory == null) { return true; }
1230  // Eject empty ammo
1231  HumanAIController.UnequipEmptyItems(Weapon);
1232  ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
1233  if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
1234  {
1235  foreach (RelatedItem requiredItem in WeaponComponent.RequiredItems[RelatedItem.RelationType.Contained])
1236  {
1237  if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
1238  ammunitionIdentifiers = requiredItem.Identifiers;
1239  break;
1240  }
1241  }
1242  else if (WeaponComponent is MeleeWeapon meleeWeapon)
1243  {
1244  ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
1245  }
1246  // No ammo
1247  if (ammunitionIdentifiers != null)
1248  {
1249  // Try reload ammunition from inventory
1250  static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
1251  Item ammunition = character.Inventory.FindItem(i =>
1252  i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive: true);
1253  if (ammunition != null)
1254  {
1255  var container = Weapon.GetComponent<ItemContainer>();
1256  if (container.Inventory.TryPutItem(ammunition, user: character))
1257  {
1258  ClearInputs();
1259  SetReloadTime(WeaponComponent);
1260  }
1261  else if (ammunition.ParentInventory == character.Inventory)
1262  {
1263  ammunition.Drop(character);
1264  }
1265  }
1266  }
1267  if (!WeaponComponent.IsEmpty(character))
1268  {
1269  return true;
1270  }
1271  else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
1272  {
1273  // Inventory not drawn = it's not interactable
1274  // If the weapon is empty and the inventory is inaccessible, it can't be reloaded
1275  if (!Weapon.OwnInventory.Container.DrawInventory) { return false; }
1276  SeekAmmunition(ammunitionIdentifiers);
1277  }
1278  return false;
1279  }
1280 
1281  private void Attack(float deltaTime)
1282  {
1283  character.CursorPosition = Enemy.WorldPosition;
1284  if (AimAccuracy < 1)
1285  {
1286  spreadTimer += deltaTime * Rand.Range(0.01f, 1f);
1287  float shake = Rand.Range(0.95f, 1.05f);
1288  float offsetAmount = (1 - AimAccuracy) * Rand.Range(300f, 500f);
1289  float distanceFactor = MathUtils.InverseLerp(0, 1000 * 1000, sqrDistance);
1290  float offset = (float)Math.Sin(spreadTimer * shake) * offsetAmount * distanceFactor;
1291  character.CursorPosition += new Vector2(0, offset);
1292  }
1293  if (character.Submarine != null)
1294  {
1295  character.CursorPosition -= character.Submarine.Position;
1296  }
1297  visibilityCheckTimer -= deltaTime;
1298  if (visibilityCheckTimer <= 0.0f)
1299  {
1300  canSeeTarget = character.CanSeeTarget(Enemy);
1301  visibilityCheckTimer = VisibilityCheckInterval;
1302  }
1303  if (!canSeeTarget)
1304  {
1305  SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
1306  return;
1307  }
1308  if (Weapon.RequireAimToUse)
1309  {
1310  character.SetInput(InputType.Aim, hit: false, held: true);
1311  }
1312  hasAimed = true;
1313  if (AllowHoldFire && holdFireTimer > 0)
1314  {
1315  holdFireTimer -= deltaTime;
1316  return;
1317  }
1318  if (aimTimer > 0)
1319  {
1320  aimTimer -= deltaTime;
1321  return;
1322  }
1323  sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
1324  distanceTimer = DistanceCheckInterval;
1325  if (WeaponComponent is MeleeWeapon meleeWeapon)
1326  {
1327  bool closeEnough = true;
1328  float sqrRange = meleeWeapon.Range * meleeWeapon.Range;
1329  if (character.AnimController.InWater)
1330  {
1331  if (sqrDistance > sqrRange)
1332  {
1333  closeEnough = false;
1334  }
1335  }
1336  else
1337  {
1338  // It's possible that the center point of the creature is out of reach, but we could still hit the character.
1339  float xDiff = Math.Abs(Enemy.WorldPosition.X - character.WorldPosition.X);
1340  if (xDiff > meleeWeapon.Range)
1341  {
1342  closeEnough = false;
1343  }
1344  float yDiff = Math.Abs(Enemy.WorldPosition.Y - character.WorldPosition.Y);
1345  if (yDiff > Math.Max(meleeWeapon.Range, 100))
1346  {
1347  closeEnough = false;
1348  }
1349  if (closeEnough && Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
1350  {
1351  // The target is probably knocked down? -> try to reach it by crouching.
1352  HumanAIController.AnimController.Crouch();
1353  }
1354  }
1355  if (reloadTimer > 0) { return; }
1356  if (holdFireCondition != null && holdFireCondition()) { return; }
1357  if (closeEnough)
1358  {
1359  UseWeapon(deltaTime);
1360  character.AIController.SteeringManager.Reset();
1361  }
1362  else if (!character.IsFacing(Enemy.WorldPosition))
1363  {
1364  // Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around.
1365  SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
1366  }
1367  }
1368  else
1369  {
1370  if (WeaponComponent is RepairTool repairTool)
1371  {
1372  float reach = AIObjectiveFixLeak.CalculateReach(repairTool, character);
1373  if (sqrDistance > reach * reach) { return; }
1374  }
1375  float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
1376  if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
1377  {
1378  myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody);
1379  // Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
1380  var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
1381  foreach (var body in pickedBodies)
1382  {
1383  Character target = body.UserData switch
1384  {
1385  Character c => c,
1386  Limb limb => limb.character,
1387  _ => null
1388  };
1389  if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
1390  {
1391  return;
1392  }
1393  }
1394  UseWeapon(deltaTime);
1395  }
1396  }
1397  }
1398 
1399  private void UseWeapon(float deltaTime)
1400  {
1401  // Never allow friendly crew (bots) to attack with deadly weapons.
1402  if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam && Enemy.IsOnPlayerTeam) { return; }
1403  character.SetInput(InputType.Shoot, hit: false, held: true);
1404  Weapon.Use(deltaTime, user: character);
1405  SetReloadTime(WeaponComponent);
1406  }
1407 
1408  private float GetReloadTime(ItemComponent weaponComponent)
1409  {
1410  float reloadTime = 0;
1411  switch (weaponComponent)
1412  {
1413  case RangedWeapon rangedWeapon:
1414  {
1415  if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
1416  {
1417  reloadTime = rangedWeapon.Reload;
1418  }
1419  break;
1420  }
1421  case MeleeWeapon mw:
1422  {
1423  reloadTime = mw.Reload;
1424  break;
1425  }
1426  }
1427  return reloadTime;
1428  }
1429 
1430  private void SetReloadTime(ItemComponent weaponComponent)
1431  {
1432  float reloadTime = GetReloadTime(weaponComponent);
1433  reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
1434  }
1435 
1436  private void ClearInputs()
1437  {
1438  //clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
1439  character.ClearInput(InputType.Aim);
1440  character.ClearInput(InputType.Shoot);
1441  }
1442 
1443  private bool ShouldUnequipWeapon =>
1444  Weapon != null &&
1445  character.Submarine != null &&
1446  character.Submarine.TeamID == character.TeamID &&
1447  Character.CharacterList.None(c => c.Submarine == character.Submarine && HumanAIController.IsActive(c) && !HumanAIController.IsFriendly(character, c) && HumanAIController.VisibleHulls.Contains(c.CurrentHull));
1448 
1449  protected override void OnCompleted()
1450  {
1451  base.OnCompleted();
1452  if (Enemy != null)
1453  {
1454  switch (Mode)
1455  {
1456  case CombatMode.Offensive when Enemy.IsUnconscious && objectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>():
1457  character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f);
1458  break;
1459  case CombatMode.Arrest when IsCompleted:
1461  (bot != HumanAIController && bot.ObjectiveManager.CurrentObjective is AIObjectiveCombat { Mode: CombatMode.Arrest } combatObj && combatObj.Enemy == Enemy) ||
1462  bot.ObjectiveManager.CurrentObjective is AIObjectiveGoTo { SourceObjective: AIObjectiveCombat combatObjective } && combatObjective.Enemy == Enemy))
1463  {
1464  // Go to the target and confiscate any stolen items, unless someone is already on it.
1465  // Added on the root level, because the lifetime of the new objective exceeds the lifetime of this objective.
1466  RemoveFollowTarget();
1467  var approachArrestTarget = new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: false, getDivingGearIfNeeded: false, closeEnough: ArrestTargetDistance)
1468  {
1469  UsePathingOutside = false,
1470  IgnoreIfTargetDead = true,
1471  TargetName = Enemy.DisplayName,
1472  AlwaysUseEuclideanDistance = false,
1473  SpeakIfFails = false,
1474  SourceObjective = this
1475  };
1476  approachArrestTarget.Completed += OnArrestTargetReached;
1477  objectiveManager.AddObjective(approachArrestTarget);
1478  }
1479  break;
1480  }
1481  }
1482  if (ShouldUnequipWeapon)
1483  {
1484  UnequipWeapon();
1485  }
1487  }
1488 
1489  protected override void OnAbandon()
1490  {
1491  base.OnAbandon();
1492  if (ShouldUnequipWeapon)
1493  {
1494  UnequipWeapon();
1495  }
1497  }
1498 
1499  public override void OnDeselected()
1500  {
1501  base.OnDeselected();
1502  if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest && (!AllowHoldFire || (hasAimed && holdFireTimer <= 0)))
1503  {
1504  // Remember that the target resisted or acted offensively (we've aimed or tried to arrest/attack)
1505  Enemy.IsCriminal = true;
1506  }
1507  }
1508 
1509  public override void Reset()
1510  {
1511  base.Reset();
1512  hasAimed = false;
1513  holdFireTimer = 0;
1514  pathBackTimer = 0;
1515  isLethalWeapon = false;
1516  canSeeTarget = false;
1517  seekWeaponObjective = null;
1518  seekAmmunitionObjective = null;
1519  retreatObjective = null;
1520  followTargetObjective = null;
1521  retreatTarget = null;
1522  firstWarningTriggered = false;
1523  lastWarningTriggered = false;
1524  }
1525 
1529  private void SpeakNoWeapons()
1530  {
1531  if (!character.IsInFriendlySub)
1532  {
1533  PlayerCrewSpeak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 30);
1534  }
1535  }
1536 
1537  private void PlayerCrewSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
1538  {
1539  if (character.IsOnPlayerTeam)
1540  {
1541  Speak(textIdentifier, delay, minDurationBetweenSimilar);
1542  }
1543  }
1544 
1545  private void FriendlyGuardSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
1546  {
1547  if (character.TeamID == CharacterTeamType.FriendlyNPC && character.IsSecurity)
1548  {
1549  Speak(textIdentifier, delay, minDurationBetweenSimilar);
1550  }
1551  }
1552 
1553  private void Speak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
1554  {
1555  LocalizedString msg = TextManager.Get(textIdentifier);
1556  if (!msg.IsNullOrEmpty())
1557  {
1558  character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDurationBetweenSimilar);
1559  }
1560  }
1561 
1562  private void SetAimTimer(float newTimer) => aimTimer = Math.Max(aimTimer, newTimer);
1563  }
1564 }
IEnumerable< Hull > VisibleHulls
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
void FaceTarget(ISpatialEntity target)
Func< bool > holdFireCondition
Don't start using a weapon if this condition is true
override void Update(float deltaTime)
override bool CheckObjectiveState()
Should return whether the objective is completed or not.
override bool AbandonWhenCannotCompleteSubObjectives
AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier=1, float coolDown=DefaultCoolDown)
static float GetLethalDamage(ItemComponent weapon)
override void Act(float deltaTime)
override Identifier Identifier
float Priority
Final priority value after all calculations.
Func< AIObjective, bool > AbortCondition
Aborts the objective when this condition is true.
const float EmergencyObjectivePriority
Priority of objectives such as finding safety, rescuing someone in a critical state or defending agai...
AIObjective CurrentObjective
Includes orders.
Order GetOrder(AIObjective objective)
Return the first order with the specified objective. Can return null.
const float MaxObjectivePriority
Highest possible priority for any objective. Used in certain cases where the character needs to react...
Attacks are used to deal damage to characters, structures and items. They can be defined in the weapo...
float GetTotalDamage(bool includeStructureDamage=false)
bool TryPutItem(Item item, IEnumerable< InvSlotType > allowedSlots)
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
bool IsCriminal
Do the outpost security officers treat the character as a criminal? Triggers when the character has e...
float GetSkillLevel(Identifier skillIdentifier)
Get the character's current skill level, taking into account any temporary boosts from wearables and ...
bool IsHostileEscortee
Set true only, if the character is turned hostile from an escort mission (See EscortMission).
bool Unequip(Item item)
Attempts to unequip an item. First tries to put the item in any slot. If that fails,...
Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos=null)
bool IsKnockedDown
Is the character knocked down regardless whether the technical state is dead, unconcious,...
bool HasHandsFull(out(Item leftHandItem, Item rightHandItem) items)
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)
static bool IsLatchedToSomeoneElse(Character target, Character character)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static bool IsTrueForAnyBotInTheCrew(Character character, Func< HumanAIController, bool > predicate)
float FindWeaponsRange
How far the character can seek new weapons from.
static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable< Item > items, Identifier containedTag=default, float conditionPercentage=0, bool requireEquipped=false, bool recursive=true, Func< Item, bool > predicate=null)
Note: uses a single list for matching items. The item is reused each time when the method is called....
bool Contains(Item item)
Is the item contained in this inventory. Does not recursively check items inside items.
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
IEnumerable< InvSlotType > AllowedSlots
bool TryInteract(Character user, bool ignoreRequiredItems=false, bool forceSelectKey=false, bool forceUseKey=false)
ImmutableArray< SkillRequirementHint > SkillRequirementHints
The base class for components holding the different functionalities of the item
bool IsEmpty(Character user)
Returns true if the item is lacking required contained items, or if there's nothing with a non-zero c...
readonly Dictionary< ActionType, List< StatusEffect > > statusEffectLists
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
readonly Identifier Identifier
Definition: Prefab.cs:34
void SteeringManual(float deltaTime, Vector2 velocity)
void SteeringSeek(Vector2 targetSimPos, float weight=1)
void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight=1)
readonly Dictionary< Submarine, DockingPort > ConnectedDockingPorts
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:26
@ Character
Characters only