Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Characters/AI/AIController.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 {
11  abstract partial class AIController : ISteerable
12  {
13  public bool Enabled;
14 
15  public readonly Character Character;
16 
17  // Update only when the value changes, not when it keeps the same.
19  // Updated each time the value is updated (also when the value is the same).
23  {
24  get { return _selectedAiTarget; }
25  protected set
26  {
28  _selectedAiTarget = value;
30  {
31  if (_previousAiTarget != null)
32  {
34  if (_selectedAiTarget != null)
35  {
37  {
38  if (i.IsOwnedBy(c)) { return; }
39  }
41  {
42  if (it.IsOwnedBy(ch)) { return; }
43  }
44  }
45  }
47  }
48  }
49  }
50 
52 
54  {
55  get { return steeringManager; }
56  }
57 
58  public Vector2 Steering
59  {
61  set { Character.AnimController.TargetMovement = value; }
62  }
63 
64  public Vector2 SimPosition
65  {
66  get { return Character.SimPosition; }
67  }
68 
69  public Vector2 WorldPosition
70  {
71  get { return Character.WorldPosition; }
72  }
73 
74  public Vector2 Velocity
75  {
77  }
78 
80  {
82  }
83 
84  public virtual bool CanFlip
85  {
86  get { return true; }
87  }
88 
89  public virtual bool IsMentallyUnstable => false;
90 
91  private IEnumerable<Hull> visibleHulls;
92  private float hullVisibilityTimer;
93  const float hullVisibilityInterval = 0.5f;
94 
99  public IEnumerable<Hull> VisibleHulls
100  {
101  get
102  {
103  visibleHulls ??= Character.GetVisibleHulls();
104  return visibleHulls;
105  }
106  private set
107  {
108  visibleHulls = value;
109  }
110  }
111 
119  public bool HasValidPath(bool requireNonDirty = true, bool requireUnfinished = true, Func<WayPoint, bool> nodePredicate = null)
120  {
121  if (SteeringManager is not IndoorsSteeringManager pathSteering) { return false; }
122  if (pathSteering.CurrentPath == null) { return false; }
123  if (pathSteering.CurrentPath.Unreachable) { return false; }
124  if (requireUnfinished && pathSteering.CurrentPath.Finished) { return false; }
125  if (requireNonDirty && pathSteering.IsPathDirty) { return false; }
126  if (nodePredicate != null)
127  {
128  return pathSteering.CurrentPath.Nodes.All(n => nodePredicate(n));
129  }
130  return true;
131  }
132 
134  public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable;
135  public bool IsCurrentPathFinished => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Finished;
136 
137  protected readonly float colliderWidth;
138  protected readonly float minGapSize;
139  protected readonly float colliderLength;
140  protected readonly float avoidLookAheadDistance;
141 
143  {
144  Character = c;
145  hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer);
146  Enabled = true;
148  colliderWidth = size.X;
149  colliderLength = size.Y;
150  avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f);
151  minGapSize = ConvertUnits.ToDisplayUnits(Math.Min(colliderWidth, colliderLength));
152  }
153 
154  public virtual void OnHealed(Character healer, float healAmount) { }
155 
156  public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
157 
158  public virtual void SelectTarget(AITarget target) { }
159 
160  public virtual void Update(float deltaTime)
161  {
162  if (hullVisibilityTimer > 0)
163  {
164  hullVisibilityTimer--;
165  }
166  else
167  {
168  hullVisibilityTimer = hullVisibilityInterval;
170  }
171  }
172 
173  public virtual void Reset()
174  {
175  ResetAITarget();
176  }
177 
178  protected void ResetAITarget()
179  {
180  _lastAiTarget = null;
181  _selectedAiTarget = null;
182  }
183 
184  public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
185 
186  public bool IsSteeringThroughGap { get; protected set; }
187  public bool IsTryingToSteerThroughGap { get; protected set; }
188 
189  public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
190  {
191  if (wall == null) { return false; }
192  if (section == null) { return false; }
193  Gap gap = section.gap;
194  if (gap == null) { return false; }
195  float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height);
196  if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; }
197  Hull targetHull = gap.FlowTargetHull;
198  if (targetHull == null) { return false; }
199  if (wall.IsHorizontal)
200  {
201  targetWorldPos.Y = targetHull.WorldRect.Y - targetHull.Rect.Height / 2;
202  }
203  else
204  {
205  targetWorldPos.X = targetHull.WorldRect.Center.X;
206  }
207  return SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance: -1);
208  }
209 
210  public virtual bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1)
211  {
212  Hull targetHull = gap.FlowTargetHull;
213  if (targetHull == null) { return false; }
214  if (maxDistance > 0)
215  {
216  if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; }
217  }
218  if (SteeringManager is IndoorsSteeringManager pathSteering)
219  {
220  pathSteering.ResetPath();
221  }
222  SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetWorldPos - Character.WorldPosition));
223  return true;
224  }
225 
226  public bool CanPassThroughHole(Structure wall, int sectionIndex, int requiredHoleCount)
227  {
228  if (!wall.SectionBodyDisabled(sectionIndex)) { return false; }
229  int holeCount = 1;
230  for (int j = sectionIndex - 1; j > sectionIndex - requiredHoleCount; j--)
231  {
232  if (wall.SectionBodyDisabled(j))
233  {
234  holeCount++;
235  }
236  else
237  {
238  break;
239  }
240  }
241  for (int j = sectionIndex + 1; j < sectionIndex + requiredHoleCount; j++)
242  {
243  if (wall.SectionBodyDisabled(j))
244  {
245  holeCount++;
246  }
247  else
248  {
249  break;
250  }
251  }
252  return holeCount >= requiredHoleCount;
253  }
254 
255  protected bool IsWallDisabled(Structure wall)
256  {
257  bool isDisabled = true;
258  for (int i = 0; i < wall.Sections.Length; i++)
259  {
260  if (!wall.SectionBodyDisabled(i))
261  {
262  isDisabled = false;
263  break;
264  }
265  }
266  return isDisabled;
267  }
268 
269  private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
270  public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false, IEnumerable<Identifier> targetTags = null)
271  {
272  var pickable = item.GetComponent<Pickable>();
273  if (pickable == null) { return false; }
274  if (wear)
275  {
276  var wearable = item.GetComponent<Wearable>();
277  if (wearable != null)
278  {
279  pickable = wearable;
280  }
281  }
282  else
283  {
284  // Not allowed to wear -> don't use the Wearable component even when it's found.
285  pickable = item.GetComponent<Holdable>();
286  }
287  if (item.ParentInventory is ItemInventory itemInventory)
288  {
289  if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
290  }
291  if (equip && pickable != null)
292  {
293  int targetSlot = -1;
294  //check if all the slots required by the item are free
295  foreach (InvSlotType slots in pickable.AllowedSlots)
296  {
297  if (slots.HasFlag(InvSlotType.Any)) { continue; }
298  if (!wear)
299  {
300  if (slots != InvSlotType.RightHand && slots != InvSlotType.LeftHand && slots != (InvSlotType.RightHand | InvSlotType.LeftHand))
301  {
302  // Don't allow other than hand slots if not allowed to wear.
303  continue;
304  }
305  }
306  for (int i = 0; i < targetInventory.Capacity; i++)
307  {
308  if (targetInventory is CharacterInventory characterInventory)
309  {
310  //slot not needed by the item, continue
311  if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; }
312  }
313  targetSlot = i;
314  //slot free, continue
315  var otherItem = targetInventory.GetItemAt(i);
316  if (otherItem == null) { continue; }
317  //try to move the existing item to LimbSlot.Any and continue if successful
318  if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.AnySlot))
319  {
320  if (storeUnequipped && targetInventory.Owner == Character)
321  {
322  unequippedItems.Add(otherItem);
323  }
324  continue;
325  }
326  if (dropOtherIfCannotMove)
327  {
328  if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
329  {
330  bool switchingToBetterSuit =
331  targetTags != null &&
332  targetTags.FirstOrDefault() == Tags.HeavyDivingGear &&
334  !AIObjectiveFindDivingGear.IsSuitablePressureProtection(otherItem, Tags.HeavyDivingGear, Character);
335  // Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items
336  // of the same type and if can't fit them all in the inventory.
337  if (!switchingToBetterSuit)
338  {
339  return false;
340  }
341  }
342  //if everything else fails, simply drop the existing item
343  otherItem.Drop(Character);
344  }
345  }
346  }
347  if (targetSlot < 0) { return false; }
348  //the item should always stay in the Any slot if it's containable in one
349  if (pickable.AllowedSlots.Contains(InvSlotType.Any))
350  {
351  targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot);
352  }
353  return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character);
354  }
355  else
356  {
357  return targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot);
358  }
359  }
360 
361  public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea);
362 
363  public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true, int? unequipMax = null) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea, unequipMax);
364 
365  public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea);
366 
367  public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
368  {
369  var inventory = parentItem.OwnInventory;
370  if (inventory == null || !inventory.Container.DrawInventory) { return; }
371  int removed = 0;
372  if (predicate == null || inventory.AllItems.Any(predicate))
373  {
374  foreach (Item containedItem in inventory.AllItemsMod)
375  {
376  if (containedItem == null) { continue; }
377  if (predicate == null || predicate(containedItem))
378  {
379  if (avoidDroppingInSea && !character.IsInFriendlySub)
380  {
381  // If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
382  if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
383  {
384  if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
385  continue;
386  }
387  }
388  containedItem.Drop(character);
389  if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
390  }
391  }
392  }
393  }
394 
395  public void ReequipUnequipped()
396  {
397  foreach (var item in unequippedItems)
398  {
399  if (item != null && !item.Removed && Character.HasItem(item))
400  {
401  TakeItem(item, Character.Inventory, equip: true, wear: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
402  }
403  }
404  unequippedItems.Clear();
405  }
406 
407  #region Escape
408  public abstract bool Escape(float deltaTime);
409 
410  public Gap EscapeTarget { get; private set; }
411 
412  private readonly float escapeTargetSeekInterval = 2;
413  private float escapeTimer;
414  protected bool allGapsSearched;
415  protected readonly HashSet<Gap> unreachableGaps = new HashSet<Gap>();
416  protected bool UpdateEscape(float deltaTime, bool canAttackDoors)
417  {
419  if (allGapsSearched)
420  {
421  escapeTimer -= deltaTime;
422  if (escapeTimer <= 0)
423  {
424  allGapsSearched = false;
425  }
426  }
427  if (Character.CurrentHull != null && pathSteering != null)
428  {
429  // Seek exit if inside
430  if (!allGapsSearched)
431  {
432  float closestDistance = 0;
433  foreach (Gap gap in Gap.GapList)
434  {
435  if (gap == null || gap.Removed) { continue; }
436  if (EscapeTarget == gap) { continue; }
437  if (unreachableGaps.Contains(gap)) { continue; }
438  if (gap.Submarine != Character.Submarine) { continue; }
439  if (gap.IsRoomToRoom) { continue; }
440  float multiplier = 1;
441  var door = gap.ConnectedDoor;
442  if (door != null)
443  {
444  if (!pathSteering.CanAccessDoor(door))
445  {
446  continue;
447  }
448  }
449  else
450  {
451  if (gap.Open < 1) { continue; }
452  if (gap.Size < minGapSize) { continue; }
453  }
455  {
456  // If the gap is in the same room, it's close enough.
457  EscapeTarget = gap;
458  break;
459  }
460  float distance = Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) * multiplier;
461  if (EscapeTarget == null || distance < closestDistance)
462  {
463  EscapeTarget = gap;
464  closestDistance = distance;
465  }
466  }
467  allGapsSearched = true;
468  escapeTimer = escapeTargetSeekInterval;
469  }
471  {
473  {
475  EscapeTarget = null;
476  allGapsSearched = false;
477  }
478  }
479  }
480  if (EscapeTarget != null)
481  {
482  var door = EscapeTarget.ConnectedDoor;
483  bool isClosedDoor = door != null && door.IsClosed;
485  float sqrDist = diff.LengthSquared();
486  bool isClose = sqrDist < MathUtils.Pow2(100);
487  if (Character.CurrentHull == null || (isClose && !isClosedDoor) || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished)
488  {
489  // Very close to the target, outside, or at the end of the path -> try to steer through the gap
492  pathSteering?.ResetPath();
493  Vector2 dir = Vector2.Normalize(diff);
494  if (Character.CurrentHull == null || isClose)
495  {
496  // Outside -> steer away from the target
497  if (EscapeTarget.FlowTargetHull != null)
498  {
500  }
501  else
502  {
503  SteeringManager.SteeringManual(deltaTime, -dir);
504  }
505  }
506  else
507  {
508  // Still inside -> steer towards the target
509  SteeringManager.SteeringManual(deltaTime, dir);
510  }
511  return sqrDist < MathUtils.Pow2(250);
512  }
513  else if (pathSteering != null)
514  {
515  pathSteering.SteeringSeek(EscapeTarget.SimPosition, weight: 1, minGapSize);
516  }
517  else
518  {
520  }
521  }
522  else
523  {
524  // Can't find the target
525  EscapeTarget = null;
526  allGapsSearched = false;
527  unreachableGaps.Clear();
528  }
529  return false;
530  }
531 
532  public void ResetEscape()
533  {
534  EscapeTarget = null;
535  allGapsSearched = false;
536  unreachableGaps.Clear();
537  }
538 
539  #endregion
540 
541  protected virtual void OnStateChanged(AIState from, AIState to) { }
542  protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
543  }
544 }
static void UnequipContainedItems(Character character, Item parentItem, Func< Item, bool > predicate, bool avoidDroppingInSea=true, int? unequipMax=null)
void UnequipContainedItems(Item parentItem, Func< Item, bool > predicate=null, bool avoidDroppingInSea=true, int? unequipMax=null)
virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget)
void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea=true)
virtual void OnHealed(Character healer, float healAmount)
bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear=false, bool dropOtherIfCannotMove=true, bool allowSwapping=false, bool storeUnequipped=false, IEnumerable< Identifier > targetTags=null)
virtual void OnAttacked(Character attacker, AttackResult attackResult)
IEnumerable< Hull > VisibleHulls
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
bool UpdateEscape(float deltaTime, bool canAttackDoors)
virtual bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance=-1)
bool CanPassThroughHole(Structure wall, int sectionIndex, int requiredHoleCount)
bool HasValidPath(bool requireNonDirty=true, bool requireUnfinished=true, Func< WayPoint, bool > nodePredicate=null)
Is the current path valid, using the provided parameters.
static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea=true)
void FaceTarget(ISpatialEntity target)
abstract bool Escape(float deltaTime)
static bool IsSuitablePressureProtection(Item item, Identifier tag, Character character)
bool HasItem(Item item, bool requireEquipped=false, InvSlotType? slotType=null)
List< Hull > GetVisibleHulls()
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
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
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
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)
bool CanAccessDoor(Door door, Func< Controller, bool > buttonFilter=null)
Item GetItemAt(int index)
Get the item stored in the specified inventory slot. If the slot contains a stack of items,...
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
readonly Identifier Identifier
Definition: Prefab.cs:34
void SteeringManual(float deltaTime, Vector2 velocity)
void SteeringSeek(Vector2 targetSimPos, float weight=1)