504 lines
24 KiB
C#
504 lines
24 KiB
C#
using Barotrauma.Extensions;
|
|
using FarseerPhysics;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
class AIObjectiveFindSafety : AIObjective
|
|
{
|
|
public override Identifier Identifier { get; set; } = "find safety".ToIdentifier();
|
|
public override bool ForceRun => true;
|
|
public override bool KeepDivingGearOn => true;
|
|
public override bool IgnoreUnsafeHulls => true;
|
|
public override bool ConcurrentObjectives => true;
|
|
public override bool AllowOutsideSubmarine => true;
|
|
public override bool AllowInAnySub => true;
|
|
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
|
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
|
|
|
|
// TODO: expose?
|
|
const float priorityIncrease = 100;
|
|
const float priorityDecrease = 10;
|
|
const float SearchHullInterval = 3.0f;
|
|
|
|
private float currenthullSafety;
|
|
|
|
private float searchHullTimer;
|
|
|
|
private AIObjectiveGoTo goToObjective;
|
|
private AIObjectiveFindDivingGear divingGearObjective;
|
|
|
|
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
|
|
|
|
protected override bool CheckObjectiveSpecific() => false;
|
|
public override bool CanBeCompleted => true;
|
|
|
|
private bool resetPriority;
|
|
|
|
protected override float GetPriority()
|
|
{
|
|
if (!IsAllowed)
|
|
{
|
|
Priority = 0;
|
|
return Priority;
|
|
}
|
|
if (character.CurrentHull == null)
|
|
{
|
|
Priority = (
|
|
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
|
|
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
|
|
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
|
|
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
|
|
&& ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
|
|
}
|
|
else
|
|
{
|
|
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
|
|
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
|
|
{
|
|
Priority = AIObjectiveManager.MaxObjectivePriority;
|
|
}
|
|
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
|
|
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
|
|
{
|
|
// Ordered to follow, hold position, or return back to main sub inside a hostile sub
|
|
// -> ignore find safety unless we need to find a diving gear
|
|
Priority = 0;
|
|
}
|
|
else if (objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
|
|
{
|
|
Priority = 0;
|
|
}
|
|
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
|
|
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
|
|
{
|
|
// Boost the priority while seeking the diving gear
|
|
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority));
|
|
}
|
|
}
|
|
return Priority;
|
|
}
|
|
|
|
public override void Update(float deltaTime)
|
|
{
|
|
if (retryTimer > 0)
|
|
{
|
|
retryTimer -= deltaTime;
|
|
if (retryTimer <= 0)
|
|
{
|
|
retryCounter = 0;
|
|
}
|
|
}
|
|
if (resetPriority)
|
|
{
|
|
Priority = 0;
|
|
resetPriority = false;
|
|
return;
|
|
}
|
|
if (character.CurrentHull == null)
|
|
{
|
|
currenthullSafety = 0;
|
|
}
|
|
else
|
|
{
|
|
currenthullSafety = HumanAIController.CurrentHullSafety;
|
|
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
|
|
{
|
|
Priority -= priorityDecrease * deltaTime;
|
|
if (currenthullSafety >= 100 && !character.IsLowInOxygen)
|
|
{
|
|
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
|
|
Priority = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float dangerFactor = (100 - currenthullSafety) / 100;
|
|
Priority += dangerFactor * priorityIncrease * deltaTime;
|
|
}
|
|
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
|
|
}
|
|
}
|
|
|
|
private Hull currentSafeHull;
|
|
private Hull previousSafeHull;
|
|
private bool cannotFindSafeHull;
|
|
private bool cannotFindDivingGear;
|
|
private readonly int findDivingGearAttempts = 2;
|
|
private int retryCounter;
|
|
private readonly float retryResetTime = 5;
|
|
private float retryTimer;
|
|
protected override void Act(float deltaTime)
|
|
{
|
|
if (resetPriority) { return; }
|
|
var currentHull = character.CurrentHull;
|
|
bool dangerousPressure = (currentHull == null || currentHull.LethalPressure > 0) && !character.IsProtectedFromPressure;
|
|
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
|
|
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
|
|
{
|
|
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
|
|
bool needsEquipment = shouldActOnSuffocation;
|
|
if (needsDivingSuit)
|
|
{
|
|
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.GetMinOxygen(character));
|
|
}
|
|
else if (needsDivingGear)
|
|
{
|
|
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.GetMinOxygen(character));
|
|
}
|
|
if (needsEquipment)
|
|
{
|
|
if (cannotFindDivingGear && retryCounter < findDivingGearAttempts)
|
|
{
|
|
retryTimer = retryResetTime;
|
|
retryCounter++;
|
|
needsDivingSuit = !needsDivingSuit;
|
|
RemoveSubObjective(ref divingGearObjective);
|
|
}
|
|
if (divingGearObjective == null)
|
|
{
|
|
cannotFindDivingGear = false;
|
|
RemoveSubObjective(ref goToObjective);
|
|
TryAddSubObjective(ref divingGearObjective,
|
|
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
|
|
onAbandon: () =>
|
|
{
|
|
searchHullTimer = Math.Min(1, searchHullTimer);
|
|
cannotFindDivingGear = true;
|
|
// Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
|
|
},
|
|
onCompleted: () =>
|
|
{
|
|
resetPriority = true;
|
|
searchHullTimer = Math.Min(1, searchHullTimer);
|
|
RemoveSubObjective(ref divingGearObjective);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
|
|
{
|
|
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
|
|
{
|
|
searchHullTimer = Math.Min(1, searchHullTimer);
|
|
}
|
|
if (searchHullTimer > 0.0f)
|
|
{
|
|
searchHullTimer -= deltaTime;
|
|
}
|
|
else
|
|
{
|
|
HullSearchStatus hullSearchStatus = FindBestHull(out Hull potentialSafeHull, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
|
|
if (hullSearchStatus != HullSearchStatus.Finished)
|
|
{
|
|
UpdateSimpleEscape(deltaTime);
|
|
return;
|
|
}
|
|
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
|
|
previousSafeHull = currentSafeHull;
|
|
currentSafeHull = potentialSafeHull;
|
|
cannotFindSafeHull = currentSafeHull == null || NeedMoreDivingGear(currentSafeHull);
|
|
currentSafeHull ??= previousSafeHull;
|
|
if (currentSafeHull != null && currentSafeHull != currentHull)
|
|
{
|
|
if (goToObjective?.Target != currentSafeHull)
|
|
{
|
|
RemoveSubObjective(ref goToObjective);
|
|
}
|
|
TryAddSubObjective(ref goToObjective,
|
|
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
|
|
{
|
|
SpeakIfFails = false,
|
|
AllowGoingOutside =
|
|
character.IsProtectedFromPressure ||
|
|
character.CurrentHull == null ||
|
|
character.CurrentHull.IsTaggedAirlock() ||
|
|
character.CurrentHull.LeadsOutside(character)
|
|
},
|
|
onCompleted: () =>
|
|
{
|
|
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
|
|
HumanAIController.NeedsDivingGear(currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
|
|
{
|
|
resetPriority = true;
|
|
searchHullTimer = Math.Min(1, searchHullTimer);
|
|
}
|
|
RemoveSubObjective(ref goToObjective);
|
|
if (cannotFindDivingGear)
|
|
{
|
|
// If diving gear objective failed, let's reset it here.
|
|
RemoveSubObjective(ref divingGearObjective);
|
|
}
|
|
},
|
|
onAbandon: () =>
|
|
{
|
|
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
|
|
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
|
|
// If the target hull is inside a submarine that is not our main sub, just ignore it normally when it cannot be reached. This happens with outposts.
|
|
if (goToObjective != null)
|
|
{
|
|
if (goToObjective.Target is Hull hull)
|
|
{
|
|
if (currentHull != null || !Submarine.MainSubs.Contains(hull.Submarine))
|
|
{
|
|
HumanAIController.UnreachableHulls.Add(hull);
|
|
}
|
|
}
|
|
}
|
|
RemoveSubObjective(ref goToObjective);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
RemoveSubObjective(ref goToObjective);
|
|
}
|
|
}
|
|
if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
|
|
UpdateSimpleEscape(deltaTime);
|
|
}
|
|
}
|
|
|
|
public void UpdateSimpleEscape(float deltaTime)
|
|
{
|
|
Vector2 escapeVel = Vector2.Zero;
|
|
if (character.CurrentHull != null)
|
|
{
|
|
foreach (Hull hull in HumanAIController.VisibleHulls)
|
|
{
|
|
foreach (FireSource fireSource in hull.FireSources)
|
|
{
|
|
Vector2 dir = character.Position - fireSource.Position;
|
|
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
|
|
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
|
|
}
|
|
}
|
|
foreach (Character enemy in Character.CharacterList)
|
|
{
|
|
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; }
|
|
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
|
|
{
|
|
Vector2 dir = character.Position - enemy.Position;
|
|
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
|
|
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
|
|
}
|
|
}
|
|
}
|
|
if (escapeVel != Vector2.Zero)
|
|
{
|
|
float left = character.CurrentHull.Rect.X + 50;
|
|
float right = character.CurrentHull.Rect.Right - 50;
|
|
//only move if we haven't reached the edge of the room
|
|
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
|
|
{
|
|
character.ReleaseSecondaryItem();
|
|
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
|
|
}
|
|
else
|
|
{
|
|
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
|
|
character.AIController.SteeringManager.Reset();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
|
}
|
|
}
|
|
|
|
public enum HullSearchStatus
|
|
{
|
|
Running,
|
|
Finished
|
|
}
|
|
|
|
private readonly List<Hull> hulls = new List<Hull>();
|
|
private int hullSearchIndex = -1;
|
|
float bestHullValue = 0;
|
|
bool bestHullIsAirlock = false;
|
|
Hull potentialBestHull;
|
|
|
|
/// <summary>
|
|
/// Tries to find the best (safe, nearby) hull the character can find a path to.
|
|
/// Checks one hull at a time, and returns HullSearchStatus.Finished when all potential hulls have been checked.
|
|
/// </summary>
|
|
public HullSearchStatus FindBestHull(out Hull bestHull, IEnumerable<Hull> ignoredHulls = null, bool allowChangingSubmarine = true)
|
|
{
|
|
if (hullSearchIndex == -1)
|
|
{
|
|
bestHullValue = 0;
|
|
potentialBestHull = null;
|
|
bestHullIsAirlock = false;
|
|
hulls.Clear();
|
|
var connectedSubs = character.Submarine?.GetConnectedSubs();
|
|
foreach (Hull hull in Hull.HullList)
|
|
{
|
|
if (hull.Submarine == null) { continue; }
|
|
// Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it.
|
|
if (hull.Submarine.Info.IsRuin) { continue; }
|
|
if (!allowChangingSubmarine && hull.Submarine != character.Submarine) { continue; }
|
|
if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; }
|
|
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
|
|
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
|
|
if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
|
|
//sort the hulls based on distance and which sub they're in
|
|
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
|
|
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
|
|
//path calculations, only to discard all of them when going through the hulls in the outpost)
|
|
float hullSuitability = EstimateHullSuitability(character, hull);
|
|
if (hulls.None())
|
|
{
|
|
hulls.Add(hull);
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < hulls.Count; i++)
|
|
{
|
|
if (hullSuitability > EstimateHullSuitability(character, hulls[i]))
|
|
{
|
|
hulls.Insert(i, hull);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (hulls.None())
|
|
{
|
|
bestHull = null;
|
|
return HullSearchStatus.Finished;
|
|
}
|
|
hullSearchIndex = 0;
|
|
}
|
|
|
|
static float EstimateHullSuitability(Character character, Hull hull)
|
|
{
|
|
float dist =
|
|
Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) +
|
|
Math.Abs(hull.WorldPosition.Y - character.WorldPosition.Y) * 3;
|
|
float suitability = -dist;
|
|
if (hull.Submarine != character.Submarine)
|
|
{
|
|
suitability -= 10000.0f;
|
|
}
|
|
return suitability;
|
|
}
|
|
|
|
Hull potentialHull = hulls[hullSearchIndex];
|
|
|
|
float hullSafety = 0;
|
|
bool hullIsAirlock = false;
|
|
bool isCharacterInside = character.CurrentHull != null && character.Submarine != null;
|
|
if (isCharacterInside)
|
|
{
|
|
hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character);
|
|
float yDist = Math.Abs(character.WorldPosition.Y - potentialHull.WorldPosition.Y);
|
|
yDist = yDist > 100 ? yDist * 3 : 0;
|
|
float dist = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
|
|
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
|
|
hullSafety *= distanceFactor;
|
|
//skip the hull if the safety is already less than the best hull
|
|
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
|
|
if (hullSafety > bestHullValue)
|
|
{
|
|
//avoid airlock modules if not allowed to change the sub
|
|
if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
|
|
{
|
|
// Don't allow to go outside if not already outside.
|
|
var path = PathSteering.PathFinder.FindPath(character.SimPosition, potentialHull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
|
|
if (path.Unreachable)
|
|
{
|
|
hullSafety = 0;
|
|
HumanAIController.UnreachableHulls.Add(potentialHull);
|
|
}
|
|
else
|
|
{
|
|
// Each unsafe node reduces the hull safety value.
|
|
// Ignore the current hull, because otherwise we couldn't find a path out.
|
|
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
|
|
hullSafety /= 1 + unsafeNodes;
|
|
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
|
|
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true))
|
|
{
|
|
hullSafety /= 10;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
hullSafety = 0;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TODO: could also target gaps that get us inside?
|
|
if (potentialHull.IsTaggedAirlock())
|
|
{
|
|
hullSafety = 100;
|
|
hullIsAirlock = true;
|
|
}
|
|
else if(!bestHullIsAirlock && potentialHull.LeadsOutside(character))
|
|
{
|
|
hullSafety = 100;
|
|
}
|
|
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
|
|
float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y);
|
|
yDist = yDist > 100 ? yDist * 3 : 0;
|
|
float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
|
|
// Huge preference for closer targets
|
|
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance));
|
|
hullSafety *= distanceFactor;
|
|
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
|
|
// Intentionally exclude wrecks from this check
|
|
if (potentialHull.Submarine.TeamID != character.TeamID && potentialHull.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
|
|
{
|
|
hullSafety /= 10;
|
|
}
|
|
}
|
|
if (hullSafety > bestHullValue || (!isCharacterInside && hullIsAirlock && !bestHullIsAirlock))
|
|
{
|
|
potentialBestHull = potentialHull;
|
|
bestHullValue = hullSafety;
|
|
bestHullIsAirlock = hullIsAirlock;
|
|
}
|
|
|
|
bestHull = potentialBestHull;
|
|
hullSearchIndex++;
|
|
|
|
if (hullSearchIndex >= hulls.Count)
|
|
{
|
|
hullSearchIndex = -1;
|
|
return HullSearchStatus.Finished;
|
|
}
|
|
return HullSearchStatus.Running;
|
|
}
|
|
|
|
public override void Reset()
|
|
{
|
|
base.Reset();
|
|
goToObjective = null;
|
|
divingGearObjective = null;
|
|
currentSafeHull = null;
|
|
previousSafeHull = null;
|
|
retryCounter = 0;
|
|
cannotFindDivingGear = false;
|
|
cannotFindSafeHull = false;
|
|
}
|
|
|
|
private bool NeedMoreDivingGear(Hull targetHull, float minOxygen = 0)
|
|
{
|
|
if (!HumanAIController.NeedsDivingGear(targetHull, out bool needsSuit)) { return false; }
|
|
if (needsSuit)
|
|
{
|
|
return !HumanAIController.HasDivingSuit(character, minOxygen);
|
|
}
|
|
else
|
|
{
|
|
return !HumanAIController.HasDivingGear(character, minOxygen);
|
|
}
|
|
}
|
|
}
|
|
}
|