3 using Microsoft.Xna.Framework;
15 private readonly
bool canOpenDoors;
18 private bool ShouldBreakDoor(
Door door) =>
20 !door.Item.Indestructible && !door.Item.InvulnerableToDamage &&
21 (door.Item.Submarine ==
null || door.Item.Submarine.TeamID != character.TeamID);
28 private Vector2 currentTargetPos;
30 private float findPathTimer;
32 private const float ButtonPressCooldown = 1;
33 private float checkDoorsTimer;
34 private float buttonPressTimer;
38 get {
return currentPath; }
43 get {
return pathFinder; }
55 public bool PathHasStairs => currentPath !=
null && currentPath.
Nodes.Any(n => n.Stairs !=
null);
66 if (currentLadder ==
null) {
return false; }
75 GetNodePenalty = GetNodePenalty,
76 GetSingleNodePenalty = GetSingleNodePenalty
79 this.canOpenDoors = canOpenDoors;
84 findPathTimer = Rand.Range(0.0f, 1.0f);
87 public override void Update(
float speed)
90 float step = 1.0f / 60.0f;
91 checkDoorsTimer -= step;
92 if (lastDoor.door ==
null || !lastDoor.shouldBeOpen || lastDoor.door.
IsFullyOpen)
98 buttonPressTimer -= step;
100 findPathTimer -= step;
105 currentTargetPos = targetPos;
107 findPathTimer = Math.Min(findPathTimer, 1.0f);
119 steering += base.DoSteeringSeek(targetSimPos, weight);
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)
125 Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
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)
145 if (!needsNewPath && character.Submarine !=
null && character.Params.PathFinderPriority > 0.5f)
150 Vector2 targetDiff = target - currentTargetPos;
151 if (targetDiff.LengthSquared() > 1)
157 if (needsNewPath || findPathTimer < -1.0f)
160 if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp)
162 if (character.Submarine !=
null && wp.Ladders ==
null && wp.ConnectedDoor ==
null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
167 if (character.Submarine ==
null && wp.CurrentHull !=
null)
172 if (Vector2.DistanceSquared(character.WorldPosition, wp.WorldPosition) > maxDist * maxDist)
178 if (findPathTimer < 0)
180 SkipCurrentPathNodes();
181 currentTargetPos = target;
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)
190 if (IsIdenticalPath())
194 else if (!character.IsClimbing)
198 useNewPath = newPath.Cost < currentPath.
Cost * MathHelper.Lerp(0.95f, 0, t);
199 if (!useNewPath && character.Submarine !=
null)
204 useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.
CurrentNode.
WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
207 if (!useNewPath && !character.CanSeeTarget(currentPath.
CurrentNode))
216 bool IsIdenticalPath()
218 int nodeCount = newPath.Nodes.Count;
219 if (nodeCount == currentPath.
Nodes.Count)
221 for (
int i = 0; i < nodeCount - 1; i++)
223 if (newPath.Nodes[i] != currentPath.
Nodes[i])
235 if (currentPath !=
null)
239 currentPath = newPath;
241 float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
242 findPathTimer = priority * Rand.Range(1.0f, 1.2f);
245 void SkipCurrentPathNodes()
247 if (!character.AnimController.InWater || character.Submarine !=
null) {
return; }
251 Submarine targetSub = lastNode.Submarine;
252 if (targetSub !=
null)
254 float subSize = Math.Max(targetSub.Borders.Size.X, targetSub.Borders.Size.Y) / 2;
256 if (Vector2.DistanceSquared(character.WorldPosition, targetSub.WorldPosition) < MathUtils.Pow2(subSize + margin))
272 float directDistance = Vector2.DistanceSquared(character.WorldPosition, waypoint.WorldPosition);
273 if (directDistance > MathUtils.Pow2(pathDistance) || !character.CanSeeTarget(waypoint))
285 Vector2 diff = DiffToCurrentNode();
286 if (diff == Vector2.Zero) {
return Vector2.Zero; }
287 return Vector2.Normalize(diff) * weight;
290 protected override Vector2
DoSteeringSeek(Vector2 target,
float weight) => CalculateSteeringSeek(target, weight);
292 private Vector2 DiffToCurrentNode()
294 if (currentPath ==
null || currentPath.
Unreachable)
305 return currentTargetPos - hostPosition;
307 bool doorsChecked =
false;
308 checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime());
309 if (!character.LockHands && checkDoorsTimer <= 0.0f)
314 if (buttonPressTimer > 0 && lastDoor.door !=
null && lastDoor.shouldBeOpen && !lastDoor.door.
IsFullyOpen)
322 bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
323 bool canClimb = character.CanClimb;
326 var ladders = currentLadder ?? nextLadder;
327 bool useLadders = canClimb && ladders !=
null;
328 var collider = character.AnimController.Collider;
329 Vector2 colliderSize = collider.GetSize();
332 if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
337 else if (!character.IsClimbing && currentPath.
NextNode !=
null && nextLadder ==
null)
340 if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
346 else if (isDiving &&
steering.Y < 1)
352 if (character.IsClimbing && !useLadders)
354 if (currentPath.
IsAtEndNode && canClimb && ladders !=
null)
361 character.StopClimbing();
364 if (useLadders && character.SelectedSecondaryItem != ladders.Item)
366 if (character.CanInteractWith(ladders.Item))
368 ladders.Item.TryInteract(character, forceSelectKey:
true);
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)
383 if (character.IsClimbing && useLadders)
385 if (currentLadder ==
null && nextLadder !=
null && character.SelectedSecondaryItem == nextLadder.
Item)
388 NextNode(!doorsChecked);
392 bool nextLadderSameAsCurrent = currentLadder == nextLadder;
393 float colliderHeight = collider.Height / 2 + collider.Radius;
395 float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
396 if (currentLadder !=
null && nextLadder !=
null)
401 if (Math.Abs(heightDiff) < colliderHeight * 1.25f)
403 if (nextLadder !=
null && !nextLadderSameAsCurrent)
406 if (character.SelectedSecondaryItem != nextLadder.
Item && character.CanInteractWith(nextLadder.
Item))
410 NextNode(!doorsChecked);
418 float colliderBottom = character.AnimController.Collider.SimPosition.Y;
419 float floorY = character.AnimController.FloorY;
420 isAboveFloor = colliderBottom > floorY;
427 float heightFromFloor = character.AnimController.GetHeightFromFloor();
428 isAboveFloor = heightFromFloor > -0.1f;
432 if (Math.Abs(diff.Y) < distanceMargin)
434 NextNode(!doorsChecked);
439 character.StopClimbing();
443 else if (currentLadder !=
null && currentPath.
NextNode !=
null)
450 NextNode(!doorsChecked);
455 else if (character.AnimController.InWater)
459 if (door ==
null || door.CanBeTraversed)
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);
467 verticalDistance *= 2;
469 float distance = horizontalDistance + verticalDistance;
470 if (ConvertUnits.ToSimUnits(distance) < targetDistance)
472 NextNode(!doorsChecked);
479 Vector2 colliderBottom = character.AnimController.GetColliderBottom();
480 Vector2 velocity = collider.LinearVelocity;
484 float minHeight = 1.6125001f;
485 float minWidth = 0.3225f;
487 float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
492 float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
493 float colliderHeight = collider.Height / 2 + collider.Radius;
497 if (heightDiff < colliderHeight)
509 if (!isNextNodeInSameStairs)
512 if (currentPath.
CurrentNode.
SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
514 isTargetTooLow =
true;
518 float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
519 if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow)
521 if (door is not { CanBeTraversed:
false } && (currentLadder ==
null || nextLadder ==
null))
523 NextNode(!doorsChecked);
531 return ConvertUnits.ToSimUnits(diff);
534 private void NextNode(
bool checkDoors)
549 if (!ShouldBreakDoor(door))
552 if (!canOpenDoors || character.LockHands) {
return false; }
557 return door.
IsOpen || door.
HasAccess(character) || ShouldBreakDoor(door);
561 bool canAccessButtons =
false;
562 bool buttonsFound =
false;
565 foreach (
Controller button
in door.
Item.GetConnectedComponents<
Controller>(recursive:
true, connectionFilter: c => c.Name is
"toggle" or
"set_state"))
568 if (CanAccessButton(button))
570 canAccessButtons =
true;
573 if (!canAccessButtons)
578 if (linked is not
Item linkedItem) {
continue; }
579 var button = linkedItem.GetComponent<
Controller>();
580 if (button ==
null) {
continue; }
582 if (CanAccessButton(button))
584 canAccessButtons =
true;
588 if (door.
IsOpen || ShouldBreakDoor(door))
593 return buttonsFound ? canAccessButtons : door.
HasAccess(character);
595 bool CanAccessButton(
Controller button) => button.
HasAccess(character) && (buttonFilter ==
null || buttonFilter(button));
599 private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize());
601 private float GetColliderLength()
603 Vector2 colliderSize = character.AnimController.Collider.GetSize();
604 return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y));
607 private (
Door door,
bool shouldBeOpen) lastDoor;
608 private float GetDoorCheckTime()
612 return character.AnimController.IsMovingFast ? 0.1f : 0.3f;
616 return float.PositiveInfinity;
620 private void CheckDoorsInPath()
622 checkDoorsTimer = GetDoorCheckTime();
623 if (!canOpenDoors) {
return; }
624 for (
int i = 0; i < 5; i++)
626 WayPoint currentWaypoint =
null;
627 WayPoint nextWaypoint =
null;
629 bool shouldBeOpen =
false;
630 if (currentPath.
Nodes.Count == 1)
632 door = currentPath.
Nodes.First().ConnectedDoor;
633 shouldBeOpen = door !=
null;
634 if (i > 0) {
break; }
641 nextWaypoint = currentPath.
NextNode;
646 if (previousIndex < 0) {
break; }
647 currentWaypoint = currentPath.
Nodes[previousIndex];
650 if (currentWaypoint?.ConnectedDoor ==
null) {
continue; }
652 if (nextWaypoint ==
null)
656 if (currentWaypoint.ConnectedDoor.LinkedGap is Gap linkedGap)
658 if (currentWaypoint.Submarine ==
null ||
659 currentWaypoint.Submarine.Info is { IsPlayer: false } ||
660 !linkedGap.IsRoomToRoom ||
661 (linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false }))
664 door = currentWaypoint.ConnectedDoor;
670 float colliderLength = GetColliderLength();
671 door = currentWaypoint.ConnectedDoor;
675 float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X;
676 shouldBeOpen = (door.
Item.
WorldPosition.X - character.WorldPosition.X) * dir > -size;
681 shouldBeOpen = (door.
Item.
WorldPosition.Y - character.WorldPosition.Y) * dir > -colliderLength;
686 if (door ==
null) {
return; }
694 if (character.AIController is HumanAIController humanAI)
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 };
702 if (
Character.CharacterList.Any(c => c != character && humanAI.IsFriendly(c) && humanAI.VisibleHulls.Contains(c.CurrentHull) && !c.IsUnconscious))
710 float closestDist = 0;
714 if (nextWaypoint !=
null)
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; }
724 if (button.Item.WorldPosition.Y * dir > door.
Item.
WorldPosition.Y * dir) { return false; }
727 float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
729 if (door.
Item.
linkedTo.Contains(button.Item)) { distance *= 0.1f; }
730 if (closestButton ==
null || distance < closestDist && character.CanSeeTarget(button.Item))
732 closestButton = button;
733 closestDist = distance;
739 bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.shouldBeOpen != shouldBeOpen;
742 if (pressButton && character.CanSeeTarget(door.
Item))
746 lastDoor = (door, shouldBeOpen);
747 buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
751 buttonPressTimer = 0;
756 else if (closestButton !=
null)
764 lastDoor = (door, shouldBeOpen);
765 buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
769 buttonPressTimer = 0;
780 var body =
Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.
Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel);
783 if (body.UserData is Item item)
785 var d = item.GetComponent<
Door>();
786 if (d ==
null || d.IsOpen) {
return; }
795 else if (shouldBeOpen)
798 DebugConsole.NewMessage($
"{character.Name}: Pathfinding error: Cannot access the door", Color.Yellow);
807 private float? GetNodePenalty(PathNode node, PathNode nextNode)
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)
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 &&
817 nextNodeAboveWaterLevel))
823 if (node.Waypoint.CurrentHull !=
null)
825 var hull = node.Waypoint.CurrentHull;
826 if (hull.FireSources.Count > 0)
828 foreach (FireSource fs
in hull.FireSources)
830 penalty += fs.Size.X * 10.0f;
833 if (character.NeedsAir)
835 if (hull.WaterVolume / hull.Rect.Width > 100.0f)
837 if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1)
842 if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
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)
851 penalty += yDist * 10.0f;
858 private float? GetSingleNodePenalty(PathNode node)
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 })
865 var door = node.Waypoint.ConnectedDoor;
872 if (!CanAccessDoor(door, button =>
875 if (door.IsHorizontal)
877 if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y))
879 return door.Item.GetDirectlyConnectedComponent<MotionSensor>() is MotionSensor ms && ms.TriggersOn(character);
884 if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X))
886 return door.Item.GetDirectlyConnectedComponent<MotionSensor>() is MotionSensor ms && ms.TriggersOn(character);
899 public static float smallRoomSize = 500;
900 public void Wander(
float deltaTime,
float wallAvoidDistance = 150,
bool stayStillInTightSpace =
true)
904 bool inWater = character.AnimController.InWater;
905 Hull currentHull = character.CurrentHull;
917 if (currentHull !=
null && !inWater)
919 float roomWidth = currentHull.
Rect.Width;
920 if (stayStillInTightSpace && roomWidth < Math.Max(wallAvoidDistance * 3, smallRoomSize))
926 float leftDist = character.Position.X - currentHull.
Rect.X;
927 float rightDist = currentHull.
Rect.Right - character.Position.X;
928 if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
930 if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2)
932 SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
935 else if (stayStillInTightSpace)
941 if (leftDist < wallAvoidDistance)
943 float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
944 SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
947 else if (rightDist < wallAvoidDistance)
949 float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
950 SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
951 WanderAngle = MathHelper.Pi;
968 SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance), 5);
virtual Vector2 WorldPosition
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)
Ladder GetCurrentLadder()
bool IsNextLadderSameAsCurrent
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))....
bool HasIntegratedButtons
override bool HasAccess(Character character)
Only checks if any of the Picked requirements are matched (used for checking id card(s))....
readonly List< MapEntity > linkedTo
override Vector2 SimPosition
void SkipToNode(int nodeIndex)
float GetLength(int? startIndex=null, int? endIndex=null)
static List< WayPoint > WayPointList
@ Character
Characters only