Client LuaCsForBarotrauma
AIObjectiveIdle.cs
3 using FarseerPhysics;
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 override Identifier Identifier { get; set; } = "idle".ToIdentifier();
14  public override bool AllowAutomaticItemUnequipping => true;
15  protected override bool AllowInAnySub => true;
16 
17  private BehaviorType behavior;
19  {
20  get { return behavior; }
21  set
22  {
23  behavior = value;
24  switch (behavior)
25  {
26  case BehaviorType.Passive:
27  case BehaviorType.StayInHull:
28  newTargetIntervalMin = 60;
29  newTargetIntervalMax = 120;
30  standStillMin = 30;
31  standStillMax = 60;
32  break;
33  case BehaviorType.Active:
34  newTargetIntervalMin = 40;
35  newTargetIntervalMax = 60;
36  standStillMin = 20;
37  standStillMax = 40;
38  break;
39  case BehaviorType.Patrol:
40  newTargetIntervalMin = 15;
41  newTargetIntervalMax = 30;
42  standStillMin = 5;
43  standStillMax = 10;
44  break;
45  }
46  }
47  }
48 
49  private float newTargetIntervalMin;
50  private float newTargetIntervalMax;
51  private float standStillMin;
52  private float standStillMax;
53  private readonly float walkDurationMin = 5;
54  private readonly float walkDurationMax = 10;
55 
56  public enum BehaviorType
57  {
58  Patrol,
59  Passive,
60  StayInHull,
61  Active
62  }
63  public Hull TargetHull { get; set; }
64  private Hull currentTarget;
65  private float newTargetTimer;
66 
67  private bool searchingNewHull;
68 
69  private float standStillTimer;
70  private float walkDuration;
71 
72  private Character tooCloseCharacter;
73 
74  const float chairCheckInterval = 5.0f;
75  private float chairCheckTimer;
76 
77  private float autonomousObjectiveRetryTimer = 10;
78 
79  private readonly List<Hull> targetHulls = new List<Hull>(20);
80  private readonly List<float> hullWeights = new List<float>(20);
81 
82  public AIObjectiveIdle(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
83  {
84  Behavior = BehaviorType.Passive;
85  standStillTimer = Rand.Range(-10.0f, 10.0f);
86  walkDuration = Rand.Range(0.0f, 10.0f);
87  chairCheckTimer = Rand.Range(0.0f, chairCheckInterval);
89  }
90 
91  protected override bool CheckObjectiveSpecific() => false;
92  public override bool CanBeCompleted => true;
93 
94  public readonly HashSet<Identifier> PreferredOutpostModuleTypes = new HashSet<Identifier>();
95 
96  public void CalculatePriority(float max = 0)
97  {
98  //Random = Rand.Range(0.5f, 1.5f);
99  //randomTimer = randomUpdateInterval;
100  //max = max > 0 ? max : Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
101  //float initiative = character.GetSkillLevel("initiative");
102  //Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
103  Priority = 1;
104  }
105 
106  protected override float GetPriority() => Priority;
107 
108  public override void Update(float deltaTime)
109  {
110  // Do nothing. Overrides the inherited devotion calculations.
111  }
112 
113  private float timerMargin;
114 
115  private void SetTargetTimerLow()
116  {
117  // Increases the margin each time the method is called -> takes longer between the path finding calls.
118  // The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path.
119  timerMargin += 0.5f;
120  timerMargin = Math.Min(timerMargin, newTargetIntervalMin);
121  newTargetTimer = Math.Max(newTargetTimer, timerMargin);
122  }
123 
124  private void SetTargetTimerHigh()
125  {
126  // This method is used to the timer between the current value and the min so that it never reaches 0.
127  // Prevents pathfinder calls.
128  newTargetTimer = Math.Max(newTargetTimer, newTargetIntervalMin);
129  timerMargin = 0;
130  }
131 
132  private void SetTargetTimerNormal()
133  {
134  newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax);
135  timerMargin = 0;
136  }
137 
138  private bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable);
139 
140  protected override void Act(float deltaTime)
141  {
142  if (PathSteering == null) { return; }
143 
145  {
146  if (autonomousObjectiveRetryTimer > 0)
147  {
148  autonomousObjectiveRetryTimer -= deltaTime;
149  }
150  else
151  {
153  }
154  }
155 
156  //don't keep dragging others when idling
157  if (character.SelectedCharacter != null)
158  {
160  }
161 
162  character.SelectedItem = null;
163 
164  if (!character.IsClimbing)
165  {
166  CleanupItems(deltaTime);
167  }
168 
169  if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null && !IsForbidden(character.CurrentHull))
170  {
172  }
173 
174  bool currentTargetIsInvalid =
175  currentTarget == null ||
176  IsForbidden(currentTarget) ||
177  (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
178 
179  if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
180  {
181  currentTarget = TargetHull;
182  bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
183  if (stayInHull)
184  {
185  Wander(deltaTime);
186  }
187  else if (currentTarget != null)
188  {
189  PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
190  }
191  else
192  {
195  }
196  }
197  else
198  {
199  if (currentTarget != null && !currentTargetIsInvalid)
200  {
201  if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
202  {
203  if (currentTarget.Submarine.TeamID != character.TeamID)
204  {
205  currentTargetIsInvalid = true;
206  }
207  }
208  else
209  {
210  if (currentTarget.Submarine != character.Submarine)
211  {
212  currentTargetIsInvalid = true;
213  }
214  }
215  }
216 
217  if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
218  {
219  if (newTargetTimer > timerMargin)
220  {
221  //don't reset to zero, otherwise the character will keep calling FindTargetHulls
222  //almost constantly when there's a small number of potential hulls to move to
223  SetTargetTimerLow();
224  }
225  }
226  else if (character.IsClimbing)
227  {
228  if (currentTarget == null)
229  {
230  SetTargetTimerLow();
231  }
232  else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
233  {
234  // Don't allow new targets when climbing straight up or down
235  SetTargetTimerHigh();
236  }
237  }
239  {
240  if (currentTarget == null)
241  {
242  SetTargetTimerLow();
243  }
244  }
245  if (newTargetTimer <= 0.0f)
246  {
247  if (!searchingNewHull)
248  {
249  //find all available hulls first
250  searchingNewHull = true;
251  FindTargetHulls();
252  }
253  else if (targetHulls.Any())
254  {
255  //choose a random available hull
256  currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
257  bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
258  bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
259  Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
260  var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
261  {
262  if (node.Waypoint.CurrentHull == null) { return false; }
263  // Check that there is no unsafe hulls on the way to the target
264  if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
265  return true;
266  //don't stop at ladders when idling
267  }, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
268  if (path.Unreachable)
269  {
270  //can't go to this room, remove it from the list and try another room
271  int index = targetHulls.IndexOf(currentTarget);
272  targetHulls.RemoveAt(index);
273  hullWeights.RemoveAt(index);
275  currentTarget = null;
276  SetTargetTimerLow();
277  return;
278  }
280  PathSteering.SetPath(targetPos, path);
281  SetTargetTimerNormal();
282  searchingNewHull = false;
283  }
284  else
285  {
286  // Couldn't find a valid hull
287  SetTargetTimerHigh();
288  searchingNewHull = false;
289  }
290  }
291  newTargetTimer -= deltaTime;
292  if (!character.IsClimbing && (PathSteering == null || PathSteering.CurrentPath == null || IsSteeringFinished()))
293  {
294  Wander(deltaTime);
295  }
296  else if (currentTarget != null)
297  {
298  PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1,
299  nodeFilter: node => node.Waypoint.CurrentHull != null,
300  endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
301  }
302  else
303  {
306  }
307  }
308  }
309 
310  public void Wander(float deltaTime)
311  {
312  if (character.IsClimbing)
313  {
314  PathSteering.Reset();
315  character.StopClimbing();
316  }
317  var currentHull = character.CurrentHull;
318  if (!character.AnimController.InWater && currentHull != null)
319  {
320  standStillTimer -= deltaTime;
321  if (standStillTimer > 0.0f)
322  {
323  walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
324  if (currentHull.Rect.Width > IndoorsSteeringManager.smallRoomSize / 2 && tooCloseCharacter == null)
325  {
326  foreach (Character c in Character.CharacterList)
327  {
328  if (c == character || !c.IsBot || c.CurrentHull != currentHull || !(c.AIController is HumanAIController humanAI)) { continue; }
329  if (Vector2.DistanceSquared(c.WorldPosition, character.WorldPosition) > 60.0f * 60.0f) { continue; }
330  if ((humanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective && idleObjective.standStillTimer > 0.0f) ||
331  (humanAI.ObjectiveManager.CurrentObjective is AIObjectiveGoTo gotoObjective && gotoObjective.IsCloseEnough))
332  {
333  //if there are characters too close on both sides, don't try to steer away from them
334  //because it'll cause the character to spaz out trying to avoid both
335  if (tooCloseCharacter != null &&
336  Math.Sign(tooCloseCharacter.WorldPosition.X - character.WorldPosition.X) != Math.Sign(c.WorldPosition.X - character.WorldPosition.X))
337  {
338  tooCloseCharacter = null;
339  break;
340  }
341  tooCloseCharacter = c;
342  }
344  }
345  }
346 
347  if (tooCloseCharacter != null && !tooCloseCharacter.Removed && Vector2.DistanceSquared(tooCloseCharacter.WorldPosition, character.WorldPosition) < 50.0f * 50.0f)
348  {
349  Vector2 diff = character.WorldPosition - tooCloseCharacter.WorldPosition;
350  if (diff.LengthSquared() < 0.0001f) { diff = Rand.Vector(1.0f); }
351  if (Math.Abs(diff.X) > 0 &&
352  (character.WorldPosition.X > currentHull.WorldRect.Right - 50 || character.WorldPosition.X < currentHull.WorldRect.Left + 50))
353  {
354  // Between a wall and a character -> move away
355  tooCloseCharacter = null;
356  PathSteering.Reset();
357  standStillTimer = 0;
358  walkDuration = Math.Min(walkDuration, walkDurationMin);
359  if (Behavior != BehaviorType.StayInHull && (currentHull.Size.X < IndoorsSteeringManager.smallRoomSize || currentHull.Size.X < (IndoorsSteeringManager.smallRoomSize / 2 * Character.CharacterList.Count(c => c.CurrentHull == currentHull))))
360  {
361  // Small room -> find another
362  newTargetTimer = Math.Min(newTargetTimer, 1);
363  }
364  }
365  else
366  {
367  character.ReleaseSecondaryItem();
368  PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
369  }
370  return;
371  }
372  else
373  {
374  PathSteering.Reset();
375  tooCloseCharacter = null;
376  }
377 
378  chairCheckTimer -= deltaTime;
379  if (chairCheckTimer <= 0.0f && character.SelectedSecondaryItem == null)
380  {
381  foreach (Item item in Item.ItemList)
382  {
383  if (item.CurrentHull != currentHull || !item.HasTag(Tags.ChairItem)) { continue; }
384  //not possible in vanilla game, but a mod might have holdable/attachable chairs
385  if (item.ParentInventory != null || item.body is { Enabled: true }) { continue; }
386  var controller = item.GetComponent<Controller>();
387  if (controller == null || controller.User != null) { continue; }
388  item.TryInteract(character, forceSelectKey: true);
389  }
390  chairCheckTimer = chairCheckInterval;
391  }
392  return;
393  }
394  if (standStillTimer < -walkDuration)
395  {
396  standStillTimer = Rand.Range(standStillMin, standStillMax);
397  }
398  }
399 
400  PathSteering.Wander(deltaTime);
401  }
402 
403  public void FaceTargetAndWait(ISpatialEntity target, float waitTime)
404  {
405  standStillTimer = waitTime;
407  currentTarget = null;
408  SetTargetTimerHigh();
409  }
410 
411  private void FindTargetHulls()
412  {
413  targetHulls.Clear();
414  hullWeights.Clear();
415  foreach (var hull in Hull.HullList)
416  {
417  if (character.Submarine == null) { break; }
418  if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
419  if (hull.Submarine == null) { continue; }
420  if (hull.Submarine.Info.IsRuin || hull.Submarine.Info.IsWreck) { continue; }
421  if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
422  {
423  if (hull.Submarine.TeamID != character.TeamID)
424  {
425  // Don't allow npcs to idle in a sub that's not in their team (like the player sub)
426  continue;
427  }
428  }
429  else
430  {
431  if (hull.Submarine.TeamID != character.Submarine.TeamID)
432  {
433  // Don't allow to idle in the subs that are not in the same team as the current sub
434  // -> the crew ai bots can't change the sub from outpost to main sub or vice versa on their own
435  continue;
436  }
437  }
438  if (IsForbidden(hull)) { continue; }
439  // Check that the hull is linked
440  if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
441  // Ignore very narrow hulls.
442  if (hull.RectWidth < 200) { continue; }
443  // Ignore hulls that are too low to stand inside.
444  if (character.AnimController is HumanoidAnimController animController)
445  {
446  if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
447  {
448  continue;
449  }
450  }
451  if (!targetHulls.Contains(hull))
452  {
453  targetHulls.Add(hull);
454  float weight = hull.RectWidth;
455  float distanceFactor = GetDistanceFactor(hull.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 2500,
456  factorAtMinDistance: 1, factorAtMaxDistance: 0);
457  if (behavior == BehaviorType.Patrol)
458  {
459  //invert when patrolling (= prefer travelling to far-away hulls)
460  distanceFactor = 1.0f - distanceFactor;
461  }
462  float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2));
463  weight *= distanceFactor * waterFactor;
464  System.Diagnostics.Debug.Assert(weight >= 0);
465  hullWeights.Add(weight);
466  }
467  }
468 
469  if (PreferredOutpostModuleTypes.Any() && character.CurrentHull != null)
470  {
471  for (int i = 0; i < targetHulls.Count; i++)
472  {
473  if (targetHulls[i].OutpostModuleTags.Any(t => PreferredOutpostModuleTypes.Contains(t)))
474  {
475  hullWeights[i] *= Rand.Range(10.0f, 100.0f);
476  }
477  }
478  }
479  }
480 
481  #region Cleaning
482  private readonly float checkItemsInterval = 1;
483  private float checkItemsTimer;
484  private readonly List<Item> itemsToClean = new List<Item>();
485  private readonly List<Item> ignoredItems = new List<Item>();
486 
487  private void CleanupItems(float deltaTime)
488  {
489  if (checkItemsTimer <= 0)
490  {
491  checkItemsTimer = checkItemsInterval * Rand.Range(0.9f, 1.1f);
492  var hull = character.CurrentHull;
493  if (hull != null)
494  {
495  itemsToClean.Clear();
496  foreach (Item item in Item.CleanableItems)
497  {
498  if (item.CurrentHull != hull) { continue; }
499  if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
500  {
501  itemsToClean.Add(item);
502  }
503  }
504  if (itemsToClean.Any())
505  {
506  var targetItem = itemsToClean.OrderBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X)).FirstOrDefault();
507  if (targetItem != null)
508  {
509  var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier);
510  cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem);
511  subObjectives.Add(cleanupObjective);
512  }
513  }
514  }
515  }
516  else
517  {
518  checkItemsTimer -= deltaTime;
519  }
520  }
521  #endregion
522 
523  public static bool IsForbidden(Hull hull) => hull == null || hull.AvoidStaying;
524 
525  public override void Reset()
526  {
527  base.Reset();
528  currentTarget = null;
529  searchingNewHull = false;
530  tooCloseCharacter = null;
531  targetHulls.Clear();
532  hullWeights.Clear();
533  checkItemsTimer = 0;;
534  itemsToClean.Clear();
535  ignoredItems.Clear();
536  autonomousObjectiveRetryTimer = 10;
537  }
538 
539  public override void OnDeselected()
540  {
541  base.OnDeselected();
542  foreach (var subObjective in SubObjectives)
543  {
544  if (subObjective is AIObjectiveCleanupItem cleanUpObjective)
545  {
546  cleanUpObjective.DropTarget();
547  }
548  }
549  }
550  }
551 }
void FaceTarget(ISpatialEntity target)
float CalculatePriority()
Call this only when the priority needs to be recalculated. Use the cached Priority property when you ...
float Priority
Final priority value after all calculations.
override Identifier Identifier
override bool CheckObjectiveSpecific()
Should return whether the objective is completed or not.
override bool AllowAutomaticItemUnequipping
void FaceTargetAndWait(ISpatialEntity target, float waitTime)
override float GetPriority()
readonly HashSet< Identifier > PreferredOutpostModuleTypes
AIObjectiveIdle(Character character, AIObjectiveManager objectiveManager, float priorityModifier=1)
override void Act(float deltaTime)
override void Update(float deltaTime)
static bool IsForbidden(Hull hull)
void Wander(float deltaTime)
void CalculatePriority(float max=0)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos=null)
AITarget AiTarget
Definition: Entity.cs:55
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static readonly List< Hull > HullList
void SteeringSeek(Vector2 target, float weight, float minGapWidth=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisiblity=true)
void SetPath(Vector2 targetPos, SteeringPath path)
bool TryInteract(Character user, bool ignoreRequiredItems=false, bool forceSelectKey=false, bool forceUseKey=false)
static readonly List< Item > ItemList
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
List< WayPoint > Nodes