Release v0.15.12.0

This commit is contained in:
Joonas Rikkonen
2021-10-27 18:50:57 +03:00
parent bf95e82d80
commit 234fb6bc06
450 changed files with 26042 additions and 10457 deletions
@@ -1,7 +1,10 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma
@@ -94,14 +97,31 @@ namespace Barotrauma
}
}
protected bool HasValidPath(bool requireNonDirty = false) =>
steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable && (!requireNonDirty || !pathSteering.IsPathDirty);
public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) =>
steeringManager is IndoorsSteeringManager pathSteering &&
pathSteering.CurrentPath != null &&
(!requireUnfinished || !pathSteering.CurrentPath.Finished) &&
!pathSteering.CurrentPath.Unreachable &&
(!requireNonDirty || !pathSteering.IsPathDirty);
public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable;
public bool IsCurrentPathFinished => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Finished;
protected readonly float colliderWidth;
protected readonly float minGapSize;
protected readonly float colliderLength;
protected readonly float avoidLookAheadDistance;
public AIController (Character c)
{
Character = c;
hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer);
Enabled = true;
var size = Character.AnimController.Collider.GetSize();
colliderWidth = size.X;
colliderLength = size.Y;
avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f);
minGapSize = ConvertUnits.ToDisplayUnits(Math.Min(colliderWidth, colliderLength));
}
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
@@ -326,7 +346,148 @@ namespace Barotrauma
unequippedItems.Clear();
}
#region Escape
public abstract bool Escape(float deltaTime);
public Gap EscapeTarget { get; private set; }
private readonly float escapeTargetSeekInterval = 2;
private float escapeTimer;
protected bool allGapsSearched;
protected readonly HashSet<Gap> unreachableGaps = new HashSet<Gap>();
protected bool UpdateEscape(float deltaTime, bool canAttackDoors)
{
IndoorsSteeringManager pathSteering = SteeringManager as IndoorsSteeringManager;
if (allGapsSearched)
{
escapeTimer -= deltaTime;
if (escapeTimer <= 0)
{
allGapsSearched = false;
}
}
if (Character.CurrentHull != null && pathSteering != null)
{
// Seek exit if inside
if (!allGapsSearched)
{
float closestDistance = 0;
foreach (Gap gap in Gap.GapList)
{
if (gap == null || gap.Removed) { continue; }
if (EscapeTarget == gap) { continue; }
if (unreachableGaps.Contains(gap)) { continue; }
if (gap.Submarine != Character.Submarine) { continue; }
if (gap.IsRoomToRoom) { continue; }
float multiplier = 1;
var door = gap.ConnectedDoor;
if (door != null)
{
if (!door.CanBeTraversed)
{
if (!door.HasAccess(Character))
{
if (!canAttackDoors) { continue; }
// Treat doors that don't have access to like they were farther, because it will take time to break them.
multiplier = 5;
}
}
}
else
{
if (gap.Open < 1) { continue; }
if (gap.Size < minGapSize) { continue; }
}
if (gap.FlowTargetHull == Character.CurrentHull)
{
// If the gap is in the same room, it's close enough.
EscapeTarget = gap;
break;
}
float distance = Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) * multiplier;
if (EscapeTarget == null || distance < closestDistance)
{
EscapeTarget = gap;
closestDistance = distance;
}
}
allGapsSearched = true;
escapeTimer = escapeTargetSeekInterval;
}
else if (EscapeTarget != null && EscapeTarget.FlowTargetHull != Character.CurrentHull)
{
if (IsCurrentPathUnreachable)
{
unreachableGaps.Add(EscapeTarget);
EscapeTarget = null;
allGapsSearched = false;
}
}
}
if (EscapeTarget != null)
{
var door = EscapeTarget.ConnectedDoor;
bool isClosedDoor = door != null && !door.IsOpen;
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared();
bool isClose = sqrDist < MathUtils.Pow2(100);
if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished)
{
// Very close to the target, outside, or at the end of the path -> try to steer through the gap
SteeringManager.Reset();
pathSteering?.ResetPath();
Vector2 dir = Vector2.Normalize(diff);
if (Character.CurrentHull == null || isClose)
{
// Outside -> steer away from the target
if (EscapeTarget.FlowTargetHull != null)
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(EscapeTarget.WorldPosition - EscapeTarget.FlowTargetHull.WorldPosition));
}
else
{
SteeringManager.SteeringManual(deltaTime, -dir);
}
}
else
{
// Still inside -> steer towards the target
SteeringManager.SteeringManual(deltaTime, dir);
}
return sqrDist < MathUtils.Pow2(250);
}
else if (pathSteering != null)
{
pathSteering.SteeringSeek(EscapeTarget.SimPosition, weight: 1, minGapSize);
}
else
{
SteeringManager.SteeringSeek(EscapeTarget.SimPosition, 10);
}
}
else
{
// Can't find the target
EscapeTarget = null;
allGapsSearched = false;
unreachableGaps.Clear();
}
return false;
}
public void ResetEscape()
{
EscapeTarget = null;
allGapsSearched = false;
unreachableGaps.Clear();
}
#endregion
protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
public virtual void ClientRead(IReadMessage msg) { }
public virtual void ServerWrite(IWriteMessage msg) { }
}
}
@@ -92,7 +92,16 @@ namespace Barotrauma
public string SonarLabel;
public string SonarIconIdentifier;
public bool Enabled => SoundRange > 0 || SightRange > 0;
private bool inDetectable;
/// <summary>
/// Should be reset to false each frame and kept indetectable by e.g. a status effect.
/// </summary>
public bool InDetectable
{
get => inDetectable || (SoundRange <= 0 && SightRange <= 0);
set => inDetectable = value;
}
public float MinSoundRange, MinSightRange;
public float MaxSoundRange = 100000, MaxSightRange = 100000;
@@ -181,14 +190,15 @@ namespace Barotrauma
public void Update(float deltaTime)
{
if (Enabled && !Static && FadeOutTime > 0)
InDetectable = false;
if (!Static && FadeOutTime > 0)
{
// The aitarget goes silent/invisible if the components don't keep it active
if (!StaticSight)
if (!StaticSight && SightRange > 0)
{
DecreaseSightRange(deltaTime);
}
if (!StaticSound)
if (!StaticSound && SoundRange > 0)
{
DecreaseSoundRange(deltaTime);
}
File diff suppressed because it is too large Load Diff
@@ -20,6 +20,11 @@ namespace Barotrauma
private float reactTimer;
private float unreachableClearTimer;
private bool shouldCrouch;
public bool IsInsideCave { get; private set; }
/// <summary>
/// Resets each frame
/// </summary>
public bool AutoFaceMovement = true;
const float reactionTime = 0.3f;
const float crouchRaycastInterval = 1;
@@ -29,9 +34,6 @@ namespace Barotrauma
private float flipTimer;
private const float FlipInterval = 0.5f;
private float teamChangeTimer;
private const float TeamChangeInterval = 0.5f;
public const float HULL_SAFETY_THRESHOLD = 40;
public const float HULL_LOW_OXYGEN_PERCENTAGE = 30;
@@ -52,7 +54,7 @@ namespace Barotrauma
private readonly float steeringBufferIncreaseSpeed = 100;
private float steeringBuffer;
private readonly float obstacleRaycastInterval = 1;
private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5;
private float obstacleRaycastTimer;
private readonly float enemyCheckInterval = 0.2f;
@@ -86,6 +88,8 @@ namespace Barotrauma
private readonly SteeringManager outsideSteering, insideSteering;
public bool UseIndoorSteeringOutside { get; set; } = false;
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
@@ -207,33 +211,78 @@ namespace Barotrauma
IgnoredItems.Clear();
}
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
bool IsCloseEnoughToTarget(float threshold, bool useTargetSub = true)
{
Entity target = SelectedAiTarget?.Entity;
if (target == null)
{
return false;
}
if (useTargetSub)
{
if (target.Submarine is Submarine sub)
{
target = sub;
threshold += Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2;
}
else
{
return false;
}
}
return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow(threshold, 2);
}
bool hasValidPath = HasValidPath();
if (Character.Submarine == null)
{
if (hasValidPath)
// When the character is outside, far enough from the target, and the direct route is blocked,
// use the indoor steering with the main and side path waypoints to help avoid getting stuck in level walls
if (SelectedAiTarget?.Entity != null && !IsCloseEnoughToTarget(2000, useTargetSub: false))
{
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
{
obstacleRaycastTimer = obstacleRaycastInterval;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
obstacleRaycastTimer = obstacleRaycastIntervalLong;
Vector2 rayEnd = SelectedAiTarget.Entity.SimPosition;
if (SelectedAiTarget.Entity.Submarine != null)
{
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
rayEnd += SelectedAiTarget.Entity.Submarine.SimPosition;
}
UseIndoorSteeringOutside = Submarine.PickBody(SimPosition, rayEnd, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null;
}
}
else
{
UseIndoorSteeringOutside = false;
if (hasValidPath)
{
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
{
obstacleRaycastTimer = obstacleRaycastIntervalShort;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
{
PathSteering.CurrentPath.Unreachable = true;
break;
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
PathSteering.CurrentPath.Unreachable = true;
break;
}
}
}
}
}
}
else
{
UseIndoorSteeringOutside = false;
}
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted)
{
@@ -273,13 +322,31 @@ namespace Barotrauma
}
}
}
if (Character.Submarine != null || hasValidPath && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer))
// Check whether the character is inside a cave
if (IsInsideCave)
{
// If the character was inside a cave, require them to move a bit further from the area to set the field back to false
// This is to avoid any twitchy behavior with the steering managers
IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c =>
{
var area = c.Area;
area.Inflate(new Vector2(100));
return area.Contains(Character.WorldPosition);
}) is Level.Cave;
}
else
{
IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => c.Area.Contains(Character.WorldPosition)) is Level.Cave;
}
if (UseIndoorSteeringOutside || IsInsideCave || Character.CurrentHull?.Submarine != null || hasValidPath && IsCloseEnoughToTarget(maxSteeringBuffer) || IsCloseEnoughToTarget(steeringBuffer))
{
if (steeringManager != insideSteering)
{
insideSteering.Reset();
steeringManager = insideSteering;
}
steeringManager = insideSteering;
steeringBuffer += steeringBufferIncreaseSpeed * deltaTime;
}
else
@@ -287,8 +354,8 @@ namespace Barotrauma
if (steeringManager != outsideSteering)
{
outsideSteering.Reset();
steeringManager = outsideSteering;
}
steeringManager = outsideSteering;
steeringBuffer = minSteeringBuffer;
}
steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
@@ -419,7 +486,7 @@ namespace Barotrauma
Character.SelectedConstruction.SecondaryUse(deltaTime, Character);
}
}
else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater)
else if (AutoFaceMovement && Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater)
{
newDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left;
}
@@ -429,6 +496,7 @@ namespace Barotrauma
flipTimer = FlipInterval;
}
}
AutoFaceMovement = true;
MentalStateManager?.Update(deltaTime);
ShipCommandManager?.Update(deltaTime);
@@ -915,10 +983,10 @@ namespace Barotrauma
return false;
}
public static void ReportProblem(Character reporter, Order order)
public static void ReportProblem(Character reporter, Order order, Hull targetHull = null)
{
if (reporter == null || order == null) { return; }
var visibleHulls = new List<Hull>(reporter.GetVisibleHulls());
var visibleHulls = targetHull is null ? new List<Hull>(reporter.GetVisibleHulls()) : new List<Hull> { targetHull };
foreach (var hull in visibleHulls)
{
PropagateHullSafety(reporter, hull);
@@ -965,16 +1033,19 @@ namespace Barotrauma
}
if (previousAttackResults.ContainsKey(attacker))
{
foreach (Affliction newAffliction in attackResult.Afflictions)
if (attackResult.Afflictions != null)
{
var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source);
if (matchingAffliction == null)
foreach (Affliction newAffliction in attackResult.Afflictions)
{
previousAttackResults[attacker].Afflictions.Add(newAffliction);
}
else
{
matchingAffliction.Strength += newAffliction.Strength;
var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source);
if (matchingAffliction == null)
{
previousAttackResults[attacker].Afflictions.Add(newAffliction);
}
else
{
matchingAffliction.Strength += newAffliction.Strength;
}
}
}
previousAttackResults[attacker] = new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb);
@@ -991,9 +1062,12 @@ namespace Barotrauma
float realDamage = attackResult.Damage;
// including poisons etc
float totalDamage = realDamage;
foreach (Affliction affliction in attackResult.Afflictions)
if (attackResult.Afflictions != null)
{
totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
foreach (Affliction affliction in attackResult.Afflictions)
{
totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
}
}
if (totalDamage <= 0.01f) { return; }
if (Character.IsBot)
@@ -1047,7 +1121,7 @@ namespace Barotrauma
{
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
// Inform other NPCs
if (cumulativeDamage > 1)
if (cumulativeDamage > 1 || totalDamage >= 10)
{
InformOtherNPCs(cumulativeDamage);
}
@@ -1184,7 +1258,7 @@ namespace Barotrauma
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
}
if (attackResult.Afflictions.Any(a => a is AfflictionHusk))
if (attackResult.Afflictions != null && attackResult.Afflictions.Any(a => a is AfflictionHusk))
{
cumulativeDamage = 100;
}
@@ -1240,10 +1314,7 @@ namespace Barotrauma
{
var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager)
{
HoldPosition =
Character.Info?.Job?.Prefab.Identifier == "watchman" ||
Character.CurrentHull == null ||
Character.IsOnPlayerTeam && !target.IsPlayer && ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>()?.Target is Character followTarget && followTarget.IsPlayer,
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
AbortCondition = abortCondition,
allowHoldFire = allowHoldFire,
};
@@ -1293,6 +1364,8 @@ namespace Barotrauma
ObjectiveManager.WaitTimer = waitDuration;
}
public override bool Escape(float deltaTime) => UpdateEscape(deltaTime, canAttackDoors: false);
private void CheckCrouching(float deltaTime)
{
crouchRaycastTimer -= deltaTime;
@@ -1415,7 +1488,7 @@ namespace Barotrauma
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage;
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss;
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
}
if (accumulatedDamage <= WarningThreshold) { return; }
@@ -1510,7 +1583,7 @@ namespace Barotrauma
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss;
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
}
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f);
@@ -1971,13 +2044,13 @@ namespace Barotrauma
if (c.Removed) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
other = c;
if (c.IsPlayer)
{
if (c.SelectedConstruction == target.Item)
{
// If the other character is player, don't try to operate
return true;
other = c;
break;
}
}
else if (c.AIController is HumanAIController operatingAI)
@@ -1991,7 +2064,8 @@ namespace Barotrauma
if (!isOrder && isTargetOrdered)
{
// If the other bot is ordered to operate the item, let him do it, unless we are ordered too
return true;
other = c;
break;
}
else
{
@@ -2012,18 +2086,20 @@ namespace Barotrauma
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
if (Character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"))
{
return true;
other = c;
break;
}
}
else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c))
{
return true;
other = c;
break;
}
}
}
}
}
return false;
return other != null;
bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
}
@@ -1,8 +1,8 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
@@ -15,6 +15,11 @@ namespace Barotrauma
private bool canOpenDoors;
public bool CanBreakDoors { get; set; }
private bool ShouldBreakDoor(Door door) =>
CanBreakDoors &&
!door.Item.Indestructible && !door.Item.InvulnerableToDamage &&
(door.Item.Submarine == null || door.Item.Submarine.TeamID != character.TeamID);
private Character character;
private Vector2 currentTarget;
@@ -23,7 +28,7 @@ namespace Barotrauma
private float buttonPressCooldown;
const float ButtonPressInterval = 0.5f;
const float ButtonPressInterval = 0.25f;
public SteeringPath CurrentPath
{
@@ -78,7 +83,7 @@ namespace Barotrauma
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host)
{
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), indoorsSteering: true);
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true);
pathFinder.GetNodePenalty = GetNodePenalty;
this.canOpenDoors = canOpenDoors;
@@ -111,9 +116,14 @@ namespace Barotrauma
IsPathDirty = true;
}
public void SteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true)
public void SteeringSeekSimple(Vector2 targetSimPos, float weight = 1)
{
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
steering += base.DoSteeringSeek(targetSimPos, weight);
}
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)
{
steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
}
/// <summary>
@@ -158,42 +168,47 @@ namespace Barotrauma
}
}
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
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)
{
Vector2 targetDiff = target - currentTarget;
if (currentPath != null && currentPath.Nodes.Any())
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished;
if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
{
//current path calculated relative to a different sub than where the character is now
//take that into account when calculating if the target has moved
Submarine currentPathSub = currentPath?.Nodes.First().Submarine;
if (currentPathSub != character.Submarine && character.Submarine != null)
Vector2 targetDiff = target - currentTarget;
if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null)
{
Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition;
targetDiff += subDiff;
//target in a different sub than where the character is now
//take that into account when calculating if the target has moved
Submarine currentPathSub = currentPath?.CurrentNode?.Submarine;
if (currentPathSub == character.Submarine) { currentPathSub = currentPath?.Nodes.LastOrDefault()?.Submarine; }
if (currentPathSub != character.Submarine && targetDiff.LengthSquared() > 1 && currentPathSub != null)
{
Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition;
targetDiff += subDiff;
}
}
if (targetDiff.LengthSquared() > 1)
{
needsNewPath = true;
}
}
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || targetDiff.LengthSquared() > 1);
//find a new path if one hasn't been found yet or the target is different from the current target
if (needsNewPath || findPathTimer < -1.0f)
{
IsPathDirty = true;
if (findPathTimer < 0)
{
SkipCurrentPathNodes();
currentTarget = target;
Vector2 currentPos = host.SimPosition;
if (character != null && character.Submarine == null)
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0;
if (newPath.Unreachable || newPath.Nodes.None())
{
var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false);
if (targetHull != null && targetHull.Submarine != null)
{
currentPos -= targetHull.Submarine.SimPosition;
}
useNewPath = false;
}
pathFinder.InsideSubmarine = character.Submarine != null;
pathFinder.ApplyPenaltyToOutsideNodes = character.PressureProtection <= 0;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
else if (!useNewPath && currentPath != null && currentPath.CurrentNode != null)
{
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
if (IsIdenticalPath())
@@ -205,7 +220,7 @@ namespace Barotrauma
// Use the new path if it has significantly lower cost (don't change the path if it has marginally smaller cost. This reduces navigating backwards due to new path that is calculated from the node just behind us).
float t = (float)currentPath.CurrentIndex / (currentPath.Nodes.Count - 1);
useNewPath = newPath.Cost < currentPath.Cost * MathHelper.Lerp(0.95f, 0, t);
if (!useNewPath)
if (!useNewPath && character.Submarine != null)
{
// It's possible that the current path was calculated from a start point that is no longer valid.
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
@@ -232,12 +247,42 @@ namespace Barotrauma
}
if (useNewPath)
{
if (currentPath != null)
{
CheckDoorsInPath();
}
currentPath = newPath;
}
float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
findPathTimer = priority * Rand.Range(1.0f, 1.2f);
IsPathDirty = false;
return DiffToCurrentNode();
void SkipCurrentPathNodes()
{
if (!character.AnimController.InWater || character.Submarine != null) { return; }
if (CurrentPath == null || CurrentPath.Unreachable || CurrentPath.Finished) { return; }
if (CurrentPath.CurrentIndex < 0 || CurrentPath.CurrentIndex >= CurrentPath.Nodes.Count - 1) { return; }
// Check if we could skip ahead to NextNode when the character is swimming and using waypoints outside.
// Do this to optimize the old path before creating and evaluating a new path.
// In general, this is to avoid behavior where:
// a) the character goes back to first reach CurrentNode when the second node would be closer; or
// b) the character moves along the path when they could cut through open space to reduce the total distance.
float pathDistance = Vector2.Distance(character.WorldPosition, CurrentPath.CurrentNode.WorldPosition);
pathDistance += CurrentPath.GetLength(startIndex: CurrentPath.CurrentIndex);
for (int i = CurrentPath.Nodes.Count - 1; i > CurrentPath.CurrentIndex + 1; i--)
{
var waypoint = CurrentPath.Nodes[i];
float directDistance = Vector2.DistanceSquared(character.WorldPosition, waypoint.WorldPosition);
if (directDistance > (pathDistance * pathDistance) || Submarine.PickBody(host.SimPosition, waypoint.SimPosition, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null)
{
pathDistance -= CurrentPath.GetLength(startIndex: i - 1, endIndex: i);
continue;
}
CurrentPath.SkipToNode(i);
break;
}
}
}
}
@@ -271,25 +316,35 @@ namespace Barotrauma
pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition;
}
return currentTarget - pos2;
}
if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f)
}
bool doorsChecked = false;
if (!character.LockHands && buttonPressCooldown <= 0.0f)
{
CheckDoorsInPath();
doorsChecked = true;
}
Vector2 pos = host.SimPosition;
if (character != null && currentPath.CurrentNode != null)
if (character != null && CurrentPath.CurrentNode != null)
{
if (CurrentPath.CurrentNode.Submarine != null)
var nodeSub = CurrentPath.CurrentNode.Submarine;
if (nodeSub != null)
{
if (character.Submarine == null)
{
pos -= CurrentPath.CurrentNode.Submarine.SimPosition;
// Going inside
pos -= ConvertUnits.ToSimUnits(nodeSub.Position);
}
else if (character.Submarine != currentPath.CurrentNode.Submarine)
else if (character.Submarine != nodeSub)
{
pos -= ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position - character.Submarine.Position);
// Different subs
pos -= ConvertUnits.ToSimUnits(nodeSub.Position - character.Submarine.Position);
}
}
else if (character.Submarine != null)
{
// Going outside
pos += ConvertUnits.ToSimUnits(character.Submarine.Position);
}
}
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
@@ -362,7 +417,7 @@ namespace Barotrauma
}
if (isAboveFloor || nextLadderSameAsCurrent)
{
currentPath.SkipToNextNode();
NextNode(!doorsChecked);
}
}
else if (nextLadder != null)
@@ -372,7 +427,7 @@ namespace Barotrauma
//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();
NextNode(!doorsChecked);
}
}
return diff;
@@ -394,7 +449,7 @@ namespace Barotrauma
float distance = horizontalDistance + verticalDistance;
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
{
currentPath.SkipToNextNode();
NextNode(!doorsChecked);
}
}
}
@@ -419,7 +474,7 @@ namespace Barotrauma
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed))
{
currentPath.SkipToNextNode();
NextNode(!doorsChecked);
}
}
if (currentPath.CurrentNode == null)
@@ -429,28 +484,51 @@ namespace Barotrauma
return currentPath.CurrentNode.SimPosition - pos;
}
private void NextNode(bool checkDoors)
{
if (checkDoors)
{
CheckDoorsInPath();
}
currentPath.SkipToNextNode();
}
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsOpen || door.IsBroken) { return true; }
if (!door.Item.IsInteractable(character)) { return false; }
if (!CanBreakDoors)
if (door.IsBroken) { return true; }
if (!door.IsOpen)
{
if (door.IsStuck || door.IsJammed) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
if (!door.Item.IsInteractable(character)) { return false; }
if (!ShouldBreakDoor(door))
{
if (door.IsStuck || door.IsJammed) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
}
}
if (door.HasIntegratedButtons)
{
return door.HasAccess(character) || CanBreakDoors;
return door.IsOpen || door.HasAccess(character) || ShouldBreakDoor(door);
}
else
{
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))) || CanBreakDoors;
// We'll want this to run each time, because the delegate is used to find a valid button component.
bool canAccessButtons = door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
return canAccessButtons || door.IsOpen || ShouldBreakDoor(door);
}
}
private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize());
private float GetColliderLength()
{
Vector2 colliderSize = character.AnimController.Collider.GetSize();
return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y));
}
private void CheckDoorsInPath()
{
for (int i = 0; i < 2; i++)
if (!canOpenDoors) { return; }
for (int i = 0; i < 5; i++)
{
WayPoint currentWaypoint = null;
WayPoint nextWaypoint = null;
@@ -461,17 +539,21 @@ namespace Barotrauma
{
door = currentPath.Nodes.First().ConnectedDoor;
shouldBeOpen = door != null;
if (i > 0) { break; }
}
else
{
if (i == 0)
bool closeDoors = character.IsBot && character.IsInFriendlySub || character.Params.AI != null && character.Params.AI.KeepDoorsClosed;
if (i == 0 || !closeDoors)
{
currentWaypoint = currentPath.CurrentNode;
nextWaypoint = currentPath.NextNode;
}
else
{
currentWaypoint = currentPath.PrevNode;
int previousIndex = currentPath.CurrentIndex - i;
if (previousIndex < 0) { break; }
currentWaypoint = currentPath.Nodes[previousIndex];
nextWaypoint = currentPath.CurrentNode;
}
if (currentWaypoint?.ConnectedDoor == null) { continue; }
@@ -480,24 +562,30 @@ namespace Barotrauma
{
//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
if (currentWaypoint.ConnectedDoor.LinkedGap != null && currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom)
if (currentWaypoint.ConnectedDoor.LinkedGap != null)
{
shouldBeOpen = true;
door = currentWaypoint.ConnectedDoor;
// Keep the airlock doors closed, but not in ruins/wrecks
if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom || currentWaypoint.Submarine?.Info.IsRuin != null || currentWaypoint.Submarine?.Info.IsWreck != null)
{
shouldBeOpen = true;
door = currentWaypoint.ConnectedDoor;
}
}
}
else
{
float colliderLength = GetColliderLength();
door = currentWaypoint.ConnectedDoor;
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -50.0f;
float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X;
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -size;
}
else
{
int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -80.0f;
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -colliderLength;
}
}
}
@@ -541,7 +629,7 @@ namespace Barotrauma
}
else if (closestButton != null)
{
if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance * 2, 2))
if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance + GetColliderLength(), 2))
{
closestButton.Item.TryInteract(character, false, true);
buttonPressCooldown = ButtonPressInterval;
@@ -19,16 +19,19 @@ namespace Barotrauma
private Body targetBody;
private Vector2 attachSurfaceNormal;
private Submarine targetSubmarine;
private Character targetCharacter;
private readonly Character character;
public bool AttachToSub { get; private set; }
public bool AttachToWalls { get; private set; }
public bool AttachToCharacters { get; private set; }
private readonly float minDeattachSpeed, maxDeattachSpeed;
private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration, coolDown;
private readonly float damageOnDetach, detachStun;
private float deattachTimer;
private readonly bool weld;
private float deattachCheckTimer;
private Vector2 wallAttachPos;
private Vector2 _attachPos;
private float attachCooldown;
@@ -38,9 +41,9 @@ namespace Barotrauma
private float jointDir;
public List<WeldJoint> AttachJoints { get; } = new List<WeldJoint>();
public List<Joint> AttachJoints { get; } = new List<Joint>();
public Vector2? WallAttachPos
public Vector2? AttachPos
{
get;
private set;
@@ -48,18 +51,22 @@ namespace Barotrauma
public bool IsAttached => AttachJoints.Count > 0;
public bool IsAttachedToSub => IsAttached && targetSubmarine != null;
public bool IsAttachedToSub => IsAttached && targetSubmarine != null && targetCharacter == null;
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
AttachToWalls = element.GetAttributeBool("attachtowalls", false);
AttachToSub = element.GetAttributeBool("attachtosub", false);
AttachToCharacters = element.GetAttributeBool("attachtocharacters", false);
minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 5.0f);
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 8.0f));
maxAttachDuration = element.GetAttributeFloat("maxattachduration", -1.0f);
coolDown = element.GetAttributeFloat("cooldown", 2f);
damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f);
detachStun = element.GetAttributeFloat("detachstun", 0.0f);
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero));
attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f));
weld = element.GetAttributeBool("weld", true);
string limbString = element.GetAttributeString("attachlimb", null);
attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase));
@@ -81,30 +88,54 @@ namespace Barotrauma
public void SetAttachTarget(Structure wall, Vector2 attachPos, Vector2 attachSurfaceNormal)
{
if (!AttachToSub) { return; }
if (wall == null) { return; }
var sub = wall.Submarine;
if (sub == null) { return; }
Reset();
targetWall = wall;
targetSubmarine = sub;
targetBody = targetSubmarine.PhysicsBody.FarseerBody;
this.attachSurfaceNormal = attachSurfaceNormal;
wallAttachPos = attachPos;
_attachPos = attachPos;
}
public void SetAttachTarget(Character target)
{
if (!AttachToCharacters) { return; }
Reset();
targetCharacter = target;
targetSubmarine = target.Submarine;
targetBody = target.AnimController.Collider.FarseerBody;
attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition);
}
public void Update(EnemyAIController enemyAI, float deltaTime)
{
if (character.Submarine != null)
{
DeattachFromBody(reset: true);
return;
if (targetCharacter != null && targetCharacter.Submarine != targetSubmarine ||
character.Submarine != null && targetSubmarine != null && targetCharacter == null)
{
DeattachFromBody(reset: true);
return;
}
}
if (AttachJoints.Count > 0)
if (IsAttached)
{
if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir))
{
AttachJoints[0].LocalAnchorA =
new Vector2(-AttachJoints[0].LocalAnchorA.X, AttachJoints[0].LocalAnchorA.Y);
AttachJoints[0].ReferenceAngle = -AttachJoints[0].ReferenceAngle;
var attachJoint = AttachJoints[0];
if (attachJoint is WeldJoint weldJoint)
{
weldJoint.LocalAnchorA = new Vector2(-weldJoint.LocalAnchorA.X, weldJoint.LocalAnchorA.Y);
weldJoint.ReferenceAngle = -weldJoint.ReferenceAngle;
}
else if (attachJoint is RevoluteJoint revoluteJoint)
{
revoluteJoint.LocalAnchorA = new Vector2(-revoluteJoint.LocalAnchorA.X, revoluteJoint.LocalAnchorA.Y);
revoluteJoint.ReferenceAngle = -revoluteJoint.ReferenceAngle;
}
jointDir = attachLimb.Dir;
}
for (int i = 0; i < AttachJoints.Count; i++)
@@ -113,31 +144,51 @@ namespace Barotrauma
if (Vector2.DistanceSquared(AttachJoints[i].WorldAnchorB, AttachJoints[i].BodyA.Position) > 10.0f * 10.0f)
{
#if DEBUG
DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach");
DebugConsole.Log("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach");
#endif
DeattachFromBody(reset: true);
return;
}
}
if (targetCharacter != null)
{
if (enemyAI.AttackingLimb?.attack == null)
{
DeattachFromBody(reset: true, cooldown: 1);
}
else
{
float range = enemyAI.AttackingLimb.attack.DamageRange * 2f;
if (Vector2.DistanceSquared(targetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range)
{
DeattachFromBody(reset: true, cooldown: 1);
}
}
}
}
if (attachCooldown > 0)
{
attachCooldown -= deltaTime;
}
if (deattachTimer > 0)
if (deattachCheckTimer > 0)
{
deattachTimer -= deltaTime;
deattachCheckTimer -= deltaTime;
}
Vector2 transformedAttachPos = wallAttachPos;
if (targetCharacter != null)
{
// Own sim pos -> target where we are
_attachPos = character.SimPosition;
}
Vector2 transformedAttachPos = _attachPos;
if (character.Submarine == null && targetSubmarine != null)
{
transformedAttachPos += ConvertUnits.ToSimUnits(targetSubmarine.Position);
}
if (transformedAttachPos != Vector2.Zero)
{
WallAttachPos = transformedAttachPos;
AttachPos = transformedAttachPos;
}
switch (enemyAI.State)
@@ -151,7 +202,7 @@ namespace Barotrauma
//check if there are any walls nearby the character could attach to
if (raycastTimer < 0.0f)
{
wallAttachPos = Vector2.Zero;
_attachPos = Vector2.Zero;
var cells = Level.Loaded.GetCells(character.WorldPosition, 1);
if (cells.Count > 0)
@@ -169,7 +220,7 @@ namespace Barotrauma
{
attachSurfaceNormal = edge.GetNormal(cell);
targetBody = cell.Body;
wallAttachPos = potentialAttachPos;
_attachPos = potentialAttachPos;
closestDist = distSqr;
}
break;
@@ -183,21 +234,20 @@ namespace Barotrauma
}
else
{
wallAttachPos = Vector2.Zero;
_attachPos = Vector2.Zero;
}
if (wallAttachPos == Vector2.Zero || targetBody == null)
if (_attachPos == Vector2.Zero || targetBody == null)
{
DeattachFromBody(reset: false);
}
else
{
float squaredDistance = Vector2.DistanceSquared(character.SimPosition, wallAttachPos);
float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos);
float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f;
if (squaredDistance < targetDistance * targetDistance)
{
//close enough to a wall -> attach
AttachToBody(wallAttachPos);
AttachToBody(_attachPos);
enemyAI.SteeringManager.Reset();
}
else
@@ -205,25 +255,22 @@ namespace Barotrauma
//move closer to the wall
DeattachFromBody(reset: false);
enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f);
enemyAI.SteeringManager.SteeringSeek(wallAttachPos);
enemyAI.SteeringManager.SteeringSeek(_attachPos);
}
}
break;
case AIState.Attack:
case AIState.Aggressive:
if (enemyAI.AttackingLimb != null)
if (enemyAI.IsSteeringThroughGap) { break; }
if (_attachPos == Vector2.Zero) { break; }
if (!AttachToSub && !AttachToCharacters) { break; }
if (enemyAI.AttackingLimb == null) { break; }
if (targetBody == null) { break; }
if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; }
Vector2 referencePos = targetCharacter != null ? targetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && targetBody != null)
{
// is not attached or is attached to something else
if (!IsAttached || IsAttached && AttachJoints[0].BodyB != targetBody)
{
if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
AttachToBody(transformedAttachPos);
}
}
}
AttachToBody(transformedAttachPos);
}
break;
default:
@@ -231,43 +278,51 @@ namespace Barotrauma
break;
}
if (IsAttached && targetBody != null && targetWall != null && targetSubmarine != null && deattachTimer <= 0.0f)
if (IsAttached && targetBody != null && deattachCheckTimer <= 0.0f)
{
bool deattach = false;
// Deattach if the wall is broken enough where we are attached to
int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
if (enemyAI.CanPassThroughHole(targetWall, targetSection))
if (maxAttachDuration > 0)
{
deattach = true;
attachCooldown = 2;
attachCooldown = coolDown;
}
if (!deattach)
if (!deattach && targetWall != null && targetSubmarine != null)
{
// Deattach if the velocity is high
float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length();
deattach = velocity > maxDeattachSpeed;
// Deattach if the wall is broken enough where we are attached to
int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
if (enemyAI.CanPassThroughHole(targetWall, targetSection))
{
deattach = true;
attachCooldown = coolDown;
}
if (!deattach)
{
if (velocity > minDeattachSpeed)
// Deattach if the velocity is high
float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length();
deattach = velocity > maxDeattachSpeed;
if (!deattach)
{
float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ?
Math.Sign(Math.Abs(velocity) - minDeattachSpeed) :
(Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed);
if (Rand.Range(0.0f, 1.0f) < velocityFactor)
if (velocity > minDeattachSpeed)
{
deattach = true;
character.AddDamage(character.WorldPosition, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true);
attachCooldown = detachStun * 2;
float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ?
Math.Sign(Math.Abs(velocity) - minDeattachSpeed) :
(Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed);
if (Rand.Range(0.0f, 1.0f) < velocityFactor)
{
deattach = true;
character.AddDamage(character.WorldPosition, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true);
attachCooldown = Math.Max(detachStun * 2, coolDown);
}
}
}
}
deattachCheckTimer = 5.0f;
}
if (deattach)
{
DeattachFromBody(reset: true);
}
deattachTimer = 5.0f;
}
}
@@ -315,16 +370,30 @@ namespace Barotrauma
}
collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2);
var colliderJoint = new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
{
FrequencyHz = 10.0f,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false,
//Length = 0.1f
};
Joint colliderJoint = weld ?
new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
{
FrequencyHz = 10.0f,
DampingRatio = 0.5f,
KinematicBodyB = true,
CollideConnected = false,
} :
new RevoluteJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
{
MotorEnabled = true,
MaxMotorTorque = 0.25f
} as Joint;
GameMain.World.Add(colliderJoint);
AttachJoints.Add(colliderJoint);
AttachJoints.Add(colliderJoint);
if (targetCharacter != null)
{
targetCharacter.Latchers.Add(this);
}
if (maxAttachDuration > 0)
{
deattachCheckTimer = maxAttachDuration;
}
}
public void DeattachFromBody(bool reset, float cooldown = 0)
@@ -342,14 +411,23 @@ namespace Barotrauma
{
Reset();
}
if (targetCharacter != null)
{
targetCharacter.Latchers.Remove(this);
}
}
private void Reset()
{
if (targetCharacter != null)
{
targetCharacter.Latchers.Remove(this);
}
targetCharacter = null;
targetWall = null;
targetSubmarine = null;
targetBody = null;
WallAttachPos = null;
AttachPos = null;
}
private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath)
@@ -94,8 +94,9 @@ namespace Barotrauma
if (_abandon)
{
#if DEBUG
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && !objectiveManager.IsCurrentOrder<AIObjectiveReturn>())
{
// TODO: dismiss
throw new Exception("Order abandoned!");
}
#endif
@@ -83,12 +83,13 @@ namespace Barotrauma
if (suitableContainer != null)
{
bool equip = item.GetComponent<Holdable>() != null ||
item.AllowedSlots.None(s =>
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes);
item.AllowedSlots.Any(s => s != InvSlotType.Any) &&
item.AllowedSlots.None(s =>
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes);
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{
@@ -101,17 +101,17 @@ namespace Barotrauma
public enum CombatMode
{
Defensive,
Offensive,
Arrest,
Retreat,
None
Defensive, // Use weapons against the enemy, but try to retreat to a safe place
Offensive, // Engage the enemy and keep attacking it
Arrest, // Try to arrest the enemy without using lethal weapons (stunning + handcuffs)
Retreat, // Run to a safe place without attacking the target
None // Don't use
}
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious;
private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f);
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private float AimSpeed => HumanAIController.AimSpeed;
@@ -252,9 +252,40 @@ namespace Barotrauma
{
case CombatMode.Offensive:
case CombatMode.Arrest:
Engage();
Engage(deltaTime);
break;
case CombatMode.Defensive:
if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
{
if ((character.CurrentHull == null || character.CurrentHull == Enemy.CurrentHull) && sqrDistance < 200 * 200)
{
Engage(deltaTime);
}
else
{
// Keep following the goto target
var gotoObjective = objectiveManager.GetOrder<AIObjectiveGoTo>();
if (gotoObjective != null)
{
gotoObjective.ForceAct(deltaTime);
if (!character.AnimController.InWater)
{
HumanAIController.FaceTarget(Enemy);
ForceWalk = true;
HumanAIController.AutoFaceMovement = false;
}
}
else
{
SteeringManager.Reset();
}
}
}
else
{
Retreat(deltaTime);
}
break;
case CombatMode.Retreat:
Retreat(deltaTime);
break;
@@ -588,8 +619,9 @@ namespace Barotrauma
// assume that it's required for the stun effect
// as we can't check the status effect conditions here.
var mobileBatteryTag = "mobilebattery";
var containers = weapon.Item.Components.Where(ic => ic is ItemContainer container &&
container.ContainableItems.Any(containable => containable.Identifiers.Any(id => id.Equals(mobileBatteryTag))));
var containers = weapon.Item.Components.Where(ic =>
ic is ItemContainer container &&
container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
// If there's no such container, assume that the melee weapon can stun without a battery.
return containers.None() || containers.Any(container =>
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
@@ -670,6 +702,14 @@ namespace Barotrauma
{
RemoveSubObjective(ref retreatObjective);
}
if (character.Submarine == null && sqrDistance < MathUtils.Pow2(maxDistance))
{
// Swim away
SteeringManager.Reset();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition));
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
return;
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
{
if (findHullTimer > 0)
@@ -684,7 +724,10 @@ namespace Barotrauma
}
if (retreatTarget != null && character.CurrentHull != retreatTarget)
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true),
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true)
{
UsePathingOutside = false
},
onAbandon: () =>
{
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
@@ -703,7 +746,7 @@ namespace Barotrauma
}
}
private void Engage()
private void Engage(float deltaTime)
{
if (WeaponComponent == null)
{
@@ -721,6 +764,21 @@ namespace Barotrauma
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekAmmunitionObjective);
RemoveSubObjective(ref seekWeaponObjective);
if (character.Submarine == null && WeaponComponent is MeleeWeapon meleeWeapon)
{
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
{
// Swim towards the target
SteeringManager.Reset();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10);
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 15);
}
else
{
SteeringManager.Reset();
}
return;
}
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
{
RemoveFollowTarget();
@@ -728,6 +786,7 @@ namespace Barotrauma
TryAddSubObjective(ref followTargetObjective,
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true, closeEnough: 50)
{
UsePathingOutside = false,
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = Enemy.DisplayName,
@@ -958,14 +1017,15 @@ namespace Barotrauma
}
if (reloadTimer > 0) { return; }
if (holdFireCondition != null && holdFireCondition()) { return; }
float sqrDist = Vector2.DistanceSquared(character.Position, Enemy.Position);
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
distanceTimer = distanceCheckInterval;
if (WeaponComponent is MeleeWeapon meleeWeapon)
{
bool closeEnough = true;
float sqrRange = meleeWeapon.Range * meleeWeapon.Range;
if (character.AnimController.InWater)
{
if (sqrDist > sqrRange)
if (sqrDistance > sqrRange)
{
closeEnough = false;
}
@@ -992,6 +1052,7 @@ namespace Barotrauma
if (closeEnough)
{
UseWeapon(deltaTime);
character.AIController.SteeringManager.Reset();
}
else if (!character.IsFacing(Enemy.WorldPosition))
{
@@ -1003,7 +1064,7 @@ namespace Barotrauma
{
if (WeaponComponent is RepairTool repairTool)
{
if (sqrDist > repairTool.Range * repairTool.Range) { return; }
if (sqrDistance > repairTool.Range * repairTool.Range) { return; }
}
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor)
@@ -53,9 +53,13 @@ namespace Barotrauma
distanceFactor = 1;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
if (severity > 0.5f && !isOrder)
if (severity > 0.75f && !isOrder &&
targetHull.RoomName != null &&
!targetHull.RoomName.Contains("reactor", StringComparison.OrdinalIgnoreCase) &&
!targetHull.RoomName.Contains("engine", StringComparison.OrdinalIgnoreCase) &&
!targetHull.RoomName.Contains("command", StringComparison.OrdinalIgnoreCase))
{
// Ignore severe fires unless ordered. (Let the fire drain all the oxygen instead).
// Ignore severe fires to prevent casualities unless ordered to extinguish.
Priority = 0;
Abandon = true;
}
@@ -25,7 +25,7 @@ namespace Barotrauma
/// <summary>
/// 0-1 based on the horizontal size of all of the fires in the hull.
/// </summary>
public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, Math.Min(hull.Rect.Width, 1000), hull.FireSources.Sum(fs => fs.Size.X)));
public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 500, hull.FireSources.Sum(fs => fs.Size.X)));
protected override IEnumerable<Hull> GetList() => Hull.hullList;
@@ -56,13 +56,15 @@ namespace Barotrauma
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.Removed) { return false; }
if (target.IsDead || target.IsUnconscious) { return false; }
if (target.IsDead) { return false; }
if (target.IsUnconscious && target.Params.Health.ConstantHealthRegeneration <= 0.0f) { return false; }
if (target == character) { return false; }
if (target.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
if (target.CurrentHull == null) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
return true;
}
}
@@ -39,6 +39,10 @@ namespace Barotrauma
return;
}
targetItem = character.Inventory.FindItemByTag(gearTag, true);
if (targetItem == null && gearTag == LIGHT_DIVING_GEAR)
{
targetItem = character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true);
}
if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
{
TryAddSubObjective(ref getDivingGear, () =>
@@ -57,23 +61,37 @@ namespace Barotrauma
};
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getDivingGear));
onCompleted: () =>
{
RemoveSubObjective(ref getDivingGear);
if (gearTag == HEAVY_DIVING_GEAR && HumanAIController.HasItem(character, LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
{
foreach (Item mask in masks)
{
if (mask != targetItem)
{
character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot);
}
}
}
});
}
else
{
// Seek oxygen that has at least 10% condition left, if we are inside a friendly sub.
// The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
float min = character.Submarine != Submarine.MainSub ? 0.01f : MIN_OXYGEN;
float min = GetMinOxygen(character);
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min))
{
TryAddSubObjective(ref getOxygen, () =>
{
if (character.IsOnPlayerTeam)
{
if (HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: min))
if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min))
{
character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f);
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1)
{
character.Speak(TextManager.Get("dialoglastoxygentank"), null, 0.0f, "dialoglastoxygentank", 30.0f);
}
}
else
{
@@ -105,7 +123,7 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
if (remainingTanks > 0 && !HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 0.01f))
if (remainingTanks > 0 && !HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: 0.01f))
{
character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f);
}
@@ -121,7 +139,7 @@ namespace Barotrauma
int ReportOxygenTankCount()
{
if (character.Submarine != Submarine.MainSub) { return 1; }
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1);
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f);
@@ -136,17 +154,6 @@ namespace Barotrauma
}
}
/// <summary>
/// Returns false only when no inventory can be found from the item.
/// </summary>
public static bool EjectEmptyTanks(Character actor, Item target, out IEnumerable<Item> containedItems)
{
containedItems = target.OwnInventory?.AllItems;
if (containedItems == null) { return false; }
AIController.UnequipEmptyItems(actor, target);
return true;
}
public override void Reset()
{
base.Reset();
@@ -154,5 +161,20 @@ namespace Barotrauma
getOxygen = null;
targetItem = null;
}
public static float GetMinOxygen(Character character)
{
// Seek oxygen that has at least 10% condition left, if we are inside a friendly sub.
// The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
float min = 0.01f;
float minOxygen = character.IsInFriendlySub ? MIN_OXYGEN : min;
if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag("oxygensource") && i.ConditionPercentage >= minOxygen))
{
// There's a valid oxygen tank in the inventory -> no need to swap the tank too early.
minOxygen = min;
}
return minOxygen;
}
}
}
@@ -46,20 +46,25 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.HasActiveObjective<AIObjectiveCombat>()) && HumanAIController.HasDivingSuit(character) ? 0 : 100;
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() ||
objectiveManager.IsCurrentOrder<AIObjectiveReturn>() ||
objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
else
{
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
(needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN) :
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN)))
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) :
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character))))
{
Priority = 100;
}
else if (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
{
// Ordered to follow/hold position inside a hostile sub -> ignore find safety unless we need to find a diving gear
// 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;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
@@ -126,11 +131,11 @@ namespace Barotrauma
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.GetMinOxygen(character));
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.GetMinOxygen(character));
}
if (needsEquipment)
{
@@ -298,17 +303,21 @@ namespace Barotrauma
Hull bestHull = null;
float bestValue = 0;
bool bestIsAirlock = false;
foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h)))
{
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 (!allowChangingTheSubmarine && 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; }
float hullSafety = 0;
if (character.CurrentHull != null && character.Submarine != null)
bool hullIsAirlock = false;
bool isCharacterInside = character.CurrentHull != null && character.Submarine != null;
if (isCharacterInside)
{
// Inside
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
@@ -325,7 +334,7 @@ namespace Barotrauma
continue;
}
// Don't allow to go outside if not already outside.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
HumanAIController.UnreachableHulls.Add(hull);
@@ -343,24 +352,16 @@ namespace Barotrauma
}
else
{
// Outside
if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase))
// TODO: could also target gaps that get us inside?
if (hull.IsTaggedAirlock())
{
hullSafety = 100;
hullIsAirlock = true;
}
else if(!bestIsAirlock && hull.LeadsOutside(character))
{
hullSafety = 100;
}
else
{
// TODO: could also target gaps that get us inside?
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull && item.HasTag("airlock"))
{
hullSafety = 100;
break;
}
}
}
// TODO: could we get a closest door to the outside and target the flowing hull if no airlock is found?
// Huge preference for closer targets
float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
@@ -372,10 +373,11 @@ namespace Barotrauma
hullSafety /= 10;
}
}
if (hullSafety > bestValue)
if (hullSafety > bestValue || (!isCharacterInside && hullIsAirlock && !bestIsAirlock))
{
bestHull = hull;
bestValue = hullSafety;
bestIsAirlock = hullIsAirlock;
}
}
return bestHull;
@@ -323,7 +323,7 @@ namespace Barotrauma
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
hasCalledPathFinder = true;
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable) { continue; }
}
currItemPriority = itemPriority;
@@ -27,6 +27,7 @@ namespace Barotrauma
public bool followControlledCharacter;
public bool mimic;
public bool SpeakIfFails { get; set; } = true;
public bool UsePathingOutside { get; set; } = true;
public float extraDistanceWhileSwimming;
public float extraDistanceOutsideSub;
@@ -121,13 +122,14 @@ namespace Barotrauma
}
private readonly float avoidLookAheadDistance = 5;
private readonly float pathWaitingTime = 3;
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
: base(character, objectiveManager, priorityModifier)
{
Target = target;
this.repeat = repeat;
waitUntilPathUnreachable = 3.0f;
waitUntilPathUnreachable = pathWaitingTime;
this.getDivingGearIfNeeded = getDivingGearIfNeeded;
if (Target is Item i)
{
@@ -159,6 +161,8 @@ namespace Barotrauma
}
}
public void ForceAct(float deltaTime) => Act(deltaTime);
protected override void Act(float deltaTime)
{
if (followControlledCharacter)
@@ -184,7 +188,6 @@ namespace Barotrauma
// Wait
character.AIController.SteeringManager.Reset();
}
waitUntilPathUnreachable -= deltaTime;
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
@@ -220,11 +223,13 @@ namespace Barotrauma
{
Abandon = true;
}
else if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty)
else if (HumanAIController.IsCurrentPathUnreachable)
{
waitUntilPathUnreachable -= deltaTime;
SteeringManager.Reset();
if (waitUntilPathUnreachable < 0)
{
waitUntilPathUnreachable = pathWaitingTime;
if (repeat)
{
SpeakCannotReach();
@@ -240,7 +245,7 @@ namespace Barotrauma
if (getDivingGearIfNeeded && !character.LockHands)
{
Character followTarget = Target as Character;
bool needsDivingSuit = targetIsOutside;
bool needsDivingSuit = !isInside || targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
if (mimic)
{
@@ -255,7 +260,7 @@ namespace Barotrauma
}
}
bool needsEquipment = false;
float minOxygen = character.Submarine == null ? 0 : AIObjectiveFindDivingGear.MIN_OXYGEN;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
@@ -323,25 +328,29 @@ namespace Barotrauma
}
else
{
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (TargetGap != null)
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
if (!isRuins || !HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: true))
{
// Check that nothing is blocking the way
Vector2 rayStart = character.SimPosition;
Vector2 rayEnd = TargetGap.SimPosition;
if (TargetGap.Submarine != null && character.Submarine == null)
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (TargetGap != null)
{
rayStart -= TargetGap.Submarine.SimPosition;
}
else if (TargetGap.Submarine == null && character.Submarine != null)
{
rayEnd -= character.Submarine.SimPosition;
}
var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
if (closestBody != null)
{
TargetGap = null;
// Check that nothing is blocking the way
Vector2 rayStart = character.SimPosition;
Vector2 rayEnd = TargetGap.SimPosition;
if (TargetGap.Submarine != null && character.Submarine == null)
{
rayStart -= TargetGap.Submarine.SimPosition;
}
else if (TargetGap.Submarine == null && character.Submarine != null)
{
rayEnd -= character.Submarine.SimPosition;
}
var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
if (closestBody != null)
{
TargetGap = null;
}
}
}
}
@@ -365,7 +374,7 @@ namespace Barotrauma
if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
string scooterTag = "scooter";
string batteryTag = "mobilebattery";
Item scooter = null;
@@ -444,18 +453,33 @@ namespace Barotrauma
}
if (SteeringManager == PathSteering)
{
Vector2 targetPos = character.GetRelativeSimPosition(Target);
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
{
nodeFilter = n => n.Waypoint.CurrentHull != null;
}
else if (!isInside && HumanAIController.UseIndoorSteeringOutside)
{
nodeFilter = n => n.Waypoint.Submarine == null;
}
PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter,
nodeFilter,
CheckVisibility);
if (!isInside && !UsePathingOutside)
{
PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
}
}
else
{
PathSteering.SteeringSeek(targetPos, weight: 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter: endNodeFilter,
nodeFilter: nodeFilter,
checkVisiblity: CheckVisibility);
}
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
if (useScooter)
@@ -501,9 +525,22 @@ namespace Barotrauma
{
character.CursorPosition -= character.Submarine.Position;
}
Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position);
if (!MathUtils.IsValid(dir)) { dir = Vector2.UnitY; }
SteeringManager.SteeringManual(1.0f, dir);
Vector2 diff = character.CursorPosition - character.Position;
Vector2 dir = Vector2.Normalize(diff);
float sqrDist = diff.LengthSquared();
if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f))
{
SteeringManager.SteeringManual(1.0f, dir);
}
else
{
float dot = Vector2.Dot(dir, VectorExtensions.Forward(character.AnimController.Collider.Rotation + MathHelper.PiOver2));
bool isFacing = dot > 0.9f;
if (!isFacing && sqrDist > MathUtils.Pow2(CloseEnough))
{
SteeringManager.SteeringManual(1.0f, dir);
}
}
character.SetInput(InputType.Aim, false, true);
character.SetInput(InputType.Shoot, false, true);
}
@@ -511,7 +548,7 @@ namespace Barotrauma
private bool useScooter;
private float checkScooterTimer;
private readonly float checkScooterTime = 0.2f;
private readonly float checkScooterTime = 0.5f;
public Hull GetTargetHull() => GetTargetHull(Target);
@@ -242,9 +242,8 @@ namespace Barotrauma
if (!searchingNewHull)
{
//find all available hulls first
FindTargetHulls();
searchingNewHull = true;
return;
FindTargetHulls();
}
else if (targetHulls.Any())
{
@@ -252,14 +251,13 @@ namespace Barotrauma
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: null, nodeFilter: node =>
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, character.Submarine, nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
// Check that there is no unsafe hulls on the way to the target
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; }
return true;
});
}, endNodeFilter: node => !isCurrentHullAllowed | !IsForbidden(node.Waypoint.CurrentHull));
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room
@@ -271,31 +269,20 @@ namespace Barotrauma
SetTargetTimerLow();
return;
}
character.AIController.SelectTarget(currentTarget.AiTarget);
PathSteering.SetPath(path);
SetTargetTimerNormal();
searchingNewHull = false;
}
else
{
// Couldn't find a target for some reason -> reset
// Couldn't find a valid hull
SetTargetTimerHigh();
searchingNewHull = false;
}
if (currentTarget != null)
{
character.AIController.SelectTarget(currentTarget.AiTarget);
string errorMsg = null;
#if DEBUG
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
SetTargetTimerNormal();
}
newTargetTimer -= deltaTime;
if (!character.IsClimbing && IsSteeringFinished())
if (!character.IsClimbing && (PathSteering == null || PathSteering.CurrentPath == null || IsSteeringFinished()))
{
Wander(deltaTime);
}
@@ -406,9 +393,10 @@ namespace Barotrauma
hullWeights.Clear();
foreach (var hull in Hull.hullList)
{
if (character.Submarine == null) { break; }
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
if (character.Submarine == null) { break; }
if (hull.Submarine.Info.IsRuin || hull.Submarine.Info.IsWreck) { continue; }
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
{
if (hull.Submarine.TeamID != character.TeamID)
@@ -163,7 +163,7 @@ namespace Barotrauma
CoroutineManager.StopCoroutines(coroutine);
DelayedObjectives.Remove(objective);
}
coroutine = CoroutineManager.InvokeAfter(() =>
coroutine = CoroutineManager.Invoke(() =>
{
//round ended before the coroutine finished
#if CLIENT
@@ -233,11 +233,7 @@ namespace Barotrauma
if (orderObjective == null) { return; }
#if DEBUG
// Note: don't automatically remove orders here. Removing orders needs to be done via dismissing.
if (orderObjective.IsCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red);
}
else if (!orderObjective.CanBeCompleted)
if (!orderObjective.CanBeCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
}
@@ -281,9 +277,9 @@ namespace Barotrauma
ForcedOrder?.CalculatePriority();
AIObjective orderWithHighestPriority = null;
float highestPriority = 0;
foreach (var currentOrder in CurrentOrders)
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{
var orderObjective = currentOrder.Objective;
var orderObjective = CurrentOrders[i].Objective;
if (orderObjective == null) { continue; }
orderObjective.CalculatePriority();
if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority)
@@ -467,6 +463,11 @@ namespace Barotrauma
AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine)
};
break;
case "return":
newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier);
newObjective.Abandoned += () => DismissSelf(order, option);
newObjective.Completed += () => DismissSelf(order, option);
break;
case "fixleaks":
newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull);
break;
@@ -586,6 +587,27 @@ namespace Barotrauma
return newObjective;
}
private void DismissSelf(Order order, string option)
{
var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order, option));
if (currentOrder.Order == null)
{
#if DEBUG
DebugConsole.ThrowError("Tried to self-dismiss an order, but no matching current order was found");
#endif
return;
}
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
{
GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, currentOrder.Order?.TargetSpatialEntity, character, character));
#endif
}
private bool IsAllowedToWait()
{
if (!character.IsOnPlayerTeam) { return false; }
@@ -606,6 +628,8 @@ namespace Barotrauma
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T).Objective as T;
/// <summary>
/// Returns the last active objective of the specific type.
/// </summary>
@@ -408,7 +408,7 @@ namespace Barotrauma
}
bool isCompleted =
AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) ||
targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold);
targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength <= a.Prefab.TreatmentThreshold);
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
{
@@ -83,7 +83,7 @@ namespace Barotrauma
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target) ||
target.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold))
target.CharacterHealth.GetAllAfflictions().All(a => a.Strength <= a.Prefab.TreatmentThreshold))
{
return false;
}
@@ -0,0 +1,273 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
namespace Barotrauma
{
class AIObjectiveReturn : AIObjective
{
public override string Identifier { get; set; } = "return";
private AIObjectiveGoTo moveInsideObjective, moveInCaveObjective, moveOutsideObjective;
private bool usingEscapeBehavior;
private bool isSteeringThroughGap;
public Submarine ReturnTarget { get; }
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
{
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
if (ReturnTarget == null)
{
DebugConsole.ThrowError("Error with a Return objective: no suitable return target found");
Abandon = true;
}
Submarine GetReturnTarget(IEnumerable<Submarine> subs)
{
var requiredTeamID = orderGiver?.TeamID ?? character?.TeamID;
Submarine returnTarget = null;
foreach (var sub in subs)
{
if (sub == null) { continue; }
if (sub.TeamID != requiredTeamID) { continue; }
returnTarget = sub;
break;
}
return returnTarget;
}
}
protected override float GetPriority()
{
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
{
Priority = objectiveManager.GetOrderPriority(this);
}
else
{
// TODO: Consider if this needs to be addressed
Priority = 0;
}
return Priority;
}
protected override void Act(float deltaTime)
{
if (ReturnTarget == null)
{
Abandon = true;
return;
}
bool shouldUseEscapeBehavior = false;
if (character.CurrentHull != null || isSteeringThroughGap)
{
if (character.Submarine == null || !character.Submarine.IsConnectedTo(ReturnTarget))
{
// Character is on another sub that is not connected to the target sub, use the escape behavior to get them out
shouldUseEscapeBehavior = true;
if (!usingEscapeBehavior)
{
HumanAIController.ResetEscape();
}
isSteeringThroughGap = HumanAIController.Escape(deltaTime);
if (!isSteeringThroughGap && (HumanAIController.EscapeTarget == null || HumanAIController.IsCurrentPathUnreachable))
{
Abandon = true;
}
}
else if (character.Submarine != ReturnTarget)
{
// Character is on another sub that is connected to the target sub, create a Go To objective to reach the target sub
if (moveInsideObjective == null)
{
Hull targetHull = null;
foreach (var d in ReturnTarget.ConnectedDockingPorts.Values)
{
if (!d.Docked) { continue; }
if (d.DockingTarget == null) { continue; }
if (d.DockingTarget.Item.Submarine != character.Submarine) { continue; }
targetHull = d.Item.CurrentHull;
break;
}
if (targetHull != null && !targetHull.IsTaggedAirlock())
{
// Target the closest airlock
float closestDist = 0;
Hull airlock = null;
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine != targetHull.Submarine) { continue; }
if (!hull.IsTaggedAirlock()) { continue; }
float dist = Vector2.DistanceSquared(targetHull.Position, hull.Position);
if (airlock == null || closestDist <= 0 || dist < closestDist)
{
airlock = hull;
closestDist = dist;
}
}
if (airlock != null)
{
targetHull = airlock;
}
}
if (targetHull != null)
{
RemoveSubObjective(ref moveInCaveObjective);
RemoveSubObjective(ref moveOutsideObjective);
TryAddSubObjective(ref moveInsideObjective,
constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager)
{
AllowGoingOutside = true,
endNodeFilter = n => n.Waypoint.Submarine == targetHull.Submarine
},
onCompleted: () => RemoveSubObjective(ref moveInsideObjective),
onAbandon: () => Abandon = true);
}
else
{
#if DEBUG
DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveInsideObjective'");
#endif
}
}
}
else
{
// Character is on the target sub, the objective is completed
IsCompleted = true;
}
}
else if (!isSteeringThroughGap && moveInCaveObjective == null && moveOutsideObjective == null)
{
if (HumanAIController.IsInsideCave)
{
WayPoint closestOutsideWaypoint = null;
float closestDistance = float.MaxValue;
foreach (var w in WayPoint.WayPointList)
{
if (w.Tunnel != null && w.Tunnel.Type == Level.TunnelType.Cave) { continue; }
if (w.linkedTo.None(l => l is WayPoint linkedWaypoint && linkedWaypoint.Tunnel?.Type == Level.TunnelType.Cave)) { continue; }
float distance = Vector2.DistanceSquared(character.WorldPosition, w.WorldPosition);
if (closestOutsideWaypoint == null || distance < closestDistance)
{
closestOutsideWaypoint = w;
closestDistance = distance;
}
}
if (closestOutsideWaypoint != null)
{
RemoveSubObjective(ref moveInsideObjective);
RemoveSubObjective(ref moveOutsideObjective);
TryAddSubObjective(ref moveInCaveObjective,
constructor: () => new AIObjectiveGoTo(closestOutsideWaypoint, character, objectiveManager)
{
endNodeFilter = n => n.Waypoint == closestOutsideWaypoint,
AllowGoingOutside = true
},
onCompleted: () => RemoveSubObjective(ref moveInCaveObjective),
onAbandon: () => Abandon = true);
}
else
{
#if DEBUG
DebugConsole.ThrowError("Error with a Return objective: no suitable main or side path node target found for 'moveOutsideObjective'");
#endif
}
}
else
{
Hull targetHull = null;
float targetDistanceSquared = float.MaxValue;
bool targetIsAirlock = false;
foreach (var hull in ReturnTarget.GetHulls(false))
{
bool hullIsAirlock = hull.IsTaggedAirlock();
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
{
float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
if (targetHull == null || distanceSquared < targetDistanceSquared)
{
targetHull = hull;
targetDistanceSquared = distanceSquared;
targetIsAirlock = hullIsAirlock;
}
}
}
if (targetHull != null)
{
RemoveSubObjective(ref moveInsideObjective);
RemoveSubObjective(ref moveInCaveObjective);
TryAddSubObjective(ref moveOutsideObjective,
constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager)
{
AllowGoingOutside = true
},
onCompleted: () => RemoveSubObjective(ref moveOutsideObjective),
onAbandon: () => Abandon = true);
}
else
{
#if DEBUG
DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveOutsideObjective'");
#endif
}
}
}
else
{
if (HumanAIController.IsInsideCave)
{
RemoveSubObjective(ref moveOutsideObjective);
}
else
{
RemoveSubObjective(ref moveInCaveObjective);
}
}
usingEscapeBehavior = shouldUseEscapeBehavior;
}
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted)
{
return true;
}
if (ReturnTarget == null)
{
Abandon = true;
return false;
}
if (character.Submarine == ReturnTarget)
{
IsCompleted = true;
}
return IsCompleted;
}
public override void Reset()
{
base.Reset();
moveInsideObjective = null;
moveInCaveObjective = null;
moveOutsideObjective = null;
usingEscapeBehavior = false;
isSteeringThroughGap = false;
HumanAIController.ResetEscape();
}
protected override void OnAbandon()
{
base.OnAbandon();
SteeringManager?.Reset();
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get("dialogcannotreturn", returnNull: true);
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotreturn", minDurationBetweenSimilar: 5.0f);
}
}
}
}
}
@@ -141,6 +141,7 @@ namespace Barotrauma
public Entity TargetEntity;
public ItemComponent TargetItemComponent;
public readonly bool UseController;
public readonly string[] ControllerTags;
public Controller ConnectedController;
public Character OrderGiver;
@@ -309,6 +310,7 @@ namespace Barotrauma
color = orderElement.GetAttributeColor("color");
FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f);
UseController = orderElement.GetAttributeBool("usecontroller", false);
ControllerTags = orderElement.GetAttributeStringArray("controllertags", new string[0]);
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
Options = orderElement.GetAttributeStringArray("options", new string[0]);
@@ -380,6 +382,7 @@ namespace Barotrauma
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
UseController = prefab.UseController;
ControllerTags = prefab.ControllerTags;
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
@@ -399,7 +402,7 @@ namespace Barotrauma
{
if (UseController)
{
ConnectedController = targetItem.Item?.FindController();
ConnectedController = targetItem.Item?.FindController(tags: ControllerTags);
if (ConnectedController == null)
{
DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any.");
@@ -450,19 +453,37 @@ namespace Barotrauma
return false;
}
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "")
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", int? priority = null)
{
orderOption ??= "";
string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier;
if (Identifier != "dismissed" && !string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; }
if (targetCharacterName == null) { targetCharacterName = ""; }
if (targetRoomName == null) { targetRoomName = ""; }
string msg = TextManager.GetWithVariables(messageTag, new string[2] { "[name]", "[roomname]" }, new string[2] { targetCharacterName, targetRoomName }, new bool[2] { false, true }, true);
if (msg == null) { return ""; }
return msg;
priority ??= CharacterInfo.HighestManualOrderPriority;
// If the order has a lesser priority, it means we are rearranging character orders
if (!TargetAllCharacters && priority != CharacterInfo.HighestManualOrderPriority && Identifier != "dismissed")
{
return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty;
}
string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}";
messageTag += $".{Identifier}";
if (!string.IsNullOrEmpty(orderOption))
{
if (Identifier != "dismissed")
{
messageTag += $".{orderOption}";
}
else
{
string[] splitOption = orderOption.Split('.');
if (splitOption.Length > 0)
{
messageTag += $".{splitOption[0]}";
}
}
}
string msg = TextManager.GetWithVariables(messageTag,
new string[2] { "[name]", "[roomname]" },
new string[2] { targetCharacterName ?? string.Empty, targetRoomName ?? string.Empty },
formatCapitals: new bool[2] { false, true },
returnNull: true);
return msg ?? string.Empty;
}
/// <summary>
@@ -505,7 +526,7 @@ namespace Barotrauma
if (item.NonInteractable) { continue; }
if (ItemComponentType != null && item.Components.None(c => c.GetType() == ItemComponentType)) { continue; }
Controller controller = null;
if (UseController && !item.TryFindController(out controller)) { continue; }
if (UseController && !item.TryFindController(out controller, tags: ControllerTags)) { continue; }
if (interactableFor != null && (!item.IsInteractable(interactableFor) || (UseController && !controller.Item.IsInteractable(interactableFor)))) { continue; }
matchingItems.Add(item);
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -78,6 +79,31 @@ namespace Barotrauma
return nodeList;
}
private bool? blocked;
public bool IsBlocked()
{
if (blocked.HasValue) { return blocked.Value; }
blocked = false;
if (Waypoint.Submarine != null) { return blocked.Value; }
if (Waypoint.Tunnel?.Type != Level.TunnelType.Cave) { return blocked.Value; }
foreach (var w in Level.Loaded.ExtraWalls)
{
if (!(w is DestructibleLevelWall d)) { return blocked.Value; }
if (d.Destroyed) { return blocked.Value; }
if (!d.IsPointInside(Waypoint.Position)) { return blocked.Value; }
blocked = true;
break;
}
return blocked.Value;
}
public void ResetBlocked()
{
blocked = null;
}
}
class PathFinder
@@ -86,96 +112,116 @@ namespace Barotrauma
public GetNodePenaltyHandler GetNodePenalty;
private readonly List<PathNode> nodes;
public readonly bool IndoorsSteering;
private readonly bool isCharacter;
public bool InsideSubmarine { get; set; }
public bool ApplyPenaltyToOutsideNodes { get; set; }
public PathFinder(List<WayPoint> wayPoints, bool indoorsSteering = false)
public PathFinder(List<WayPoint> wayPoints, bool isCharacter)
{
nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => w.Submarine != null == indoorsSteering), removeOrphans: true);
var filtered = isCharacter ? wayPoints : wayPoints.FindAll(w => w.Submarine == null);
nodes = PathNode.GenerateNodes(filtered, removeOrphans: true);
foreach (WayPoint wp in wayPoints)
{
wp.linkedTo.CollectionChanged += WaypointLinksChanged;
wp.OnLinksChanged += WaypointLinksChanged;
}
IndoorsSteering = indoorsSteering;
this.isCharacter = isCharacter;
}
void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
void WaypointLinksChanged(WayPoint wp)
{
if (Submarine.Unloading) { return; }
var waypoints = sender as IEnumerable<MapEntity>;
var node = nodes.Find(n => n.Waypoint == wp);
if (node == null) { return; }
foreach (MapEntity me in waypoints)
for (int i = node.connections.Count - 1; i >= 0; i--)
{
WayPoint wp = me as WayPoint;
if (me == null) { continue; }
var node = nodes.Find(n => n.Waypoint == wp);
if (node == null) { return; }
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
//remove connection if the waypoint isn't connected anymore
if (wp.linkedTo.FirstOrDefault(l => l == node.connections[i].Waypoint) == null)
{
for (int i = node.connections.Count - 1; i >= 0; i--)
{
//remove connection if the waypoint isn't connected anymore
if (wp.linkedTo.FirstOrDefault(l => l == node.connections[i].Waypoint) == null)
{
node.connections.RemoveAt(i);
node.distances.RemoveAt(i);
}
}
node.connections.RemoveAt(i);
node.distances.RemoveAt(i);
}
else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
}
for (int i = 0; i < wp.linkedTo.Count; i++)
{
if (!(wp.linkedTo[i] is WayPoint connected)) { continue; }
//already connected, continue
if (node.connections.Any(n => n.Waypoint == connected)) { continue; }
var matchingNode = nodes.Find(n => n.Waypoint == connected);
if (matchingNode == null)
{
for (int i = 0; i < wp.linkedTo.Count; i++)
{
if (!(wp.linkedTo[i] is WayPoint connected)) { continue; }
//already connected, continue
if (node.connections.Any(n => n.Waypoint == connected)) { continue; }
var matchingNode = nodes.Find(n => n.Waypoint == connected);
if (matchingNode == null)
{
#if DEBUG
DebugConsole.ThrowError("Waypoint connections were changed, no matching path node found in PathFinder");
DebugConsole.ThrowError("Waypoint connections were changed, no matching path node found in PathFinder");
#endif
return;
}
node.connections.Add(matchingNode);
node.distances.Add(Vector2.Distance(node.Position, matchingNode.Position));
}
return;
}
node.connections.Add(matchingNode);
node.distances.Add(Vector2.Distance(node.Position, matchingNode.Position));
}
}
private static readonly List<PathNode> sortedNodes = new List<PathNode>();
private readonly List<PathNode> sortedNodes = new List<PathNode>();
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{
foreach (PathNode node in nodes)
{
node.ResetBlocked();
}
//sort nodes roughly according to distance
sortedNodes.Clear();
foreach (PathNode node in nodes)
{
node.TempPosition = node.Position;
if (hostSub != null)
var wpSub = node.Waypoint.Submarine;
if (hostSub != null && wpSub == null)
{
Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition;
node.TempPosition -= diff;
// inside and targeting outside
node.TempPosition -= hostSub.SimPosition;
}
else if (wpSub != null && hostSub != null && wpSub != hostSub)
{
// different subs
node.TempPosition -= hostSub.SimPosition - wpSub.SimPosition;
}
else if (hostSub == null && wpSub != null)
{
// Outside and targeting inside
node.TempPosition += wpSub.SimPosition;
}
float xDiff = Math.Abs(start.X - node.TempPosition.X);
float yDiff = Math.Abs(start.Y - node.TempPosition.Y);
if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null) { yDiff += 10.0f; }
node.TempDistance = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
if (InsideSubmarine && !(node.Waypoint.Submarine?.Info?.IsRuin ?? false))
{
//higher cost for vertical movement when inside the sub
if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null)
{
yDiff += 10.0f;
}
node.TempDistance = xDiff + yDiff * 10.0f;
}
else
{
node.TempDistance = xDiff + yDiff;
}
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && ApplyPenaltyToOutsideNodes) { node.TempDistance *= 10.0f; }
//optimization:
//node extremely far, don't try to use it as a start node
if (node.TempDistance > 800.0f)
{
continue;
}
//prefer nodes that are closer to the end position
node.TempDistance += (Math.Abs(end.X - node.TempPosition.X) + Math.Abs(end.Y - node.TempPosition.Y)) / 100.0f;
@@ -187,31 +233,39 @@ namespace Barotrauma
sortedNodes.Insert(i, node);
}
bool IsWaypointVisible(PathNode node, Vector2 rayStart, bool checkVisibility = true)
{
//if searching for a path inside the sub, make sure the waypoint is visible
if (checkVisibility && isCharacter)
{
if (node.Waypoint.isObstructed) { return false; }
var body = Submarine.PickBody(rayStart, node.TempPosition,
collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
if (body.UserData is Submarine) { return false; }
if (body.UserData is Structure s && !s.IsPlatform) { return false; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { return false; }
}
}
return true;
}
//find the most suitable start node, starting from the ones that are the closest
PathNode startNode = null;
foreach (PathNode node in sortedNodes)
{
if (startNode == null || node.TempDistance < startNode.TempDistance)
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
// Always check the visibility for the start node
if (!IsWaypointVisible(node, start)) { continue; }
if (node.IsBlocked()) { continue; }
if (node.Waypoint.ConnectedGap != null)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (IndoorsSteering)
{
if (node.Waypoint.isObstructed) { continue; }
// Always check the visibility for the start node
var body = Submarine.PickBody(
start, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
startNode = node;
if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; }
}
startNode = node;
break;
}
if (startNode == null)
@@ -252,28 +306,17 @@ namespace Barotrauma
PathNode endNode = null;
foreach (PathNode node in sortedNodes)
{
if (endNode == null || node.TempDistance < endNode.TempDistance)
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
// Only check the visibility for the end node when allowed (fix leaks)
if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { continue; }
if (node.IsBlocked()) { continue; }
if (node.Waypoint.ConnectedGap != null)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
if (IndoorsSteering)
{
if (node.Waypoint.isObstructed) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (checkVisibility)
{
// Only check the visibility for the end node when allowed (fix leaks)
var body = Submarine.PickBody(end, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
}
endNode = node;
if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; }
}
endNode = node;
break;
}
if (endNode == null)
@@ -284,40 +327,12 @@ namespace Barotrauma
return new SteeringPath(true);
}
var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr);
var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize);
return path;
}
public SteeringPath FindPath(WayPoint start, WayPoint end)
{
PathNode startNode = null, endNode = null;
foreach (PathNode node in nodes)
{
if (node.Waypoint == start)
{
startNode = node;
if (endNode != null) { break; }
}
if (node.Waypoint == end)
{
endNode = node;
if (startNode != null) { break; }
}
}
if (startNode == null || endNode == null)
{
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints.", Color.DarkRed);
#endif
return new SteeringPath(true);
}
return FindPath(startNode, endNode);
}
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "")
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "", float minGapSize = 0)
{
if (start == end)
{
@@ -342,14 +357,16 @@ namespace Barotrauma
float dist = float.MaxValue;
foreach (PathNode node in nodes)
{
if (node.state != 1) { continue; }
if (IndoorsSteering && node.Waypoint.isObstructed) { continue; }
if (node.state != 1 || node.F > dist) { continue; }
if (isCharacter && node.Waypoint.isObstructed) { continue; }
if (filter != null && !filter(node)) { continue; }
if (node.F < dist)
if (node.IsBlocked()) { continue; }
if (node.Waypoint.ConnectedGap != null)
{
dist = node.F;
currNode = node;
}
if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; }
}
dist = node.F;
currNode = node;
}
if (currNode == null || currNode == end) { break; }
@@ -451,6 +468,8 @@ namespace Barotrauma
return path;
}
private bool CanFitThroughGap(Gap gap, float minWidth) => gap.IsHorizontal ? gap.RectHeight > minWidth : gap.RectWidth > minWidth;
}
}
@@ -57,7 +57,10 @@ namespace Barotrauma
public void SteeringManual(float deltaTime, Vector2 velocity)
{
steering += velocity;
if (MathUtils.IsValid(velocity))
{
steering += velocity;
}
}
public void Reset()
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
@@ -24,16 +25,47 @@ namespace Barotrauma
if (Unreachable) { return float.PositiveInfinity; }
if (!totalLength.HasValue)
{
totalLength = 0.0f;
for (int i = 0; i < nodes.Count - 1; i++)
{
totalLength += Vector2.Distance(nodes[i].WorldPosition, nodes[i + 1].WorldPosition);
}
CalculateTotalLength();
}
return totalLength.Value;
}
}
public float GetLength(int? startIndex = null, int? endIndex = null)
{
if (Unreachable) { return float.PositiveInfinity; }
startIndex ??= 0;
endIndex ??= Nodes.Count - 1;
if (startIndex == 0 && endIndex == Nodes.Count - 1)
{
return TotalLength;
}
if (!totalLength.HasValue)
{
CalculateTotalLength();
}
float length = 0.0f;
for (int i = startIndex.Value; i < endIndex.Value; i++)
{
length += nodeDistances[i];
}
return length;
}
private void CalculateTotalLength()
{
totalLength = 0.0f;
nodeDistances.Clear();
for (int i = 0; i < nodes.Count - 1; i++)
{
float distance = Vector2.Distance(nodes[i].WorldPosition, nodes[i + 1].WorldPosition);
totalLength += distance;
nodeDistances.Add(distance);
}
}
private readonly List<float> nodeDistances = new List<float>();
public SteeringPath(bool unreachable = false)
{
nodes = new List<WayPoint>();
@@ -107,6 +139,11 @@ namespace Barotrauma
currentIndex++;
}
public void SkipToNode(int nodeIndex)
{
currentIndex = nodeIndex;
}
public WayPoint CheckProgress(Vector2 simPosition, float minSimDistance = 0.1f)
{
if (nodes.Count == 0 || currentIndex > nodes.Count - 1) { return null; }
@@ -161,8 +161,8 @@ namespace Barotrauma
for (int i = 0; i < container.Inventory.Capacity; i++)
{
if (container.Inventory.GetItemAt(i) != null) { continue; }
if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab i && container.CanBeContained(i) &&
Config.ForbiddenAmmunition.None(id => id.Equals(i.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab)
if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) &&
Config.ForbiddenAmmunition.None(id => id.Equals(ip.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab)
{
Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck);
if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false))