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;
324 bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
327 var ladders = currentLadder ?? nextLadder;
328 bool useLadders = canClimb && ladders !=
null;
329 var collider = character.AnimController.Collider;
330 Vector2 colliderSize = collider.GetSize();
333 if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
338 else if (!character.IsClimbing && currentPath.
NextNode !=
null && nextLadder ==
null)
341 if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
347 else if (isDiving &&
steering.Y < 1)
353 if (character.IsClimbing && !useLadders)
355 if (currentPath.
IsAtEndNode && canClimb && ladders !=
null)
362 character.StopClimbing();
365 if (useLadders && character.SelectedSecondaryItem != ladders.Item)
367 if (character.CanInteractWith(ladders.Item))
369 ladders.Item.TryInteract(character, forceSelectKey:
true);
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)
384 if (character.IsClimbing && useLadders)
386 if (currentLadder ==
null && nextLadder !=
null && character.SelectedSecondaryItem == nextLadder.
Item)
389 NextNode(!doorsChecked);
393 bool nextLadderSameAsCurrent = currentLadder == nextLadder;
394 float colliderHeight = collider.Height / 2 + collider.Radius;
396 float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
397 if (currentLadder !=
null && nextLadder !=
null)
402 if (Math.Abs(heightDiff) < colliderHeight * 1.25f)
404 if (nextLadder !=
null && !nextLadderSameAsCurrent)
407 if (character.SelectedSecondaryItem != nextLadder.
Item && character.CanInteractWith(nextLadder.
Item))
411 NextNode(!doorsChecked);
419 float colliderBottom = character.AnimController.Collider.SimPosition.Y;
420 float floorY = character.AnimController.FloorY;
421 isAboveFloor = colliderBottom > floorY;
428 float heightFromFloor = character.AnimController.GetHeightFromFloor();
429 isAboveFloor = heightFromFloor > -0.1f;
433 if (Math.Abs(diff.Y) < distanceMargin)
435 NextNode(!doorsChecked);
440 character.StopClimbing();
444 else if (currentLadder !=
null && currentPath.
NextNode !=
null)
451 NextNode(!doorsChecked);
456 else if (character.AnimController.InWater)
460 if (door ==
null || door.CanBeTraversed)
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);
468 verticalDistance *= 2;
470 float distance = horizontalDistance + verticalDistance;
471 if (ConvertUnits.ToSimUnits(distance) < targetDistance)
473 NextNode(!doorsChecked);
480 Vector2 colliderBottom = character.AnimController.GetColliderBottom();
481 Vector2 velocity = collider.LinearVelocity;
485 float minHeight = 1.6125001f;
486 float minWidth = 0.3225f;
488 float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
493 float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
494 float colliderHeight = collider.Height / 2 + collider.Radius;
498 if (heightDiff < colliderHeight)
510 if (!isNextNodeInSameStairs)
513 if (currentPath.
CurrentNode.
SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
515 isTargetTooLow =
true;
519 float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
520 if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow)
522 if (door is not { CanBeTraversed:
false } && (currentLadder ==
null || nextLadder ==
null))
524 NextNode(!doorsChecked);
532 return ConvertUnits.ToSimUnits(diff);
535 private void NextNode(
bool checkDoors)
550 if (!ShouldBreakDoor(door))
553 if (!canOpenDoors || character.LockHands) {
return false; }
558 return door.
IsOpen || door.
HasAccess(character) || ShouldBreakDoor(door);
563 bool canAccessButtons =
false;
564 foreach (var button
in door.
Item.GetConnectedComponents<
Controller>(
true, connectionFilter: c => c.Name ==
"toggle" || c.Name ==
"set_state"))
566 if (button.HasAccess(character) && (buttonFilter ==
null || buttonFilter(button)))
568 canAccessButtons =
true;
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)))
578 canAccessButtons =
true;
581 return canAccessButtons || door.
IsOpen || ShouldBreakDoor(door);
585 private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize());
587 private float GetColliderLength()
589 Vector2 colliderSize = character.AnimController.Collider.GetSize();
590 return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y));
593 private (
Door door,
bool shouldBeOpen) lastDoor;
594 private float GetDoorCheckTime()
598 return character.AnimController.IsMovingFast ? 0.1f : 0.3f;
602 return float.PositiveInfinity;
606 private void CheckDoorsInPath()
608 checkDoorsTimer = GetDoorCheckTime();
609 if (!canOpenDoors) {
return; }
610 for (
int i = 0; i < 5; i++)
612 WayPoint currentWaypoint =
null;
613 WayPoint nextWaypoint =
null;
615 bool shouldBeOpen =
false;
616 if (currentPath.
Nodes.Count == 1)
618 door = currentPath.
Nodes.First().ConnectedDoor;
619 shouldBeOpen = door !=
null;
620 if (i > 0) {
break; }
627 nextWaypoint = currentPath.
NextNode;
632 if (previousIndex < 0) {
break; }
633 currentWaypoint = currentPath.
Nodes[previousIndex];
636 if (currentWaypoint?.ConnectedDoor ==
null) {
continue; }
638 if (nextWaypoint ==
null)
642 if (currentWaypoint.ConnectedDoor.LinkedGap is Gap linkedGap)
644 if (currentWaypoint.Submarine ==
null ||
645 currentWaypoint.Submarine.Info is { IsPlayer: false } ||
646 !linkedGap.IsRoomToRoom ||
647 (linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false }))
650 door = currentWaypoint.ConnectedDoor;
656 float colliderLength = GetColliderLength();
657 door = currentWaypoint.ConnectedDoor;
661 float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X;
662 shouldBeOpen = (door.
Item.
WorldPosition.X - character.WorldPosition.X) * dir > -size;
667 shouldBeOpen = (door.
Item.
WorldPosition.Y - character.WorldPosition.Y) * dir > -colliderLength;
672 if (door ==
null) {
return; }
680 if (character.AIController is HumanAIController humanAI)
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 };
688 if (
Character.CharacterList.Any(c => c != character && humanAI.IsFriendly(c) && humanAI.VisibleHulls.Contains(c.CurrentHull) && !c.IsUnconscious))
696 float closestDist = 0;
700 if (nextWaypoint !=
null)
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; }
710 if (button.Item.WorldPosition.Y * dir > door.
Item.
WorldPosition.Y * dir) { return false; }
713 float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
715 if (door.
Item.
linkedTo.Contains(button.Item)) { distance *= 0.1f; }
716 if (closestButton ==
null || distance < closestDist && character.CanSeeTarget(button.Item))
718 closestButton = button;
719 closestDist = distance;
725 bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.shouldBeOpen != shouldBeOpen;
728 if (pressButton && character.CanSeeTarget(door.
Item))
732 lastDoor = (door, shouldBeOpen);
733 buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
737 buttonPressTimer = 0;
742 else if (closestButton !=
null)
750 lastDoor = (door, shouldBeOpen);
751 buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0;
755 buttonPressTimer = 0;
766 var body =
Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.
Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel);
769 if (body.UserData is Item item)
771 var d = item.GetComponent<
Door>();
772 if (d ==
null || d.IsOpen) {
return; }
781 else if (shouldBeOpen)
784 DebugConsole.NewMessage($
"{character.Name}: Pathfinding error: Cannot access the door", Color.Yellow);
793 private float? GetNodePenalty(PathNode node, PathNode nextNode)
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;
800 if (!(character.AnimController is HumanoidAnimController))
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 &&
804 nextNodeAboveWaterLevel))
810 if (node.Waypoint.CurrentHull !=
null)
812 var hull = node.Waypoint.CurrentHull;
813 if (hull.FireSources.Count > 0)
815 foreach (FireSource fs
in hull.FireSources)
817 penalty += fs.Size.X * 10.0f;
820 if (character.NeedsAir)
822 if (hull.WaterVolume / hull.Rect.Width > 100.0f)
824 if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1)
829 if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
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)
838 penalty += yDist * 10.0f;
845 private float? GetSingleNodePenalty(PathNode node)
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)
852 var door = node.Waypoint.ConnectedDoor;
859 if (!CanAccessDoor(door, button =>
862 if (door.IsHorizontal)
864 if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y))
871 if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X))
886 public static float smallRoomSize = 500;
887 public void Wander(
float deltaTime,
float wallAvoidDistance = 150,
bool stayStillInTightSpace =
true)
891 bool inWater = character.AnimController.InWater;
892 Hull currentHull = character.CurrentHull;
904 if (currentHull !=
null && !inWater)
906 float roomWidth = currentHull.
Rect.Width;
907 if (stayStillInTightSpace && roomWidth < Math.Max(wallAvoidDistance * 3, smallRoomSize))
913 float leftDist = character.Position.X - currentHull.
Rect.X;
914 float rightDist = currentHull.
Rect.Right - character.Position.X;
915 if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
917 if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2)
919 SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
922 else if (stayStillInTightSpace)
928 if (leftDist < wallAvoidDistance)
930 float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
931 SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
934 else if (rightDist < wallAvoidDistance)
936 float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
937 SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
938 WanderAngle = MathHelper.Pi;
955 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)
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