Server 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 }
virtual void SelectTarget(AITarget target)
void FaceTarget(ISpatialEntity target)
float CalculatePriority()
Call this only when the priority needs to be recalculated. Use the cached Priority property when you ...
Definition: AIObjective.cs:314
float Priority
Final priority value after all calculations.
Definition: AIObjective.cs:84
readonly Character character
Definition: AIObjective.cs:112
IndoorsSteeringManager PathSteering
Definition: AIObjective.cs:173
readonly AIObjectiveManager objectiveManager
Definition: AIObjective.cs:113
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)
Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos=null)
Item? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
AITarget AiTarget
Definition: Entity.cs:55
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static readonly List< Hull > HullList
readonly HashSet< Hull > UnsafeHulls
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
Vector2? TargetMovement
Definition: Ragdoll.cs:298
List< WayPoint > Nodes