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 CheckObjectiveState() => 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  if (character.SelectedItem != null)
162  {
164  {
165  character.SelectedItem = null;
166  }
167  else
168  {
169  return;
170  }
171  }
172 
173  if (!character.IsClimbing)
174  {
175  CleanupItems(deltaTime);
176  }
177 
178  if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null && !IsForbidden(character.CurrentHull))
179  {
181  }
182 
183  bool currentTargetIsInvalid =
184  currentTarget == null ||
185  IsForbidden(currentTarget) ||
186  (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
187 
188  if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
189  {
190  currentTarget = TargetHull;
191  bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
192  if (stayInHull)
193  {
194  Wander(deltaTime);
195  }
196  else if (currentTarget != null)
197  {
198  PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
199  }
200  else
201  {
204  }
205  }
206  else
207  {
208  if (currentTarget != null && !currentTargetIsInvalid)
209  {
210  if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
211  {
212  if (currentTarget.Submarine.TeamID != character.TeamID)
213  {
214  currentTargetIsInvalid = true;
215  }
216  }
217  else
218  {
219  if (currentTarget.Submarine != character.Submarine)
220  {
221  currentTargetIsInvalid = true;
222  }
223  }
224  }
225 
226  if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
227  {
228  if (newTargetTimer > timerMargin)
229  {
230  //don't reset to zero, otherwise the character will keep calling FindTargetHulls
231  //almost constantly when there's a small number of potential hulls to move to
232  SetTargetTimerLow();
233  }
234  }
235  else if (character.IsClimbing)
236  {
237  if (currentTarget == null)
238  {
239  SetTargetTimerLow();
240  }
241  else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
242  {
243  // Don't allow new targets when climbing straight up or down
244  SetTargetTimerHigh();
245  }
246  }
248  {
249  if (currentTarget == null)
250  {
251  SetTargetTimerLow();
252  }
253  }
254  if (newTargetTimer <= 0.0f)
255  {
256  if (!searchingNewHull)
257  {
258  //find all available hulls first
259  searchingNewHull = true;
260  FindTargetHulls();
261  }
262  else if (targetHulls.Any())
263  {
264  //choose a random available hull
265  currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
266  bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
267  bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
268  Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
269  var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
270  {
271  if (node.Waypoint.CurrentHull == null) { return false; }
272  // Check that there is no unsafe hulls on the way to the target
273  if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
274  return true;
275  //don't stop at ladders when idling
276  }, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
277  if (path.Unreachable)
278  {
279  //can't go to this room, remove it from the list and try another room
280  int index = targetHulls.IndexOf(currentTarget);
281  targetHulls.RemoveAt(index);
282  hullWeights.RemoveAt(index);
284  currentTarget = null;
285  SetTargetTimerLow();
286  return;
287  }
289  PathSteering.SetPath(targetPos, path);
290  SetTargetTimerNormal();
291  searchingNewHull = false;
292  }
293  else
294  {
295  // Couldn't find a valid hull
296  SetTargetTimerHigh();
297  searchingNewHull = false;
298  }
299  }
300  newTargetTimer -= deltaTime;
301  if (!character.IsClimbing && (PathSteering == null || PathSteering.CurrentPath == null || IsSteeringFinished()))
302  {
303  Wander(deltaTime);
304  }
305  else if (currentTarget != null)
306  {
307  PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1,
308  nodeFilter: node => node.Waypoint.CurrentHull != null,
309  endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
310  }
311  else
312  {
315  }
316  }
317  }
318 
319  public void Wander(float deltaTime)
320  {
321  if (character.IsClimbing)
322  {
323  PathSteering.Reset();
324  character.StopClimbing();
325  }
326  var currentHull = character.CurrentHull;
327  if (!character.AnimController.InWater && currentHull != null)
328  {
329  standStillTimer -= deltaTime;
330  if (standStillTimer > 0.0f)
331  {
332  walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
333  if (currentHull.Rect.Width > IndoorsSteeringManager.smallRoomSize / 2 && tooCloseCharacter == null)
334  {
335  foreach (Character c in Character.CharacterList)
336  {
337  if (c == character || !c.IsBot || c.CurrentHull != currentHull || !(c.AIController is HumanAIController humanAI)) { continue; }
338  if (Vector2.DistanceSquared(c.WorldPosition, character.WorldPosition) > 60.0f * 60.0f) { continue; }
339  if ((humanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective && idleObjective.standStillTimer > 0.0f) ||
340  (humanAI.ObjectiveManager.CurrentObjective is AIObjectiveGoTo gotoObjective && gotoObjective.IsCloseEnough))
341  {
342  //if there are characters too close on both sides, don't try to steer away from them
343  //because it'll cause the character to spaz out trying to avoid both
344  if (tooCloseCharacter != null &&
345  Math.Sign(tooCloseCharacter.WorldPosition.X - character.WorldPosition.X) != Math.Sign(c.WorldPosition.X - character.WorldPosition.X))
346  {
347  tooCloseCharacter = null;
348  break;
349  }
350  tooCloseCharacter = c;
351  }
353  }
354  }
355 
356  if (tooCloseCharacter != null && !tooCloseCharacter.Removed && Vector2.DistanceSquared(tooCloseCharacter.WorldPosition, character.WorldPosition) < 50.0f * 50.0f)
357  {
358  Vector2 diff = character.WorldPosition - tooCloseCharacter.WorldPosition;
359  if (diff.LengthSquared() < 0.0001f) { diff = Rand.Vector(1.0f); }
360  if (Math.Abs(diff.X) > 0 &&
361  (character.WorldPosition.X > currentHull.WorldRect.Right - 50 || character.WorldPosition.X < currentHull.WorldRect.Left + 50))
362  {
363  // Between a wall and a character -> move away
364  tooCloseCharacter = null;
365  PathSteering.Reset();
366  standStillTimer = 0;
367  walkDuration = Math.Min(walkDuration, walkDurationMin);
368  if (Behavior != BehaviorType.StayInHull && (currentHull.Size.X < IndoorsSteeringManager.smallRoomSize || currentHull.Size.X < (IndoorsSteeringManager.smallRoomSize / 2 * Character.CharacterList.Count(c => c.CurrentHull == currentHull))))
369  {
370  // Small room -> find another
371  newTargetTimer = Math.Min(newTargetTimer, 1);
372  }
373  }
374  else
375  {
376  character.ReleaseSecondaryItem();
377  PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
378  }
379  return;
380  }
381  else
382  {
383  PathSteering.Reset();
384  tooCloseCharacter = null;
385  }
386 
387  chairCheckTimer -= deltaTime;
388  if (chairCheckTimer <= 0.0f && character.SelectedSecondaryItem == null)
389  {
390  foreach (Item item in Item.ItemList)
391  {
392  if (item.CurrentHull != currentHull || !item.HasTag(Tags.ChairItem)) { continue; }
393  //not possible in vanilla game, but a mod might have holdable/attachable chairs
394  if (item.ParentInventory != null || item.body is { Enabled: true }) { continue; }
395  var controller = item.GetComponent<Controller>();
396  if (controller == null || controller.User != null) { continue; }
397  item.TryInteract(character, forceSelectKey: true);
398  }
399  chairCheckTimer = chairCheckInterval;
400  }
401  return;
402  }
403  if (standStillTimer < -walkDuration)
404  {
405  standStillTimer = Rand.Range(standStillMin, standStillMax);
406  }
407  }
408 
409  PathSteering.Wander(deltaTime);
410  }
411 
412  public void FaceTargetAndWait(ISpatialEntity target, float waitTime)
413  {
414  standStillTimer = waitTime;
416  currentTarget = null;
417  SetTargetTimerHigh();
418  }
419 
420  private void FindTargetHulls()
421  {
422  targetHulls.Clear();
423  hullWeights.Clear();
424  foreach (var hull in Hull.HullList)
425  {
426  if (character.Submarine == null) { break; }
427  if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
428  if (hull.Submarine == null) { continue; }
429  if (hull.Submarine.Info.IsRuin || hull.Submarine.Info.IsWreck) { continue; }
430  if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
431  {
432  if (hull.Submarine.TeamID != character.TeamID)
433  {
434  // Don't allow npcs to idle in a sub that's not in their team (like the player sub)
435  continue;
436  }
437  }
438  else
439  {
440  if (hull.Submarine.TeamID != character.Submarine.TeamID)
441  {
442  // Don't allow to idle in the subs that are not in the same team as the current sub
443  // -> the crew ai bots can't change the sub from outpost to main sub or vice versa on their own
444  continue;
445  }
446  }
447  if (IsForbidden(hull)) { continue; }
448  // Check that the hull is linked
449  if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
450  // Ignore very narrow hulls.
451  if (hull.RectWidth < 200) { continue; }
452  // Ignore hulls that are too low to stand inside.
453  if (character.AnimController is HumanoidAnimController animController)
454  {
455  if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
456  {
457  continue;
458  }
459  }
460  if (!targetHulls.Contains(hull))
461  {
462  targetHulls.Add(hull);
463  float weight = hull.RectWidth;
464  float distanceFactor = GetDistanceFactor(hull.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 2500,
465  factorAtMinDistance: 1, factorAtMaxDistance: 0);
466  if (behavior == BehaviorType.Patrol)
467  {
468  //invert when patrolling (= prefer travelling to far-away hulls)
469  distanceFactor = 1.0f - distanceFactor;
470  }
471  float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2));
472  weight *= distanceFactor * waterFactor;
473  System.Diagnostics.Debug.Assert(weight >= 0);
474  hullWeights.Add(weight);
475  }
476  }
477 
478  if (PreferredOutpostModuleTypes.Any() && character.CurrentHull != null)
479  {
480  for (int i = 0; i < targetHulls.Count; i++)
481  {
482  if (targetHulls[i].OutpostModuleTags.Any(t => PreferredOutpostModuleTypes.Contains(t)))
483  {
484  hullWeights[i] *= Rand.Range(10.0f, 100.0f);
485  }
486  }
487  }
488  }
489 
490  #region Cleaning
491  private readonly float checkItemsInterval = 1;
492  private float checkItemsTimer;
493  private readonly List<Item> itemsToClean = new List<Item>();
494  private readonly List<Item> ignoredItems = new List<Item>();
495 
496  private void CleanupItems(float deltaTime)
497  {
498  if (checkItemsTimer <= 0)
499  {
500  checkItemsTimer = checkItemsInterval * Rand.Range(0.9f, 1.1f);
501  if (character.Submarine is not Submarine sub) { return; }
502  if (sub.TeamID != character.TeamID) { return; }
503  if (character.CurrentHull is not Hull currentHull) { return; }
504  itemsToClean.Clear();
505  foreach (Item item in Item.CleanableItems)
506  {
507  if (item.CurrentHull != currentHull) { continue; }
508  if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
509  {
510  itemsToClean.Add(item);
511  }
512  }
513  if (itemsToClean.Any())
514  {
515  var targetItem = itemsToClean.MinBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X));
516  if (targetItem != null)
517  {
518  var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier);
519  cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem);
520  subObjectives.Add(cleanupObjective);
521  }
522  }
523  }
524  else
525  {
526  checkItemsTimer -= deltaTime;
527  }
528  }
529  #endregion
530 
531  public static bool IsForbidden(Hull hull) => hull == null || hull.AvoidStaying;
532 
533  public override void Reset()
534  {
535  base.Reset();
536  currentTarget = null;
537  searchingNewHull = false;
538  tooCloseCharacter = null;
539  targetHulls.Clear();
540  hullWeights.Clear();
541  checkItemsTimer = 0;;
542  itemsToClean.Clear();
543  ignoredItems.Clear();
544  autonomousObjectiveRetryTimer = 10;
545  timerMargin = 0;
546  newTargetTimer = 0;
547  }
548 
549  public override void OnDeselected()
550  {
551  base.OnDeselected();
552  foreach (var subObjective in SubObjectives)
553  {
554  if (subObjective is AIObjectiveCleanupItem cleanUpObjective)
555  {
556  cleanUpObjective.DropTarget();
557  }
558  }
559  }
560  }
561 }
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 CheckObjectiveState()
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