Server LuaCsForBarotrauma
HumanAIController.cs
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 
9 namespace Barotrauma
10 {
12  {
13  public static bool DebugAI;
14  public static bool DisableCrewAI;
15 
16  private readonly AIObjectiveManager objectiveManager;
17 
18  public float SortTimer { get; set; }
19  private float crouchRaycastTimer;
20  private float reactTimer;
21  private float unreachableClearTimer;
22  private bool shouldCrouch;
26  public bool AutoFaceMovement = true;
27 
28  const float reactionTime = 0.3f;
29  const float crouchRaycastInterval = 1;
30  const float sortObjectiveInterval = 1;
31  const float clearUnreachableInterval = 30;
32 
33  private float flipTimer;
34  private const float FlipInterval = 0.5f;
35 
36  public const float HULL_SAFETY_THRESHOLD = 40;
37  public const float HULL_LOW_OXYGEN_PERCENTAGE = 30;
38 
39  private static readonly float characterWaitOnSwitch = 5;
40 
41  public readonly HashSet<Hull> UnreachableHulls = new HashSet<Hull>();
42  public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
43  public readonly List<Item> IgnoredItems = new List<Item>();
44 
45  private readonly HashSet<Hull> dirtyHullSafetyCalculations = new HashSet<Hull>();
46 
47  private float respondToAttackTimer;
48  private const float RespondToAttackInterval = 1.0f;
49  private bool wasConscious;
50 
51  private bool freezeAI;
52 
53  private readonly float maxSteeringBuffer = 5000;
54  private readonly float minSteeringBuffer = 500;
55  private readonly float steeringBufferIncreaseSpeed = 100;
56  private float steeringBuffer;
57 
58  private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5;
59  private float obstacleRaycastTimer;
60  private bool isBlocked;
61 
62  private readonly float enemyCheckInterval = 0.2f;
63  private readonly float enemySpotDistanceOutside = 800;
64  private readonly float enemySpotDistanceInside = 1000;
65  private float enemyCheckTimer;
66 
67  private readonly float reportProblemsInterval = 1.0f;
68  private float reportProblemsTimer;
69 
75  public float Hearing { get; set; } = 1.0f;
76 
80  public float ReportRange { get; set; } = float.PositiveInfinity;
81 
85  public float FindWeaponsRange { get; set; } = float.PositiveInfinity;
86 
87  private float _aimSpeed = 1;
88  public float AimSpeed
89  {
90  get { return _aimSpeed; }
91  set { _aimSpeed = Math.Max(value, 0.01f); }
92  }
93 
94  private float _aimAccuracy = 1;
95  public float AimAccuracy
96  {
97  get { return _aimAccuracy; }
98  set { _aimAccuracy = Math.Clamp(value, 0f, 1f); }
99  }
100 
104  private readonly Dictionary<Character, AttackResult> previousAttackResults = new Dictionary<Character, AttackResult>();
105  private readonly Dictionary<Character, float> previousHealAmounts = new Dictionary<Character, float>();
106 
107  private readonly SteeringManager outsideSteering, insideSteering;
108 
112  public bool UseOutsideWaypoints { get; private set; }
113 
116 
117  public AIObjectiveManager ObjectiveManager => objectiveManager;
118 
119  public float CurrentHullSafety { get; private set; } = 100;
120 
121  private readonly Dictionary<Character, float> structureDamageAccumulator = new Dictionary<Character, float>();
122  private readonly Dictionary<Hull, HullSafety> knownHulls = new Dictionary<Hull, HullSafety>();
123  private class HullSafety
124  {
125  public float safety;
126  public float timer;
127 
128  public bool IsStale => timer <= 0;
129 
130  public HullSafety(float safety)
131  {
132  Reset(safety);
133  }
134 
135  public void Reset(float safety)
136  {
137  this.safety = safety;
138  // How long before the hull safety is considered stale
139  timer = 0.5f;
140  }
141 
145  public bool Update(float deltaTime)
146  {
147  timer = Math.Max(timer - deltaTime, 0);
148  return IsStale;
149  }
150  }
151 
152  public MentalStateManager MentalStateManager { get; private set; }
153 
155  {
156  if (MentalStateManager == null)
157  {
159  }
160  MentalStateManager.Active = true;
161  }
162 
163  public override bool IsMentallyUnstable =>
165  {
166  CurrentMentalType:
167  MentalStateManager.MentalType.Afraid or
168  MentalStateManager.MentalType.Desperate or
170  };
171 
172  public ShipCommandManager ShipCommandManager { get; private set; }
173 
175  {
176  if (ShipCommandManager == null)
177  {
179  }
180  ShipCommandManager.Active = true;
181  }
182 
183  public HumanAIController(Character c) : base(c)
184  {
185  insideSteering = new IndoorsSteeringManager(this, true, false);
186  outsideSteering = new SteeringManager(this);
187  objectiveManager = new AIObjectiveManager(c);
188  reactTimer = GetReactionTime();
189  SortTimer = Rand.Range(0f, sortObjectiveInterval);
190  reportProblemsTimer = Rand.Range(0f, reportProblemsInterval);
191  }
192 
193  public override void Update(float deltaTime)
194  {
195  if (DisableCrewAI || Character.Removed) { return; }
196 
197  bool isIncapacitated = Character.IsIncapacitated;
198  if (freezeAI && !isIncapacitated)
199  {
200  freezeAI = false;
201  }
202  if (isIncapacitated) { return; }
203 
204  wasConscious = true;
205 
206  respondToAttackTimer -= deltaTime;
207  if (respondToAttackTimer <= 0.0f)
208  {
209  foreach (var previousAttackResult in previousAttackResults)
210  {
211  RespondToAttack(previousAttackResult.Key, previousAttackResult.Value);
212  if (previousHealAmounts.ContainsKey(previousAttackResult.Key))
213  {
214  //gradually forget past heals
215  previousHealAmounts[previousAttackResult.Key] = Math.Min(previousHealAmounts[previousAttackResult.Key] - 5.0f, 100.0f);
216  if (previousHealAmounts[previousAttackResult.Key] <= 0.0f)
217  {
218  previousHealAmounts.Remove(previousAttackResult.Key);
219  }
220  }
221  }
222  previousAttackResults.Clear();
223  respondToAttackTimer = RespondToAttackInterval;
224  }
225 
226  base.Update(deltaTime);
227 
228  foreach (var values in knownHulls)
229  {
230  HullSafety hullSafety = values.Value;
231  hullSafety.Update(deltaTime);
232  }
233 
234  if (unreachableClearTimer > 0)
235  {
236  unreachableClearTimer -= deltaTime;
237  }
238  else
239  {
240  unreachableClearTimer = clearUnreachableInterval;
241  UnreachableHulls.Clear();
242  IgnoredItems.Clear();
243  }
244 
245  // Note: returns false when useTargetSub is 'true' and the target is outside (targetSub is 'null')
246  bool IsCloseEnoughToTarget(float threshold, bool targetSub = true)
247  {
248  Entity target = SelectedAiTarget?.Entity;
249  if (target == null)
250  {
251  return false;
252  }
253  if (targetSub)
254  {
255  if (target.Submarine is Submarine sub)
256  {
257  target = sub;
258  threshold += Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2;
259  }
260  else
261  {
262  return false;
263  }
264  }
265  return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow2(threshold);
266  }
267 
268  bool isOutside = Character.Submarine == null;
269  if (isOutside)
270  {
271  obstacleRaycastTimer -= deltaTime;
272  if (obstacleRaycastTimer <= 0)
273  {
274  bool hasValidPath = HasValidPath();
275  isBlocked = false;
276  UseOutsideWaypoints = false;
277  obstacleRaycastTimer = obstacleRaycastIntervalLong;
278  ISpatialEntity spatialTarget = SelectedAiTarget?.Entity ?? ObjectiveManager.GetLastActiveObjective<AIObjectiveGoTo>()?.Target;
279  if (spatialTarget != null && (spatialTarget.Submarine == null || !IsCloseEnoughToTarget(2000, targetSub: false)))
280  {
281  // If the target is behind a level wall, switch to the pathing to get around the obstacles.
282  IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
283  Vector2 rayEnd = spatialTarget.SimPosition;
284  Submarine targetSub = spatialTarget.Submarine;
285  if (targetSub != null)
286  {
287  rayEnd += targetSub.SimPosition;
288  ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
289  }
290  var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
291  isBlocked = obstacle != null;
292  // Don't use outside waypoints when blocked by a sub, because we should use the waypoints linked to the sub instead.
293  UseOutsideWaypoints = isBlocked && (obstacle.UserData is not Submarine sub || sub.Info.IsRuin);
294  bool resetPath = false;
296  {
297  bool isUsingInsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine != null || n.Ruin != null);
298  if (isUsingInsideWaypoints)
299  {
300  resetPath = true;
301  }
302  }
303  else
304  {
305  bool isUsingOutsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine == null && n.Ruin == null);
306  if (isUsingOutsideWaypoints)
307  {
308  resetPath = true;
309  }
310  }
311  if (resetPath)
312  {
314  }
315  }
316  else if (hasValidPath)
317  {
318  obstacleRaycastTimer = obstacleRaycastIntervalShort;
319  // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
320  if (Submarine.MainSub != null)
321  {
322  foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
323  {
324  if (connectedSub == Submarine.MainSub) { continue; }
325  Vector2 rayStart = SimPosition - connectedSub.SimPosition;
327  Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
328  if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
329  {
331  break;
332  }
333  }
334  }
335  }
336  }
337  }
338  else
339  {
340  UseOutsideWaypoints = false;
341  isBlocked = false;
342  }
343 
345  {
346  // Spot enemies while staying outside or inside an enemy ship.
347  // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
348  enemyCheckTimer -= deltaTime;
349  if (enemyCheckTimer < 0)
350  {
351  CheckEnemies();
352  enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
353  }
354  }
355  bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
356  if (useInsideSteering)
357  {
358  if (steeringManager != insideSteering)
359  {
360  insideSteering.Reset();
362  steeringManager = insideSteering;
363  }
364  if (IsCloseEnoughToTarget(maxSteeringBuffer))
365  {
366  steeringBuffer += steeringBufferIncreaseSpeed * deltaTime;
367  }
368  else
369  {
370  steeringBuffer = minSteeringBuffer;
371  }
372  }
373  else
374  {
375  if (steeringManager != outsideSteering)
376  {
377  outsideSteering.Reset();
378  steeringManager = outsideSteering;
379  }
380  steeringBuffer = minSteeringBuffer;
381  }
382  steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
383 
384  AnimController.Crouching = shouldCrouch;
385  CheckCrouching(deltaTime);
387 
388  if (SortTimer > 0.0f)
389  {
390  SortTimer -= deltaTime;
391  }
392  else
393  {
394  objectiveManager.SortObjectives();
395  SortTimer = sortObjectiveInterval;
396  }
397  objectiveManager.UpdateObjectives(deltaTime);
398 
399  UpdateDragged(deltaTime);
400 
401  if (reportProblemsTimer > 0)
402  {
403  reportProblemsTimer -= deltaTime;
404  }
405  if (reactTimer > 0.0f)
406  {
407  reactTimer -= deltaTime;
408  if (findItemState != FindItemState.None)
409  {
410  // Update every frame only when seeking items
411  UnequipUnnecessaryItems();
412  }
413  }
414  else
415  {
417  if (Character.CurrentHull != null)
418  {
420  {
421  foreach (Hull h in VisibleHulls)
422  {
424  dirtyHullSafetyCalculations.Remove(h);
425  }
426  }
427  else
428  {
429  foreach (Hull h in VisibleHulls)
430  {
431  RefreshHullSafety(h);
432  dirtyHullSafetyCalculations.Remove(h);
433  }
434  }
435  foreach (Hull h in dirtyHullSafetyCalculations)
436  {
437  RefreshHullSafety(h);
438  }
439  }
440  dirtyHullSafetyCalculations.Clear();
441  if (reportProblemsTimer <= 0.0f)
442  {
444  {
445  ReportProblems();
446 
447  }
448  else
449  {
450  // Allows bots to heal targets autonomously while swimming outside of the sub.
452  {
453  AddTargets<AIObjectiveRescueAll, Character>(Character, Character);
454  }
455  }
456  reportProblemsTimer = reportProblemsInterval;
457  }
458  SpeakAboutIssues();
459  UnequipUnnecessaryItems();
460  reactTimer = GetReactionTime();
461  }
462 
463  if (objectiveManager.CurrentObjective == null) { return; }
464 
465  objectiveManager.DoCurrentObjective(deltaTime);
466  var currentObjective = objectiveManager.CurrentObjective;
467  bool run = !currentObjective.ForceWalk && (currentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority);
468  if (currentObjective is AIObjectiveGoTo goTo)
469  {
470  if (run && goTo == objectiveManager.ForcedOrder && goTo.IsWaitOrder && !Character.IsOnPlayerTeam)
471  {
472  // NPCs with a wait order don't run.
473  run = false;
474  }
475  else if (goTo.Target != null)
476  {
477  if (Character.CurrentHull == null)
478  {
479  run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300;
480  }
481  else
482  {
483  float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y;
484  if (Math.Abs(yDiff) > 100)
485  {
486  run = true;
487  }
488  else
489  {
490  float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X;
491  run = Math.Abs(xDiff) > 500;
492  }
493  }
494  }
495  }
496 
497  //if someone is grabbing the bot and the bot isn't trying to run anywhere, let them keep dragging and "control" the bot
498  if (Character.SelectedBy == null || run)
499  {
501  }
502 
504  if (steeringManager == insideSteering)
505  {
506  var currPath = PathSteering.CurrentPath;
507  if (currPath != null && currPath.CurrentNode != null)
508  {
509  if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
510  {
511  // Don't allow to jump from too high.
512  float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2;
513  float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y);
514  ignorePlatforms = height < allowedJumpHeight;
515  }
516  }
518  {
520  }
521  }
522  Character.AnimController.IgnorePlatforms = ignorePlatforms;
523 
524  Vector2 targetMovement = AnimController.TargetMovement;
526  {
527  targetMovement = new Vector2(Character.AnimController.TargetMovement.X, MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f));
528  }
530 
531  flipTimer -= deltaTime;
532  if (flipTimer <= 0.0f)
533  {
535  if (Character.IsKeyDown(InputType.Aim))
536  {
537  var cursorDiffX = Character.CursorPosition.X - Character.Position.X;
538  if (cursorDiffX > 10.0f)
539  {
540  newDir = Direction.Right;
541  }
542  else if (cursorDiffX < -10.0f)
543  {
544  newDir = Direction.Left;
545  }
546  if (Character.SelectedItem != null)
547  {
549  }
550  }
552  {
553  newDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left;
554  }
555  if (newDir != Character.AnimController.TargetDir)
556  {
558  flipTimer = FlipInterval;
559  }
560  }
561  AutoFaceMovement = true;
562 
563  MentalStateManager?.Update(deltaTime);
564  ShipCommandManager?.Update(deltaTime);
565  }
566 
567  private void CheckEnemies()
568  {
569  //already in combat, no need to check
570  if (objectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return; }
571 
572  float closestDistance = 0;
573  Character closestEnemy = null;
574  foreach (Character c in Character.CharacterList)
575  {
576  if (c.Submarine != Character.Submarine) { continue; }
577  if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
578  if (IsFriendly(c)) { continue; }
579  Vector2 toTarget = c.WorldPosition - WorldPosition;
580  float dist = toTarget.LengthSquared();
581  float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
582  if (dist > maxDistance * maxDistance) { continue; }
583  if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; }
584  var head = Character.AnimController.GetLimb(LimbType.Head);
585  if (head == null) { continue; }
586  float rotation = head.body.TransformedRotation;
587  Vector2 forward = VectorExtensions.Forward(rotation);
588  float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
589  if (angle > 70) { continue; }
590  if (!Character.CanSeeTarget(c)) { continue; }
591  if (dist < closestDistance || closestEnemy == null)
592  {
593  closestEnemy = c;
594  closestDistance = dist;
595  }
596  }
597  if (closestEnemy != null)
598  {
599  AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
600  }
601  }
602 
603  private void UnequipUnnecessaryItems()
604  {
605  if (Character.LockHands) { return; }
606  if (ObjectiveManager.CurrentObjective == null) { return; }
607  if (Character.CurrentHull == null) { return; }
608  bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, Tags.OxygenSource, out _, conditionPercentage: 1);
609  bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
610 
611  bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
612  {
613  bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
614  Hull targetHull = gotoObjective.GetTargetHull();
615  return (gotoObjective.Target != null && targetHull == null && !Character.IsImmuneToPressure) ||
616  NeedsDivingGear(targetHull, out _) ||
617  (insideSteering && ((PathSteering.CurrentPath.HasOutdoorsNodes && !Character.IsImmuneToPressure) || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _))));
618  }
619 
620  if (isCarrying)
621  {
622  if (findItemState != FindItemState.OtherItem)
623  {
624  var decontain = ObjectiveManager.GetActiveObjectives<AIObjectiveDecontainItem>().LastOrDefault();
625  if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(Tags.HeavyDivingGear) &&
626  ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
627  {
628  // Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker.
629  gotoObjective.Abandon = true;
630  }
631  }
632  if (!shouldActOnSuffocation)
633  {
634  return;
635  }
636  }
637 
638  // Diving gear
639  if (shouldActOnSuffocation || findItemState != FindItemState.OtherItem)
640  {
641  bool needsGear = NeedsDivingGear(Character.CurrentHull, out _);
642  if (!needsGear || shouldActOnSuffocation)
643  {
644  bool isCurrentObjectiveFindSafety = ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>();
645  bool shouldKeepTheGearOn =
646  isCurrentObjectiveFindSafety ||
650  Character.Submarine == null ||
653  ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
654  ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
657  bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo { IsWaitOrder: true };
658  bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
659  if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
660  {
661  shouldKeepTheGearOn = false;
662  // Remove the suit before we pass out
663  removeDivingSuit = true;
664  }
665  bool takeMaskOff = !shouldKeepTheGearOn;
666  if (!shouldKeepTheGearOn && !shouldActOnSuffocation)
667  {
668  if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
669  {
670  removeDivingSuit = true;
671  takeMaskOff = true;
672  }
673  else
674  {
675  bool removeSuit = false;
676  bool removeMask = false;
677  foreach (var objective in ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(includingSelf: true))
678  {
679  if (objective is AIObjectiveGoTo gotoObjective)
680  {
681  if (NeedsDivingGearOnPath(gotoObjective))
682  {
683  removeDivingSuit = false;
684  takeMaskOff = false;
685  break;
686  }
687  else if (gotoObjective.Mimic)
688  {
689  bool targetHasDivingGear = HasDivingGear(gotoObjective.Target as Character, requireOxygenTank: false);
690  if (!removeSuit)
691  {
692  removeDivingSuit = !targetHasDivingGear;
693  if (removeDivingSuit)
694  {
695  removeSuit = true;
696  }
697  }
698  if (!removeMask)
699  {
700  takeMaskOff = !targetHasDivingGear;
701  if (takeMaskOff)
702  {
703  removeMask = true;
704  }
705  }
706  }
707  }
708  }
709  }
710  }
711  if (removeDivingSuit)
712  {
713  var divingSuit = Character.Inventory.FindEquippedItemByTag(Tags.HeavyDivingGear);
714  if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(Character))
715  {
716  if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
717  {
718  divingSuit.Drop(Character);
719  HandleRelocation(divingSuit);
721  }
722  else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
723  {
724  findItemState = FindItemState.DivingSuit;
725  if (FindSuitableContainer(divingSuit, out Item targetContainer))
726  {
727  findItemState = FindItemState.None;
728  itemIndex = 0;
729  if (targetContainer != null)
730  {
731  var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
732  {
733  DropIfFails = false
734  };
735  decontainObjective.Abandoned += () =>
736  {
738  IgnoredItems.Add(targetContainer);
739  };
740  decontainObjective.Completed += () => ReequipUnequipped();
741  ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
742  return;
743  }
744  else
745  {
746  divingSuit.Drop(Character);
747  HandleRelocation(divingSuit);
749  }
750  }
751  }
752  }
753  }
754  if (takeMaskOff)
755  {
756  var mask = Character.Inventory.FindEquippedItemByTag(Tags.LightDivingGear);
757  if (mask != null)
758  {
759  if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
760  {
761  if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
762  {
763  mask.Drop(Character);
764  HandleRelocation(mask);
766  }
767  else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
768  {
769  findItemState = FindItemState.DivingMask;
770  if (FindSuitableContainer(mask, out Item targetContainer))
771  {
772  findItemState = FindItemState.None;
773  itemIndex = 0;
774  if (targetContainer != null)
775  {
776  var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
777  decontainObjective.Abandoned += () =>
778  {
780  IgnoredItems.Add(targetContainer);
781  };
782  decontainObjective.Completed += () => ReequipUnequipped();
783  ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
784  return;
785  }
786  else
787  {
788  mask.Drop(Character);
789  HandleRelocation(mask);
791  }
792  }
793  }
794  }
795  else
796  {
798  }
799  }
800  }
801  }
802  }
803  // Other items
804  if (isCarrying) { return; }
806 
807  if (Character.Submarine?.TeamID == Character.TeamID && findItemState is FindItemState.None or FindItemState.OtherItem)
808  {
809  // Only unequip other items inside a friendly sub.
810  foreach (Item item in Character.HeldItems)
811  {
812  if (item == null || !item.IsInteractable(Character)) { continue; }
813  if (Character.TryPutItemInAnySlot(item)) { continue; }
814  if (Character.TryPutItemInBag(item)) { continue; }
815  if (item.HasTag(Tags.Weapon))
816  {
817  // Don't decontain weapons, because it could be that we are holding a weapon that cannot be placed on back (if we have a toolbelt) nor in the any slot, such as an HMG.
818  // Could check that we only ignore weapons when we've had an order to find a weapon, but it could also be that we picked the weapon for self-defence, on ad-hoc basis.
819  // And I don't think it would make sense to decontain those weapons either.
820  continue;
821  }
822  findItemState = FindItemState.OtherItem;
823  if (FindSuitableContainer(item, out Item targetContainer))
824  {
825  findItemState = FindItemState.None;
826  itemIndex = 0;
827  if (targetContainer != null)
828  {
829  var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
830  decontainObjective.Abandoned += () =>
831  {
833  IgnoredItems.Add(targetContainer);
834  };
835  ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
836  return;
837  }
838  else
839  {
840  item.Drop(Character);
841  HandleRelocation(item);
842  }
843  }
844  }
845  }
846  }
847 
848  private readonly HashSet<Item> itemsToRelocate = new HashSet<Item>();
849 
850  public void HandleRelocation(Item item)
851  {
852  if (item.SpawnedInCurrentOutpost) { return; }
853  if (item.Submarine == null || Submarine.MainSub == null) { return; }
854  // Only affects bots in the player team
855  if (!Character.IsOnPlayerTeam) { return; }
856  // Don't relocate if the item is on a sub of the same team
857  if (item.Submarine.TeamID == Character.TeamID) { return; }
858  if (itemsToRelocate.Contains(item)) { return; }
859  itemsToRelocate.Add(item);
860  if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort))
861  {
862  myPort.OnUnDocked += Relocate;
863  }
864  var campaign = GameMain.GameSession.Campaign;
865  if (campaign != null)
866  {
867  // In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
868  campaign.BeforeLevelLoading += Relocate;
869  campaign.OnSaveAndQuit += Relocate;
870  campaign.ItemsRelocatedToMainSub = true;
871  }
872 #if CLIENT
873  HintManager.OnItemMarkedForRelocation();
874 #endif
875  void Relocate()
876  {
877  if (item == null || item.Removed) { return; }
878  if (!itemsToRelocate.Contains(item)) { return; }
879  var mainSub = Submarine.MainSub;
880  if (mainSub == null) { return; }
881  Entity owner = item.GetRootInventoryOwner();
882  if (owner != null)
883  {
884  if (owner is Character c)
885  {
886  if (c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2)
887  {
888  // Taken by a player/bot (if npc or monster would take the item, we'd probably still want it to spawn back to the main sub.
889  return;
890  }
891  }
892  else if (owner.Submarine == mainSub)
893  {
894  // Placed inside an inventory that's already in the main sub.
895  return;
896  }
897  }
898  // Laying on the ground inside the main sub.
899  if (item.Submarine == mainSub)
900  {
901  return;
902  }
903  if (owner != null && owner != item)
904  {
905  item.Drop(null);
906  }
907  item.Submarine = mainSub;
908  Item newContainer = mainSub.FindContainerFor(item, onlyPrimary: false);
909  if (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null))
910  {
911  WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub) ?? WayPoint.GetRandom(SpawnType.Path, null, mainSub);
912  if (wp != null)
913  {
914  item.SetTransform(wp.SimPosition, 0.0f, findNewHull: false, setPrevTransform: false);
915  }
916  else
917  {
918  DebugConsole.AddWarning($"Failed to relocate item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!");
919  }
920  }
921  itemsToRelocate.Remove(item);
922  DebugConsole.Log($"Relocated item {item.Prefab.Identifier} ({item.ID}) back to the main sub.");
923  }
924  }
925 
926  private enum FindItemState
927  {
928  None,
929  DivingSuit,
930  DivingMask,
931  OtherItem
932  }
933  private FindItemState findItemState;
934  private int itemIndex;
935 
936  public bool FindSuitableContainer(Item containableItem, out Item suitableContainer) => FindSuitableContainer(Character, containableItem, IgnoredItems, ref itemIndex, out suitableContainer);
937 
938  public static bool FindSuitableContainer(Character character, Item containableItem, List<Item> ignoredItems, ref int itemIndex, out Item suitableContainer)
939  {
940  suitableContainer = null;
941  if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i =>
942  {
943  if (!i.HasAccess(character)) { return 0; }
944  var container = i.GetComponent<ItemContainer>();
945  if (container == null) { return 0; }
946  if (!container.Inventory.CanBePut(containableItem)) { return 0; }
947  var rootContainer = container.Item.RootContainer ?? container.Item;
948  if (rootContainer.GetComponent<Fabricator>() != null || rootContainer.GetComponent<Deconstructor>() != null) { return 0; }
949  if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
950  {
951  if (isRestrictionsDefined)
952  {
953  return 10;
954  }
955  else
956  {
957  if (containableItem.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
958  {
959  return isPreferencesDefined ? isSecondary ? 2 : 5 : 1;
960  }
961  else
962  {
963  if (isPreferencesDefined)
964  {
965  // Use any valid locker as a fall back container.
966  return container.Item.HasTag(Tags.FallbackLocker) ? 0.5f : 0;
967  }
968  return 1;
969  }
970  }
971  }
972  else
973  {
974  return 0;
975  }
976  }))
977  {
978  if (targetContainer != null &&
979  character.AIController is HumanAIController humanAI &&
980  humanAI.PathSteering.PathFinder.FindPath(character.SimPosition, targetContainer.SimPosition, character.Submarine, errorMsgStr: $"FindSuitableContainer ({character.DisplayName})", nodeFilter: node => node.Waypoint.CurrentHull != null).Unreachable)
981  {
982  ignoredItems.Add(targetContainer);
983  itemIndex = 0;
984  return false;
985  }
986  else
987  {
988  suitableContainer = targetContainer;
989  return true;
990  }
991  }
992  return false;
993  }
994 
995  private float draggedTimer;
996  private float refuseDraggingTimer;
1000  private const float RefuseDraggingThresholdHigh = 10.0f;
1004  private const float RefuseDraggingThresholdLow = 0.5f;
1005  private const float RefuseDraggingDuration = 30.0f;
1006 
1007  private void UpdateDragged(float deltaTime)
1008  {
1009  if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; }
1010  if (Character.IsEscorted) { return; }
1011  if (Character.LockHands) { return; }
1012 
1013  //don't allow player characters who aren't in the same team to drag us for more than x seconds
1014  if (Character.SelectedBy == null ||
1015  !Character.SelectedBy.IsPlayer ||
1016  Character.SelectedBy.TeamID == Character.TeamID)
1017  {
1018  refuseDraggingTimer -= deltaTime;
1019  return;
1020  }
1021 
1022  draggedTimer += deltaTime;
1023  if (draggedTimer > RefuseDraggingThresholdHigh ||
1024  (refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow))
1025  {
1026  draggedTimer = 0.0f;
1027  refuseDraggingTimer = RefuseDraggingDuration;
1028  Character.SelectedBy.DeselectCharacter();
1029  Character.Speak(TextManager.Get("dialogrefusedragging").Value, delay: 0.5f, identifier: "refusedragging".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
1030  }
1031  }
1032 
1033  protected void ReportProblems()
1034  {
1035  Order newOrder = null;
1036  Hull targetHull = null;
1037 
1038  // for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
1039  bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted;
1040  if (Character.CurrentHull != null)
1041  {
1042  bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
1043  bool isFleeing = ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>();
1044  foreach (var hull in VisibleHulls)
1045  {
1046  foreach (Character target in Character.CharacterList)
1047  {
1048  if (target.CurrentHull != hull || !target.Enabled) { continue; }
1050  {
1051  if (!target.IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
1052  {
1053  var orderPrefab = OrderPrefab.Prefabs["reportintruders"];
1054  newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
1055  targetHull = hull;
1056  if (target.IsEscorted)
1057  {
1058  if (!Character.IsPrisoner && target.IsPrisoner)
1059  {
1060  LocalizedString msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", ("[roomname]", targetHull.DisplayName, FormatCapitals.No));
1061  Character.Speak(msg.Value, ChatMessageType.Order);
1062  speak = false;
1063  }
1064  else if (!IsMentallyUnstable && target.AIController.IsMentallyUnstable)
1065  {
1066  LocalizedString msg = TextManager.GetWithVariables("orderdialog.mentalcase", ("[roomname]", targetHull.DisplayName, FormatCapitals.No));
1067  Character.Speak(msg.Value, ChatMessageType.Order);
1068  speak = false;
1069  }
1070  }
1071  }
1072  }
1073  }
1075  {
1076  if (AddTargets<AIObjectiveExtinguishFires, Hull>(Character, hull) && newOrder == null)
1077  {
1078  var orderPrefab = OrderPrefab.Prefabs["reportfire"];
1079  newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
1080  targetHull = hull;
1081  }
1082  }
1083  if (IsBallastFloraNoticeable(Character, hull) && newOrder == null)
1084  {
1085  var orderPrefab = OrderPrefab.Prefabs["reportballastflora"];
1086  newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
1087  targetHull = hull;
1088  }
1089  if (!isFighting)
1090  {
1091  foreach (var gap in hull.ConnectedGaps)
1092  {
1094  {
1095  if (AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap) && newOrder == null && !gap.IsRoomToRoom)
1096  {
1097  var orderPrefab = OrderPrefab.Prefabs["reportbreach"];
1098  newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
1099  targetHull = hull;
1100  }
1101  }
1102  }
1103  if (!isFleeing)
1104  {
1105  foreach (Character target in Character.CharacterList)
1106  {
1107  if (target.CurrentHull != hull) { continue; }
1108  if (AIObjectiveRescueAll.IsValidTarget(target, Character, out _))
1109  {
1110  if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
1111  {
1112  var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"];
1113  newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
1114  targetHull = hull;
1115  }
1116  }
1117  }
1118  foreach (Item item in Item.RepairableItems)
1119  {
1120  if (item.CurrentHull != hull) { continue; }
1122  {
1123  if (!item.Repairables.Any(r => r.IsBelowRepairIconThreshold)) { continue; }
1124  if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
1125  {
1126  var orderPrefab = OrderPrefab.Prefabs["reportbrokendevices"];
1127  newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character);
1128  targetHull = hull;
1129  }
1130  }
1131  }
1132  }
1133  }
1134  }
1135  }
1136  if (newOrder != null && speak)
1137  {
1138  if (Character.TeamID == CharacterTeamType.FriendlyNPC)
1139  {
1140  Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
1141  identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
1142  minDurationBetweenSimilar: 60.0f);
1143  }
1145  {
1146  Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
1147 #if SERVER
1149  .WithManualPriority(CharacterInfo.HighestManualOrderPriority)
1150  .WithTargetEntity(targetHull)
1151  .WithOrderGiver(Character), "", null, Character));
1152 #endif
1153  }
1154  }
1155  }
1156 
1157  public static bool IsBallastFloraNoticeable(Character character, Hull hull)
1158  {
1159  foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList)
1160  {
1161  if (ballastFlora.Parent?.Submarine != character.Submarine) { continue; }
1162  if (!ballastFlora.HasBrokenThrough) { continue; }
1163  // Don't react to the first two branches, because they are usually in the very edges of the room.
1164  if (ballastFlora.Branches.Count(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull) > 2)
1165  {
1166  return true;
1167  }
1168  }
1169  return false;
1170  }
1171 
1172  public static void ReportProblem(Character reporter, Order order, Hull targetHull = null)
1173  {
1174  if (reporter == null || order == null) { return; }
1175  var visibleHulls = targetHull is null ? new List<Hull>(reporter.GetVisibleHulls()) : new List<Hull> { targetHull };
1176  foreach (var hull in visibleHulls)
1177  {
1178  PropagateHullSafety(reporter, hull);
1179  RefreshTargets(reporter, order, hull);
1180  }
1181  }
1182 
1183  private void SpeakAboutIssues()
1184  {
1185  if (!Character.IsOnPlayerTeam) { return; }
1186  if (Character.SpeechImpediment >= 100) { return; }
1187  float minDelay = 0.5f, maxDelay = 2f;
1188  if (Character.Oxygen < CharacterHealth.InsufficientOxygenThreshold)
1189  {
1190  string msgId = "DialogLowOxygen";
1191  Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1192  }
1193  if (Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !Character.IsMedic)
1194  {
1195  string msgId = "DialogBleeding";
1196  Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1197  }
1198  if ((Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0) && !Character.IsProtectedFromPressure)
1199  {
1200  if (Character.PressureProtection > 0)
1201  {
1202  string msgId = "DialogInsufficientPressureProtection";
1203  Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1204  }
1205  else if (Character.CurrentHull?.DisplayName != null)
1206  {
1207  string msgId = "DialogPressure";
1208  Character.Speak(TextManager.GetWithVariable(msgId, "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1209  }
1210  }
1211  }
1212 
1213  public override void OnHealed(Character healer, float healAmount)
1214  {
1215  if (healer == null || healAmount <= 0.0f) { return; }
1216  if (previousHealAmounts.ContainsKey(healer))
1217  {
1218  previousHealAmounts[healer] += healAmount;
1219  }
1220  else
1221  {
1222  previousHealAmounts.Add(healer, healAmount);
1223  }
1224  }
1225 
1226  public override void OnAttacked(Character attacker, AttackResult attackResult)
1227  {
1228  // The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs
1229  if (wasConscious && (Character.IsIncapacitated || Character.Stun > 0.0f))
1230  {
1231  RespondToAttack(attacker, attackResult);
1232  wasConscious = false;
1233  return;
1234  }
1235  if (Character.IsDead) { return; }
1236  if (attacker == null || Character.IsPlayer)
1237  {
1238  // The player characters need to "respond" to the attack always, because the update loop doesn't run for them.
1239  // Otherwise other NPCs totally ignore when player characters are attacked.
1240  RespondToAttack(attacker, attackResult);
1241  return;
1242  }
1243  if (previousAttackResults.ContainsKey(attacker))
1244  {
1245  if (attackResult.Afflictions != null)
1246  {
1247  foreach (Affliction newAffliction in attackResult.Afflictions)
1248  {
1249  var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source);
1250  if (matchingAffliction == null)
1251  {
1252  previousAttackResults[attacker].Afflictions.Add(newAffliction);
1253  }
1254  else
1255  {
1256  matchingAffliction.Strength += newAffliction.Strength;
1257  }
1258  }
1259  }
1260  previousAttackResults[attacker] = new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb);
1261  }
1262  else
1263  {
1264  previousAttackResults.Add(attacker, attackResult);
1265  }
1266  }
1267 
1268  private void RespondToAttack(Character attacker, AttackResult attackResult)
1269  {
1270  float healAmount = 0.0f;
1271  if (attacker != null)
1272  {
1273  previousHealAmounts.TryGetValue(attacker, out healAmount);
1274  }
1275  // excluding poisons etc
1276  float realDamage = attackResult.Damage - healAmount;
1277  // including poisons etc
1278  float totalDamage = realDamage;
1279  if (attackResult.Afflictions != null)
1280  {
1281  foreach (Affliction affliction in attackResult.Afflictions)
1282  {
1283  totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
1284  }
1285  }
1286  if (totalDamage <= 0.01f) { return; }
1287  if (Character.IsBot)
1288  {
1289  if (!freezeAI && !Character.IsDead && Character.IsIncapacitated)
1290  {
1291  // Removes the combat objective and resets all objectives.
1292  objectiveManager.CreateAutonomousObjectives();
1293  objectiveManager.SortObjectives();
1294  freezeAI = true;
1295  }
1296  }
1297  if (attacker == null || attacker.IsUnconscious || attacker.Removed)
1298  {
1299  // Don't react to the damage if there's no attacker.
1300  // We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying.
1301  // But fires and enemies should already be handled by the FindSafetyObjective.
1302  return;
1303  // Ignore damage from falling etc that we shouldn't react to.
1304  //if (Character.LastDamageSource == null) { return; }
1305  //AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
1306  }
1307 
1308  bool sameTeam =
1309  attacker.TeamID == Character.TeamID ||
1310  // consider escorted characters to be in the same team (otherwise accidental damage or side-effects from healing trigger them too easily)
1311  (attacker.TeamID == CharacterTeamType.Team1 && Character.IsEscorted);
1312 
1313  if (realDamage <= 0 && (attacker.IsBot || sameTeam))
1314  {
1315  // Don't react to damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player
1316  return;
1317  }
1318  if (attacker.Submarine == null && Character.Submarine != null)
1319  {
1320  // Don't react to attackers that are outside of the sub (e.g. AoE attacks)
1321  return;
1322  }
1323  bool isAttackerInfected = false;
1324  bool isAttackerFightingEnemy = false;
1325  float minorDamageThreshold = 5;
1326  float majorDamageThreshold = 20;
1327  if (sameTeam && !attacker.IsInstigator)
1328  {
1329  minorDamageThreshold = 10;
1330  majorDamageThreshold = 40;
1331  }
1332  bool eitherIsMentallyUnstable = IsMentallyUnstable || attacker.AIController is { IsMentallyUnstable: true };
1333  if (IsFriendly(attacker))
1334  {
1335  if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
1336  {
1337  // Don't attack characters that damage you while doing cpr, because let's assume that they are helping you.
1338  // Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
1339  return;
1340  }
1341  float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker);
1342  bool isAccidental = attacker.IsBot && !eitherIsMentallyUnstable && attacker.CombatAction == null;
1343  if (isAccidental)
1344  {
1345  if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold))
1346  {
1347  AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
1348  }
1349  }
1350  else
1351  {
1352  isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectedType) > 0;
1353  // Inform other NPCs
1354  if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
1355  {
1356  if (GameMain.IsMultiplayer || !attacker.IsPlayer || Character.TeamID != attacker.TeamID)
1357  {
1358  InformOtherNPCs(cumulativeDamage);
1359  }
1360  }
1361  if (Character.IsBot)
1362  {
1363  var combatMode = DetermineCombatMode(Character, cumulativeDamage);
1364  if (attacker.IsPlayer && !Character.IsInstigator && !ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>())
1365  {
1366  switch (combatMode)
1367  {
1368  case AIObjectiveCombat.CombatMode.Defensive:
1369  case AIObjectiveCombat.CombatMode.Retreat:
1370  if (Character.IsSecurity)
1371  {
1372  Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse").Value, null, 0.5f, "attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1373  }
1374  else
1375  {
1376  Character.Speak(TextManager.Get("DialogAttackedByFriendly").Value, null, 0.5f, "attackedbyfriendly".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1377  }
1378  break;
1379  case AIObjectiveCombat.CombatMode.Offensive:
1380  case AIObjectiveCombat.CombatMode.Arrest:
1381  Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest").Value, null, 0.5f, "attackedbyfriendlysecurityarrest".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1382  break;
1383  case AIObjectiveCombat.CombatMode.None:
1384  if (Character.IsSecurity && realDamage > 1)
1385  {
1386  Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse").Value, null, 0.5f, "attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1387  }
1388  break;
1389  }
1390  }
1391  // If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay.
1392  AddCombatObjective(combatMode, attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
1393  }
1394  if (!isAttackerFightingEnemy)
1395  {
1396  (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
1397  }
1398  }
1399  }
1400  else
1401  {
1402  if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
1403  {
1404  // Non-friendly
1405  InformOtherNPCs();
1406  }
1407  if (Character.IsBot)
1408  {
1409  AddCombatObjective(DetermineCombatMode(Character), attacker);
1410  }
1411  }
1412 
1413  void InformOtherNPCs(float cumulativeDamage = 0)
1414  {
1415  foreach (Character otherCharacter in Character.CharacterList)
1416  {
1417  if (otherCharacter == Character || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; }
1418  if (otherCharacter.Submarine != Character.Submarine) { continue; }
1419  if (otherCharacter.Submarine != attacker.Submarine) { continue; }
1420  if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; }
1421  if (otherCharacter.IsPlayer) { continue; }
1422  if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; }
1423  if (!otherHumanAI.IsFriendly(Character)) { continue; }
1424  if (attacker.AIController is EnemyAIController enemyAI && otherHumanAI.IsFriendly(attacker))
1425  {
1426  // Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
1427  continue;
1428  }
1429  bool isWitnessing =
1430  otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) ||
1431  otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
1432  otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
1433  if (!isWitnessing)
1434  {
1435  //if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
1436  //don't react to the attack
1437  if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID || !CheckReportRange(Character, otherCharacter, ReportRange))
1438  {
1439  continue;
1440  }
1441  }
1442  var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing);
1443  float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
1444  otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
1445  }
1446  }
1447 
1448  AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false)
1449  {
1450  if (c.AIController is not HumanAIController humanAI) { return AIObjectiveCombat.CombatMode.None; }
1451  if (!IsFriendly(attacker))
1452  {
1453  if (c.Submarine == null)
1454  {
1455  // Outside
1456  return attacker.Submarine == null ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
1457  }
1458  if (!c.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
1459  {
1460  // Attacked from an unconnected submarine (pirate/pvp)
1461  return
1462  humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() is Controller ?
1463  AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
1464  }
1465  if (c.IsKiller)
1466  {
1467  return AIObjectiveCombat.CombatMode.Offensive;
1468  }
1469  return humanAI.ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
1470  }
1471  else
1472  {
1473  if (isAttackerInfected)
1474  {
1475  cumulativeDamage = 100;
1476  }
1477  if (attacker.IsPlayer && c.TeamID == attacker.TeamID)
1478  {
1479  if (GameMain.IsSingleplayer || c.TeamID != attacker.TeamID)
1480  {
1481  // Bots in the player team never act aggressively in single player when attacked by the player
1482  // In multiplayer, they react only to players attacking them or other crew members
1483  return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
1484  }
1485  }
1486  if (c.Submarine == null || !c.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
1487  {
1488  // Outside or attacked from an unconnected submarine -> don't react.
1489  return AIObjectiveCombat.CombatMode.None;
1490  }
1491  // If there are any enemies around, just ignore the friendly fire
1492  if (Character.CharacterList.Any(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
1493  {
1494  isAttackerFightingEnemy = true;
1495  return AIObjectiveCombat.CombatMode.None;
1496  }
1497  if (c.IsKiller)
1498  {
1499  return AIObjectiveCombat.CombatMode.Offensive;
1500  }
1501  if (isWitnessing && c.CombatAction != null && !c.IsSecurity)
1502  {
1503  return c.CombatAction.WitnessReaction;
1504  }
1505  if (!attacker.IsInstigator && c.IsOnFriendlyTeam(attacker) && FindInstigator() is Character instigator)
1506  {
1507  // The guards don't react to player's aggressions when there's an instigator around
1508  isAttackerFightingEnemy = true;
1509  return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : instigator.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
1510  }
1511  if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !eitherIsMentallyUnstable)
1512  {
1513  if (c.IsSecurity)
1514  {
1515  return attacker.CombatAction?.GuardReaction ?? AIObjectiveCombat.CombatMode.Offensive;
1516  }
1517  else
1518  {
1519  return attacker.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
1520  }
1521  }
1522  else
1523  {
1524  if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>() is AIObjectiveCombat currentCombatObjective && currentCombatObjective.Enemy == attacker)
1525  {
1526  // Already targeting the attacker -> treat as a more serious threat.
1527  cumulativeDamage *= 2;
1528  currentCombatObjective.AllowHoldFire = false;
1529  c.IsCriminal = true;
1530  }
1531  if (c.IsCriminal)
1532  {
1533  // Always react if the attacker has been misbehaving earlier.
1534  cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold);
1535  }
1536  if (cumulativeDamage > majorDamageThreshold)
1537  {
1538  c.IsCriminal = true;
1539  if (c.IsSecurity)
1540  {
1541  return AIObjectiveCombat.CombatMode.Offensive;
1542  }
1543  else
1544  {
1545  return c == Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
1546  }
1547  }
1548  else if (cumulativeDamage > minorDamageThreshold)
1549  {
1550  return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
1551  }
1552  else
1553  {
1554  return AIObjectiveCombat.CombatMode.None;
1555  }
1556  }
1557 
1558  Character FindInstigator()
1559  {
1560  if (Character.IsInstigator)
1561  {
1562  return Character;
1563  }
1564  if (attacker.IsInstigator)
1565  {
1566  return attacker;
1567  }
1568  if (c.IsInstigator)
1569  {
1570  return c;
1571  }
1572  if (c.AIController is HumanAIController humanAi)
1573  {
1574  return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull));
1575  }
1576  return null;
1577  }
1578  }
1579  }
1580  }
1581 
1582  public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false, bool speakWarnings = false)
1583  {
1584  if (mode == AIObjectiveCombat.CombatMode.None) { return; }
1586  if (!Character.IsBot) { return; }
1587  if (ObjectiveManager.Objectives.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective)
1588  {
1589  // Don't replace offensive mode with something else
1590  if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; }
1591  if (combatObjective.Mode != mode || combatObjective.Enemy != target || (combatObjective.Enemy == null && target == null))
1592  {
1593  // Replace the old objective with the new.
1594  ObjectiveManager.Objectives.Remove(combatObjective);
1595  ObjectiveManager.AddObjective(CreateCombatObjective());
1596  }
1597  }
1598  else
1599  {
1600  if (delay > 0)
1601  {
1602  ObjectiveManager.AddObjective(CreateCombatObjective(), delay);
1603  }
1604  else
1605  {
1606  ObjectiveManager.AddObjective(CreateCombatObjective());
1607  }
1608  }
1609 
1610  AIObjectiveCombat CreateCombatObjective()
1611  {
1612  var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager)
1613  {
1614  HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
1615  AbortCondition = abortCondition,
1616  AllowHoldFire = allowHoldFire,
1617  SpeakWarnings = speakWarnings
1618  };
1619  if (onAbort != null)
1620  {
1621  objective.Abandoned += onAbort;
1622  }
1623  if (onCompleted != null)
1624  {
1625  objective.Completed += onCompleted;
1626  }
1627  return objective;
1628  }
1629  }
1630 
1631  public void SetOrder(Order order, bool speak = true)
1632  {
1633  objectiveManager.SetOrder(order, speak);
1634 #if CLIENT
1635  HintManager.OnSetOrder(Character, order);
1636 #endif
1637  }
1638 
1640  {
1641  var objective = ObjectiveManager.CreateObjective(order);
1642  ObjectiveManager.SetForcedOrder(objective);
1643  return objective;
1644  }
1645 
1646  public void ClearForcedOrder()
1647  {
1648  ObjectiveManager.ClearForcedOrder();
1649  }
1650 
1651  public override void SelectTarget(AITarget target)
1652  {
1653  SelectedAiTarget = target;
1654  }
1655 
1656  public override void Reset()
1657  {
1658  base.Reset();
1659  objectiveManager.SortObjectives();
1660  SortTimer = sortObjectiveInterval;
1661  float waitDuration = characterWaitOnSwitch;
1662  if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
1663  {
1664  waitDuration *= 2;
1665  }
1666  ObjectiveManager.WaitTimer = waitDuration;
1667  }
1668 
1669  public override bool Escape(float deltaTime) => UpdateEscape(deltaTime, canAttackDoors: false);
1670 
1671  private void CheckCrouching(float deltaTime)
1672  {
1673  crouchRaycastTimer -= deltaTime;
1674  if (crouchRaycastTimer > 0.0f) { return; }
1675 
1676  crouchRaycastTimer = crouchRaycastInterval;
1677 
1678  //start the raycast in front of the character in the direction it's heading to
1679  Vector2 startPos = Character.SimPosition;
1680  startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f);
1681 
1682  //do a raycast upwards to find any walls
1683  if (!Character.AnimController.TryGetCollider(0, out PhysicsBody mainCollider))
1684  {
1685  mainCollider = Character.AnimController.Collider;
1686  }
1687  float margin = 0.1f;
1688  if (shouldCrouch)
1689  {
1690  margin *= 2;
1691  }
1692  float minCeilingDist = mainCollider.Height / 2 + mainCollider.Radius + margin;
1693 
1694  shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null;
1695  }
1696 
1698  {
1699  if (Character == null || Character.Removed || Character.IsIncapacitated) { return false; }
1700 
1701  switch (ObjectiveManager.CurrentObjective)
1702  {
1703  case AIObjectiveCombat _:
1704  case AIObjectiveFindSafety _:
1707  case AIObjectiveFixLeaks _:
1708  return false;
1709  }
1710  return true;
1711  }
1712 
1713  public bool NeedsDivingGear(Hull hull, out bool needsSuit)
1714  {
1715  needsSuit = false;
1717  if (hull == null ||
1718  hull.WaterPercentage > 90 ||
1719  hull.LethalPressure > 0 ||
1720  hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
1721  {
1722  needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
1723  return needsAir || needsSuit;
1724  }
1725  if (hull.WaterPercentage > 60 || (hull.IsWetRoom && hull.WaterPercentage > 10) || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
1726  {
1727  return needsAir;
1728  }
1729  return false;
1730  }
1731 
1732  public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) =>
1733  HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
1734 
1738  public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true, bool requireSuitablePressureProtection = true)
1739  => HasItem(character, Tags.HeavyDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true,
1740  predicate: (Item item) =>
1741  character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes) &&
1742  (!requireSuitablePressureProtection || AIObjectiveFindDivingGear.IsSuitablePressureProtection(item, Tags.HeavyDivingGear, character)));
1743 
1747  public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
1748  => HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true);
1749 
1750  private static List<Item> matchingItems = new List<Item>();
1751 
1756  public 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)
1757  {
1758  matchingItems.Clear();
1759  items = matchingItems;
1760  if (character?.Inventory == null) { return false; }
1761  matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
1762  i.ConditionPercentage >= conditionPercentage &&
1763  (!requireEquipped || character.HasEquippedItem(i)) &&
1764  (predicate == null || predicate(i)), recursive, matchingItems);
1765  items = matchingItems;
1766  foreach (var item in matchingItems)
1767  {
1768  if (item == null) { continue; }
1769 
1770  if (containedTag.IsEmpty || item.OwnInventory == null)
1771  {
1772  //no contained items required, this item's ok
1773  return true;
1774  }
1775  var suitableSlot = item.GetComponent<ItemContainer>().FindSuitableSubContainerIndex(containedTag);
1776  if (suitableSlot == null)
1777  {
1778  //no restrictions on the suitable slot
1779  return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage);
1780  }
1781  else
1782  {
1783  return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage && it.ParentInventory.IsInSlot(it, suitableSlot.Value));
1784  }
1785  }
1786  return false;
1787  }
1788 
1789  public static void StructureDamaged(Structure structure, float damageAmount, Character character)
1790  {
1791  const float MaxDamagePerSecond = 5.0f;
1792  const float MaxDamagePerFrame = MaxDamagePerSecond * (float)Timing.Step;
1793 
1794  const float WarningThreshold = 5.0f;
1795  const float ArrestThreshold = 20.0f;
1796  const float KillThreshold = 50.0f;
1797 
1798  if (character == null || damageAmount <= 0.0f) { return; }
1799  if (structure?.Submarine == null || !structure.Submarine.Info.IsOutpost || character.TeamID == structure.Submarine.TeamID) { return; }
1800  //structure not indestructible = something that's "meant" to be destroyed, like an ice wall in mines
1801  if (!structure.Prefab.IndestructibleInOutposts) { return; }
1802 
1803  bool someoneSpoke = false;
1804  float maxAccumulatedDamage = 0.0f;
1805  foreach (Character otherCharacter in Character.CharacterList)
1806  {
1807  if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
1808  otherCharacter.Info?.Job == null ||
1809  otherCharacter.AIController is not HumanAIController otherHumanAI ||
1810  Vector2.DistanceSquared(otherCharacter.WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f)
1811  {
1812  continue;
1813  }
1814  if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
1815 
1816  otherHumanAI.structureDamageAccumulator.TryAdd(character, 0.0f);
1817  float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
1818  otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame);
1819  float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
1820  maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage);
1821 
1822  if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer)
1823  {
1824  var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage;
1826  }
1827 
1828  if (!character.IsCriminal)
1829  {
1830  if (accumulatedDamage <= WarningThreshold) { return; }
1831 
1832  if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
1833  !someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
1834  {
1835  //if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
1836  if (accumulatedDamage < ArrestThreshold)
1837  {
1838  if (otherHumanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective)
1839  {
1840  idleObjective.FaceTargetAndWait(character, 5.0f);
1841  }
1842  }
1843  otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f);
1844  someoneSpoke = true;
1845  }
1846  }
1847 
1848  // React if we are security
1849  if (character.IsCriminal ||
1850  (accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
1851  (accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold))
1852  {
1853  var combatMode = accumulatedDamage > KillThreshold ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
1854  if (combatMode == AIObjectiveCombat.CombatMode.Offensive)
1855  {
1856  character.IsCriminal = true;
1857  }
1858  if (!TriggerSecurity(otherHumanAI, combatMode))
1859  {
1860  // Else call the others
1861  foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
1862  {
1863  if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
1864  {
1865  // Only alert one guard at a time
1866  return;
1867  }
1868  }
1869  }
1870  }
1871  }
1872 
1873  bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode)
1874  {
1875  if (humanAI == null) { return false; }
1876  if (!humanAI.Character.IsSecurity) { return false; }
1877  if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
1878  humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(),
1879  onCompleted: () =>
1880  {
1881  //if the target is arrested successfully, reset the damage accumulator
1882  foreach (Character anyCharacter in Character.CharacterList)
1883  {
1884  if (anyCharacter.AIController is HumanAIController anyAI)
1885  {
1886  anyAI.structureDamageAccumulator?.Remove(character);
1887  }
1888  }
1889  });
1890  return true;
1891  }
1892  }
1893 
1894  public static void ItemTaken(Item item, Character thief)
1895  {
1896  if (item == null || thief == null || item.GetComponent<LevelResource>() != null) { return; }
1897  bool someoneSpoke = false;
1898  if (item.Illegitimate && item.GetRootInventoryOwner() is Character itemOwner && itemOwner != thief && itemOwner.TeamID == thief.TeamID)
1899  {
1900  // The player attempts to use a bot as a mule or get them arrested -> just arrest the player instead.
1901  thief.IsCriminal = true;
1902  }
1903  bool foundIllegitimateItems = item.Illegitimate || item.OwnInventory?.FindItem(it => it.Illegitimate, recursive: true) != null;
1904  if (foundIllegitimateItems && thief.TeamID != CharacterTeamType.FriendlyNPC)
1905  {
1906  foreach (Character otherCharacter in Character.CharacterList)
1907  {
1908  if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f ||
1909  otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI || otherCharacter.IsEscorted ||
1910  Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f)
1911  {
1912  continue;
1913  }
1914  //if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
1915  if (!otherCharacter.CanSeeTarget(thief, seeThroughWindows: true)) { continue; }
1916  // Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding
1917  // -> allow them to use the emergency items
1918  if (thief.Submarine != null)
1919  {
1920  var connectedHulls = thief.Submarine.GetHulls(alsoFromConnectedSubs: true);
1921  if (item.HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) { continue; }
1922  if (item.HasTag(Tags.DivingGear) && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; }
1923  }
1924  if (item.HasTag(Tags.Handcuffs) && thief.HasEquippedItem(item))
1925  {
1926  // Handcuffed -> don't react.
1927  continue;
1928  }
1929  if (!item.StolenDuringRound)
1930  {
1931  item.StolenDuringRound = true;
1932  ApplyStealingReputationLoss(item);
1933 #if CLIENT
1934  HintManager.OnStoleItem(thief, item);
1935 #endif
1936  }
1937  if (!someoneSpoke)
1938  {
1939  otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
1940  someoneSpoke = true;
1941  }
1942  // React if we are security
1943  if (!TriggerSecurity(otherHumanAI))
1944  {
1945  // Else call the others
1946  foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition)))
1947  {
1948  if (TriggerSecurity(security.AIController as HumanAIController))
1949  {
1950  // Only alert one guard at a time
1951  break;
1952  }
1953  }
1954  }
1955  }
1956  }
1957  else if (item.OwnInventory?.FindItem(it => it.Illegitimate, true) is { } foundItem)
1958  {
1959  ItemTaken(foundItem, thief);
1960  }
1961 
1962  bool TriggerSecurity(HumanAIController humanAI)
1963  {
1964  if (humanAI == null) { return false; }
1965  if (!humanAI.Character.IsSecurity) { return false; }
1966  if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
1967  if (humanAI.ObjectiveManager.GetObjective<AIObjectiveFindThieves>() is { } findThieves)
1968  {
1969  findThieves.InspectEveryone();
1970  }
1971  bool isCriminal = thief.IsCriminal;
1972  humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
1973  abortCondition: obj => !isCriminal && thief.Inventory.FindItem(it => it.Illegitimate, recursive: true) == null,
1974  onAbort: () =>
1975  {
1976  if (!item.Removed && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
1977  {
1978  humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false)
1979  {
1980  BasePriority = 10
1981  });
1982  }
1983  },
1984  allowHoldFire: !isCriminal,
1985  speakWarnings: !isCriminal);
1986  return true;
1987  }
1988  }
1989 
1990  public static void ApplyStealingReputationLoss(Item item)
1991  {
1992  if (Level.Loaded?.Type == LevelData.LevelType.Outpost &&
1994  {
1995  var reputationLoss = MathHelper.Clamp(
1999  }
2000  }
2001 
2002  // 0.225 - 0.375
2003  private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
2004 
2010  public static void PropagateHullSafety(Character character, Hull hull)
2011  {
2012  DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
2013  }
2014 
2015  public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull);
2016 
2017  private void RefreshHullSafety(Hull hull)
2018  {
2019  var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : VisibleHulls;
2020  float hullSafety = GetHullSafety(hull, Character, visibleHulls);
2021  if (hullSafety > HULL_SAFETY_THRESHOLD)
2022  {
2023  UnsafeHulls.Remove(hull);
2024  }
2025  else
2026  {
2027  UnsafeHulls.Add(hull);
2028  }
2029  }
2030 
2031  public static void RefreshTargets(Character character, Order order, Hull hull)
2032  {
2033  switch (order.Identifier.Value.ToLowerInvariant())
2034  {
2035  case "reportfire":
2036  AddTargets<AIObjectiveExtinguishFires, Hull>(character, hull);
2037  break;
2038  case "reportbreach":
2039  foreach (var gap in hull.ConnectedGaps)
2040  {
2041  if (AIObjectiveFixLeaks.IsValidTarget(gap, character))
2042  {
2043  AddTargets<AIObjectiveFixLeaks, Gap>(character, gap);
2044  }
2045  }
2046  break;
2047  case "reportbrokendevices":
2048  foreach (var item in Item.RepairableItems)
2049  {
2050  if (item.CurrentHull != hull) { continue; }
2051  if (AIObjectiveRepairItems.IsValidTarget(item, character))
2052  {
2053  if (item.Repairables.All(r => r.IsBelowRepairThreshold)) { continue; }
2054  AddTargets<AIObjectiveRepairItems, Item>(character, item);
2055  }
2056  }
2057  break;
2058  case "reportintruders":
2059  foreach (var enemy in Character.CharacterList)
2060  {
2061  if (enemy.CurrentHull != hull) { continue; }
2062  if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, false))
2063  {
2064  AddTargets<AIObjectiveFightIntruders, Character>(character, enemy);
2065  }
2066  }
2067  break;
2068  case "requestfirstaid":
2069  foreach (var c in Character.CharacterList)
2070  {
2071  if (c.CurrentHull != hull) { continue; }
2072  if (AIObjectiveRescueAll.IsValidTarget(c, character, out _))
2073  {
2074  AddTargets<AIObjectiveRescueAll, Character>(character, c);
2075  }
2076  }
2077  break;
2078  default:
2079 #if DEBUG
2080  DebugConsole.ThrowError(order.Identifier + " not implemented!");
2081 #endif
2082  break;
2083  }
2084  }
2085 
2086  private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
2087  {
2088  bool targetAdded = false;
2089  DoForEachBot(caller, humanAI =>
2090  {
2091  if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
2092  var objective = humanAI.ObjectiveManager.GetObjective<T1>();
2093  if (objective != null)
2094  {
2095  if (!targetAdded && objective.AddTarget(target))
2096  {
2097  targetAdded = true;
2098  }
2099  }
2100  }, range: (caller.AIController as HumanAIController)?.ReportRange ?? float.PositiveInfinity);
2101  return targetAdded;
2102  }
2103 
2104  public static void RemoveTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
2105  {
2106  DoForEachBot(caller, humanAI =>
2107  humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
2108  }
2109 
2110  private void StoreHullSafety(Hull hull, HullSafety safety)
2111  {
2112  if (knownHulls.ContainsKey(hull))
2113  {
2114  // Update existing. Shouldn't currently happen, but things might change.
2115  knownHulls[hull] = safety;
2116  }
2117  else
2118  {
2119  // Add new
2120  knownHulls.Add(hull, safety);
2121  }
2122  }
2123 
2124  private float CalculateHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
2125  {
2126  bool isCurrentHull = character == Character && character.CurrentHull == hull;
2127  if (hull == null)
2128  {
2129  float hullSafety = character.IsProtectedFromPressure ? 0 : 100;
2130  if (isCurrentHull)
2131  {
2132  CurrentHullSafety = hullSafety;
2133  }
2134  return hullSafety;
2135  }
2136  if (isCurrentHull && visibleHulls == null)
2137  {
2138  // Use the cached visible hulls
2139  visibleHulls = VisibleHulls;
2140  }
2141  bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
2142  bool ignoreOxygen = HasDivingGear(character);
2143  bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
2144  float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
2145  if (isCurrentHull)
2146  {
2147  CurrentHullSafety = safety;
2148  }
2149  return safety;
2150  }
2151 
2152  private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
2153  {
2154  bool isProtectedFromPressure = character.IsProtectedFromPressure;
2155  if (hull == null) { return isProtectedFromPressure ? 100 : 0; }
2156  if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; }
2157  // Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
2158  // With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
2159  float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
2160  float waterFactor = 1;
2161  if (!ignoreWater)
2162  {
2163  if (visibleHulls != null)
2164  {
2165  // Take the visible hulls into account too, because otherwise multi-hull rooms on several floors (with platforms) will yield unexpected results.
2166  float relativeWaterVolume = visibleHulls.Sum(s => s.WaterVolume) / visibleHulls.Sum(s => s.Volume);
2167  waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
2168  }
2169  else
2170  {
2171  float relativeWaterVolume = hull.WaterVolume / hull.Volume;
2172  waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
2173  }
2174  }
2175  if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
2176  {
2177  oxygenFactor = 1;
2178  }
2179  if (isProtectedFromPressure)
2180  {
2181  waterFactor = 1;
2182  }
2183  float fireFactor = 1;
2184  if (!ignoreFire)
2185  {
2186  static float CalculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
2187  // Even the smallest fire reduces the safety by 50%
2188  float fire = visibleHulls?.Sum(CalculateFire) ?? CalculateFire(hull);
2189  fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
2190  }
2191  float enemyFactor = 1;
2192  if (!ignoreEnemies)
2193  {
2194  int enemyCount = 0;
2195  foreach (Character c in Character.CharacterList)
2196  {
2197  if (visibleHulls == null)
2198  {
2199  if (c.CurrentHull != hull) { continue; }
2200  }
2201  else
2202  {
2203  if (!visibleHulls.Contains(c.CurrentHull)) { continue; }
2204  }
2205  if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed)
2206  {
2207  enemyCount++;
2208  }
2209  }
2210  // The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages)
2211  enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
2212  }
2213  float dangerousItemsFactor = 1f;
2214  foreach (Item item in Item.DangerousItems)
2215  {
2216  if (item.CurrentHull == hull)
2217  {
2218  dangerousItemsFactor = 0;
2219  break;
2220  }
2221  }
2222  float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor;
2223  return MathHelper.Clamp(safety * 100, 0, 100);
2224  }
2225 
2226  public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
2227  {
2228  if (hull == null)
2229  {
2230  return CalculateHullSafety(hull, character, visibleHulls);
2231  }
2232  if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
2233  {
2234  hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls));
2235  StoreHullSafety(hull, hullSafety);
2236  }
2237  else if (hullSafety.IsStale)
2238  {
2239  hullSafety.Reset(CalculateHullSafety(hull, character, visibleHulls));
2240  }
2241  return hullSafety.safety;
2242  }
2243 
2244  public static float GetHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
2245  {
2246  if (hull == null)
2247  {
2248  return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
2249  }
2250  HullSafety hullSafety;
2251  if (character.AIController is HumanAIController controller)
2252  {
2253  if (!controller.knownHulls.TryGetValue(hull, out hullSafety))
2254  {
2255  hullSafety = new HullSafety(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies));
2256  controller.StoreHullSafety(hull, hullSafety);
2257  }
2258  else if (hullSafety.IsStale)
2259  {
2260  hullSafety.Reset(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies));
2261  }
2262  }
2263  else
2264  {
2265 #if DEBUG
2266  DebugConsole.ThrowError("Cannot store the hull safety, because was unable to cast the AIController as HumanAIController. This should never happen!");
2267 #endif
2268  return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
2269  }
2270  return hullSafety.safety;
2271  }
2272 
2273  public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
2274  {
2275  if (other.IsHusk)
2276  {
2277  // Disguised as husk
2278  return me.IsDisguisedAsHusk;
2279  }
2280  else
2281  {
2282  if (other.IsPrisoner && me.IsPrisoner)
2283  {
2284  // Both prisoners
2285  return true;
2286  }
2287  if (other.IsHostileEscortee && me.IsHostileEscortee)
2288  {
2289  // Both hostile escortees
2290  return true;
2291  }
2292  }
2293  bool sameTeam = me.TeamID == other.TeamID;
2294  bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
2295  if (!teamGood)
2296  {
2297  return false;
2298  }
2299  if (other.IsPet)
2300  {
2301  // Hostile NPCs are hostile to all pets, unless they are in the same team.
2302  return sameTeam || me.TeamID != CharacterTeamType.None;
2303  }
2304  else
2305  {
2306  if (!me.IsSameSpeciesOrGroup(other)) { return false; }
2307  }
2309  {
2310  if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) ||
2311  (me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
2312  {
2313  Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
2314 
2315  //NPCs that allow some campaign interaction are not turned hostile by low reputation
2316  if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; }
2317 
2318  if (npc.AIController is HumanAIController npcAI)
2319  {
2320  return !npcAI.IsInHostileFaction();
2321  }
2322  }
2323  }
2324  return true;
2325  }
2326 
2327  public bool IsInHostileFaction()
2328  {
2329  if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; }
2330  if (Character.IsEscorted) { return false; }
2331 
2332  Identifier npcFaction = Character.Faction;
2333  Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
2334 
2335  if (npcFaction.IsEmpty)
2336  {
2337  //if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost
2338  npcFaction = currentLocationFaction;
2339  }
2340  if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
2341  {
2342  if (campaign.CurrentLocation is { IsFactionHostile: true })
2343  {
2344  return true;
2345  }
2346  }
2347  return false;
2348  }
2349 
2350  public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious;
2351 
2352  public static bool IsTrueForAllBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate)
2353  {
2354  if (character == null) { return false; }
2355  foreach (var c in Character.CharacterList)
2356  {
2357  if (!IsBotInTheCrew(character, c)) { continue; }
2358  if (!predicate(c.AIController as HumanAIController))
2359  {
2360  return false;
2361  }
2362  }
2363  return true;
2364  }
2365 
2366  public static bool IsTrueForAnyBotInTheCrew(Character character, Func<HumanAIController, bool> predicate)
2367  {
2368  if (character == null) { return false; }
2369  foreach (var c in Character.CharacterList)
2370  {
2371  if (!IsBotInTheCrew(character, c)) { continue; }
2372  if (predicate(c.AIController as HumanAIController))
2373  {
2374  return true;
2375  }
2376  }
2377  return false;
2378  }
2379 
2380  public static int CountBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate = null)
2381  {
2382  if (character == null) { return 0; }
2383  int count = 0;
2384  foreach (var other in Character.CharacterList)
2385  {
2386  if (!IsBotInTheCrew(character, other)) { continue; }
2387  if (predicate == null || predicate(other.AIController as HumanAIController))
2388  {
2389  count++;
2390  }
2391  }
2392  return count;
2393  }
2394 
2398  public bool IsTrueForAnyCrewMember(Func<Character, bool> predicate, bool onlyActive = true, bool onlyConnectedSubs = false)
2399  {
2400  foreach (var c in Character.CharacterList)
2401  {
2402  if (!IsActive(c)) { continue; }
2403  if (c.TeamID != Character.TeamID) { continue; }
2404  if (onlyActive && c.IsIncapacitated) { continue; }
2405  if (onlyConnectedSubs)
2406  {
2407  if (Character.Submarine == null)
2408  {
2409  if (c.Submarine != null)
2410  {
2411  return false;
2412  }
2413  }
2414  else if (c.Submarine != Character.Submarine && !Character.Submarine.GetConnectedSubs().Contains(c.Submarine))
2415  {
2416  return false;
2417  }
2418  }
2419  if (predicate(c))
2420  {
2421  return true;
2422  }
2423  }
2424  return false;
2425  }
2426 
2427  private static void DoForEachBot(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
2428  {
2429  if (character == null) { return; }
2430  foreach (var c in Character.CharacterList)
2431  {
2432  if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
2433  {
2434  action(c.AIController as HumanAIController);
2435  }
2436  }
2437  }
2438 
2439  private static bool CheckReportRange(Character character, Character target, float range)
2440  {
2441  if (float.IsPositiveInfinity(range)) { return true; }
2442  if (character.CurrentHull == null || target.CurrentHull == null)
2443  {
2444  return Vector2.DistanceSquared(character.WorldPosition, target.WorldPosition) <= range * range;
2445  }
2446  else
2447  {
2448  return character.CurrentHull.GetApproximateDistance(character.Position, target.Position, target.CurrentHull, range, distanceMultiplierPerClosedDoor: 2) <= range;
2449  }
2450  }
2451 
2452  private static bool IsBotInTheCrew(Character self, Character other) => IsActive(other) && other.TeamID == self.TeamID && !other.IsIncapacitated && other.IsBot && other.AIController is HumanAIController;
2453 
2454  public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
2455  {
2456  operatingCharacter = null;
2457  if (target?.Item == null) { return false; }
2458  float highestPriority = -1.0f;
2459  float highestPriorityModifier = -1.0f;
2460  foreach (Character c in Character.CharacterList)
2461  {
2462  if (c == null) { continue; }
2463  if (c.Removed) { continue; }
2464  if (c.TeamID != team) { continue; }
2465  if (c.IsIncapacitated) { continue; }
2466  if (c.SelectedItem == target.Item)
2467  {
2468  operatingCharacter = c;
2469  return true;
2470  }
2471  if (c.AIController is HumanAIController humanAI && humanAI.ObjectiveManager is AIObjectiveManager objectiveManager)
2472  {
2473  foreach (var objective in objectiveManager.Objectives)
2474  {
2475  if (!(objective is AIObjectiveOperateItem operateObjective)) { continue; }
2476  if (operateObjective.Component?.Item != target.Item) { continue; }
2477  if (operateObjective.Priority < highestPriority) { continue; }
2478  if (operateObjective.PriorityModifier < highestPriorityModifier) { continue; }
2479  operatingCharacter = c;
2480  highestPriority = operateObjective.Priority;
2481  highestPriorityModifier = operateObjective.PriorityModifier;
2482  }
2483  }
2484  }
2485  return operatingCharacter != null;
2486  }
2487 
2488  // There's some duplicate logic in the two methods below, but making them use the same code would require some changes in the target classes so that we could use exactly the same checks.
2489  // And even then there would be some differences that could end up being confusing (like the exception for steering).
2490  public bool IsItemOperatedByAnother(ItemComponent target, out Character other)
2491  {
2492  other = null;
2493  if (target?.Item == null) { return false; }
2494  bool isOrder = IsOrderedToOperateTarget(this);
2495  foreach (Character c in Character.CharacterList)
2496  {
2497  if (!IsActive(c)) { continue; }
2498  if (c == Character) { continue; }
2499  if (c.TeamID != Character.TeamID) { continue; }
2500  if (c.IsPlayer)
2501  {
2502  if (c.SelectedItem == target.Item)
2503  {
2504  // If the other character is player, don't try to operate
2505  other = c;
2506  break;
2507  }
2508  }
2509  else if (c.AIController is HumanAIController otherAI)
2510  {
2511  if (otherAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
2512  {
2513  // Not targeting the same item.
2514  continue;
2515  }
2516  bool isTargetOrdered = IsOrderedToOperateTarget(otherAI);
2517  if (!isOrder && isTargetOrdered)
2518  {
2519  // If the other bot is ordered to operate the item, let him do it, unless we are ordered too
2520  other = c;
2521  break;
2522  }
2523  else
2524  {
2525  if (isOrder && !isTargetOrdered)
2526  {
2527  // We are ordered and the target is not -> allow to operate
2528  continue;
2529  }
2530  else
2531  {
2532  if (!IsOperatingTarget(otherAI))
2533  {
2534  // The other bot is doing something else -> stick to the target.
2535  continue;
2536  }
2537  if (target is Steering)
2538  {
2539  // Steering is hard-coded -> cannot use the required skills collection defined in the xml
2540  if (Character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill))
2541  {
2542  other = c;
2543  break;
2544  }
2545  }
2546  else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c))
2547  {
2548  other = c;
2549  break;
2550  }
2551  }
2552  }
2553  }
2554  }
2555  return other != null;
2556  bool IsOrderedToOperateTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
2557  bool IsOperatingTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item;
2558  }
2559 
2560  public bool IsItemRepairedByAnother(Item target, out Character other)
2561  {
2562  other = null;
2563  if (Character == null) { return false; }
2564  if (target == null) { return false; }
2565  bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
2566  foreach (var c in Character.CharacterList)
2567  {
2568  if (!IsActive(c)) { continue; }
2569  if (c == Character) { continue; }
2570  if (c.TeamID != Character.TeamID) { continue; }
2571  other = c;
2572  if (c.IsPlayer)
2573  {
2574  if (target.Repairables.Any(r => r.CurrentFixer == c))
2575  {
2576  // If the other character is player, don't try to repair
2577  return true;
2578  }
2579  }
2580  else if (c.AIController is HumanAIController operatingAI)
2581  {
2582  var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
2583  if (repairItemsObjective == null) { continue; }
2584  if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
2585  {
2586  // Not targeting the same item.
2587  continue;
2588  }
2589  bool isTargetOrdered = IsOrderedToRepairThis(operatingAI);
2590  if (!isOrder && isTargetOrdered)
2591  {
2592  // If the other bot is ordered to repair the item, let him do it, unless we are ordered too
2593  return true;
2594  }
2595  else
2596  {
2597  if (isOrder && !isTargetOrdered)
2598  {
2599  // We are ordered and the target is not -> allow to repair
2600  continue;
2601  }
2602  else
2603  {
2604  if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
2605  {
2606  // The other bot is ordered to do something else
2607  continue;
2608  }
2609  return target.Repairables.Max(r => r.DegreeOfSuccess(Character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c));
2610  }
2611  }
2612  }
2613  }
2614  return false;
2615  bool IsOrderedToRepairThis(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target;
2616  }
2617 
2618  #region Wrappers
2619  public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam);
2620  public bool IsTrueForAnyBotInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAnyBotInTheCrew(Character, predicate);
2621  public bool IsTrueForAllBotsInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAllBotsInTheCrew(Character, predicate);
2622  public int CountBotsInTheCrew(Func<HumanAIController, bool> predicate = null) => CountBotsInTheCrew(Character, predicate);
2623  #endregion
2624  }
2625 }
readonly Character Character
Definition: AIController.cs:15
virtual bool IsMentallyUnstable
Definition: AIController.cs:89
IEnumerable< Hull > VisibleHulls
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
SteeringManager SteeringManager
Definition: AIController.cs:54
SteeringManager steeringManager
Definition: AIController.cs:51
bool HasValidPath(bool requireNonDirty=true, bool requireUnfinished=true, Func< WayPoint, bool > nodePredicate=null)
Is the current path valid, using the provided parameters.
override bool IsValidTarget(Hull hull)
override bool IsValidTarget(Character target)
static bool IsSuitablePressureProtection(Item item, Identifier tag, Character character)
override bool IsValidTarget(Gap gap)
IEnumerable< AIObjective > GetSubObjectivesRecursive(bool includingSelf=false)
Definition: AIObjective.cs:136
void AddSubObjective(AIObjective objective, bool addFirst=false)
Definition: AIObjective.cs:206
virtual bool AllowAutomaticItemUnequipping
There's a separate property for diving suit and mask: KeepDivingGearOn.
Definition: AIObjective.cs:41
An objective that creates specific kinds of subobjectives for specific types of targets,...
AIObjective GetActiveObjective()
const float RunPriority
Objectives with a priority equal to or higher than this make the character run.
AIObjective CreateObjective(Order order, float priorityModifier=1)
float GetCurrentPriority()
Returns the highest priority of the current objective and its subobjectives.
List< AIObjective > Objectives
Excluding the current order.
void SetForcedOrder(AIObjective objective)
void AddObjective(AIObjective objective)
AIObjective CurrentObjective
Includes orders.
AIObjective?? CurrentOrder
The AIObjective in CurrentOrders with the highest AIObjective.Priority
float? WaitTimer
When set above zero, the character will stand still doing nothing until the timer runs out....
override bool IsValidTarget(Item item)
override bool IsValidTarget(Character target)
virtual float Strength
Definition: Affliction.cs:31
Character Source
Which character gave this affliction
Definition: Affliction.cs:88
readonly AfflictionPrefab Prefab
Definition: Affliction.cs:12
float GetCurrentSpeed(bool useMaxSpeed)
Action BeforeLevelLoading
Automatically cleared after triggering -> no need to unregister
float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions=true)
bool TryPutItemInAnySlot(Item item)
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
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 IsSameSpeciesOrGroup(Character other)
bool IsHostileEscortee
Set true only, if the character is turned hostile from an escort mission (See EscortMission).
bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable< Identifier > identifiers=null, bool ignoreBroken=true, IEnumerable< Item > ignoredItems=null, IEnumerable< Identifier > ignoredContainerIdentifiers=null, Func< Item, bool > customPredicate=null, Func< Item, float > customPriorityFunction=null, float maxItemDistance=10000, ISpatialEntity positionalReference=null)
Finds the closest item seeking by identifiers or tags from the world. Ignores items that are outside ...
static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
bool TryPutItemInBag(Item item)
Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
List< Hull > GetVisibleHulls()
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
IEnumerable< Item >?? HeldItems
Items the character has in their hand slots. Doesn't return nulls and only returns items held in both...
Item? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity=null, bool seeThroughWindows=false, bool checkFacing=false)
Stores information about the Character that is needed between rounds in the menu etc....
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
AIObjectiveCombat.CombatMode WitnessReaction
Definition: CombatAction.cs:22
AIObjectiveCombat.CombatMode GuardReaction
Definition: CombatAction.cs:19
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static GameServer Server
Definition: GameMain.cs:39
static GameSession GameSession
Definition: GameMain.cs:45
CampaignMode? Campaign
Definition: GameSession.cs:128
CrewManager? CrewManager
Definition: GameSession.cs:75
IEnumerable< Hull > GetConnectedHulls(bool includingThis, int? searchDepth=null, bool ignoreClosedGaps=false)
static bool IsTrueForAnyBotInTheCrew(Character character, Func< HumanAIController, bool > predicate)
static int CountBotsInTheCrew(Character character, Func< HumanAIController, bool > predicate=null)
float Hearing
Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI...
static bool HasDivingSuit(Character character, float conditionPercentage=0, bool requireOxygenTank=true, bool requireSuitablePressureProtection=true)
Check whether the character has a diving suit in usable condition, suitable pressure protection for t...
bool IsItemOperatedByAnother(ItemComponent target, out Character other)
int CountBotsInTheCrew(Func< HumanAIController, bool > predicate=null)
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 IsTrueForAllBotsInTheCrew(Func< HumanAIController, bool > predicate)
static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
static bool HasDivingGear(Character character, float conditionPercentage=0, bool requireOxygenTank=true)
void SetOrder(Order order, bool speak=true)
readonly HashSet< Hull > UnreachableHulls
static bool IsActive(Character c)
static void ApplyStealingReputationLoss(Item item)
static bool FindSuitableContainer(Character character, Item containableItem, List< Item > ignoredItems, ref int itemIndex, out Item suitableContainer)
void AskToRecalculateHullSafety(Hull hull)
float GetHullSafety(Hull hull, Character character, IEnumerable< Hull > visibleHulls=null)
override void OnHealed(Character healer, float healAmount)
bool NeedsDivingGear(Hull hull, out bool needsSuit)
override void OnAttacked(Character attacker, AttackResult attackResult)
bool IsItemRepairedByAnother(Item target, out Character other)
override void Update(float deltaTime)
readonly List< Item > IgnoredItems
bool UseOutsideWaypoints
Waypoints that are not linked to a sub (e.g. main path).
bool AutoFaceMovement
Resets each frame
bool IsTrueForAnyCrewMember(Func< Character, bool > predicate, bool onlyActive=true, bool onlyConnectedSubs=false)
Including the player characters in the same team.
static void PropagateHullSafety(Character character, Hull hull)
Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (ma...
AIObjectiveManager ObjectiveManager
AIObjective SetForcedOrder(Order order)
static bool IsFriendly(Character me, Character other, bool onlySameTeam=false)
static void ItemTaken(Item item, Character thief)
bool IsTrueForAnyBotInTheCrew(Func< HumanAIController, bool > predicate)
void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay=0, Func< AIObjective, bool > abortCondition=null, Action onAbort=null, Action onCompleted=null, bool allowHoldFire=false, bool speakWarnings=false)
readonly HashSet< Hull > UnsafeHulls
bool FindSuitableContainer(Item containableItem, out Item suitableContainer)
static bool IsBallastFloraNoticeable(Character character, Hull hull)
MentalStateManager MentalStateManager
override void SelectTarget(AITarget target)
bool IsFriendly(Character other, bool onlySameTeam=false)
static bool IsTrueForAllBotsInTheCrew(Character character, Func< HumanAIController, bool > predicate)
static void ReportProblem(Character reporter, Order order, Hull targetHull=null)
static bool HasDivingMask(Character character, float conditionPercentage=0, bool requireOxygenTank=true)
Check whether the character has a diving mask in usable condition plus some oxygen.
IndoorsSteeringManager PathSteering
static void RefreshTargets(Character character, Order order, Hull hull)
float ReportRange
How far other characters can hear reports done by this character (e.g. reports for fires,...
ShipCommandManager ShipCommandManager
override bool Escape(float deltaTime)
static void StructureDamaged(Structure structure, float damageAmount, Character character)
static float GetHullSafety(Hull hull, IEnumerable< Hull > visibleHulls, Character character, bool ignoreWater=false, bool ignoreOxygen=false, bool ignoreFire=false, bool ignoreEnemies=false)
Item FindItem(Func< Item, bool > predicate, bool recursive)
List< Item > FindAllItems(Func< Item, bool > predicate=null, bool recursive=false, List< Item > list=null)
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
void SetTransform(Vector2 simPosition, float rotation, bool findNewHull=true, bool setPrevTransform=true)
bool Illegitimate
Item shouldn't be in the player's inventory. If the guards find it, they will consider it as a theft.
void SecondaryUse(float deltaTime, Character character=null)
static IReadOnlyCollection< Item > RepairableItems
Items that have one more more Repairable component
Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id=Entity.NullEntityID, bool callOnItemLoaded=true)
bool StolenDuringRound
Was the item stolen during the current round. Note that it's possible for the items to be found in th...
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
The base class for components holding the different functionalities of the item
JobPrefab Prefab
Definition: Job.cs:18
PhysicsBody body
Definition: Limb.cs:217
Reputation Reputation
Definition: Location.cs:106
override Vector2 SimPosition
Definition: MapEntity.cs:227
Location CurrentLocation
Definition: Map.cs:47
void Update(float deltaTime)
void SendOrderChatMessage(OrderChatMessage message)
Definition: GameServer.cs:3908
string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption=default, bool isNewOrder=true)
float FadeOutTime
Definition: Order.cs:549
Order WithTargetEntity(Entity entity)
Definition: Order.cs:720
Identifier Identifier
Definition: Order.cs:536
Order WithOrderGiver(Character orderGiver)
Definition: Order.cs:710
static readonly PrefabCollection< OrderPrefab > Prefabs
Definition: Order.cs:41
Vector2 GetLocalFront(float? spritesheetRotation=null)
Returns the farthest point towards the forward of the body. For capsules and circles,...
float TransformedRotation
Takes flipping (Dir) into account.
readonly Identifier Identifier
Definition: Prefab.cs:34
bool TryGetCollider(int index, out PhysicsBody collider)
Definition: Ragdoll.cs:152
float?? ImpactTolerance
Definition: Ragdoll.cs:318
Vector2? TargetMovement
Definition: Ragdoll.cs:298
PhysicsBody? Collider
Definition: Ragdoll.cs:145
Vector2 GetColliderBottom()
Definition: Ragdoll.cs:2183
Limb GetLimb(LimbType limbType, bool excludeSevered=true, bool excludeLimbsWithSecondaryType=false, bool useSecondaryType=false)
Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
Definition: Ragdoll.cs:2130
bool IgnorePlatforms
Definition: Ragdoll.cs:373
Direction TargetDir
Definition: Ragdoll.cs:135
const float MaxReputationLossPerStolenItem
Definition: Reputation.cs:14
const float ReputationLossPerStolenItemPrice
Definition: Reputation.cs:12
const float MaxReputationLossFromWallDamage
Maximum amount of reputation loss you can get from damaging outpost walls per round
Definition: Reputation.cs:23
const float MinReputationLossPerStolenItem
Definition: Reputation.cs:13
void AddReputation(float reputationChange, float maxReputationChangePerRound=float.MaxValue)
Definition: Reputation.cs:95
const float ReputationLossPerWallDamage
Definition: Reputation.cs:11
void Update(float deltaTime)
virtual void Update(float speed)
Update speed for the steering. Should normally match the characters current animation speed.
List< WayPoint > Nodes
readonly Dictionary< Submarine, DockingPort > ConnectedDockingPorts
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel=false, bool ignoreSubs=false, bool ignoreSensors=true, bool ignoreDisabledWalls=true, bool ignoreBranches=true, Predicate< Fixture > blocksVisibilityPredicate=null)
Check visibility between two points (in sim units).
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
List< Hull > GetHulls(bool alsoFromConnectedSubs)
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
bool HasTag(SubmarineTag tag)
static WayPoint GetRandom(SpawnType spawnType=SpawnType.Human, JobPrefab assignedJob=null, Submarine sub=null, bool useSyncedRand=false, string spawnPointTag=null, bool ignoreSubmarine=false)
Definition: WayPoint.cs:933
LimbType
Definition: Limb.cs:19
@ Character
Characters only
readonly List< Affliction > Afflictions
Definition: Attack.cs:68