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