Client LuaCsForBarotrauma
AIObjectiveFindSafety.cs
2 using FarseerPhysics;
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 
8 namespace Barotrauma
9 {
11  {
12  public override Identifier Identifier { get; set; } = "find safety".ToIdentifier();
13  public override bool ForceRun => true;
14  public override bool KeepDivingGearOn => true;
15  public override bool IgnoreUnsafeHulls => true;
16  protected override bool ConcurrentObjectives => true;
17  protected override bool AllowOutsideSubmarine => true;
18  protected override bool AllowInAnySub => true;
19  public override bool AbandonWhenCannotCompleteSubObjectives => false;
20 
21  private const float PriorityIncrease = 100;
22  private const float PriorityDecrease = 10;
23  private const float SearchHullInterval = 3.0f;
24 
25  private float currentHullSafety;
26 
27  private float searchHullTimer;
28 
29  private AIObjectiveGoTo goToObjective;
30  private AIObjectiveFindDivingGear divingGearObjective;
31 
32  public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
33 
34  protected override bool CheckObjectiveSpecific() => false;
35  public override bool CanBeCompleted => true;
36 
37  private bool resetPriority;
38 
39  protected override float GetPriority()
40  {
41  if (character.CurrentHull == null)
42  {
43  Priority = (
44  objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
45  objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
48  }
49  else
50  {
51  bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
52  static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true);
53 
54  if (isSuffocatingInDivingSuit || (!objectiveManager.HasActiveObjective<AIObjectiveFindDivingGear>() && IsSuffocatingWithoutDivingGear(character)))
55  {
57  }
59  {
61  HumanAIController.HasDivingSuit(character, requireSuitablePressureProtection: false))
62  {
63  //we have a suit that's not suitable for the pressure,
64  //but we've failed to find a better one
65  // shit, not much we can do here, let's just allow the bot to get on with their current objective
66  Priority = 0;
67  }
68  else
69  {
71  }
72  }
73  else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
75  {
76  // Ordered to follow, hold position, or return back to main sub inside a hostile sub
77  // -> ignore find safety unless we need to find a diving gear
78  Priority = 0;
79  }
80  else if (objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
81  {
82  Priority = 0;
83  }
85  if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
86  {
87  // Boost the priority while seeking the diving gear
89  }
90  }
91  return Priority;
92  }
93 
94  public override void Update(float deltaTime)
95  {
96  if (retryTimer > 0)
97  {
98  retryTimer -= deltaTime;
99  if (retryTimer <= 0)
100  {
101  retryCounter = 0;
102  }
103  }
104  if (resetPriority)
105  {
106  Priority = 0;
107  resetPriority = false;
108  return;
109  }
110  if (character.CurrentHull == null)
111  {
112  currentHullSafety = 0;
113  }
114  else
115  {
116  currentHullSafety = HumanAIController.CurrentHullSafety;
117  if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
118  {
119  Priority -= PriorityDecrease * deltaTime;
120  if (currentHullSafety >= 100 && !character.IsLowInOxygen)
121  {
122  // Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
123  Priority = 0;
124  }
125  }
126  else
127  {
128  float dangerFactor = (100 - currentHullSafety) / 100;
129  Priority += dangerFactor * PriorityIncrease * deltaTime;
130  }
132  }
133  }
134 
135  private Hull currentSafeHull;
136  private Hull previousSafeHull;
137  private bool cannotFindSafeHull;
138  private bool cannotFindDivingGear;
139  private readonly int findDivingGearAttempts = 2;
140  private int retryCounter;
141  private readonly float retryResetTime = 5;
142  private float retryTimer;
143  protected override void Act(float deltaTime)
144  {
145  if (resetPriority) { return; }
146  var currentHull = character.CurrentHull;
147  bool dangerousPressure = (currentHull == null || currentHull.LethalPressure > 0) && !character.IsProtectedFromPressure;
148  bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
149  if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
150  {
151  bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
152  bool needsEquipment = shouldActOnSuffocation;
153  if (needsDivingSuit)
154  {
156  }
157  else if (needsDivingGear)
158  {
160  }
161  if (needsEquipment)
162  {
163  if (cannotFindDivingGear && retryCounter < findDivingGearAttempts)
164  {
165  retryTimer = retryResetTime;
166  retryCounter++;
167  needsDivingSuit = !needsDivingSuit;
168  RemoveSubObjective(ref divingGearObjective);
169  }
170  if (divingGearObjective == null)
171  {
172  cannotFindDivingGear = false;
173  RemoveSubObjective(ref goToObjective);
174  TryAddSubObjective(ref divingGearObjective,
175  constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
176  onAbandon: () =>
177  {
178  searchHullTimer = Math.Min(1, searchHullTimer);
179  cannotFindDivingGear = true;
180  // Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
181  },
182  onCompleted: () =>
183  {
184  resetPriority = true;
185  searchHullTimer = Math.Min(1, searchHullTimer);
186  RemoveSubObjective(ref divingGearObjective);
187  });
188  }
189  }
190  }
191  if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
192  {
193  if (currentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
194  {
195  searchHullTimer = Math.Min(1, searchHullTimer);
196  }
197  if (searchHullTimer > 0.0f)
198  {
199  searchHullTimer -= deltaTime;
200  }
201  else
202  {
203  HullSearchStatus hullSearchStatus = FindBestHull(out Hull potentialSafeHull, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
204  if (hullSearchStatus != HullSearchStatus.Finished)
205  {
206  UpdateSimpleEscape(deltaTime);
207  return;
208  }
209  searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
210  previousSafeHull = currentSafeHull;
211  currentSafeHull = potentialSafeHull;
212  cannotFindSafeHull = currentSafeHull == null || NeedMoreDivingGear(currentSafeHull);
213  currentSafeHull ??= previousSafeHull;
214  if (currentSafeHull != null && currentSafeHull != currentHull)
215  {
216  if (goToObjective?.Target != currentSafeHull)
217  {
218  RemoveSubObjective(ref goToObjective);
219  }
220  TryAddSubObjective(ref goToObjective,
221  constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
222  {
223  SpeakIfFails = false,
224  AllowGoingOutside =
226  character.CurrentHull == null ||
229  },
230  onCompleted: () =>
231  {
232  if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
234  {
235  resetPriority = true;
236  searchHullTimer = Math.Min(1, searchHullTimer);
237  }
238  RemoveSubObjective(ref goToObjective);
239  if (cannotFindDivingGear)
240  {
241  // If diving gear objective failed, let's reset it here.
242  RemoveSubObjective(ref divingGearObjective);
243  }
244  },
245  onAbandon: () =>
246  {
247  // Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
248  // If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
249  // If the target hull is inside a submarine that is not our main sub, just ignore it normally when it cannot be reached. This happens with outposts.
250  if (goToObjective != null)
251  {
252  if (goToObjective.Target is Hull hull)
253  {
254  if (currentHull != null || !Submarine.MainSubs.Contains(hull.Submarine))
255  {
257  }
258  }
259  }
260  RemoveSubObjective(ref goToObjective);
261  });
262  }
263  else
264  {
265  RemoveSubObjective(ref goToObjective);
266  }
267  }
268  if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
269  UpdateSimpleEscape(deltaTime);
270 
271  bool inFriendlySub =
274  if (cannotFindSafeHull && !inFriendlySub && character.IsOnPlayerTeam && objectiveManager.Objectives.None(o => o is AIObjectiveReturn))
275  {
276  if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab))
277  {
279  }
280  }
281  }
282  }
283 
284  public void UpdateSimpleEscape(float deltaTime)
285  {
286  Vector2 escapeVel = Vector2.Zero;
287  if (character.CurrentHull != null)
288  {
289  foreach (Hull hull in HumanAIController.VisibleHulls)
290  {
291  foreach (FireSource fireSource in hull.FireSources)
292  {
293  Vector2 dir = character.Position - fireSource.Position;
294  float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
295  escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
296  }
297  }
298  foreach (Character enemy in Character.CharacterList)
299  {
300  if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsHandcuffed) { continue; }
301  if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
302  {
303  Vector2 dir = character.Position - enemy.Position;
304  float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
305  escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
306  }
307  }
308  }
309  if (escapeVel != Vector2.Zero)
310  {
311  float left = character.CurrentHull.Rect.X + 50;
312  float right = character.CurrentHull.Rect.Right - 50;
313  //only move if we haven't reached the edge of the room
314  if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
315  {
318  }
319  else
320  {
321  character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
323  }
324  }
325  else
326  {
327  objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
328  }
329  }
330 
331  public enum HullSearchStatus
332  {
333  Running,
334  Finished
335  }
336 
337  private readonly List<Hull> hulls = new List<Hull>();
338  private int hullSearchIndex = -1;
339  float bestHullValue = 0;
340  bool bestHullIsAirlock = false;
341  Hull potentialBestHull;
342 
347  public HullSearchStatus FindBestHull(out Hull bestHull, IEnumerable<Hull> ignoredHulls = null, bool allowChangingSubmarine = true)
348  {
349  if (hullSearchIndex == -1)
350  {
351  bestHullValue = 0;
352  potentialBestHull = null;
353  bestHullIsAirlock = false;
354  hulls.Clear();
355  var connectedSubs = character.Submarine?.GetConnectedSubs();
356  foreach (Hull hull in Hull.HullList)
357  {
358  if (hull.Submarine == null) { continue; }
359  // Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it.
360  if (hull.Submarine.Info.IsRuin) { continue; }
361  if (!allowChangingSubmarine && hull.Submarine != character.Submarine) { continue; }
362  if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; }
363  if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
364  if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
365  if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
366  //sort the hulls based on distance and which sub they're in
367  //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
368  //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
369  //path calculations, only to discard all of them when going through the hulls in the outpost)
370  float hullSuitability = EstimateHullSuitability(character, hull);
371  if (hulls.None())
372  {
373  hulls.Add(hull);
374  }
375  else
376  {
377  for (int i = 0; i < hulls.Count; i++)
378  {
379  if (hullSuitability > EstimateHullSuitability(character, hulls[i]))
380  {
381  hulls.Insert(i, hull);
382  break;
383  }
384  }
385  }
386  }
387  if (hulls.None())
388  {
389  bestHull = null;
390  return HullSearchStatus.Finished;
391  }
392  hullSearchIndex = 0;
393  }
394 
395  static float EstimateHullSuitability(Character character, Hull hull)
396  {
397  float dist =
398  Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) +
399  Math.Abs(hull.WorldPosition.Y - character.WorldPosition.Y) * 3;
400  float suitability = -dist;
401  if (hull.Submarine != character.Submarine)
402  {
403  suitability -= 10000.0f;
404  }
405  return suitability;
406  }
407 
408  Hull potentialHull = hulls[hullSearchIndex];
409 
410  float hullSafety = 0;
411  bool hullIsAirlock = false;
412  bool isCharacterInside = character.CurrentHull != null && character.Submarine != null;
413  if (isCharacterInside)
414  {
415  hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character);
416  float distanceFactor = GetDistanceFactor(potentialHull.WorldPosition, factorAtMaxDistance: 0.9f);
417  hullSafety *= distanceFactor;
418  //skip the hull if the safety is already less than the best hull
419  //(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
420  if (hullSafety > bestHullValue)
421  {
422  //avoid airlock modules if not allowed to change the sub
423  if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
424  {
425  // Don't allow to go outside if not already outside.
426  var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
427  if (path.Unreachable)
428  {
429  hullSafety = 0;
430  HumanAIController.UnreachableHulls.Add(potentialHull);
431  }
432  else
433  {
434  // Each unsafe node reduces the hull safety value.
435  // Ignore the current hull, because otherwise we couldn't find a path out.
436  int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
437  hullSafety /= 1 + unsafeNodes;
438  // If the target is not inside a friendly submarine, considerably reduce the hull safety.
439  if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true))
440  {
441  hullSafety /= 10;
442  }
443  }
444  }
445  else
446  {
447  hullSafety = 0;
448  }
449  }
450  }
451  else
452  {
453  if (potentialHull.IsAirlock)
454  {
455  hullSafety = 100;
456  hullIsAirlock = true;
457  }
458  else if (!bestHullIsAirlock && potentialHull.LeadsOutside(character))
459  {
460  hullSafety = 100;
461  }
462  float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
463  // Huge preference for closer targets
464  float distanceFactor = GetDistanceFactor(new Vector2(character.WorldPosition.X, characterY), potentialHull.WorldPosition, factorAtMaxDistance: 0.2f);
465  hullSafety *= distanceFactor;
466  // If the target is not inside a friendly submarine, considerably reduce the hull safety.
467  // Intentionally exclude wrecks from this check
468  if (potentialHull.Submarine.TeamID != character.TeamID && potentialHull.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
469  {
470  hullSafety /= 10;
471  }
472  }
473  if (hullSafety > bestHullValue || (!isCharacterInside && hullIsAirlock && !bestHullIsAirlock))
474  {
475  potentialBestHull = potentialHull;
476  bestHullValue = hullSafety;
477  bestHullIsAirlock = hullIsAirlock;
478  }
479 
480  bestHull = potentialBestHull;
481  hullSearchIndex++;
482 
483  if (hullSearchIndex >= hulls.Count)
484  {
485  hullSearchIndex = -1;
486  return HullSearchStatus.Finished;
487  }
488  return HullSearchStatus.Running;
489  }
490 
491  public override void Reset()
492  {
493  base.Reset();
494  goToObjective = null;
495  divingGearObjective = null;
496  currentSafeHull = null;
497  previousSafeHull = null;
498  retryCounter = 0;
499  cannotFindDivingGear = false;
500  cannotFindSafeHull = false;
501  }
502 
503  private bool NeedMoreDivingGear(Hull targetHull, float minOxygen = 0)
504  {
505  if (!HumanAIController.NeedsDivingGear(targetHull, out bool needsSuit)) { return false; }
506  if (needsSuit)
507  {
508  return !HumanAIController.HasDivingSuit(character, minOxygen);
509  }
510  else
511  {
512  return !HumanAIController.HasDivingGear(character, minOxygen);
513  }
514  }
515  }
516 }
IEnumerable< Hull > VisibleHulls
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
static float GetMinOxygen(Character character)
override void Update(float deltaTime)
HullSearchStatus FindBestHull(out Hull bestHull, IEnumerable< Hull > ignoredHulls=null, bool allowChangingSubmarine=true)
Tries to find the best (safe, nearby) hull the character can find a path to. Checks one hull at a tim...
AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier=1)
override bool CheckObjectiveSpecific()
Should return whether the objective is completed or not.
override void Act(float deltaTime)
float Priority
Final priority value after all calculations.
static float GetDistanceFactor(Vector2 selfPos, Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier=3, float maxDistance=10000.0f, float factorAtMinDistance=1.0f)
Get a normalized value representing how close the target position is. The value is a rough estimation...
const float EmergencyObjectivePriority
Priority of objectives such as finding safety, rescuing someone in a critical state or defending agai...
List< AIObjective > Objectives
Excluding the current order.
void AddObjective(AIObjective objective)
const float MaxObjectivePriority
Highest possible priority for any objective. Used in certain cases where the character needs to react...
Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos=null)
static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
bool IsProtectedFromPressure
Is the character currently protected from the pressure by immunity/ability or a status effect (e....
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
IEnumerable< Identifier > OutpostModuleTags
Inherited flags from outpost generation.
bool LeadsOutside(Character character)
Does this hull have any doors leading outside?
static readonly List< Hull > HullList
IEnumerable< Hull > GetConnectedHulls(bool includingThis, int? searchDepth=null, bool ignoreClosedGaps=false)
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...
static bool HasDivingGear(Character character, float conditionPercentage=0, bool requireOxygenTank=true)
static bool IsActive(Character c)
float GetHullSafety(Hull hull, Character character, IEnumerable< Hull > visibleHulls=null)
static bool IsFriendly(Character me, Character other, bool onlySameTeam=false)
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 readonly PrefabCollection< OrderPrefab > Prefabs
Definition: Order.cs:41
SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub=null, string errorMsgStr=null, float minGapSize=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisibility=true)
Definition: PathFinder.cs:173
float ColliderHeightFromFloor
In sim units. Joint scale applied.
void SteeringManual(float deltaTime, Vector2 velocity)
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs, bool allowDifferentTeam=false, bool allowDifferentType=false)