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  bool canClimb = character.CanClimb;
324  Ladder currentLadder = GetCurrentLadder();
325  Ladder nextLadder = GetNextLadder();
326  var ladders = currentLadder ?? nextLadder;
327  bool useLadders = canClimb && ladders != null;
328  var collider = character.AnimController.Collider;
329  Vector2 colliderSize = collider.GetSize();
330  if (useLadders)
331  {
332  if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
333  {
334  // If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders.
335  useLadders = false;
336  }
337  else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null)
338  {
339  Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos;
340  if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
341  {
342  // If the next node is horizontally farther from us than vertically, we don't want to start climbing.
343  useLadders = false;
344  }
345  }
346  else if (isDiving && steering.Y < 1)
347  {
348  // When diving, only use ladders to get upwards (towards the surface), otherwise we can just ignore them.
349  useLadders = false;
350  }
351  }
352  if (character.IsClimbing && !useLadders)
353  {
354  if (currentPath.IsAtEndNode && canClimb && ladders != null)
355  {
356  // Don't release the ladders when ending a path in ladders.
357  useLadders = true;
358  }
359  else
360  {
361  character.StopClimbing();
362  }
363  }
364  if (useLadders && character.SelectedSecondaryItem != ladders.Item)
365  {
366  if (character.CanInteractWith(ladders.Item))
367  {
368  ladders.Item.TryInteract(character, forceSelectKey: true);
369  }
370  else
371  {
372  // Cannot interact with the current (or next) ladder,
373  // Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder.
374  // The intention of this code is to prevent the bots from dropping from the "double ladders".
375  var previousLadders = currentPath.PrevNode?.Ladders;
376  if (previousLadders != null && previousLadders != ladders && character.SelectedSecondaryItem != previousLadders.Item &&
377  character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5)
378  {
379  previousLadders.Item.TryInteract(character, forceSelectKey: true);
380  }
381  }
382  }
383  if (character.IsClimbing && useLadders)
384  {
385  if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
386  {
387  // Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
388  NextNode(!doorsChecked);
389  }
390  else
391  {
392  bool nextLadderSameAsCurrent = currentLadder == nextLadder;
393  float colliderHeight = collider.Height / 2 + collider.Radius;
394  float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
395  float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
396  if (currentLadder != null && nextLadder != null)
397  {
398  //climbing ladders -> don't move horizontally
399  diff.X = 0.0f;
400  }
401  if (Math.Abs(heightDiff) < colliderHeight * 1.25f)
402  {
403  if (nextLadder != null && !nextLadderSameAsCurrent)
404  {
405  // Try to change the ladder (hatches between two submarines)
406  if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
407  {
408  if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
409  {
410  NextNode(!doorsChecked);
411  }
412  }
413  }
414  bool isAboveFloor;
415  if (diff.Y < 0)
416  {
417  // When climbing down, let's use the collider bottom to prevent getting stuck at the bottom of the ladders.
418  float colliderBottom = character.AnimController.Collider.SimPosition.Y;
419  float floorY = character.AnimController.FloorY;
420  isAboveFloor = colliderBottom > floorY;
421  }
422  else
423  {
424  // When climbing up, let's use the lowest collider (feet).
425  // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative,
426  // when a foot is still below the platform.
427  float heightFromFloor = character.AnimController.GetHeightFromFloor();
428  isAboveFloor = heightFromFloor > -0.1f;
429  }
430  if (isAboveFloor)
431  {
432  if (Math.Abs(diff.Y) < distanceMargin)
433  {
434  NextNode(!doorsChecked);
435  }
436  else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin)))
437  {
438  // Can't skip the node -> Release the ladders, because the next node is not on a ladder or it's horizontally too far.
439  character.StopClimbing();
440  }
441  }
442  }
443  else if (currentLadder != null && currentPath.NextNode != null)
444  {
445  if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
446  {
447  //if the current node is below the character and the next one is above (or vice versa)
448  //and both are on ladders, we can skip directly to the next one
449  //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
450  NextNode(!doorsChecked);
451  }
452  }
453  }
454  }
455  else if (character.AnimController.InWater)
456  {
457  // Swimming
458  var door = currentPath.CurrentNode.ConnectedDoor;
459  if (door == null || door.CanBeTraversed)
460  {
461  float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
462  float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
463  float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
464  float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
465  if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
466  {
467  verticalDistance *= 2;
468  }
469  float distance = horizontalDistance + verticalDistance;
470  if (ConvertUnits.ToSimUnits(distance) < targetDistance)
471  {
472  NextNode(!doorsChecked);
473  }
474  }
475  }
476  else
477  {
478  // Walking horizontally
479  Vector2 colliderBottom = character.AnimController.GetColliderBottom();
480  Vector2 velocity = collider.LinearVelocity;
481  // If the character is very short, it would fail to use the waypoint nodes because they are always too high.
482  // If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
483  // Both values are based on the human size. So basically anything smaller than humans are considered as equal in size.
484  float minHeight = 1.6125001f;
485  float minWidth = 0.3225f;
486  // Cannot use the head position, because not all characters have head or it can be below the total height of the character
487  float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
488  float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
489  bool isTargetTooHigh = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y + characterHeight;
490  bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y;
491  var door = currentPath.CurrentNode.ConnectedDoor;
492  float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
493  float colliderHeight = collider.Height / 2 + collider.Radius;
494  if (currentPath.CurrentNode.Stairs == null)
495  {
496  float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
497  if (heightDiff < colliderHeight)
498  {
499  // Original comment:
500  //the waypoint is between the top and bottom of the collider, no need to move vertically.
501  // Note that the waypoint can be below collider too! This might be incorrect.
502  diff.Y = 0.0f;
503  }
504  }
505  else
506  {
507  // In stairs
508  bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs;
509  if (!isNextNodeInSameStairs)
510  {
511  margin = 1;
512  if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
513  {
514  isTargetTooLow = true;
515  }
516  }
517  }
518  float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
519  if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow)
520  {
521  if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null))
522  {
523  NextNode(!doorsChecked);
524  }
525  }
526  }
527  if (currentPath.CurrentNode == null)
528  {
529  return Vector2.Zero;
530  }
531  return ConvertUnits.ToSimUnits(diff);
532  }
533 
534  private void NextNode(bool checkDoors)
535  {
536  if (checkDoors)
537  {
538  CheckDoorsInPath();
539  }
540  currentPath.SkipToNextNode();
541  }
542 
543  public bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
544  {
545  if (door.CanBeTraversed) { return true; }
546  if (door.IsClosed)
547  {
548  if (!door.Item.IsInteractable(character)) { return false; }
549  if (!ShouldBreakDoor(door))
550  {
551  if (door.IsStuck || door.IsJammed) { return false; }
552  if (!canOpenDoors || character.LockHands) { return false; }
553  }
554  }
555  if (door.HasIntegratedButtons)
556  {
557  return door.IsOpen || door.HasAccess(character) || ShouldBreakDoor(door);
558  }
559  else
560  {
561  bool canAccessButtons = false;
562  bool buttonsFound = false;
563  // Check wired controllers (e.g. buttons)
564  // Always run the buttonFilter delegate (inside CanAccessButton method), if defined, because it's used for find a valid controller component that can be used for closing the door, when needed.
565  foreach (Controller button in door.Item.GetConnectedComponents<Controller>(recursive: true, connectionFilter: c => c.Name is "toggle" or "set_state"))
566  {
567  buttonsFound = true;
568  if (CanAccessButton(button))
569  {
570  canAccessButtons = true;
571  }
572  }
573  if (!canAccessButtons)
574  {
575  // Check linked controllers (more complex circuits)
576  foreach (MapEntity linked in door.Item.linkedTo)
577  {
578  if (linked is not Item linkedItem) { continue; }
579  var button = linkedItem.GetComponent<Controller>();
580  if (button == null) { continue; }
581  buttonsFound = true;
582  if (CanAccessButton(button))
583  {
584  canAccessButtons = true;
585  }
586  }
587  }
588  if (door.IsOpen || ShouldBreakDoor(door))
589  {
590  return true;
591  }
592  // If no buttons were found, just trust it if we should have the access to the door. Could be there's some other mechanism controlling the door.
593  return buttonsFound ? canAccessButtons : door.HasAccess(character);
594 
595  bool CanAccessButton(Controller button) => button.HasAccess(character) && (buttonFilter == null || buttonFilter(button));
596  }
597  }
598 
599  private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize());
600 
601  private float GetColliderLength()
602  {
603  Vector2 colliderSize = character.AnimController.Collider.GetSize();
604  return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y));
605  }
606 
607  private (Door door, bool shouldBeOpen) lastDoor;
608  private float GetDoorCheckTime()
609  {
610  if (steering.LengthSquared() > 0)
611  {
612  return character.AnimController.IsMovingFast ? 0.1f : 0.3f;
613  }
614  else
615  {
616  return float.PositiveInfinity;
617  }
618  }
619 
620  private void CheckDoorsInPath()
621  {
622  checkDoorsTimer = GetDoorCheckTime();
623  if (!canOpenDoors) { return; }
624  for (int i = 0; i < 5; i++)
625  {
626  WayPoint currentWaypoint = null;
627  WayPoint nextWaypoint = null;
628  Door door = null;
629  bool shouldBeOpen = false;
630  if (currentPath.Nodes.Count == 1)
631  {
632  door = currentPath.Nodes.First().ConnectedDoor;
633  shouldBeOpen = door != null;
634  if (i > 0) { break; }
635  }
636  else
637  {
638  if (i == 0)
639  {
640  currentWaypoint = currentPath.CurrentNode;
641  nextWaypoint = currentPath.NextNode;
642  }
643  else
644  {
645  int previousIndex = currentPath.CurrentIndex - i;
646  if (previousIndex < 0) { break; }
647  currentWaypoint = currentPath.Nodes[previousIndex];
648  nextWaypoint = currentPath.CurrentNode;
649  }
650  if (currentWaypoint?.ConnectedDoor == null) { continue; }
651 
652  if (nextWaypoint == null)
653  {
654  //the node we're heading towards is the last one in the path, and at a door
655  //the door needs to be open for the character to reach the node
656  if (currentWaypoint.ConnectedDoor.LinkedGap is Gap linkedGap)
657  {
658  if (currentWaypoint.Submarine == null ||
659  currentWaypoint.Submarine.Info is { IsPlayer: false } ||
660  !linkedGap.IsRoomToRoom ||
661  (linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false }))
662  {
663  shouldBeOpen = true;
664  door = currentWaypoint.ConnectedDoor;
665  }
666  }
667  }
668  else
669  {
670  float colliderLength = GetColliderLength();
671  door = currentWaypoint.ConnectedDoor;
672  if (door.LinkedGap.IsHorizontal)
673  {
674  int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
675  float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X;
676  shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -size;
677  }
678  else
679  {
680  int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
681  shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -colliderLength;
682  }
683  }
684  }
685 
686  if (door == null) { return; }
687 
688  if (door.BotsShouldKeepOpen) { shouldBeOpen = true; }
689 
690  if ((door.IsOpen || door.IsBroken) != shouldBeOpen)
691  {
692  if (!shouldBeOpen)
693  {
694  if (character.AIController is HumanAIController humanAI)
695  {
696  bool keepDoorsClosed = character.IsBot && door.Item.Submarine?.TeamID == character.TeamID || character.Params.AI != null && character.Params.AI.KeepDoorsClosed;
697  if (!keepDoorsClosed) { return; }
698  bool isInAirlock = door.Item.CurrentHull is { IsWetRoom: true } || character.CurrentHull is { IsWetRoom: true };
699  if (!isInAirlock)
700  {
701  // Don't slam the door at anyones face
702  if (Character.CharacterList.Any(c => c != character && humanAI.IsFriendly(c) && humanAI.VisibleHulls.Contains(c.CurrentHull) && !c.IsUnconscious))
703  {
704  return;
705  }
706  }
707  }
708  }
709  Controller closestButton = null;
710  float closestDist = 0;
711  bool canAccess = CanAccessDoor(door, button =>
712  {
713  // Check that the button is on the right side of the door.
714  if (nextWaypoint != null)
715  {
716  if (door.LinkedGap.IsHorizontal)
717  {
718  int dir = Math.Sign((nextWaypoint).WorldPosition.X - door.Item.WorldPosition.X);
719  if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; }
720  }
721  else
722  {
723  int dir = Math.Sign((nextWaypoint).WorldPosition.Y - door.Item.WorldPosition.Y);
724  if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; }
725  }
726  }
727  float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
728  //heavily prefer buttons linked to the door, so sub builders can help the bots figure out which button to use by linking them
729  if (door.Item.linkedTo.Contains(button.Item)) { distance *= 0.1f; }
730  if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item))
731  {
732  closestButton = button;
733  closestDist = distance;
734  }
735  return true;
736  });
737  if (canAccess)
738  {
739  bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.shouldBeOpen != shouldBeOpen;
740  if (door.HasIntegratedButtons)
741  {
742  if (pressButton && character.CanSeeTarget(door.Item))
743  {
744  if (door.Item.TryInteract(character, forceSelectKey: true))
745  {
746  lastDoor = (door, shouldBeOpen);
747  buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
748  }
749  else
750  {
751  buttonPressTimer = 0;
752  }
753  }
754  break;
755  }
756  else if (closestButton != null)
757  {
758  if (closestDist < MathUtils.Pow2(closestButton.Item.InteractDistance + GetColliderLength()))
759  {
760  if (pressButton)
761  {
762  if (closestButton.Item.TryInteract(character, forceSelectKey: true))
763  {
764  lastDoor = (door, shouldBeOpen);
765  buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
766  }
767  else
768  {
769  buttonPressTimer = 0;
770  }
771  }
772  break;
773  }
774  else
775  {
776  // Can't reach the button closest to the character.
777  // It's possible that we could reach another buttons.
778  // If this becomes an issue, we could go through them here and check if any of them are reachable
779  // (would have to cache a collection of buttons instead of a single reference in the CanAccess filter method above)
780  var body = Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel);
781  if (body != null)
782  {
783  if (body.UserData is Item item)
784  {
785  var d = item.GetComponent<Door>();
786  if (d == null || d.IsOpen) { return; }
787  }
788  // The button is on the wrong side of the door or a wall
789  currentPath.Unreachable = true;
790  }
791  return;
792  }
793  }
794  }
795  else if (shouldBeOpen)
796  {
797 #if DEBUG
798  DebugConsole.NewMessage($"{character.Name}: Pathfinding error: Cannot access the door", Color.Yellow);
799 #endif
800  currentPath.Unreachable = true;
801  return;
802  }
803  }
804  }
805  }
806 
807  private float? GetNodePenalty(PathNode node, PathNode nextNode)
808  {
809  if (character == null) { return 0.0f; }
810  float? penalty = GetSingleNodePenalty(nextNode);
811  if (penalty == null) { return null; }
812  bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
813  if (!character.CanClimb)
814  {
815  if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) ||
816  (nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
817  nextNodeAboveWaterLevel)) //upper node not underwater
818  {
819  return null;
820  }
821  }
822 
823  if (node.Waypoint.CurrentHull != null)
824  {
825  var hull = node.Waypoint.CurrentHull;
826  if (hull.FireSources.Count > 0)
827  {
828  foreach (FireSource fs in hull.FireSources)
829  {
830  penalty += fs.Size.X * 10.0f;
831  }
832  }
833  if (character.NeedsAir)
834  {
835  if (hull.WaterVolume / hull.Rect.Width > 100.0f)
836  {
837  if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1)
838  {
839  penalty += 500.0f;
840  }
841  }
842  if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
843  {
844  penalty += 1000.0f;
845  }
846  }
847 
848  float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y);
849  if (nextNodeAboveWaterLevel && node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null)
850  {
851  penalty += yDist * 10.0f;
852  }
853  }
854 
855  return penalty;
856  }
857 
858  private float? GetSingleNodePenalty(PathNode node)
859  {
860  if (!node.Waypoint.IsTraversable) { return null; }
861  if (node.IsBlocked()) { return null; }
862  float penalty = 0.0f;
863  if (node.Waypoint.ConnectedGap is { Open: < 0.9f })
864  {
865  var door = node.Waypoint.ConnectedDoor;
866  if (door == null)
867  {
868  penalty = 100.0f;
869  }
870  else
871  {
872  if (!CanAccessDoor(door, button =>
873  {
874  // Ignore buttons that are on the wrong side of the door, unless there's a motion sensor connected to the door, which can be triggered by the character.
875  if (door.IsHorizontal)
876  {
877  if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y))
878  {
879  return door.Item.GetDirectlyConnectedComponent<MotionSensor>() is MotionSensor ms && ms.TriggersOn(character);
880  }
881  }
882  else
883  {
884  if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X))
885  {
886  return door.Item.GetDirectlyConnectedComponent<MotionSensor>() is MotionSensor ms && ms.TriggersOn(character);
887  }
888  }
889  return true;
890  }))
891  {
892  return null;
893  }
894  }
895  }
896  return penalty;
897  }
898 
899  public static float smallRoomSize = 500;
900  public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true)
901  {
902  //steer away from edges of the hull
903  bool wander = false;
904  bool inWater = character.AnimController.InWater;
905  Hull currentHull = character.CurrentHull;
906  // 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.
907  // if (!inWater)
908  // {
909  // Vector2 colliderBottomPos = ConvertUnits.ToDisplayUnits(character.AnimController.GetColliderBottom());
910  // if (Hull.FindHull(colliderBottomPos, guess: currentHull, useWorldCoordinates: false) is Hull lowestHull)
911  // {
912  // // Use the hull found at the collider bottom, if found.
913  // // 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.
914  // currentHull = lowestHull;
915  // }
916  // }
917  if (currentHull != null && !inWater)
918  {
919  float roomWidth = currentHull.Rect.Width;
920  if (stayStillInTightSpace && roomWidth < Math.Max(wallAvoidDistance * 3, smallRoomSize))
921  {
922  Reset();
923  }
924  else
925  {
926  float leftDist = character.Position.X - currentHull.Rect.X;
927  float rightDist = currentHull.Rect.Right - character.Position.X;
928  if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
929  {
930  if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2)
931  {
932  SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
933  return;
934  }
935  else if (stayStillInTightSpace)
936  {
937  Reset();
938  return;
939  }
940  }
941  if (leftDist < wallAvoidDistance)
942  {
943  float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
944  SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
945  WanderAngle = 0.0f;
946  }
947  else if (rightDist < wallAvoidDistance)
948  {
949  float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
950  SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
951  WanderAngle = MathHelper.Pi;
952  }
953  else
954  {
955  wander = true;
956  }
957  }
958  }
959  else
960  {
961  wander = true;
962  }
963  if (wander)
964  {
965  SteeringWander();
966  if (inWater)
967  {
968  SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance), 5);
969  }
970  }
971  if (!inWater)
972  {
973  //reset vertical steering to prevent dropping down from platforms etc
974  ResetY();
975  }
976  }
977  }
978 }
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))....
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
@ Character
Characters only