using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma { class AIObjectiveIdle : AIObjective { public override string DebugTag => "idle"; const float WallAvoidDistance = 150.0f; private readonly float newTargetIntervalMin = 5; private readonly float newTargetIntervalMax = 15; private readonly float standStillMin = 1; private readonly float standStillMax = 10; private readonly float walkDurationMin = 3; private readonly float walkDurationMax = 10; private Hull currentTarget; private float newTargetTimer; private float standStillTimer; private float walkDuration; public AIObjectiveIdle(Character character) : base(character, "") { standStillTimer = Rand.Range(-10.0f, 10.0f); walkDuration = Rand.Range(0.0f, 10.0f); } public override bool IsCompleted() => false; public override bool CanBeCompleted => true; public override float GetPriority(AIObjectiveManager objectiveManager) { return 1.0f; } protected override void Act(float deltaTime) { if (PathSteering == null) return; //don't keep dragging others when idling if (character.SelectedCharacter != null) { character.DeselectCharacter(); } if (!character.IsClimbing) { character.SelectedConstruction = null; } bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); if (currentTargetIsInvalid || (currentTarget == null && IsForbidden(character.CurrentHull))) { newTargetTimer = 0; standStillTimer = 0; } if (character.AnimController.InWater || character.IsClimbing) { standStillTimer = 0; } if (newTargetTimer <= 0.0f) { currentTarget = FindRandomHull(); if (currentTarget != null) { string errorMsg = null; #if DEBUG bool isRoomNameFound = currentTarget.RoomName != null; errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.RoomName : currentTarget.ToString()) + ")"; #endif var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsg); PathSteering.SetPath(path); } newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax); } newTargetTimer -= deltaTime; //wander randomly // - if reached the end of the path // - if the target is unreachable // - if the path requires going outside if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null && (PathSteering.CurrentPath.NextNode == null || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes))) { standStillTimer -= deltaTime; if (standStillTimer > 0.0f) { walkDuration = Rand.Range(walkDurationMin, walkDurationMax); PathSteering.Reset(); return; } if (standStillTimer < -walkDuration) { standStillTimer = Rand.Range(standStillMin, standStillMax); } Wander(deltaTime); return; } if (currentTarget != null) { character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition); } } public void Wander(float deltaTime) { //steer away from edges of the hull if (character.AnimController.CurrentHull != null && !character.IsClimbing) { float leftDist = character.Position.X - character.AnimController.CurrentHull.Rect.X; float rightDist = character.AnimController.CurrentHull.Rect.Right - character.Position.X; if (leftDist < WallAvoidDistance && rightDist < WallAvoidDistance) { if (Math.Abs(rightDist - leftDist) > WallAvoidDistance / 2) { PathSteering.SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist)); } else { PathSteering.Reset(); } } else if (leftDist < WallAvoidDistance) { //PathSteering.SteeringManual(deltaTime, Vector2.UnitX * (WallAvoidDistance - leftDist) / WallAvoidDistance); PathSteering.SteeringManual(deltaTime, Vector2.UnitX); PathSteering.WanderAngle = 0.0f; } else if (rightDist < WallAvoidDistance) { //PathSteering.SteeringManual(deltaTime, -Vector2.UnitX * (WallAvoidDistance - rightDist) / WallAvoidDistance); PathSteering.SteeringManual(deltaTime, -Vector2.UnitX); PathSteering.WanderAngle = MathHelper.Pi; } else { SteeringManager.SteeringWander(); } } else { SteeringManager.SteeringWander(); } if (!character.IsClimbing && !character.AnimController.InWater) { //reset vertical steering to prevent dropping down from platforms etc character.AIController.SteeringManager.ResetY(); } } private readonly List targetHulls = new List(20); private readonly List hullWeights = new List(20); private Hull FindRandomHull() { var idCard = character.Inventory.FindItemByIdentifier("idcard"); Hull targetHull = null; bool isCurrentHullOK = !HumanAIController.UnsafeHulls.Contains(character.CurrentHull) && !IsForbidden(character.CurrentHull); //random chance of navigating back to the room where the character spawned if (Rand.Int(5) == 1 && idCard != null) { foreach (WayPoint wp in WayPoint.WayPointList) { if (wp.SpawnType != SpawnType.Human || wp.CurrentHull == null) { continue; } foreach (string tag in wp.IdCardTags) { if (idCard.HasTag(tag)) { targetHull = wp.CurrentHull; } } } } if (targetHull == null) { targetHulls.Clear(); hullWeights.Clear(); foreach (var hull in Hull.hullList) { if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; } if (hull.Submarine == null) { continue; } if (hull.Submarine.TeamID != character.TeamID) { continue; } // If the character is inside, only take connected hulls into account. if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; } if (IsForbidden(hull)) { continue; } // Ignore hulls that are too low to stand inside if (character.AnimController is HumanoidAnimController animController) { if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value)) { continue; } } if (isCurrentHullOK) { // Check that there is no unsafe or forbidden hulls on the way to the target // Only do this when the current hull is ok, because otherwise the would block all paths from the current hull to the target hull. var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition); if (path.Unreachable) { continue; } if (path.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull) || IsForbidden(n.CurrentHull))) { continue; } } // If we want to do a steering check, we should do it here, before setting the path //if (path.Cost > 1000.0f) { continue; } if (!targetHulls.Contains(hull)) { targetHulls.Add(hull); hullWeights.Add(hull.Volume); } } return ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); } return targetHull; } private bool IsForbidden(Hull hull) { if (hull == null) { return true; } string hullName = hull.RoomName?.ToLowerInvariant(); bool isForbidden = hullName == "ballast" || hullName == "airlock"; foreach (Item item in Item.ItemList) { if (item.CurrentHull == hull && (item.HasTag("ballast") || item.HasTag("airlock"))) { isForbidden = true; break; } } return isForbidden; } public override bool IsDuplicate(AIObjective otherObjective) { return (otherObjective is AIObjectiveIdle); } } }