Release v0.15.12.0
This commit is contained in:
@@ -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
|
||||
|
||||
+7
-6
@@ -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>())
|
||||
{
|
||||
|
||||
+75
-14
@@ -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)
|
||||
|
||||
+6
-2
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
+3
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+41
-19
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-26
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+70
-33
@@ -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);
|
||||
|
||||
|
||||
+11
-23
@@ -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)
|
||||
|
||||
+32
-8
@@ -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>
|
||||
|
||||
+1
-1
@@ -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)
|
||||
{
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+273
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user