using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Linq; namespace Barotrauma { class IndoorsSteeringManager : SteeringManager { private PathFinder pathFinder; private SteeringPath currentPath; private bool canOpenDoors, canBreakDoors; private Character character; private Vector2 currentTarget; private float findPathTimer; private float buttonPressCooldown; const float ButtonPressInterval = 0.5f; public SteeringPath CurrentPath { get { return currentPath; } } public PathFinder PathFinder { get { return pathFinder; } } public Vector2 CurrentTarget { get { return currentTarget; } } public bool IsPathDirty { get; private set; } public bool InLadders => currentPath != null && currentPath.CurrentNode != null && currentPath.CurrentNode.Ladders != null; public bool IsNextNodeLadder => currentPath.NextNode != null && currentPath.CurrentNode.Ladders != null; public bool IsNextLadderSameAsCurrent => IsNextNodeLadder && currentPath.CurrentNode.Ladders == currentPath.NextNode.Ladders; public bool InStairs => currentPath != null && currentPath.CurrentNode != null && currentPath.CurrentNode.Stairs != null; public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host) { pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true); pathFinder.GetNodePenalty = GetNodePenalty; this.canOpenDoors = canOpenDoors; this.canBreakDoors = canBreakDoors; character = (host as AIController).Character; findPathTimer = Rand.Range(0.0f, 1.0f); } public override void Update(float speed) { base.Update(speed); buttonPressCooldown -= 1.0f / 60.0f; findPathTimer -= 1.0f / 60.0f; } public void SetPath(SteeringPath path) { currentPath = path; if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition; findPathTimer = 1.0f; IsPathDirty = false; } protected override Vector2 DoSteeringSeek(Vector2 target, float weight) { //find a new path if one hasn't been found yet or the target is different from the current target if (currentPath == null || Vector2.Distance(target, currentTarget) > 1.0f || findPathTimer < -1.0f) { IsPathDirty = true; if (findPathTimer > 0.0f) return Vector2.Zero; currentTarget = target; Vector2 pos = host.SimPosition; if (character != null && character.Submarine == null) { var targetHull = Hull.FindHull(FarseerPhysics.ConvertUnits.ToDisplayUnits(target), null, false); if (targetHull != null && targetHull.Submarine != null) { pos -= targetHull.Submarine.SimPosition; } } currentPath = pathFinder.FindPath(pos, target, "(Character: " + character.Name + ")"); findPathTimer = Rand.Range(1.0f, 1.2f); IsPathDirty = false; return DiffToCurrentNode(); } Vector2 diff = DiffToCurrentNode(); var collider = character.AnimController.Collider; //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically if (!character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius) { diff.Y = 0.0f; } if (diff.LengthSquared() < 0.001f) return -host.Steering; return Vector2.Normalize(diff) * weight; } private Vector2 DiffToCurrentNode() { if (currentPath == null || currentPath.Unreachable) return Vector2.Zero; if (currentPath.Finished) { Vector2 pos2 = host.SimPosition; if (character != null && character.Submarine == null && CurrentPath.Nodes.Count > 0 && CurrentPath.Nodes.Last().Submarine != null) { pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition; } return currentTarget - pos2; } if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f) { CheckDoorsInPath(); } Vector2 pos = host.SimPosition; if (character != null && currentPath.CurrentNode != null) { if (CurrentPath.CurrentNode.Submarine != null) { if (character.Submarine == null) { pos -= CurrentPath.CurrentNode.Submarine.SimPosition; } else if (character.Submarine != currentPath.CurrentNode.Submarine) { pos -= FarseerPhysics.ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position-character.Submarine.Position); } } } //only humanoids can climb ladders if (character.AnimController is HumanoidAnimController && InLadders && IsNextLadderSameAsCurrent) { if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item && currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition)) { currentPath.CurrentNode.Ladders.Item.TryInteract(character, false, true); } } var collider = character.AnimController.Collider; if (character.IsClimbing) { Vector2 diff = currentPath.CurrentNode.SimPosition - pos; if (IsNextLadderSameAsCurrent) { //climbing ladders -> don't move horizontally diff.X = 0.0f; } //at the same height as the waypoint if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f) { float heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY; if (heightFromFloor <= 0.0f) { diff.Y = Math.Max(diff.Y, 1.0f); } bool aboveFloor = heightFromFloor > 0.0f && heightFromFloor < collider.height * 1.5f; if (aboveFloor || IsNextNodeLadder) { if (!IsNextLadderSameAsCurrent) { character.AnimController.Anim = AnimController.Animation.None; } currentPath.SkipToNextNode(); } } else if (IsNextLadderSameAsCurrent) { //if the current node is below the character and the next one is above (or vice versa) //and both are on ladders, we can skip directly to the next one //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) { currentPath.SkipToNextNode(); } } character.AnimController.IgnorePlatforms = false; return diff; } else if (character.AnimController.InWater) { if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < MathUtils.Pow(collider.radius * 3, 2)) { currentPath.SkipToNextNode(); } } else { Vector2 colliderBottom = character.AnimController.GetColliderBottom(); Vector2 colliderSize = collider.GetSize(); // Cannot use the head position, because not all characters have head or it can be below the total height of the character float characterHeight = colliderSize.Y + character.AnimController.ColliderHeightFromFloor; float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X); bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y; bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight; if (horizontalDistance < collider.radius * 3 && isAboveFeet && isNotTooHigh) { currentPath.SkipToNextNode(); } } if (currentPath.CurrentNode == null) return Vector2.Zero; return currentPath.CurrentNode.SimPosition - pos; } private void CheckDoorsInPath() { for (int i = 0; i < 2; i++) { Door door = null; bool shouldBeOpen = false; if (currentPath.Nodes.Count == 1) { door = currentPath.Nodes.First().ConnectedDoor; shouldBeOpen = door != null; } else { WayPoint node = null; WayPoint nextNode = null; if (i == 0) { node = currentPath.CurrentNode; nextNode = currentPath.NextNode; } else { node = currentPath.PrevNode; nextNode = currentPath.CurrentNode; } if (node?.ConnectedDoor == null) continue; if (nextNode == null) { //the node we're heading towards is the last one in the path, and at a door //the door needs to be open for the character to reach the node shouldBeOpen = true; } else { door = node.ConnectedGap.ConnectedDoor; if (door.LinkedGap.IsHorizontal) { int currentDir = Math.Sign(nextNode.WorldPosition.X - door.Item.WorldPosition.X); shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f; } else { int currentDir = Math.Sign(nextNode.WorldPosition.Y - door.Item.WorldPosition.Y); shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -80.0f; } } } if (door == null) return; //toggle the door if it's the previous node and open, or if it's current node and closed if (door.IsOpen != shouldBeOpen) { var buttons = door.Item.GetConnectedComponents(true); Controller closestButton = null; float closestDist = 0.0f; foreach (Controller controller in buttons) { float dist = Vector2.Distance(controller.Item.WorldPosition, character.WorldPosition); if (dist > controller.Item.InteractDistance * 2.0f) continue; if (dist < closestDist || closestButton == null) { closestButton = controller; closestDist = dist; } } if (closestButton != null) { if (!closestButton.HasRequiredItems(character, false) && shouldBeOpen) { currentPath.Unreachable = true; return; } closestButton.Item.TryInteract(character, false, true, false); buttonPressCooldown = ButtonPressInterval; break; } } } } private float? GetNodePenalty(PathNode node, PathNode nextNode) { if (character == null) return 0.0f; float penalty = 0.0f; if (nextNode.Waypoint.ConnectedGap != null && nextNode.Waypoint.ConnectedGap.Open < 0.9f) { if (nextNode.Waypoint.ConnectedDoor == null) { penalty = 100.0f; } if (!canBreakDoors) { //door closed and the character can't open doors -> node can't be traversed if (!canOpenDoors || character.LockHands) return null; var doorButtons = nextNode.Waypoint.ConnectedDoor.Item.GetConnectedComponents(); if (!doorButtons.Any()) return null; foreach (Controller button in doorButtons) { if (Math.Sign(button.Item.Position.X - nextNode.Waypoint.Position.X) != Math.Sign(node.Position.X - nextNode.Position.X)) continue; if (!button.HasRequiredItems(character, false)) return null; } } } //non-humanoids can't climb up ladders if (!(character.AnimController is HumanoidAnimController)) { if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y) //upper node not underwater { return null; } } if (node.Waypoint != null && node.Waypoint.CurrentHull != null) { var hull = node.Waypoint.CurrentHull; if (hull.FireSources.Count > 0) { foreach (FireSource fs in hull.FireSources) { penalty += fs.Size.X * 10.0f; } } if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f) penalty += 500.0f; if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume) penalty += 1000.0f; } return penalty; } } }