Client LuaCsForBarotrauma
IndoorsSteeringManager.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Linq;
6 using FarseerPhysics;
7 
8 namespace Barotrauma
9 {
11  {
12  private readonly PathFinder pathFinder;
13  private SteeringPath currentPath;
14 
15  private readonly bool canOpenDoors;
16  public bool CanBreakDoors { get; set; }
17 
18  private bool ShouldBreakDoor(Door door) =>
19  CanBreakDoors &&
20  !door.Item.Indestructible && !door.Item.InvulnerableToDamage &&
21  (door.Item.Submarine == null || door.Item.Submarine.TeamID != character.TeamID);
22 
23  private readonly Character character;
24 
28  private Vector2 currentTargetPos;
29 
30  private float findPathTimer;
31 
32  private const float ButtonPressCooldown = 1;
33  private float checkDoorsTimer;
34  private float buttonPressTimer;
35 
37  {
38  get { return currentPath; }
39  }
40 
42  {
43  get { return pathFinder; }
44  }
45 
46  public bool IsPathDirty
47  {
48  get;
49  private set;
50  }
51 
55  public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
56 
57  public bool IsCurrentNodeLadder => GetCurrentLadder() != null;
58 
59  public bool IsNextNodeLadder => GetNextLadder() != null;
60 
62  {
63  get
64  {
65  var currentLadder = GetCurrentLadder();
66  if (currentLadder == null) { return false; }
67  return currentLadder == GetNextLadder();
68  }
69  }
70 
71  public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host)
72  {
73  pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true)
74  {
75  GetNodePenalty = GetNodePenalty,
76  GetSingleNodePenalty = GetSingleNodePenalty
77  };
78 
79  this.canOpenDoors = canOpenDoors;
80  this.CanBreakDoors = canBreakDoors;
81 
82  character = (host as AIController).Character;
83 
84  findPathTimer = Rand.Range(0.0f, 1.0f);
85  }
86 
87  public override void Update(float speed)
88  {
89  base.Update(speed);
90  float step = 1.0f / 60.0f;
91  checkDoorsTimer -= step;
92  if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsFullyOpen)
93  {
94  buttonPressTimer = 0;
95  }
96  else
97  {
98  buttonPressTimer -= step;
99  }
100  findPathTimer -= step;
101  }
102 
103  public void SetPath(Vector2 targetPos, SteeringPath path)
104  {
105  currentTargetPos = targetPos;
106  currentPath = path;
107  findPathTimer = Math.Min(findPathTimer, 1.0f);
108  IsPathDirty = false;
109  }
110 
111  public void ResetPath()
112  {
113  currentPath = null;
114  IsPathDirty = true;
115  }
116 
117  public void SteeringSeekSimple(Vector2 targetSimPos, float weight = 1)
118  {
119  steering += base.DoSteeringSeek(targetSimPos, weight);
120  }
121 
122  public 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)
123  {
124  // Have to use a variable here or resetting doesn't work.
125  Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
126  steering += addition;
127  }
128 
129  public Ladder GetCurrentLadder() => GetLadder(currentPath?.CurrentNode);
130 
131  public Ladder GetNextLadder() => GetLadder(currentPath?.NextNode);
132 
133  private Ladder GetLadder(WayPoint wp)
134  {
135  if (wp?.Ladders?.Item is Item item && item.IsInteractable(character))
136  {
137  return wp.Ladders;
138  }
139  return null;
140  }
141 
142  private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
143  {
144  bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null;
145  if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
146  {
147  // If the target has moved, we need a new path.
148  // Different subs are already taken into account before setting the target.
149  // Triggers when either the target or we have changed subs, but only once (until the new path has been accepted).
150  Vector2 targetDiff = target - currentTargetPos;
151  if (targetDiff.LengthSquared() > 1)
152  {
153  needsNewPath = true;
154  }
155  }
156  //find a new path if one hasn't been found yet or the target is different from the current target
157  if (needsNewPath || findPathTimer < -1.0f)
158  {
159  IsPathDirty = true;
160  if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp)
161  {
162  if (character.Submarine != null && wp.Ladders == null && wp.ConnectedDoor == null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
163  {
164  // Not moving -> need a new path.
165  needsNewPath = true;
166  }
167  if (character.Submarine == null && wp.CurrentHull != null)
168  {
169  // Current node inside, while we are outside
170  // -> Check that the current node is not too far (can happen e.g. if someone controls the character in the meanwhile)
171  float maxDist = 200;
172  if (Vector2.DistanceSquared(character.WorldPosition, wp.WorldPosition) > maxDist * maxDist)
173  {
174  needsNewPath = true;
175  }
176  }
177  }
178  if (findPathTimer < 0)
179  {
180  SkipCurrentPathNodes();
181  currentTargetPos = target;
182  Vector2 currentPos = host.SimPosition;
183  pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
184  pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
185  var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
186  bool useNewPath = needsNewPath;
187  if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
188  {
189  // Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
190  if (IsIdenticalPath())
191  {
192  useNewPath = false;
193  }
194  else if (!character.IsClimbing)
195  {
196  // Use the new path if it has significantly lower cost (don't change the path if it has marginally smaller cost. This reduces navigating backwards due to new path that is calculated from the node just behind us).
197  float t = (float)currentPath.CurrentIndex / (currentPath.Nodes.Count - 1);
198  useNewPath = newPath.Cost < currentPath.Cost * MathHelper.Lerp(0.95f, 0, t);
199  if (!useNewPath && character.Submarine != null)
200  {
201  // It's possible that the current path was calculated from a start point that is no longer valid.
202  // Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
203  // This is a special case for cases e.g. where the character falls and thus needs a new path.
204  useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
205  }
206  }
207  if (!useNewPath && !character.CanSeeTarget(currentPath.CurrentNode))
208  {
209  // If we are set to disregard the new path, ensure that we can actually see the current node of the old path,
210  // because it's possible that there's e.g. a closed door between us and the current node,
211  // and in that case we'd want to use the new path instead of the old.
212  // There's visibility checks in the pathfinder calls, so the new path should always be ok.
213  useNewPath = true;
214  }
215 
216  bool IsIdenticalPath()
217  {
218  int nodeCount = newPath.Nodes.Count;
219  if (nodeCount == currentPath.Nodes.Count)
220  {
221  for (int i = 0; i < nodeCount - 1; i++)
222  {
223  if (newPath.Nodes[i] != currentPath.Nodes[i])
224  {
225  return false;
226  }
227  }
228  return true;
229  }
230  return false;
231  }
232  }
233  if (useNewPath)
234  {
235  if (currentPath != null)
236  {
237  CheckDoorsInPath();
238  }
239  currentPath = newPath;
240  }
241  float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
242  findPathTimer = priority * Rand.Range(1.0f, 1.2f);
243  IsPathDirty = false;
244 
245  void SkipCurrentPathNodes()
246  {
247  if (!character.AnimController.InWater || character.Submarine != null) { return; }
248  if (CurrentPath == null || CurrentPath.Unreachable || CurrentPath.Finished) { return; }
249  if (CurrentPath.CurrentIndex < 0 || CurrentPath.CurrentIndex >= CurrentPath.Nodes.Count - 1) { return; }
250  var lastNode = CurrentPath.Nodes.Last();
251  Submarine targetSub = lastNode.Submarine;
252  if (targetSub != null)
253  {
254  float subSize = Math.Max(targetSub.Borders.Size.X, targetSub.Borders.Size.Y) / 2;
255  float margin = 500;
256  if (Vector2.DistanceSquared(character.WorldPosition, targetSub.WorldPosition) < MathUtils.Pow2(subSize + margin))
257  {
258  // Don't skip nodes when close to the target submarine.
259  return;
260  }
261  }
262  // Check if we could skip ahead to NextNode when the character is swimming and using waypoints outside.
263  // Do this to optimize the old path before creating and evaluating a new path.
264  // In general, this is to avoid behavior where:
265  // a) the character goes back to first reach CurrentNode when the second node would be closer; or
266  // b) the character moves along the path when they could cut through open space to reduce the total distance.
267  float pathDistance = Vector2.Distance(character.WorldPosition, CurrentPath.CurrentNode.WorldPosition);
268  pathDistance += CurrentPath.GetLength(startIndex: CurrentPath.CurrentIndex);
269  for (int i = CurrentPath.Nodes.Count - 1; i > CurrentPath.CurrentIndex + 1; i--)
270  {
271  var waypoint = CurrentPath.Nodes[i];
272  float directDistance = Vector2.DistanceSquared(character.WorldPosition, waypoint.WorldPosition);
273  if (directDistance > MathUtils.Pow2(pathDistance) || !character.CanSeeTarget(waypoint))
274  {
275  pathDistance -= CurrentPath.GetLength(startIndex: i - 1, endIndex: i);
276  continue;
277  }
279  break;
280  }
281  }
282  }
283  }
284 
285  Vector2 diff = DiffToCurrentNode();
286  if (diff == Vector2.Zero) { return Vector2.Zero; }
287  return Vector2.Normalize(diff) * weight;
288  }
289 
290  protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight);
291 
292  private Vector2 DiffToCurrentNode()
293  {
294  if (currentPath == null || currentPath.Unreachable)
295  {
296  return Vector2.Zero;
297  }
298  if (currentPath.Finished)
299  {
300  Vector2 hostPosition = host.SimPosition;
301  if (character != null && character.Submarine == null && CurrentPath.Nodes.Count > 0 && CurrentPath.Nodes.Last().Submarine != null)
302  {
303  hostPosition -= CurrentPath.Nodes.Last().Submarine.SimPosition;
304  }
305  return currentTargetPos - hostPosition;
306  }
307  bool doorsChecked = false;
308  checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime());
309  if (!character.LockHands && checkDoorsTimer <= 0.0f)
310  {
311  CheckDoorsInPath();
312  doorsChecked = true;
313  }
314  if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsFullyOpen)
315  {
316  // We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
317  Reset();
318  return Vector2.Zero;
319  }
320  Vector2 pos = host.WorldPosition;
321  Vector2 diff = currentPath.CurrentNode.WorldPosition - pos;
322  bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
323  // Only humanoids can climb ladders
324  bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
325  Ladder currentLadder = GetCurrentLadder();
326  Ladder nextLadder = GetNextLadder();
327  var ladders = currentLadder ?? nextLadder;
328  bool useLadders = canClimb && ladders != null;
329  var collider = character.AnimController.Collider;
330  Vector2 colliderSize = collider.GetSize();
331  if (useLadders)
332  {
333  if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
334  {
335  // If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders.
336  useLadders = false;
337  }
338  else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null)
339  {
340  Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos;
341  if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
342  {
343  // If the next node is horizontally farther from us than vertically, we don't want to start climbing.
344  useLadders = false;
345  }
346  }
347  else if (isDiving && steering.Y < 1)
348  {
349  // When diving, only use ladders to get upwards (towards the surface), otherwise we can just ignore them.
350  useLadders = false;
351  }
352  }
353  if (character.IsClimbing && !useLadders)
354  {
355  if (currentPath.IsAtEndNode && canClimb && ladders != null)
356  {
357  // Don't release the ladders when ending a path in ladders.
358  useLadders = true;
359  }
360  else
361  {
362  character.StopClimbing();
363  }
364  }
365  if (useLadders && character.SelectedSecondaryItem != ladders.Item)
366  {
367  if (character.CanInteractWith(ladders.Item))
368  {
369  ladders.Item.TryInteract(character, forceSelectKey: true);
370  }
371  else
372  {
373  // Cannot interact with the current (or next) ladder,
374  // Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder.
375  // The intention of this code is to prevent the bots from dropping from the "double ladders".
376  var previousLadders = currentPath.PrevNode?.Ladders;
377  if (previousLadders != null && previousLadders != ladders && character.SelectedSecondaryItem != previousLadders.Item &&
378  character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5)
379  {
380  previousLadders.Item.TryInteract(character, forceSelectKey: true);
381  }
382  }
383  }
384  if (character.IsClimbing && useLadders)
385  {
386  if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
387  {
388  // Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
389  NextNode(!doorsChecked);
390  }
391  else
392  {
393  bool nextLadderSameAsCurrent = currentLadder == nextLadder;
394  float colliderHeight = collider.Height / 2 + collider.Radius;
395  float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
396  float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
397  if (currentLadder != null && nextLadder != null)
398  {
399  //climbing ladders -> don't move horizontally
400  diff.X = 0.0f;
401  }
402  if (Math.Abs(heightDiff) < colliderHeight * 1.25f)
403  {
404  if (nextLadder != null && !nextLadderSameAsCurrent)
405  {
406  // Try to change the ladder (hatches between two submarines)
407  if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
408  {
409  if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
410  {
411  NextNode(!doorsChecked);
412  }
413  }
414  }
415  bool isAboveFloor;
416  if (diff.Y < 0)
417  {
418  // When climbing down, let's use the collider bottom to prevent getting stuck at the bottom of the ladders.
419  float colliderBottom = character.AnimController.Collider.SimPosition.Y;
420  float floorY = character.AnimController.FloorY;
421  isAboveFloor = colliderBottom > floorY;
422  }
423  else
424  {
425  // When climbing up, let's use the lowest collider (feet).
426  // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative,
427  // when a foot is still below the platform.
428  float heightFromFloor = character.AnimController.GetHeightFromFloor();
429  isAboveFloor = heightFromFloor > -0.1f;
430  }
431  if (isAboveFloor)
432  {
433  if (Math.Abs(diff.Y) < distanceMargin)
434  {
435  NextNode(!doorsChecked);
436  }
437  else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin)))
438  {
439  // Can't skip the node -> Release the ladders, because the next node is not on a ladder or it's horizontally too far.
440  character.StopClimbing();
441  }
442  }
443  }
444  else if (currentLadder != null && currentPath.NextNode != null)
445  {
446  if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
447  {
448  //if the current node is below the character and the next one is above (or vice versa)
449  //and both are on ladders, we can skip directly to the next one
450  //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
451  NextNode(!doorsChecked);
452  }
453  }
454  }
455  }
456  else if (character.AnimController.InWater)
457  {
458  // Swimming
459  var door = currentPath.CurrentNode.ConnectedDoor;
460  if (door == null || door.CanBeTraversed)
461  {
462  float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
463  float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
464  float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
465  float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
466  if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
467  {
468  verticalDistance *= 2;
469  }
470  float distance = horizontalDistance + verticalDistance;
471  if (ConvertUnits.ToSimUnits(distance) < targetDistance)
472  {
473  NextNode(!doorsChecked);
474  }
475  }
476  }
477  else
478  {
479  // Walking horizontally
480  Vector2 colliderBottom = character.AnimController.GetColliderBottom();
481  Vector2 velocity = collider.LinearVelocity;
482  // If the character is very short, it would fail to use the waypoint nodes because they are always too high.
483  // If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
484  // Both values are based on the human size. So basically anything smaller than humans are considered as equal in size.
485  float minHeight = 1.6125001f;
486  float minWidth = 0.3225f;
487  // Cannot use the head position, because not all characters have head or it can be below the total height of the character
488  float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
489  float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
490  bool isTargetTooHigh = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y + characterHeight;
491  bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y;
492  var door = currentPath.CurrentNode.ConnectedDoor;
493  float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
494  float colliderHeight = collider.Height / 2 + collider.Radius;
495  if (currentPath.CurrentNode.Stairs == null)
496  {
497  float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
498  if (heightDiff < colliderHeight)
499  {
500  // Original comment:
501  //the waypoint is between the top and bottom of the collider, no need to move vertically.
502  // Note that the waypoint can be below collider too! This might be incorrect.
503  diff.Y = 0.0f;
504  }
505  }
506  else
507  {
508  // In stairs
509  bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs;
510  if (!isNextNodeInSameStairs)
511  {
512  margin = 1;
513  if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
514  {
515  isTargetTooLow = true;
516  }
517  }
518  }
519  float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
520  if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow)
521  {
522  if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null))
523  {
524  NextNode(!doorsChecked);
525  }
526  }
527  }
528  if (currentPath.CurrentNode == null)
529  {
530  return Vector2.Zero;
531  }
532  return ConvertUnits.ToSimUnits(diff);
533  }
534 
535  private void NextNode(bool checkDoors)
536  {
537  if (checkDoors)
538  {
539  CheckDoorsInPath();
540  }
541  currentPath.SkipToNextNode();
542  }
543 
544  public bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
545  {
546  if (door.CanBeTraversed) { return true; }
547  if (door.IsClosed)
548  {
549  if (!door.Item.IsInteractable(character)) { return false; }
550  if (!ShouldBreakDoor(door))
551  {
552  if (door.IsStuck || door.IsJammed) { return false; }
553  if (!canOpenDoors || character.LockHands) { return false; }
554  }
555  }
556  if (door.HasIntegratedButtons)
557  {
558  return door.IsOpen || door.HasAccess(character) || ShouldBreakDoor(door);
559  }
560  else
561  {
562  // We'll want this to run each time, because the delegate is used to find a valid button component.
563  bool canAccessButtons = false;
564  foreach (var button in door.Item.GetConnectedComponents<Controller>(true, connectionFilter: c => c.Name == "toggle" || c.Name == "set_state"))
565  {
566  if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
567  {
568  canAccessButtons = true;
569  }
570  }
571  foreach (var linked in door.Item.linkedTo)
572  {
573  if (linked is not Item linkedItem) { continue; }
574  var button = linkedItem.GetComponent<Controller>();
575  if (button == null) { continue; }
576  if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
577  {
578  canAccessButtons = true;
579  }
580  }
581  return canAccessButtons || door.IsOpen || ShouldBreakDoor(door);
582  }
583  }
584 
585  private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize());
586 
587  private float GetColliderLength()
588  {
589  Vector2 colliderSize = character.AnimController.Collider.GetSize();
590  return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y));
591  }
592 
593  private (Door door, bool shouldBeOpen) lastDoor;
594  private float GetDoorCheckTime()
595  {
596  if (steering.LengthSquared() > 0)
597  {
598  return character.AnimController.IsMovingFast ? 0.1f : 0.3f;
599  }
600  else
601  {
602  return float.PositiveInfinity;
603  }
604  }
605 
606  private void CheckDoorsInPath()
607  {
608  checkDoorsTimer = GetDoorCheckTime();
609  if (!canOpenDoors) { return; }
610  for (int i = 0; i < 5; i++)
611  {
612  WayPoint currentWaypoint = null;
613  WayPoint nextWaypoint = null;
614  Door door = null;
615  bool shouldBeOpen = false;
616  if (currentPath.Nodes.Count == 1)
617  {
618  door = currentPath.Nodes.First().ConnectedDoor;
619  shouldBeOpen = door != null;
620  if (i > 0) { break; }
621  }
622  else
623  {
624  if (i == 0)
625  {
626  currentWaypoint = currentPath.CurrentNode;
627  nextWaypoint = currentPath.NextNode;
628  }
629  else
630  {
631  int previousIndex = currentPath.CurrentIndex - i;
632  if (previousIndex < 0) { break; }
633  currentWaypoint = currentPath.Nodes[previousIndex];
634  nextWaypoint = currentPath.CurrentNode;
635  }
636  if (currentWaypoint?.ConnectedDoor == null) { continue; }
637 
638  if (nextWaypoint == null)
639  {
640  //the node we're heading towards is the last one in the path, and at a door
641  //the door needs to be open for the character to reach the node
642  if (currentWaypoint.ConnectedDoor.LinkedGap is Gap linkedGap)
643  {
644  if (currentWaypoint.Submarine == null ||
645  currentWaypoint.Submarine.Info is { IsPlayer: false } ||
646  !linkedGap.IsRoomToRoom ||
647  (linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false }))
648  {
649  shouldBeOpen = true;
650  door = currentWaypoint.ConnectedDoor;
651  }
652  }
653  }
654  else
655  {
656  float colliderLength = GetColliderLength();
657  door = currentWaypoint.ConnectedDoor;
658  if (door.LinkedGap.IsHorizontal)
659  {
660  int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
661  float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X;
662  shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -size;
663  }
664  else
665  {
666  int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
667  shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -colliderLength;
668  }
669  }
670  }
671 
672  if (door == null) { return; }
673 
674  if (door.BotsShouldKeepOpen) { shouldBeOpen = true; }
675 
676  if ((door.IsOpen || door.IsBroken) != shouldBeOpen)
677  {
678  if (!shouldBeOpen)
679  {
680  if (character.AIController is HumanAIController humanAI)
681  {
682  bool keepDoorsClosed = character.IsBot && door.Item.Submarine?.TeamID == character.TeamID || character.Params.AI != null && character.Params.AI.KeepDoorsClosed;
683  if (!keepDoorsClosed) { return; }
684  bool isInAirlock = door.Item.CurrentHull is { IsWetRoom: true } || character.CurrentHull is { IsWetRoom: true };
685  if (!isInAirlock)
686  {
687  // Don't slam the door at anyones face
688  if (Character.CharacterList.Any(c => c != character && humanAI.IsFriendly(c) && humanAI.VisibleHulls.Contains(c.CurrentHull) && !c.IsUnconscious))
689  {
690  return;
691  }
692  }
693  }
694  }
695  Controller closestButton = null;
696  float closestDist = 0;
697  bool canAccess = CanAccessDoor(door, button =>
698  {
699  // Check that the button is on the right side of the door.
700  if (nextWaypoint != null)
701  {
702  if (door.LinkedGap.IsHorizontal)
703  {
704  int dir = Math.Sign((nextWaypoint).WorldPosition.X - door.Item.WorldPosition.X);
705  if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; }
706  }
707  else
708  {
709  int dir = Math.Sign((nextWaypoint).WorldPosition.Y - door.Item.WorldPosition.Y);
710  if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; }
711  }
712  }
713  float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
714  //heavily prefer buttons linked to the door, so sub builders can help the bots figure out which button to use by linking them
715  if (door.Item.linkedTo.Contains(button.Item)) { distance *= 0.1f; }
716  if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item))
717  {
718  closestButton = button;
719  closestDist = distance;
720  }
721  return true;
722  });
723  if (canAccess)
724  {
725  bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.shouldBeOpen != shouldBeOpen;
726  if (door.HasIntegratedButtons)
727  {
728  if (pressButton && character.CanSeeTarget(door.Item))
729  {
730  if (door.Item.TryInteract(character, forceSelectKey: true))
731  {
732  lastDoor = (door, shouldBeOpen);
733  buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
734  }
735  else
736  {
737  buttonPressTimer = 0;
738  }
739  }
740  break;
741  }
742  else if (closestButton != null)
743  {
744  if (closestDist < MathUtils.Pow2(closestButton.Item.InteractDistance + GetColliderLength()))
745  {
746  if (pressButton)
747  {
748  if (closestButton.Item.TryInteract(character, forceSelectKey: true))
749  {
750  lastDoor = (door, shouldBeOpen);
751  buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
752  }
753  else
754  {
755  buttonPressTimer = 0;
756  }
757  }
758  break;
759  }
760  else
761  {
762  // Can't reach the button closest to the character.
763  // It's possible that we could reach another buttons.
764  // If this becomes an issue, we could go through them here and check if any of them are reachable
765  // (would have to cache a collection of buttons instead of a single reference in the CanAccess filter method above)
766  var body = Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel);
767  if (body != null)
768  {
769  if (body.UserData is Item item)
770  {
771  var d = item.GetComponent<Door>();
772  if (d == null || d.IsOpen) { return; }
773  }
774  // The button is on the wrong side of the door or a wall
775  currentPath.Unreachable = true;
776  }
777  return;
778  }
779  }
780  }
781  else if (shouldBeOpen)
782  {
783 #if DEBUG
784  DebugConsole.NewMessage($"{character.Name}: Pathfinding error: Cannot access the door", Color.Yellow);
785 #endif
786  currentPath.Unreachable = true;
787  return;
788  }
789  }
790  }
791  }
792 
793  private float? GetNodePenalty(PathNode node, PathNode nextNode)
794  {
795  if (character == null) { return 0.0f; }
796  float? penalty = GetSingleNodePenalty(nextNode);
797  if (penalty == null) { return null; }
798  bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
799  //non-humanoids can't climb up ladders
800  if (!(character.AnimController is HumanoidAnimController))
801  {
802  if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands)||
803  (nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
804  nextNodeAboveWaterLevel)) //upper node not underwater
805  {
806  return null;
807  }
808  }
809 
810  if (node.Waypoint.CurrentHull != null)
811  {
812  var hull = node.Waypoint.CurrentHull;
813  if (hull.FireSources.Count > 0)
814  {
815  foreach (FireSource fs in hull.FireSources)
816  {
817  penalty += fs.Size.X * 10.0f;
818  }
819  }
820  if (character.NeedsAir)
821  {
822  if (hull.WaterVolume / hull.Rect.Width > 100.0f)
823  {
824  if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1)
825  {
826  penalty += 500.0f;
827  }
828  }
829  if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
830  {
831  penalty += 1000.0f;
832  }
833  }
834 
835  float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y);
836  if (nextNodeAboveWaterLevel && node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null)
837  {
838  penalty += yDist * 10.0f;
839  }
840  }
841 
842  return penalty;
843  }
844 
845  private float? GetSingleNodePenalty(PathNode node)
846  {
847  if (!node.Waypoint.IsTraversable) { return null; }
848  if (node.IsBlocked()) { return null; }
849  float penalty = 0.0f;
850  if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f)
851  {
852  var door = node.Waypoint.ConnectedDoor;
853  if (door == null)
854  {
855  penalty = 100.0f;
856  }
857  else
858  {
859  if (!CanAccessDoor(door, button =>
860  {
861  // Ignore buttons that are on the wrong side of the door
862  if (door.IsHorizontal)
863  {
864  if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y))
865  {
866  return false;
867  }
868  }
869  else
870  {
871  if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X))
872  {
873  return false;
874  }
875  }
876  return true;
877  }))
878  {
879  return null;
880  }
881  }
882  }
883  return penalty;
884  }
885 
886  public static float smallRoomSize = 500;
887  public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true)
888  {
889  //steer away from edges of the hull
890  bool wander = false;
891  bool inWater = character.AnimController.InWater;
892  Hull currentHull = character.CurrentHull;
893  // TODO: disabled for now, because seems to cause bots to walk towards walls/doors in some places. In some places it's because how the hulls are defined, but there is probably something else too, is it seems to happen also elsewhere.
894  // if (!inWater)
895  // {
896  // Vector2 colliderBottomPos = ConvertUnits.ToDisplayUnits(character.AnimController.GetColliderBottom());
897  // if (Hull.FindHull(colliderBottomPos, guess: currentHull, useWorldCoordinates: false) is Hull lowestHull)
898  // {
899  // // Use the hull found at the collider bottom, if found.
900  // // Makes difference in some rooms that have multiple hulls, of which the lowest hull where the feet are might not be the same as where the center position of the main collider is.
901  // currentHull = lowestHull;
902  // }
903  // }
904  if (currentHull != null && !inWater)
905  {
906  float roomWidth = currentHull.Rect.Width;
907  if (stayStillInTightSpace && roomWidth < Math.Max(wallAvoidDistance * 3, smallRoomSize))
908  {
909  Reset();
910  }
911  else
912  {
913  float leftDist = character.Position.X - currentHull.Rect.X;
914  float rightDist = currentHull.Rect.Right - character.Position.X;
915  if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
916  {
917  if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2)
918  {
919  SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
920  return;
921  }
922  else if (stayStillInTightSpace)
923  {
924  Reset();
925  return;
926  }
927  }
928  if (leftDist < wallAvoidDistance)
929  {
930  float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
931  SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
932  WanderAngle = 0.0f;
933  }
934  else if (rightDist < wallAvoidDistance)
935  {
936  float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
937  SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
938  WanderAngle = MathHelper.Pi;
939  }
940  else
941  {
942  wander = true;
943  }
944  }
945  }
946  else
947  {
948  wander = true;
949  }
950  if (wander)
951  {
952  SteeringWander();
953  if (inWater)
954  {
955  SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance), 5);
956  }
957  }
958  if (!inWater)
959  {
960  //reset vertical steering to prevent dropping down from platforms etc
961  ResetY();
962  }
963  }
964  }
965 }
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
void Wander(float deltaTime, float wallAvoidDistance=150, bool stayStillInTightSpace=true)
IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors)
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)
override Vector2 DoSteeringSeek(Vector2 target, float weight)
override void Update(float speed)
Update speed for the steering. Should normally match the characters current animation speed.
bool PathHasStairs
Returns true if any node in the path is in stairs
bool CanAccessDoor(Door door, Func< Controller, bool > buttonFilter=null)
void SteeringSeekSimple(Vector2 targetSimPos, float weight=1)
bool IsInteractable(Character character)
Returns interactibility based on whether the character is on a player team
bool TryInteract(Character user, bool ignoreRequiredItems=false, bool forceSelectKey=false, bool forceUseKey=false)
override bool HasAccess(Character character)
Only checks if any of the Picked requirements are matched (used for checking id card(s))....
void SkipToNode(int nodeIndex)
List< WayPoint > Nodes
float GetLength(int? startIndex=null, int? endIndex=null)
Definition: SteeringPath.cs:34