Client LuaCsForBarotrauma
AIObjectiveGoTo.cs
1 using Microsoft.Xna.Framework;
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
7 
8 namespace Barotrauma
9 {
11  {
12  public override Identifier Identifier { get; set; } = "go to".ToIdentifier();
13 
14  public override bool KeepDivingGearOn => GetTargetHull() == null;
15 
16  private AIObjectiveFindDivingGear findDivingGear;
17  private readonly bool repeat;
18  //how long until the path to the target is declared unreachable
19  private float waitUntilPathUnreachable;
20  private readonly bool getDivingGearIfNeeded;
21 
25  public Func<bool> requiredCondition;
26  public Func<PathNode, bool> endNodeFilter;
27 
28  public Func<float> PriorityGetter;
29 
30  public bool IsFollowOrder;
31  public bool IsWaitOrder;
32  public bool Mimic;
33 
34  public bool SpeakIfFails { get; set; } = true;
35  public bool DebugLogWhenFails { get; set; } = true;
36  public bool UsePathingOutside { get; set; } = true;
37 
40  private float _closeEnoughMultiplier = 1;
41  public float CloseEnoughMultiplier
42  {
43  get { return _closeEnoughMultiplier; }
44  set { _closeEnoughMultiplier = Math.Max(value, 1); }
45  }
46  private float _closeEnough = 50;
47  private readonly float minDistance = 50;
48  private readonly float seekGapsInterval = 1;
49  private float seekGapsTimer;
50  private bool cantFindDivingGear;
51 
55  public float CloseEnough
56  {
57  get
58  {
59  if (IsFollowOrder && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
60  {
61  // Keep close when the target is going inside/outside
62  return minDistance;
63  }
64  float dist = _closeEnough * CloseEnoughMultiplier;
65  float extraMultiplier = Math.Clamp(CloseEnoughMultiplier * 0.6f, 1, 3);
67  {
68  dist += ExtraDistanceWhileSwimming * extraMultiplier;
69  }
70  if (character.CurrentHull == null)
71  {
72  dist += ExtraDistanceOutsideSub * extraMultiplier;
73  }
74  return dist;
75  }
76  set
77  {
78  _closeEnough = Math.Max(minDistance, value);
79  }
80  }
81  public bool IgnoreIfTargetDead { get; set; }
82  public bool AllowGoingOutside { get; set; }
83 
84  public bool FaceTargetOnCompleted { get; set; } = true;
85 
86  public bool AlwaysUseEuclideanDistance { get; set; } = true;
87 
91  public bool UseDistanceRelativeToAimSourcePos { get; set; } = false;
92 
93  public override bool AbandonWhenCannotCompleteSubObjectives => false;
94 
95  protected override bool AllowOutsideSubmarine => AllowGoingOutside;
96  protected override bool AllowInAnySub => true;
97 
98  public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
99  private readonly Identifier ExoSuitRefuel = "dialog.exosuit.refuel".ToIdentifier();
100  private readonly Identifier ExoSuitOutOfFuel = "dialog.exosuit.outoffuel".ToIdentifier();
101 
102  public LocalizedString TargetName { get; set; }
103 
104  public ISpatialEntity Target { get; private set; }
105 
106  public float? OverridePriority = null;
107 
108  public Func<bool> SpeakCannotReachCondition { get; set; }
109 
110  protected override float GetPriority()
111  {
112  bool isOrder = objectiveManager.IsOrder(this);
113  if (!IsAllowed)
114  {
115  Priority = 0;
116  Abandon = !isOrder;
117  return Priority;
118  }
119  if (Target == null || Target is Entity e && e.Removed)
120  {
121  Priority = 0;
122  Abandon = !isOrder;
123  }
125  {
126  Priority = 0;
127  Abandon = !isOrder;
128  }
129  else
130  {
131  if (PriorityGetter != null)
132  {
134  }
135  else if (OverridePriority.HasValue)
136  {
137  Priority = OverridePriority.Value;
138  }
139  else
140  {
141  Priority = isOrder ? objectiveManager.GetOrderPriority(this) : 10;
142  }
143  }
144  return Priority;
145  }
146 
147  private readonly float avoidLookAheadDistance = 5;
148  private readonly float pathWaitingTime = 3;
149 
150  public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
151  : base(character, objectiveManager, priorityModifier)
152  {
153  Target = target;
154  this.repeat = repeat;
155  waitUntilPathUnreachable = pathWaitingTime;
156  this.getDivingGearIfNeeded = getDivingGearIfNeeded;
157  if (Target is Item i)
158  {
159  CloseEnough = Math.Max(CloseEnough, i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2);
160  }
161  else if (Target is Character)
162  {
163  //if closeEnough value is given, allow setting CloseEnough as low as 50, otherwise above AIObjectiveGetItem.DefaultReach
164  CloseEnough = Math.Max(closeEnough, MathUtils.NearlyEqual(closeEnough, 0.0f) ? AIObjectiveGetItem.DefaultReach : minDistance);
165  }
166  else
167  {
168  CloseEnough = closeEnough;
169  }
170  }
171 
172  private void SpeakCannotReach()
173  {
174 #if DEBUG
175  if (DebugLogWhenFails)
176  {
177  DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
178  }
179 #endif
180  if (!character.IsOnPlayerTeam) { return; }
182  if (DialogueIdentifier == null) { return; }
183  if (!SpeakIfFails) { return; }
184  if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; }
185  LocalizedString msg = TargetName == null ?
186  TextManager.Get(DialogueIdentifier) :
187  TextManager.GetWithVariable(DialogueIdentifier, "[name]".ToIdentifier(), TargetName, formatCapitals: Target is Character ? FormatCapitals.No : FormatCapitals.Yes);
188  if (msg.IsNullOrEmpty() || !msg.Loaded) { return; }
189  character.Speak(msg.Value, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
190  }
191 
192  public void ForceAct(float deltaTime) => Act(deltaTime);
193 
194  protected override void Act(float deltaTime)
195  {
196  if (Target == null)
197  {
198  Abandon = true;
199  return;
200  }
201  if (checkExoSuitTimer <= 0)
202  {
203  checkExoSuitTimer = CheckExoSuitTime * Rand.Range(0.9f, 1.1f);
204  if (character.GetEquippedItem(Tags.PoweredDivingSuit, InvSlotType.OuterClothes) is { OwnInventory: Inventory exoSuitInventory } exoSuit &&
205  exoSuit.GetComponent<Powered>() is not { HasPower: true })
206  {
207  if (HumanAIController.HasItem(character, Tags.DivingSuitFuel, out IEnumerable<Item> fuelRods, conditionPercentage: 1, recursive: true))
208  {
209  // Try to switch the fuel sources
211  {
212  character.Speak(TextManager.Get(ExoSuitRefuel).Value, minDurationBetweenSimilar: 10f, identifier: ExoSuitRefuel);
213  }
214  // Have to copy the list, because it's modified when we unequip the item.
215  foreach (Item containedItem in exoSuit.ContainedItems.ToList())
216  {
217  if (containedItem.HasTag(Tags.DivingSuitFuel) && containedItem.Condition <= 0)
218  {
219  character.Unequip(containedItem);
220  }
221  }
222  // Refuel
223  // The information about the target slot is defined in a status effect. We could parse it, but let's keep it simple and just presume that the target slot is the second slot, as it the case with the vanilla exosuits.
224  const int targetSlot = 1;
225  Item fuelRod = fuelRods.MaxBy(b => b.Condition);
226  exoSuitInventory.TryPutItem(fuelRod, targetSlot, allowSwapping: true, allowCombine: true, user: character);
227  }
228  else if (character.IsOnPlayerTeam)
229  {
230  character.Speak(TextManager.Get(ExoSuitOutOfFuel).Value, minDurationBetweenSimilar: 30.0f, identifier: ExoSuitOutOfFuel);
231  }
232  }
233  }
234  else
235  {
236  checkExoSuitTimer -= deltaTime;
237  }
239  {
240  // Wait
242  return;
243  }
244  character.SelectedItem = null;
246  {
248  }
249  if (Target is Entity e)
250  {
251  if (e.Removed)
252  {
253  Abandon = true;
254  return;
255  }
256  else
257  {
258  character.AIController.SelectTarget(e.AiTarget);
259  }
260  }
261  Hull targetHull = GetTargetHull();
262  if (!IsFollowOrder)
263  {
264  // Abandon if going through unsafe paths or targeting unsafe hulls.
265  bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull);
267  {
268  // Wait orders check this so that the bot temporarily leaves the unsafe hull.
269  // Non-orders (that are not set to ignore the unsafe hulls) abandon. In practice this means e.g. repair and clean up item subobjectives (of the looping parent objective).
270  // Other orders are only abandoned if the hull is unreachable, because the path is invalid or not found at all.
272  {
273  if (HumanAIController.UnsafeHulls.Contains(targetHull))
274  {
275  isUnreachable = true;
277  }
278  else if (PathSteering?.CurrentPath != null)
279  {
280  foreach (WayPoint wp in PathSteering.CurrentPath.Nodes)
281  {
282  if (wp.CurrentHull == null) { continue; }
283  if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull))
284  {
285  isUnreachable = true;
287  }
288  }
289  }
290  }
291  }
292  if (isUnreachable)
293  {
295  if (PathSteering?.CurrentPath != null)
296  {
298  }
299  if (repeat)
300  {
301  SpeakCannotReach();
302  }
303  else
304  {
305  Abandon = true;
306  }
307  return;
308  }
309  }
310  bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
311  bool isInside = character.CurrentHull != null;
312  bool hasOutdoorNodes = insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes;
313  if (isInside && hasOutdoorNodes && !AllowGoingOutside)
314  {
315  Abandon = true;
316  }
318  {
319  waitUntilPathUnreachable -= deltaTime;
321  {
323  if (waitUntilPathUnreachable < 0)
324  {
325  waitUntilPathUnreachable = pathWaitingTime;
326  if (repeat && !IsCompleted)
327  {
328  if (!IsDoneFollowing())
329  {
330  SpeakCannotReach();
331  }
332  }
333  else
334  {
335  Abandon = true;
336  }
337  }
338  }
339  else if (HumanAIController.HasValidPath(requireUnfinished: false))
340  {
341  waitUntilPathUnreachable = pathWaitingTime;
342  }
343  }
344  if (Abandon) { return; }
345  if (getDivingGearIfNeeded)
346  {
347  Character followTarget = Target as Character;
348  bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
349  bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
350  bool tryToGetDivingSuit = needsDivingSuit;
352  {
353  if (HumanAIController.HasDivingSuit(followTarget))
354  {
355  tryToGetDivingGear = true;
356  tryToGetDivingSuit = true;
357  }
359  {
360  tryToGetDivingGear = true;
361  }
362  }
363  bool needsEquipment = false;
365  if (tryToGetDivingSuit)
366  {
367  needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
368  }
369  else if (tryToGetDivingGear)
370  {
371  needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
372  }
373  if (character.LockHands)
374  {
375  cantFindDivingGear = true;
376  }
377  if (cantFindDivingGear && needsDivingSuit)
378  {
379  // Don't try to reach the target without a suit because it's lethal.
380  Abandon = true;
381  return;
382  }
383  if (needsEquipment && !cantFindDivingGear)
384  {
386  TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
387  onAbandon: () =>
388  {
389  cantFindDivingGear = true;
390  if (needsDivingSuit)
391  {
392  // Shouldn't try to reach the target without a suit, because it's lethal.
393  Abandon = true;
394  }
395  else
396  {
397  // Try again without requiring the diving suit (or mask)
398  RemoveSubObjective(ref findDivingGear);
399  TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager),
400  onAbandon: () =>
401  {
402  Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
403  RemoveSubObjective(ref findDivingGear);
404  },
405  onCompleted: () =>
406  {
407  RemoveSubObjective(ref findDivingGear);
408  });
409  }
410  },
411  onCompleted: () => RemoveSubObjective(ref findDivingGear));
412  return;
413  }
414  }
415  if (IsDoneFollowing())
416  {
417  OnCompleted();
418  return;
419  }
420  float maxGapDistance = 500;
421  Character targetCharacter = Target as Character;
423  {
424  if (character.CurrentHull == null ||
425  IsFollowOrder &&
426  targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
427  Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
428  {
429  if (seekGapsTimer > 0)
430  {
431  seekGapsTimer -= deltaTime;
432  }
433  else
434  {
435  bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
436  bool isEitherOneInside = isInside || Target.Submarine != null;
437  if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath()))
438  {
439  SeekGaps(maxGapDistance);
440  seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
441  if (TargetGap != null)
442  {
443  // Check that nothing is blocking the way
444  Vector2 rayStart = character.SimPosition;
445  Vector2 rayEnd = TargetGap.SimPosition;
446  if (TargetGap.Submarine != null && character.Submarine == null)
447  {
448  rayStart -= TargetGap.Submarine.SimPosition;
449  }
450  else if (TargetGap.Submarine == null && character.Submarine != null)
451  {
452  rayEnd -= character.Submarine.SimPosition;
453  }
454  var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
455  if (closestBody != null)
456  {
457  TargetGap = null;
458  }
459  }
460  }
461  else
462  {
463  TargetGap = null;
464  }
465  }
466  }
467  else
468  {
469  TargetGap = null;
470  }
471  if (TargetGap != null)
472  {
474  {
475  SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
476  return;
477  }
478  else
479  {
480  TargetGap = null;
481  }
482  }
483  if (checkScooterTimer <= 0)
484  {
485  useScooter = false;
486  checkScooterTimer = CheckScooterTime * Rand.Range(0.9f, 1.1f);
487  Item scooter = null;
488  bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(Tags.Scooter, allowBroken: false);
489  if (!shouldUseScooter)
490  {
491  float threshold = 500;
492  if (isInside)
493  {
494  Vector2 diff = Target.WorldPosition - character.WorldPosition;
495  shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150;
496  }
497  else
498  {
499  shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
500  }
501  }
502  if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
503  {
504  // Currently equipped scooter
505  scooter = equippedScooters.FirstOrDefault();
506  }
507  else if (shouldUseScooter)
508  {
509  bool hasHandsFull = character.HasHandsFull(out (Item leftHandItem, Item rightHandItem) items);
510  if (hasHandsFull)
511  {
512  hasHandsFull = !character.TryPutItemInAnySlot(items.leftHandItem) &&
513  !character.TryPutItemInAnySlot(items.rightHandItem) &&
514  !character.TryPutItemInBag(items.leftHandItem) &&
515  !character.TryPutItemInBag(items.rightHandItem);
516  }
517  if (!hasHandsFull)
518  {
519  bool hasBattery = false;
520  if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScootersWithBattery, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false))
521  {
522  scooter = nonEquippedScootersWithBattery.FirstOrDefault();
523  hasBattery = true;
524  }
525  else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScootersWithoutBattery, requireEquipped: false))
526  {
527  scooter = nonEquippedScootersWithoutBattery.FirstOrDefault();
528  // Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
529  hasBattery = HumanAIController.HasItem(character, Tags.MobileBattery, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
530  }
531  if (scooter != null && hasBattery)
532  {
533  // Equip only if we have a battery available
534  HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
535  }
536  }
537  }
538  if (scooter != null && character.HasEquippedItem(scooter))
539  {
540  if (shouldUseScooter)
541  {
542  useScooter = true;
543  // Check the battery
544  if (scooter.ContainedItems.None(i => i.Condition > 0))
545  {
546  // Try to switch batteries
547  if (HumanAIController.HasItem(character, Tags.MobileBattery, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
548  {
549  scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot));
550  if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
551  {
552  useScooter = false;
553  }
554  }
555  else
556  {
557  useScooter = false;
558  }
559  }
560  }
561  if (!useScooter)
562  {
564  }
565  }
566  }
567  else
568  {
569  checkScooterTimer -= deltaTime;
570  }
571  }
572  else
573  {
574  TargetGap = null;
575  useScooter = false;
576  checkScooterTimer = 0;
577  }
579  {
580  Vector2 targetPos = character.GetRelativeSimPosition(Target);
581  Func<PathNode, bool> nodeFilter = null;
582  if (isInside && !AllowGoingOutside)
583  {
584  nodeFilter = n => n.Waypoint.CurrentHull != null;
585  }
586  else if (!isInside)
587  {
589  {
590  nodeFilter = n => n.Waypoint.Submarine == null;
591  }
592  else
593  {
594  nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null;
595  }
596  }
597  if (!isInside && !UsePathingOutside)
598  {
602  {
603  SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
604  }
605  }
606  else
607  {
608  PathSteering.SteeringSeek(targetPos, weight: 1,
609  startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
611  nodeFilter: nodeFilter,
612  checkVisiblity: Target is Item || Target is Character);
613  }
615  {
616  if (useScooter)
617  {
618  UseScooter(Target.WorldPosition);
619  }
620  else
621  {
625  {
626  SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
627  }
628  }
629  }
630  else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
631  {
633  }
634  }
635  else
636  {
637  if (useScooter)
638  {
639  UseScooter(Target.WorldPosition);
640  }
641  else
642  {
646  {
647  SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
648  }
649  }
650  }
651 
652  void UseScooter(Vector2 targetWorldPos)
653  {
654  if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; }
657  character.CursorPosition = targetWorldPos;
658  if (character.Submarine != null)
659  {
661  }
662  Vector2 diff = character.CursorPosition - character.Position;
663  Vector2 dir = Vector2.Normalize(diff);
664  if (character.CurrentHull == null && IsFollowOrder)
665  {
666  float sqrDist = diff.LengthSquared();
667  if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f))
668  {
669  SteeringManager.SteeringManual(1.0f, dir);
670  }
671  else
672  {
673  float dot = Vector2.Dot(dir, VectorExtensions.Forward(character.AnimController.Collider.Rotation + MathHelper.PiOver2));
674  bool isFacing = dot > 0.9f;
675  if (!isFacing && sqrDist > MathUtils.Pow2(CloseEnough))
676  {
677  SteeringManager.SteeringManual(1.0f, dir);
678  }
679  }
680  }
681  else
682  {
683  SteeringManager.SteeringManual(1.0f, dir);
684  }
685  character.SetInput(InputType.Aim, false, true);
686  character.SetInput(InputType.Shoot, false, true);
687  }
688 
689  bool IsDoneFollowing()
690  {
691  if (repeat && IsCloseEnough)
692  {
693  if (requiredCondition == null || requiredCondition())
694  {
696  {
697  return true;
698  }
699  }
700  }
701  return false;
702  }
703  }
704 
705  private bool useScooter;
706  private float checkScooterTimer;
707  private const float CheckScooterTime = 0.5f;
708 
709  private float checkExoSuitTimer;
710  private const float CheckExoSuitTime = 2.0f;
711 
713 
714  public static Hull GetTargetHull(ISpatialEntity target)
715  {
716  if (target is Hull h)
717  {
718  return h;
719  }
720  else if (target is Item i)
721  {
722  return i.CurrentHull;
723  }
724  else if (target is Character c)
725  {
726  return c.CurrentHull ?? c.AnimController.CurrentHull;
727  }
728  else if (target is Structure structure)
729  {
730  return Hull.FindHull(structure.Position, useWorldCoordinates: false);
731  }
732  else if (target is Gap g)
733  {
734  return g.FlowTargetHull;
735  }
736  else if (target is WayPoint wp)
737  {
738  return wp.CurrentHull;
739  }
740  else if (target is FireSource fs)
741  {
742  return fs.Hull;
743  }
744  else if (target is OrderTarget ot)
745  {
746  return ot.Hull;
747  }
748  return null;
749  }
750 
751  public Gap TargetGap { get; private set; }
752  private void SeekGaps(float maxDistance)
753  {
754  Gap selectedGap = null;
755  float selectedDistance = -1;
756  Vector2 toTargetNormalized = Vector2.Normalize(Target.WorldPosition - character.WorldPosition);
757  foreach (Gap gap in Gap.GapList)
758  {
759  if (gap.Open < 1) { continue; }
760  if (gap.Submarine == null) { continue; }
761  if (!IsFollowOrder)
762  {
763  if (gap.FlowTargetHull == null) { continue; }
764  if (gap.Submarine != Target.Submarine) { continue; }
765  }
766  Vector2 toGap = gap.WorldPosition - character.WorldPosition;
767  if (Vector2.Dot(Vector2.Normalize(toGap), toTargetNormalized) < 0) { continue; }
768  float squaredDistance = toGap.LengthSquared();
769  if (squaredDistance > maxDistance * maxDistance) { continue; }
770  if (selectedGap == null || squaredDistance < selectedDistance)
771  {
772  selectedGap = gap;
773  selectedDistance = squaredDistance;
774  }
775  }
776  TargetGap = selectedGap;
777  }
778 
779  public bool IsCloseEnough
780  {
781  get
782  {
783  if (character.IsClimbing)
784  {
786  {
788  {
789  // The target is still above us
790  return false;
791  }
793  {
794  // Going through a hatch
795  return false;
796  }
797  }
798  }
800  {
801  float yDist = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y);
802  if (yDist > CloseEnough) { return false; }
803  float xDist = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X);
804  return xDist <= CloseEnough;
805  }
807  return Vector2.DistanceSquared(Target.WorldPosition, sourcePos) < CloseEnough * CloseEnough;
808  }
809  }
810 
811  protected override bool CheckObjectiveState()
812  {
813  // First check the distance and then if can interact (heaviest)
814  if (Target == null)
815  {
816  Abandon = true;
817  return false;
818  }
819  if (repeat)
820  {
821  return false;
822  }
823  else
824  {
825  if (IsCloseEnough)
826  {
827  if (requiredCondition == null || requiredCondition())
828  {
829  if (Target is Item item)
830  {
831  if (character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; }
832  }
833  else if (Target is Character targetCharacter)
834  {
835  character.SelectCharacter(targetCharacter);
836  if (character.CanInteractWith(targetCharacter, skipDistanceCheck: true)) { IsCompleted = true; }
838  }
839  else
840  {
841  IsCompleted = true;
842  }
843  }
844  }
845  }
846  return IsCompleted;
847  }
848 
849  protected override void OnAbandon()
850  {
851  StopMovement();
853  {
855  }
856  SpeakCannotReach();
857  base.OnAbandon();
858  }
859 
860  private void StopMovement()
861  {
863  if (FaceTargetOnCompleted && Target is Entity { Removed: false })
864  {
866  }
867  }
868 
869  protected override void OnCompleted()
870  {
871  StopMovement();
872  if (Target is WayPoint { Ladders: null })
873  {
874  // Release ladders when ordered to wait at a spawnpoint.
875  // This is a special case specifically meant for NPCs that spawn in outposts with a wait order.
876  // Otherwise they might keep holding to the ladders when the target is just next to it.
878  {
880  }
881  }
882  base.OnCompleted();
883  }
884 
885  public override void Reset()
886  {
887  base.Reset();
888  findDivingGear = null;
889  seekGapsTimer = 0;
890  TargetGap = null;
891  if (SteeringManager is IndoorsSteeringManager pathSteering)
892  {
893  pathSteering.ResetPath();
894  }
895  }
896  }
897 }
virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear=false, bool dropOtherIfCannotMove=true, bool allowSwapping=false, bool storeUnequipped=false, IEnumerable< Identifier > targetTags=null)
bool HasValidPath(bool requireNonDirty=true, bool requireUnfinished=true, Func< WayPoint, bool > nodePredicate=null)
Is the current path valid, using the provided parameters.
void FaceTarget(ISpatialEntity target)
static float GetMinOxygen(Character character)
Func< PathNode, bool > endNodeFilter
override float GetPriority()
Func< bool > requiredCondition
Doesn't allow the objective to complete if this condition is false
float CloseEnough
Display units
override Identifier Identifier
AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat=false, bool getDivingGearIfNeeded=true, float priorityModifier=1, float closeEnough=0)
void ForceAct(float deltaTime)
static Hull GetTargetHull(ISpatialEntity target)
Func< bool > SpeakCannotReachCondition
bool UseDistanceRelativeToAimSourcePos
If true, the distance to the destination is calculated from the character's AimSourcePos (= shoulder)...
override bool CheckObjectiveState()
Should return whether the objective is completed or not.
override void Act(float deltaTime)
override bool AbandonWhenCannotCompleteSubObjectives
override bool AllowOutsideSubmarine
float Priority
Final priority value after all calculations.
AIObjective CurrentObjective
Includes orders.
AIObjective?? CurrentOrder
The AIObjective in CurrentOrders with the highest AIObjective.Priority
float GetOrderPriority(AIObjective objective)
bool IsOrder(AIObjective objective)
bool TryPutItemInAnySlot(Item item)
void SetInput(InputType inputType, bool hit, bool held)
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
bool CanInteractWith(Character c, float maxDist=200.0f, bool checkVisibility=true, bool skipDistanceCheck=false)
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
bool Unequip(Item item)
Attempts to unequip an item. First tries to put the item in any slot. If that fails,...
Item GetEquippedItem(Identifier tagOrIdentifier=default, InvSlotType? slotType=null)
Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos=null)
Item SelectedSecondaryItem
The secondary selected item. It's an item other than a device (see SelectedItem), e....
bool TryPutItemInBag(Item item)
bool HasHandsFull(out(Item leftHandItem, Item rightHandItem) items)
bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity=null, bool seeThroughWindows=false, bool checkFacing=false)
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
static Hull FindHull(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any)
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 HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable< Item > items, Identifier containedTag=default, float conditionPercentage=0, bool requireEquipped=false, bool recursive=true, Func< Item, bool > predicate=null)
Note: uses a single list for matching items. The item is reused each time when the method is called....
static bool HasDivingGear(Character character, float conditionPercentage=0, bool requireOxygenTank=true)
void AskToRecalculateHullSafety(Hull hull)
bool UseOutsideWaypoints
Waypoints that are not linked to a sub (e.g. main path).
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.
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 SteeringSeekSimple(Vector2 targetSimPos, float weight=1)
bool Combine(Item item, Character user)
void SteeringManual(float deltaTime, Vector2 velocity)
void SteeringSeek(Vector2 targetSimPos, float weight=1)
void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight=1)
List< WayPoint > Nodes
static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel=false, bool ignoreSubs=false, bool ignoreSensors=true, bool ignoreDisabledWalls=true, bool ignoreBranches=true, Predicate< Fixture > blocksVisibilityPredicate=null)
Check visibility between two points (in sim units).