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 CheckObjectiveSpecific()
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 Unequip()
903  {
904  if (!character.LockHands && character.HeldItems.Contains(Weapon))
905  {
906  if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
907  {
908  if (Weapon.AllowedSlots.Contains(InvSlotType.Bag))
909  {
910  if (character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Bag }))
911  {
912  return;
913  }
914  }
915  Weapon.Drop(character);
916  }
917  }
918  }
919 
920  private bool Equip()
921  {
922  if (character.LockHands) { return false; }
923  if (WeaponComponent.IsEmpty(character))
924  {
925  return false;
926  }
927  if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType))
928  {
929  ClearInputs();
930  Weapon.TryInteract(character, forceSelectKey: true);
931  var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType);
932  if (character.Inventory.TryPutItem(Weapon, character, slots))
933  {
934  SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
935  SetReloadTime(WeaponComponent);
936  }
937  else
938  {
939  SpeakNoWeapons();
940  Weapon = null;
941  Mode = CombatMode.Retreat;
942  return false;
943  }
944  }
945  return true;
946  }
947 
948  private float findHullTimer;
949  private const float findHullInterval = 1.0f;
950 
951  private void Retreat(float deltaTime)
952  {
954  {
955  // Only relevant when we are retreating from monsters and are not inside a friendly sub.
956  PlayerCrewSpeak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDurationBetweenSimilar: 20);
957  }
958  RemoveFollowTarget();
959  RemoveSubObjective(ref seekAmmunitionObjective);
960  if (retreatTarget != null)
961  {
963  {
964  // In the same hull with the enemy
965  if (retreatTarget == character.CurrentHull)
966  {
967  // Go elsewhere
968  retreatTarget = null;
969  }
970  }
971  }
972  if (retreatObjective != null && retreatObjective.Target != retreatTarget)
973  {
974  RemoveSubObjective(ref retreatObjective);
975  }
976  if (character.Submarine == null && sqrDistance < MathUtils.Pow2(MaxDistance))
977  {
978  // Swim away
981  SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition));
982  SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
983  return;
984  }
985  if (retreatTarget == null || retreatObjective is { CanBeCompleted: false })
986  {
987  if (findHullTimer > 0)
988  {
989  findHullTimer -= deltaTime;
990  }
991  else
992  {
993  HullSearchStatus hullSearchStatus = findSafety.FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
994  if (hullSearchStatus != HullSearchStatus.Finished)
995  {
996  findSafety.UpdateSimpleEscape(deltaTime);
997  return;
998  }
999  retreatTarget = potentialSafeHull;
1000  findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
1001  }
1002  }
1003  if (retreatTarget != null && character.CurrentHull != retreatTarget)
1004  {
1005  TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager)
1006  {
1007  UsePathingOutside = false,
1008  SpeakIfFails = false
1009  },
1010  onAbandon: () =>
1011  {
1012  if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
1013  {
1014  // If in the same room with an enemy -> don't try to escape because we'd want to fight it
1016  RemoveSubObjective(ref retreatObjective);
1017  }
1018  else
1019  {
1020  // else abandon and fall back to find safety mode
1021  Abandon = true;
1022  }
1023  },
1024  onCompleted: () => RemoveSubObjective(ref retreatObjective));
1025  }
1026  }
1027 
1028  private void Engage(float deltaTime)
1029  {
1030  if (WeaponComponent == null)
1031  {
1032  RemoveFollowTarget();
1034  return;
1035  }
1036  if (character.LockHands || Enemy == null)
1037  {
1038  Mode = CombatMode.Retreat;
1040  return;
1041  }
1042  retreatTarget = null;
1043  RemoveSubObjective(ref retreatObjective);
1044  RemoveSubObjective(ref seekAmmunitionObjective);
1045  RemoveSubObjective(ref seekWeaponObjective);
1046  if (character.Submarine == null && WeaponComponent is MeleeWeapon meleeWeapon)
1047  {
1048  if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
1049  {
1051  // Swim towards the target
1054  SteeringManager.SteeringAvoid(deltaTime, 5, weight: 15);
1055  }
1056  else
1057  {
1059  }
1060  return;
1061  }
1063  {
1064  // An outpost guard following the target (possibly a player) to another sub -> don't go further, unless can see the enemy.
1066  {
1068  RemoveFollowTarget();
1069  return;
1070  }
1071  }
1072  if (followTargetObjective != null && followTargetObjective.Target != Enemy)
1073  {
1074  RemoveFollowTarget();
1075  }
1076  TryAddSubObjective(ref followTargetObjective,
1077  constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true, closeEnough: 50)
1078  {
1079  UsePathingOutside = false,
1080  IgnoreIfTargetDead = true,
1081  TargetName = Enemy.DisplayName,
1082  AlwaysUseEuclideanDistance = false,
1083  SpeakIfFails = false
1084  },
1085  onAbandon: () =>
1086  {
1087  if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
1088  {
1089  // If in the same room with an enemy -> don't try to escape because we'd want to fight it
1091  RemoveSubObjective(ref followTargetObjective);
1092  }
1093  else
1094  {
1095  // else abandon and fall back to find safety mode
1096  Abandon = true;
1097  }
1098  });
1099  if (followTargetObjective == null) { return; }
1100  if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown && !arrestingRegistered)
1101  {
1102  bool hasHandCuffs = HumanAIController.HasItem(character, Tags.HandLockerItem, out _);
1103  if (!hasHandCuffs && character.TeamID == CharacterTeamType.FriendlyNPC)
1104  {
1105  // Spawn handcuffs
1106  ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier());
1107  if (prefab != null)
1108  {
1109  Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: i =>
1110  {
1111  i.SpawnedInCurrentOutpost = true;
1112  i.AllowStealing = false;
1113  });
1114  }
1115  }
1116  arrestingRegistered = true;
1117  followTargetObjective.Completed += OnArrestTargetReached;
1118  followTargetObjective.CloseEnough = ArrestTargetDistance;
1119  }
1120  if (!arrestingRegistered)
1121  {
1122  followTargetObjective.CloseEnough =
1123  WeaponComponent switch
1124  {
1125  RangedWeapon => 1000,
1126  MeleeWeapon mw => mw.Range,
1127  RepairTool rt => rt.Range,
1128  _ => 50
1129  };
1130  }
1131  }
1132 
1133  private void RemoveFollowTarget()
1134  {
1135  if (followTargetObjective != null)
1136  {
1137  if (arrestingRegistered)
1138  {
1139  followTargetObjective.Completed -= OnArrestTargetReached;
1140  }
1141  RemoveSubObjective(ref followTargetObjective);
1142  }
1143  arrestingRegistered = false;
1144  }
1145 
1146  private void OnArrestTargetReached()
1147  {
1148  if (!Enemy.IsKnockedDown)
1149  {
1150  RemoveFollowTarget();
1151  return;
1152  }
1153  if (character.TeamID == CharacterTeamType.FriendlyNPC)
1154  {
1155  // Confiscate stolen goods and all weapons
1156  foreach (var item in Enemy.Inventory.AllItemsMod)
1157  {
1158  // Ignore handcuffs already on the target.
1159  if (item.HasTag(Tags.HandLockerItem) && Enemy.HasEquippedItem(item)) { continue; }
1160  if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
1161  {
1162  item.Drop(character);
1163  character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
1164  }
1165  }
1166  }
1167 
1168  //prefer using handcuffs already on the enemy's inventory
1169  if (!HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out IEnumerable<Item> matchingItems))
1170  {
1171  HumanAIController.HasItem(character, Tags.HandLockerItem, out matchingItems);
1172  }
1173 
1174  if (matchingItems.Any() &&
1175  !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
1176  {
1177  var handCuffs = matchingItems.First();
1178  if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
1179  {
1180 #if DEBUG
1181  DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
1182 #endif
1183  if (objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
1184  {
1185  Abandon = true;
1186  return;
1187  }
1188  }
1189  character.Speak(TextManager.Get("DialogTargetArrested").Value, null, 3.0f, "targetarrested".ToIdentifier(), 30.0f);
1190  }
1191  if (!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
1192  {
1193  IsCompleted = true;
1194  }
1195  }
1196 
1200  private void SeekAmmunition(ImmutableHashSet<Identifier> ammunitionIdentifiers)
1201  {
1202  retreatTarget = null;
1203  RemoveSubObjective(ref retreatObjective);
1204  RemoveSubObjective(ref seekWeaponObjective);
1205  RemoveFollowTarget();
1206  var itemContainer = Weapon.GetComponent<ItemContainer>();
1207  TryAddSubObjective(ref seekAmmunitionObjective,
1208  constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
1209  {
1210  ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
1211  checkInventory = false,
1212  MoveWholeStack = true
1213  },
1214  onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective),
1215  onAbandon: () =>
1216  {
1217  SteeringManager.Reset();
1218  RemoveSubObjective(ref seekAmmunitionObjective);
1219  ignoredWeapons.Add(Weapon);
1220  Weapon = null;
1221  });
1222  }
1223 
1228  private bool Reload(bool seekAmmo)
1229  {
1230  if (WeaponComponent == null) { return false; }
1231  if (Weapon.OwnInventory == null) { return true; }
1232  // Eject empty ammo
1233  HumanAIController.UnequipEmptyItems(Weapon);
1234  ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
1235  if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
1236  {
1237  foreach (RelatedItem requiredItem in WeaponComponent.RequiredItems[RelatedItem.RelationType.Contained])
1238  {
1239  if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
1240  ammunitionIdentifiers = requiredItem.Identifiers;
1241  break;
1242  }
1243  }
1244  else if (WeaponComponent is MeleeWeapon meleeWeapon)
1245  {
1246  ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
1247  }
1248  // No ammo
1249  if (ammunitionIdentifiers != null)
1250  {
1251  // Try reload ammunition from inventory
1252  static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
1253  Item ammunition = character.Inventory.FindItem(i =>
1254  i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive: true);
1255  if (ammunition != null)
1256  {
1257  var container = Weapon.GetComponent<ItemContainer>();
1258  if (container.Inventory.TryPutItem(ammunition, user: character))
1259  {
1260  ClearInputs();
1261  SetReloadTime(WeaponComponent);
1262  }
1263  else if (ammunition.ParentInventory == character.Inventory)
1264  {
1265  ammunition.Drop(character);
1266  }
1267  }
1268  }
1269  if (!WeaponComponent.IsEmpty(character))
1270  {
1271  return true;
1272  }
1273  else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
1274  {
1275  // Inventory not drawn = it's not interactable
1276  // If the weapon is empty and the inventory is inaccessible, it can't be reloaded
1277  if (!Weapon.OwnInventory.Container.DrawInventory) { return false; }
1278  SeekAmmunition(ammunitionIdentifiers);
1279  }
1280  return false;
1281  }
1282 
1283  private void Attack(float deltaTime)
1284  {
1285  character.CursorPosition = Enemy.WorldPosition;
1286  if (AimAccuracy < 1)
1287  {
1288  spreadTimer += deltaTime * Rand.Range(0.01f, 1f);
1289  float shake = Rand.Range(0.95f, 1.05f);
1290  float offsetAmount = (1 - AimAccuracy) * Rand.Range(300f, 500f);
1291  float distanceFactor = MathUtils.InverseLerp(0, 1000 * 1000, sqrDistance);
1292  float offset = (float)Math.Sin(spreadTimer * shake) * offsetAmount * distanceFactor;
1293  character.CursorPosition += new Vector2(0, offset);
1294  }
1295  if (character.Submarine != null)
1296  {
1297  character.CursorPosition -= character.Submarine.Position;
1298  }
1299  visibilityCheckTimer -= deltaTime;
1300  if (visibilityCheckTimer <= 0.0f)
1301  {
1302  canSeeTarget = character.CanSeeTarget(Enemy);
1303  visibilityCheckTimer = VisibilityCheckInterval;
1304  }
1305  if (!canSeeTarget)
1306  {
1307  SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
1308  return;
1309  }
1310  if (Weapon.RequireAimToUse)
1311  {
1312  character.SetInput(InputType.Aim, hit: false, held: true);
1313  }
1314  hasAimed = true;
1315  if (AllowHoldFire && holdFireTimer > 0)
1316  {
1317  holdFireTimer -= deltaTime;
1318  return;
1319  }
1320  if (aimTimer > 0)
1321  {
1322  aimTimer -= deltaTime;
1323  return;
1324  }
1325  if (reloadTimer > 0) { return; }
1326  if (holdFireCondition != null && holdFireCondition()) { return; }
1327  sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
1328  distanceTimer = DistanceCheckInterval;
1329  if (WeaponComponent is MeleeWeapon meleeWeapon)
1330  {
1331  bool closeEnough = true;
1332  float sqrRange = meleeWeapon.Range * meleeWeapon.Range;
1333  if (character.AnimController.InWater)
1334  {
1335  if (sqrDistance > sqrRange)
1336  {
1337  closeEnough = false;
1338  }
1339  }
1340  else
1341  {
1342  // It's possible that the center point of the creature is out of reach, but we could still hit the character.
1343  float xDiff = Math.Abs(Enemy.WorldPosition.X - character.WorldPosition.X);
1344  if (xDiff > meleeWeapon.Range)
1345  {
1346  closeEnough = false;
1347  }
1348  float yDiff = Math.Abs(Enemy.WorldPosition.Y - character.WorldPosition.Y);
1349  if (yDiff > Math.Max(meleeWeapon.Range, 100))
1350  {
1351  closeEnough = false;
1352  }
1353  if (closeEnough && Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
1354  {
1355  // The target is probably knocked down? -> try to reach it by crouching.
1356  HumanAIController.AnimController.Crouching = true;
1357  }
1358  }
1359  if (closeEnough)
1360  {
1361  UseWeapon(deltaTime);
1362  character.AIController.SteeringManager.Reset();
1363  }
1364  else if (!character.IsFacing(Enemy.WorldPosition))
1365  {
1366  // 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.
1367  SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
1368  }
1369  }
1370  else
1371  {
1372  if (WeaponComponent is RepairTool repairTool)
1373  {
1374  if (sqrDistance > repairTool.Range * repairTool.Range) { return; }
1375  }
1376  float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
1377  if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
1378  {
1379  myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody);
1380  // 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)
1381  var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
1382  foreach (var body in pickedBodies)
1383  {
1384  Character target = body.UserData switch
1385  {
1386  Character c => c,
1387  Limb limb => limb.character,
1388  _ => null
1389  };
1390  if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
1391  {
1392  return;
1393  }
1394  }
1395  UseWeapon(deltaTime);
1396  }
1397  }
1398  }
1399 
1400  private void UseWeapon(float deltaTime)
1401  {
1402  // Never allow friendly crew (bots) to attack with deadly weapons.
1403  if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam && Enemy.IsOnPlayerTeam) { return; }
1404  character.SetInput(InputType.Shoot, hit: false, held: true);
1405  Weapon.Use(deltaTime, user: character);
1406  SetReloadTime(WeaponComponent);
1407  }
1408 
1409  private float GetReloadTime(ItemComponent weaponComponent)
1410  {
1411  float reloadTime = 0;
1412  switch (weaponComponent)
1413  {
1414  case RangedWeapon rangedWeapon:
1415  {
1416  if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
1417  {
1418  reloadTime = rangedWeapon.Reload;
1419  }
1420  break;
1421  }
1422  case MeleeWeapon mw:
1423  {
1424  if (character.AnimController is HumanoidAnimController { Crouching: false })
1425  {
1426  reloadTime = mw.Reload;
1427  }
1428  break;
1429  }
1430  }
1431  return reloadTime;
1432  }
1433 
1434  private void SetReloadTime(ItemComponent weaponComponent)
1435  {
1436  float reloadTime = GetReloadTime(weaponComponent);
1437  reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
1438  }
1439 
1440  private void ClearInputs()
1441  {
1442  //clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
1443  character.ClearInput(InputType.Aim);
1444  character.ClearInput(InputType.Shoot);
1445  }
1446 
1447  private bool ShouldUnequipWeapon =>
1448  Weapon != null &&
1449  character.Submarine != null &&
1450  character.Submarine.TeamID == character.TeamID &&
1451  Character.CharacterList.None(c => c.Submarine == character.Submarine && HumanAIController.IsActive(c) && !HumanAIController.IsFriendly(character, c) && HumanAIController.VisibleHulls.Contains(c.CurrentHull));
1452 
1453  protected override void OnCompleted()
1454  {
1455  base.OnCompleted();
1456  if (Enemy != null)
1457  {
1458  switch (Mode)
1459  {
1460  case CombatMode.Offensive when Enemy.IsUnconscious && objectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>():
1461  character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f);
1462  break;
1463  case CombatMode.Arrest when IsCompleted:
1465  (bot != HumanAIController && bot.ObjectiveManager.CurrentObjective is AIObjectiveCombat { Mode: CombatMode.Arrest } combatObj && combatObj.Enemy == Enemy) ||
1466  bot.ObjectiveManager.CurrentObjective is AIObjectiveGoTo { SourceObjective: AIObjectiveCombat combatObjective } && combatObjective.Enemy == Enemy))
1467  {
1468  // Go to the target and confiscate any stolen items, unless someone is already on it.
1469  // Added on the root level, because the lifetime of the new objective exceeds the lifetime of this objective.
1470  RemoveFollowTarget();
1471  var approachArrestTarget = new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: false, getDivingGearIfNeeded: false, closeEnough: ArrestTargetDistance)
1472  {
1473  UsePathingOutside = false,
1474  IgnoreIfTargetDead = true,
1475  TargetName = Enemy.DisplayName,
1476  AlwaysUseEuclideanDistance = false,
1477  SpeakIfFails = false,
1478  SourceObjective = this
1479  };
1480  approachArrestTarget.Completed += OnArrestTargetReached;
1481  objectiveManager.AddObjective(approachArrestTarget);
1482  }
1483  break;
1484  }
1485  }
1486  if (ShouldUnequipWeapon)
1487  {
1488  Unequip();
1489  }
1491  }
1492 
1493  protected override void OnAbandon()
1494  {
1495  base.OnAbandon();
1496  if (ShouldUnequipWeapon)
1497  {
1498  Unequip();
1499  }
1501  }
1502 
1503  public override void OnDeselected()
1504  {
1505  base.OnDeselected();
1506  if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest && (!AllowHoldFire || (hasAimed && holdFireTimer <= 0)))
1507  {
1508  // Remember that the target resisted or acted offensively (we've aimed or tried to arrest/attack)
1509  Enemy.IsCriminal = true;
1510  }
1511  }
1512 
1513  public override void Reset()
1514  {
1515  base.Reset();
1516  hasAimed = false;
1517  holdFireTimer = 0;
1518  pathBackTimer = 0;
1519  isLethalWeapon = false;
1520  canSeeTarget = false;
1521  seekWeaponObjective = null;
1522  seekAmmunitionObjective = null;
1523  retreatObjective = null;
1524  followTargetObjective = null;
1525  retreatTarget = null;
1526  firstWarningTriggered = false;
1527  lastWarningTriggered = false;
1528  }
1529 
1533  private void SpeakNoWeapons()
1534  {
1535  if (!character.IsInFriendlySub)
1536  {
1537  PlayerCrewSpeak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 30);
1538  }
1539  }
1540 
1541  private void PlayerCrewSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
1542  {
1543  if (character.IsOnPlayerTeam)
1544  {
1545  Speak(textIdentifier, delay, minDurationBetweenSimilar);
1546  }
1547  }
1548 
1549  private void FriendlyGuardSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
1550  {
1551  if (character.TeamID == CharacterTeamType.FriendlyNPC && character.IsSecurity)
1552  {
1553  Speak(textIdentifier, delay, minDurationBetweenSimilar);
1554  }
1555  }
1556 
1557  private void Speak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
1558  {
1559  LocalizedString msg = TextManager.Get(textIdentifier);
1560  if (!msg.IsNullOrEmpty())
1561  {
1562  character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDurationBetweenSimilar);
1563  }
1564  }
1565 
1566  private void SetAimTimer(float newTimer) => aimTimer = Math.Max(aimTimer, newTimer);
1567  }
1568 }
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 CheckObjectiveSpecific()
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)
float GetSkillLevel(string skillIdentifier)
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...
bool IsHostileEscortee
Set true only, if the character is turned hostile from an escort mission (See EscortMission).
Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos=null)
bool IsKnockedDown
Is the character knocked down regardless whether the technical state is dead, unconcious,...
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)
override bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns 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....
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
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:19