This commit is contained in:
EvilFactory
2023-06-15 12:13:50 -03:00
210 changed files with 4491 additions and 2580 deletions
@@ -95,10 +95,7 @@ namespace Barotrauma
{
get
{
if (visibleHulls == null)
{
visibleHulls = Character.GetVisibleHulls();
}
visibleHulls ??= Character.GetVisibleHulls();
return visibleHulls;
}
private set
@@ -425,14 +422,9 @@ namespace Barotrauma
var door = gap.ConnectedDoor;
if (door != null)
{
if (!door.CanBeTraversed)
if (!pathSteering.CanAccessDoor(door))
{
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;
}
continue;
}
}
else
@@ -473,7 +465,7 @@ namespace Barotrauma
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)
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
Character.ReleaseSecondaryItem();
@@ -369,7 +369,13 @@ namespace Barotrauma
}
else if (targetCharacter.AIController is EnemyAIController enemy)
{
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
if (enemy.PetBehavior != null && (PetBehavior != null || AIParams.HasTag("pet")))
{
// Pets see other pets as pets by default.
// Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below.
targetingTag = "pet";
}
else if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
{
targetingTag = "husk";
}
@@ -695,6 +701,9 @@ namespace Barotrauma
// Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group.
if (Character.IsSameSpeciesOrGroup(c)) { return false; }
if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; }
//don't try to attack targets in a sub that belongs to a different team
//(for example, targets in an outpost if we're in the main sub)
if (c.Submarine?.TeamID != Character.Submarine?.TeamID) { return false; }
if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
{
return a.Damage >= selectedTargetingParams.Threshold;
@@ -894,7 +903,7 @@ namespace Barotrauma
_previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
{
// Keep heading to the last known position of the target
var memory = GetTargetMemory(target, false);
var memory = GetTargetMemory(target);
if (memory != null)
{
var location = memory.Location;
@@ -981,7 +990,7 @@ namespace Barotrauma
}
else
{
PathSteering.SetPath(path);
PathSteering.SetPath(patrolTarget.SimPosition, path);
patrolTimerMargin = 0;
newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f);
searchingNewHull = false;
@@ -1088,13 +1097,13 @@ namespace Barotrauma
Character owner = GetOwner(item);
if (owner != null)
{
if (Character.IsFriendly(owner))
if (Character.IsFriendly(owner) || owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{
ResetAITarget();
State = AIState.Idle;
return;
}
else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
else
{
SelectedAiTarget = owner.AiTarget;
}
@@ -2186,7 +2195,7 @@ namespace Barotrauma
}
}
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true);
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true, keepAlive: true);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt;
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
@@ -2531,8 +2540,10 @@ namespace Barotrauma
if (Math.Abs(limbDiff.X) < itemBodyExtent &&
Math.Abs(limbDiff.Y) < Character.AnimController.Collider.GetMaxExtent() + Character.AnimController.ColliderHeightFromFloor)
{
Vector2 velocity = limbDiff;
if (limbDiff.LengthSquared() > 0.01f) { velocity = Vector2.Normalize(velocity); }
item.body.LinearVelocity *= 0.9f;
item.body.LinearVelocity -= limbDiff * 0.25f;
item.body.LinearVelocity -= velocity * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
@@ -2924,7 +2935,8 @@ namespace Barotrauma
}
}
}
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0)
//no need to eat if the character is already in full health (except if it's a pet - pets actually need to eat to stay alive, not just to regain health)
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0 && !Character.IsPet)
{
valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f);
}
@@ -3021,7 +3033,7 @@ namespace Barotrauma
//if the target is very close, the distance doesn't make much difference
// -> just ignore the distance and target whatever has the highest priority
dist = Math.Max(dist, 100.0f);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true, keepAlive: SelectedAiTarget != aiTarget);
if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null)
{
float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y;
@@ -3090,12 +3102,20 @@ namespace Barotrauma
if (aiTarget.Entity is Item i)
{
Character owner = GetOwner(i);
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (owner == Character) { continue; }
if (owner != null && (Character.IsFriendly(owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)))
if (owner != null)
{
continue;
if (owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)) { continue; }
if (Character.IsFriendly(owner))
{
// Don't target items that we own. This is a rare case, and almost entirely related to Humanhusks (in the vanilla game).
continue;
}
if (owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{
// ignore if owner is tagged to be explicitly ignored (Feign Death)
continue;
}
}
}
if (targetCharacter != null)
@@ -3418,7 +3438,7 @@ namespace Barotrauma
return false;
}
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound)
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound = false, bool keepAlive = false)
{
if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
{
@@ -3428,9 +3448,8 @@ namespace Barotrauma
targetMemories.Add(target, memory);
}
}
if (addIfNotFound)
if (keepAlive)
{
// Keep the memory alive.
memory.Priority = Math.Max(memory.Priority, minPriority);
}
return memory;
@@ -3446,7 +3465,7 @@ namespace Barotrauma
}
else if (CanPerceive(_selectedAiTarget, checkVisibility: false))
{
var memory = GetTargetMemory(_selectedAiTarget, false);
var memory = GetTargetMemory(_selectedAiTarget);
if (memory != null)
{
memory.Location = _selectedAiTarget.WorldPosition;
@@ -3504,10 +3523,10 @@ namespace Barotrauma
private readonly float stateResetCooldown = 10;
private float stateResetTimer;
private bool isStateChanged;
private readonly Dictionary<AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<AITrigger> inactiveTriggers = new HashSet<AITrigger>();
private readonly Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<StatusEffect.AITrigger> inactiveTriggers = new HashSet<StatusEffect.AITrigger>();
public void LaunchTrigger(AITrigger trigger)
public void LaunchTrigger(StatusEffect.AITrigger trigger)
{
if (trigger.IsTriggered) { return; }
if (activeTriggers.ContainsKey(trigger)) { return; }
@@ -3527,7 +3546,7 @@ namespace Barotrauma
{
foreach (var triggerObject in activeTriggers)
{
AITrigger trigger = triggerObject.Key;
StatusEffect.AITrigger trigger = triggerObject.Key;
if (trigger.IsPermanent) { continue; }
trigger.UpdateTimer(deltaTime);
if (!trigger.IsActive)
@@ -3537,7 +3556,7 @@ namespace Barotrauma
inactiveTriggers.Add(trigger);
}
}
foreach (AITrigger trigger in inactiveTriggers)
foreach (StatusEffect.AITrigger trigger in inactiveTriggers)
{
activeTriggers.Remove(trigger);
}
@@ -3643,7 +3662,11 @@ namespace Barotrauma
{
isStateChanged = true;
SetStateResetTimer();
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
if (!Character.IsPet || !target.IsHuman)
{
//don't turn pets hostile to all humans when attacked by one
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
}
if (target.IsHuman)
{
priority = GetTargetParams("human")?.Priority;
@@ -42,6 +42,8 @@ namespace Barotrauma
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>();
private readonly HashSet<Hull> dirtyHullSafetyCalculations = new HashSet<Hull>();
private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f;
private bool wasConscious;
@@ -436,6 +438,7 @@ namespace Barotrauma
foreach (Hull h in VisibleHulls)
{
PropagateHullSafety(Character, h);
dirtyHullSafetyCalculations.Remove(h);
}
}
else
@@ -443,9 +446,15 @@ namespace Barotrauma
foreach (Hull h in VisibleHulls)
{
RefreshHullSafety(h);
dirtyHullSafetyCalculations.Remove(h);
}
}
foreach (Hull h in dirtyHullSafetyCalculations)
{
RefreshHullSafety(h);
}
}
dirtyHullSafetyCalculations.Clear();
if (reportProblemsTimer <= 0.0f)
{
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
@@ -615,7 +624,7 @@ namespace Barotrauma
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
Character.CurrentHull.IsWetRoom;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo { IsWaitOrder: true };
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
{
@@ -900,7 +909,7 @@ namespace Barotrauma
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
var rootContainer = container.Item.GetRootContainer() ?? container.Item;
var rootContainer = container.Item.RootContainer ?? container.Item;
if (rootContainer.GetComponent<Fabricator>() != null || rootContainer.GetComponent<Deconstructor>() != null) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
@@ -1145,7 +1154,7 @@ namespace Barotrauma
string msgId = "DialogLowOxygen";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
if (Character.Bleeding > 2.0f && !Character.IsMedic)
if (Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !Character.IsMedic)
{
string msgId = "DialogBleeding";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
@@ -1658,7 +1667,7 @@ namespace Barotrauma
/// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes));
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes));
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
@@ -1891,7 +1900,7 @@ namespace Barotrauma
private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
/// <summary>
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threads.
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threats.
/// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders.
/// There's now a cached value per each hull, which should prevent too frequent calculations.
/// </summary>
@@ -1900,9 +1909,13 @@ namespace Barotrauma
DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
}
public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull);
private void RefreshHullSafety(Hull hull)
{
if (GetHullSafety(hull, Character, VisibleHulls) > HULL_SAFETY_THRESHOLD)
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : VisibleHulls;
float hullSafety = GetHullSafety(hull, Character, visibleHulls);
if (hullSafety > HULL_SAFETY_THRESHOLD)
{
UnsafeHulls.Remove(hull);
}
@@ -22,7 +22,10 @@ namespace Barotrauma
private readonly Character character;
private Vector2 currentTarget;
/// <summary>
/// In sim units.
/// </summary>
private Vector2 currentTargetPos;
private float findPathTimer;
@@ -40,11 +43,6 @@ namespace Barotrauma
get { return pathFinder; }
}
public Vector2 CurrentTarget
{
get { return currentTarget; }
}
public bool IsPathDirty
{
get;
@@ -54,9 +52,9 @@ namespace Barotrauma
/// <summary>
/// Returns true if any node in the path is in stairs
/// </summary>
public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character);
public bool IsCurrentNodeLadder => GetCurrentLadder() != null;
public bool IsNextNodeLadder => GetNextLadder() != null;
@@ -64,14 +62,9 @@ namespace Barotrauma
{
get
{
if (currentPath == null) { return false; }
if (currentPath.CurrentNode == null) { return false; }
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
var currentLadder = GetCurrentLadder();
if (currentLadder == null) { return false; }
if (!currentLadder.Item.IsInteractable(character)) { return false; }
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
return currentLadder == GetNextLadder();
}
}
@@ -107,13 +100,10 @@ namespace Barotrauma
findPathTimer -= step;
}
public void SetPath(SteeringPath path)
public void SetPath(Vector2 targetPos, SteeringPath path)
{
currentTargetPos = targetPos;
currentPath = path;
if (path.Nodes.Any())
{
currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
}
findPathTimer = Math.Min(findPathTimer, 1.0f);
IsPathDirty = false;
}
@@ -136,46 +126,17 @@ namespace Barotrauma
steering += addition;
}
/// <summary>
/// Seeks the ladder from the next and next + 1 nodes.
/// </summary>
public Ladder GetNextLadder()
{
if (currentPath == null) { return null; }
if (currentPath.NextNode == null) { return null; }
if (currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character))
{
return currentPath.NextNode.Ladders;
}
else
{
int index = currentPath.CurrentIndex + 2;
if (currentPath.Nodes.Count > index)
{
var node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
{
return node.Ladders;
}
//if the next node is a hatch, check if the node after that is a ladder
else if (node.ConnectedDoor != null && node.ConnectedDoor.IsHorizontal)
{
index++;
if (currentPath.Nodes.Count > index)
{
node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
{
return node.Ladders;
}
}
}
public Ladder GetCurrentLadder() => GetLadder(currentPath?.CurrentNode);
}
return null;
public Ladder GetNextLadder() => GetLadder(currentPath?.NextNode);
private Ladder GetLadder(WayPoint wp)
{
if (wp?.Ladders?.Item is Item item && item.IsInteractable(character))
{
return wp.Ladders;
}
return null;
}
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)
@@ -183,19 +144,10 @@ namespace Barotrauma
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null;
if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
{
Vector2 targetDiff = target - currentTarget;
if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null)
{
//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 the target has moved, we need a new path.
// Different subs are already taken into account before setting the target.
// Triggers when either the target or we have changed subs, but only once (until the new path has been accepted).
Vector2 targetDiff = target - currentTargetPos;
if (targetDiff.LengthSquared() > 1)
{
needsNewPath = true;
@@ -205,14 +157,14 @@ namespace Barotrauma
if (needsNewPath || findPathTimer < -1.0f)
{
IsPathDirty = true;
if (!needsNewPath && findPathTimer < -1)
if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp)
{
if (character.Submarine != null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
if (character.Submarine != null && wp.Ladders == null && wp.ConnectedDoor == null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
{
// Not moving -> need a new path.
needsNewPath = true;
}
if (character.Submarine == null && currentPath?.CurrentNode is WayPoint wp && wp.CurrentHull != null)
if (character.Submarine == null && wp.CurrentHull != null)
{
// Current node inside, while we are outside
// -> Check that the current node is not too far (can happen e.g. if someone controls the character in the meanwhile)
@@ -226,7 +178,7 @@ namespace Barotrauma
if (findPathTimer < 0)
{
SkipCurrentPathNodes();
currentTarget = target;
currentTargetPos = target;
Vector2 currentPos = host.SimPosition;
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
@@ -252,6 +204,14 @@ namespace Barotrauma
useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
}
if (!useNewPath && !character.CanSeeTarget(currentPath.CurrentNode))
{
// If we are set to disregard the new path, ensure that we can actually see the current node of the old path,
// because it's possible that there's e.g. a closed door between us and the current node,
// and in that case we'd want to use the new path instead of the old.
// There's visibility checks in the pathfinder calls, so the new path should always be ok.
useNewPath = true;
}
bool IsIdenticalPath()
{
@@ -330,6 +290,7 @@ namespace Barotrauma
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius)
{
// TODO: might cause some edge cases -> do we need this?
diff.Y = 0.0f;
}
if (diff == Vector2.Zero) { return Vector2.Zero; }
@@ -346,12 +307,12 @@ namespace Barotrauma
}
if (currentPath.Finished)
{
Vector2 pos2 = host.SimPosition;
Vector2 hostPosition = host.SimPosition;
if (character != null && character.Submarine == null && CurrentPath.Nodes.Count > 0 && CurrentPath.Nodes.Last().Submarine != null)
{
pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition;
hostPosition -= CurrentPath.Nodes.Last().Submarine.SimPosition;
}
return currentTarget - pos2;
return currentTargetPos - hostPosition;
}
bool doorsChecked = false;
checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime());
@@ -371,14 +332,46 @@ namespace Barotrauma
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
Ladder currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder != null && !currentLadder.Item.IsInteractable(character))
{
currentLadder = null;
}
Ladder currentLadder = GetCurrentLadder();
Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder;
bool useLadders = canClimb && ladders != null && steering.LengthSquared() > 0.1f && (!isDiving || steering.Y > 1);
bool useLadders = canClimb && ladders != null;
var collider = character.AnimController.Collider;
Vector2 colliderSize = collider.GetSize();
if (useLadders)
{
if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
{
// If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders.
useLadders = false;
}
else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null)
{
Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos;
if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
{
// If the next node is horizontally farther from us than vertically, we don't want to start climbing.
useLadders = false;
}
}
else if (isDiving && steering.Y < 1)
{
// When diving, only use ladders to get upwards (towards the surface), otherwise we can just ignore them.
useLadders = false;
}
}
if (character.IsClimbing && !useLadders)
{
if (currentPath.IsAtEndNode && canClimb && ladders != null)
{
// Don't release the ladders when ending a path in ladders.
useLadders = true;
}
else
{
character.StopClimbing();
}
}
if (useLadders && character.SelectedSecondaryItem != ladders.Item)
{
if (character.CanInteractWith(ladders.Item))
@@ -398,40 +391,28 @@ namespace Barotrauma
}
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !useLadders)
{
character.StopClimbing();
}
if (character.IsClimbing && useLadders)
{
if (currentLadder == null && nextLadder != null)
if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
{
// Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
NextNode(!doorsChecked);
}
else
{
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
bool nextLadderSameAsCurrent = currentLadder == nextLadder;
if (currentLadder != null && nextLadder != null)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
//at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f;
if (heightDiff < colliderSize)
float colliderHeight = collider.Height / 2 + collider.Radius;
float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
if (heightDiff < colliderHeight * 1.25f)
{
float heightFromFloor = character.AnimController.GetHeightFromFloor();
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
{
character.StopClimbing();
}
else if (nextLadder != null && !nextLadderSameAsCurrent)
if (nextLadder != null && !nextLadderSameAsCurrent)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
@@ -442,12 +423,36 @@ namespace Barotrauma
}
}
}
if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10)
bool isAboveFloor;
if (diff.Y < 0)
{
NextNode(!doorsChecked);
// When climbing down, let's use the collider bottom to prevent getting stuck at the bottom of the ladders.
float colliderBottom = character.AnimController.Collider.SimPosition.Y;
float floorY = character.AnimController.FloorY;
isAboveFloor = colliderBottom > floorY;
}
else
{
// When climbing up, let's use the lowest collider (feet).
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative,
// when a foot is still below the platform.
float heightFromFloor = character.AnimController.GetHeightFromFloor();
isAboveFloor = heightFromFloor > -0.1f;
}
if (isAboveFloor)
{
if (Math.Abs(diff.Y) < distanceMargin)
{
NextNode(!doorsChecked);
}
else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin)))
{
// Can't skip the node -> Release the ladders, because the next node is not on a ladder or it's horizontally too far.
character.StopClimbing();
}
}
}
else if (nextLadder != null)
else if (currentLadder != null && currentPath.NextNode != null)
{
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
@@ -466,7 +471,6 @@ namespace Barotrauma
if (door == null || door.CanBeTraversed)
{
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
Vector2 colliderSize = collider.GetSize();
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
@@ -485,7 +489,6 @@ namespace Barotrauma
{
// Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
Vector2 velocity = collider.LinearVelocity;
// If the character is very short, it would fail to use the waypoint nodes because they are always too high.
// If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
@@ -512,9 +515,12 @@ namespace Barotrauma
}
}
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && currentLadder == null && (door == null || door.CanBeTraversed))
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow)
{
NextNode(!doorsChecked);
if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null))
{
NextNode(!doorsChecked);
}
}
}
if (currentPath.CurrentNode == null)
@@ -533,9 +539,9 @@ namespace Barotrauma
currentPath.SkipToNextNode();
}
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
public bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsBroken) { return true; }
if (door.CanBeTraversed) { return true; }
if (door.IsClosed)
{
if (!door.Item.IsInteractable(character)) { return false; }
@@ -631,10 +637,12 @@ 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)
if (currentWaypoint.ConnectedDoor.LinkedGap is Gap linkedGap)
{
// Keep the airlock doors closed, but not in ruins/wrecks
if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false } || currentWaypoint.Submarine == null || currentWaypoint.Submarine.Info.IsRuin || currentWaypoint.Submarine.Info.IsWreck)
if (currentWaypoint.Submarine == null ||
currentWaypoint.Submarine.Info is { IsPlayer: false } ||
!linkedGap.IsRoomToRoom ||
(linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false }))
{
shouldBeOpen = true;
door = currentWaypoint.ConnectedDoor;
@@ -213,7 +213,7 @@ namespace Barotrauma
{
foreach (Voronoi2.GraphEdge edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
{
Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos);
@@ -506,6 +506,8 @@ namespace Barotrauma
}
}
public virtual void SpeakAfterOrderReceived() { }
protected static bool CanEquip(Character character, Item item, bool allowWearing)
{
if (item == null) { return false; }
@@ -14,6 +14,10 @@ namespace Barotrauma
public readonly List<Item> prioritizedItems = new List<Item>();
public static readonly Identifier AllowCleanupTag = "allowcleanup".ToIdentifier();
protected override int MaxTargets => 100;
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -81,8 +85,8 @@ namespace Barotrauma
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
allowUnloading &&
container.HasTag(AllowCleanupTag) &&
container.HasAccess(character) &&
container.HasTag("allowcleanup") &&
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
container.GetComponent<ItemContainer>() != null &&
IsItemInsideValidSubmarine(container, character) &&
@@ -91,7 +95,6 @@ namespace Barotrauma
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
{
if (item == null) { return false; }
if (!item.HasAccess(character)) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
@@ -102,6 +105,7 @@ namespace Barotrauma
}
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
}
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
var wire = item.GetComponent<Wire>();
@@ -995,10 +995,18 @@ namespace Barotrauma
}
}
}
if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy))
//prefer using handcuffs already on the enemy's inventory
if (!HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems))
{
HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out matchingItems);
}
if (matchingItems.Any() &&
!Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
{
var handCuffs = matchingItems.First();
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true))
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
@@ -198,7 +198,7 @@ namespace Barotrauma
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) ||
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
(container.Item.RootContainer?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
@@ -30,7 +30,8 @@ namespace Barotrauma
public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier();
public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier();
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
protected override bool CheckObjectiveSpecific() =>
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
@@ -51,7 +52,7 @@ namespace Barotrauma
TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
}
if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) &&
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
{
TryAddSubObjective(ref getDivingGear, () =>
@@ -65,7 +66,7 @@ namespace Barotrauma
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes,
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
Wear = true
};
},
@@ -406,7 +406,7 @@ namespace Barotrauma
if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
{
// Don't allow to go outside if not already outside.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, potentialHull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
hullSafety = 0;
@@ -376,7 +376,7 @@ namespace Barotrauma
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective);
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrder);
}
bool checkPath = CheckPathForEachItem;
// Reset if the character has switched subs.
@@ -536,7 +536,7 @@ namespace Barotrauma
{
if (itemCandidates.FirstOrDefault() is { } itemCandidate)
{
var path = PathSteering.PathFinder.FindPath(character.SimPosition, itemCandidate.item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
// Remove the invalid candidates and continue on the next frame.
@@ -26,7 +26,8 @@ namespace Barotrauma
public Func<float> PriorityGetter;
public bool IsFollowOrderObjective;
public bool IsFollowOrder;
public bool IsWaitOrder;
public bool Mimic;
public bool SpeakIfFails { get; set; } = true;
@@ -59,7 +60,7 @@ namespace Barotrauma
{
get
{
if (IsFollowOrderObjective && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
if (IsFollowOrder && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
{
// Keep close when the target is going inside/outside
return minDistance;
@@ -220,15 +221,45 @@ namespace Barotrauma
}
}
Hull targetHull = GetTargetHull();
if (!IsFollowOrderObjective)
if (!IsFollowOrder)
{
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
&& PathSteering != null && PathSteering.CurrentPath != null
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull);
if (!objectiveManager.CurrentObjective.IgnoreUnsafeHulls)
{
Abandon = true;
if (HumanAIController.UnsafeHulls.Contains(targetHull))
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(targetHull);
}
else if (PathSteering?.CurrentPath != null)
{
foreach (WayPoint wp in PathSteering.CurrentPath.Nodes)
{
if (wp.CurrentHull == null) { continue; }
if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull))
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull);
}
}
}
}
if (isUnreachable)
{
SteeringManager.Reset();
if (PathSteering?.CurrentPath != null)
{
PathSteering.CurrentPath.Unreachable = true;
}
if (repeat)
{
SpeakCannotReach();
}
else
{
Abandon = true;
}
return;
}
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
@@ -250,6 +281,7 @@ namespace Barotrauma
if (repeat)
{
SpeakCannotReach();
return;
}
else
{
@@ -262,310 +294,285 @@ namespace Barotrauma
waitUntilPathUnreachable = pathWaitingTime;
}
}
if (!Abandon)
if (Abandon) { return; }
if (getDivingGearIfNeeded)
{
if (getDivingGearIfNeeded)
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
if (Mimic && !character.IsImmuneToPressure)
{
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
if (Mimic && !character.IsImmuneToPressure)
if (HumanAIController.HasDivingSuit(followTarget))
{
if (HumanAIController.HasDivingSuit(followTarget))
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
tryToGetDivingGear = true;
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
tryToGetDivingGear = true;
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
if (repeat)
{
if (IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
if (needsDivingSuit)
{
if (character.CanSeeTarget(Target))
{
OnCompleted();
return;
}
}
}
}
float maxGapDistance = 500;
Character targetCharacter = Target as Character;
if (character.AnimController.InWater)
{
if (character.CurrentHull == null ||
IsFollowOrderObjective &&
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
{
if (seekGapsTimer > 0)
{
seekGapsTimer -= deltaTime;
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
bool isEitherOneInside = isInside || Target.Submarine != null;
if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath()))
{
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (TargetGap != null)
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
// 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;
}
}
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
else
{
TargetGap = null;
}
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
if (repeat && IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
{
if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder))
{
OnCompleted();
return;
}
}
}
float maxGapDistance = 500;
Character targetCharacter = Target as Character;
if (character.AnimController.InWater)
{
if (character.CurrentHull == null ||
IsFollowOrder &&
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
{
if (seekGapsTimer > 0)
{
seekGapsTimer -= deltaTime;
}
else
{
TargetGap = null;
}
if (TargetGap != null)
{
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
bool isEitherOneInside = isInside || Target.Submarine != null;
if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath()))
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
return;
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (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;
}
}
}
else
{
TargetGap = null;
}
}
if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
Identifier scooterTag = "scooter".ToIdentifier();
Identifier batteryTag = "mobilebattery".ToIdentifier();
Item scooter = null;
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
if (!shouldUseScooter)
{
float threshold = 500;
if (isInside)
{
Vector2 diff = Target.WorldPosition - character.WorldPosition;
shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150;
}
else
{
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
}
}
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
scooter = equippedScooters.FirstOrDefault();
}
else if (shouldUseScooter)
{
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
if (!handsFull)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
{
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
// Equip only if we have a battery available
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
}
if (scooter != null && character.HasEquippedItem(scooter))
{
if (shouldUseScooter)
{
useScooter = true;
// Check the battery
if (scooter.ContainedItems.None(i => i.Condition > 0))
{
// Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
{
useScooter = false;
}
}
else
{
useScooter = false;
}
}
}
if (!useScooter)
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
}
}
}
else
{
checkScooterTimer -= deltaTime;
}
}
else
{
TargetGap = null;
useScooter = false;
checkScooterTimer = 0;
}
if (SteeringManager == PathSteering)
if (TargetGap != null)
{
Vector2 targetPos = character.GetRelativeSimPosition(Target);
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrder ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
{
nodeFilter = n => n.Waypoint.CurrentHull != null;
}
else if (!isInside)
{
if (HumanAIController.UseOutsideWaypoints)
{
nodeFilter = n => n.Waypoint.Submarine == null;
}
else
{
nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null;
}
}
if (!isInside && !UsePathingOutside)
{
character.ReleaseSecondaryItem();
PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
}
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
return;
}
else
{
PathSteering.SteeringSeek(targetPos, weight: 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter: endNodeFilter,
nodeFilter: nodeFilter,
checkVisiblity: Target is Item || Target is Character);
TargetGap = null;
}
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
}
if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
Identifier scooterTag = "scooter".ToIdentifier();
Identifier batteryTag = "mobilebattery".ToIdentifier();
Item scooter = null;
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
if (!shouldUseScooter)
{
if (useScooter)
float threshold = 500;
if (isInside)
{
UseScooter(Target.WorldPosition);
Vector2 diff = Target.WorldPosition - character.WorldPosition;
shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150;
}
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
}
}
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
scooter = equippedScooters.FirstOrDefault();
}
else if (shouldUseScooter)
{
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
if (!handsFull)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
// Equip only if we have a battery available
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
}
else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
if (scooter != null && character.HasEquippedItem(scooter))
{
UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition);
if (shouldUseScooter)
{
useScooter = true;
// Check the battery
if (scooter.ContainedItems.None(i => i.Condition > 0))
{
// Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
{
useScooter = false;
}
}
else
{
useScooter = false;
}
}
}
if (!useScooter)
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
}
}
}
else
{
checkScooterTimer -= deltaTime;
}
}
else
{
TargetGap = null;
useScooter = false;
checkScooterTimer = 0;
}
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)
{
if (HumanAIController.UseOutsideWaypoints)
{
nodeFilter = n => n.Waypoint.Submarine == null;
}
else
{
nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null;
}
}
if (!isInside && !UsePathingOutside)
{
character.ReleaseSecondaryItem();
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: Target is Item || Target is Character);
}
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
if (useScooter)
{
@@ -574,13 +581,33 @@ namespace Barotrauma
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
}
}
}
else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
{
UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition);
}
}
else
{
if (useScooter)
{
UseScooter(Target.WorldPosition);
}
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
}
}
}
void UseScooter(Vector2 targetWorldPos)
@@ -595,7 +622,7 @@ namespace Barotrauma
}
Vector2 diff = character.CursorPosition - character.Position;
Vector2 dir = Vector2.Normalize(diff);
if (character.CurrentHull == null && IsFollowOrderObjective)
if (character.CurrentHull == null && IsFollowOrder)
{
float sqrDist = diff.LengthSquared();
if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f))
@@ -674,7 +701,7 @@ namespace Barotrauma
{
if (gap.Open < 1) { continue; }
if (gap.Submarine == null) { continue; }
if (!IsFollowOrderObjective)
if (!IsFollowOrder)
{
if (gap.FlowTargetHull == null) { continue; }
if (gap.Submarine != Target.Submarine) { continue; }
@@ -792,8 +819,10 @@ namespace Barotrauma
// Release ladders when ordered to wait at a spawnpoint.
// This is a special case specifically meant for NPCs that spawn in outposts with a wait order.
// Otherwise they might keep holding to the ladders when the target is just next to it.
// Releasing too early should be handled inside the IsCloseEnough property.
character.ReleaseSecondaryItem();
if (character.IsClimbing && character.AnimController.IsAboveFloor)
{
character.StopClimbing();
}
}
base.OnCompleted();
}
@@ -168,7 +168,7 @@ namespace Barotrauma
CleanupItems(deltaTime);
}
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null && !IsForbidden(character.CurrentHull))
{
TargetHull = character.CurrentHull;
}
@@ -178,7 +178,7 @@ namespace Barotrauma
IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
{
currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
@@ -258,7 +258,8 @@ 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, character.Submarine, nodeFilter: node =>
Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe hulls on the way to the target
@@ -278,7 +279,7 @@ namespace Barotrauma
return;
}
character.AIController.SelectTarget(currentTarget.AiTarget);
PathSteering.SetPath(path);
PathSteering.SetPath(targetPos, path);
SetTargetTimerNormal();
searchingNewHull = false;
}
@@ -98,7 +98,7 @@ namespace Barotrauma
{
foreach (var item in itemContainer.ContainableItems)
{
if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished)
if (CheckStatusEffects(item.StatusEffects) == CheckStatus.Finished)
{
return CheckStatus.Finished;
}
@@ -48,6 +48,8 @@ namespace Barotrauma
protected virtual bool ResetWhenClearingIgnoreList => true;
protected virtual bool ForceOrderPriority => true;
protected virtual int MaxTargets => int.MaxValue;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
public override void Update(float deltaTime)
@@ -188,6 +190,10 @@ namespace Barotrauma
if (!ignoreList.Contains(target))
{
Targets.Add(target);
if (Targets.Count > MaxTargets)
{
break;
}
}
}
}
@@ -436,7 +436,7 @@ namespace Barotrauma
ExtraDistanceWhileSwimming = 100,
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
IsFollowOrderObjective = true,
IsFollowOrder = true,
Mimic = character.IsOnPlayerTeam,
DialogueIdentifier = "dialogcannotreachplace".ToIdentifier()
};
@@ -444,7 +444,11 @@ namespace Barotrauma
case "wait":
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = true
AllowGoingOutside = true,
IsWaitOrder = true,
DebugLogWhenFails = false,
SpeakIfFails = false,
CloseEnough = 100
};
break;
case "return":
@@ -13,6 +13,8 @@ namespace Barotrauma
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
private readonly HashSet<Character> charactersWithMinorInjuries = new HashSet<Character>();
private const float vitalityThreshold = 75;
private const float vitalityThresholdForOrders = 90;
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
@@ -23,17 +25,34 @@ namespace Barotrauma
}
else
{
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? vitalityThresholdForOrders : vitalityThreshold;
}
}
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override bool Filter(Character target)
{
if (!IsValidTarget(target, character, requireTreatableAfflictions: false)) { return false; }
if (GetTreatableAfflictions(target).Any())
{
return true;
}
else
{
//the target might be at a low enough health to be considered a valid target,
//but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them
// -> make the bot speak to make it clear the bot intentionally ignores very minor injuries
if (!charactersWithMinorInjuries.Contains(character))
{
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
null, 1.0f, $"notreatableafflictions{target.Name}".ToIdentifier(), 10.0f);
charactersWithMinorInjuries.Add(character);
}
return false;
}
}
protected override IEnumerable<Character> GetList() => Character.CharacterList;
@@ -68,7 +87,7 @@ namespace Barotrauma
float vitality = 100;
vitality -= character.Bleeding * 2;
vitality += Math.Min(character.Oxygen, 0);
foreach (Affliction affliction in GetTreatableAfflictions(character))
foreach (Affliction affliction in GetTreatableAfflictions(character, ignoreTreatmentThreshold: true))
{
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
@@ -84,12 +103,19 @@ namespace Barotrauma
return Math.Clamp(vitality, 0, 100);
}
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold = false)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (affliction.Prefab.IsBuff) { continue; }
if (!ignoreTreatmentThreshold)
{
//other afflictions of the same type increase the "treatability"
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
float totalAfflictionStrength = character.CharacterHealth.GetTotalAdjustedAfflictionStrength(affliction);
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
}
if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
@@ -102,7 +128,7 @@ namespace Barotrauma
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
public static bool IsValidTarget(Character target, Character character)
public static bool IsValidTarget(Character target, Character character, bool requireTreatableAfflictions = true)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target.IsInstigator) { return false; }
@@ -112,7 +138,7 @@ namespace Barotrauma
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target))
{
return false;
return false;
}
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
{
@@ -127,6 +153,10 @@ namespace Barotrauma
return false;
}
}
if (requireTreatableAfflictions && GetTreatableAfflictions(target).None())
{
return false;
}
}
else
{
@@ -159,5 +189,11 @@ namespace Barotrauma
}
return character.GetDamageDoneByAttacker(target) <= 0;
}
public override void Reset()
{
base.Reset();
charactersWithMinorInjuries.Clear();
}
}
}
@@ -51,7 +51,7 @@ namespace Barotrauma
return;
}
if (!IsRemotePlayer && !(AIController is HumanAIController))
if (!IsRemotePlayer && AIController is not HumanAIController)
{
float characterDistSqr = GetDistanceSqrToClosestPlayer();
if (characterDistSqr > MathUtils.Pow2(Params.DisableDistance * 0.5f))
@@ -63,6 +63,10 @@ namespace Barotrauma
AnimController.SimplePhysicsEnabled = false;
}
}
else
{
AnimController.SimplePhysicsEnabled = false;
}
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (Controlled == this) { return; }
@@ -1,10 +1,10 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -1550,6 +1550,10 @@ namespace Barotrauma
target.AnimController.ResetPullJoints();
}
bool targetPoseControlled =
target.SelectedItem?.GetComponent<Controller>() is { ControlCharacterPose: true } ||
target.SelectedSecondaryItem?.GetComponent<Controller>() is { ControlCharacterPose: true };
if (IsClimbing)
{
//cannot drag up ladders if the character is conscious
@@ -1725,13 +1729,12 @@ namespace Barotrauma
targetForce = 5000.0f;
}
targetLimb.PullJointEnabled = true;
targetLimb.PullJointMaxForce = targetForce;
targetLimb.PullJointWorldAnchorB = targetAnchor;
targetLimb.Disabled = true;
if (diff.LengthSquared() > 0.1f)
if (!targetPoseControlled)
{
targetLimb.PullJointEnabled = true;
targetLimb.PullJointMaxForce = targetForce;
targetLimb.PullJointWorldAnchorB = targetAnchor;
targetLimb.Disabled = true;
target.AnimController.movement = -diff;
}
}
@@ -1757,7 +1760,7 @@ namespace Barotrauma
target.AnimController.IgnorePlatforms = IgnorePlatforms;
target.AnimController.TargetMovement = TargetMovement;
}
else if (target is AICharacter && target != Character.Controlled)
else if (target is AICharacter && target != Character.Controlled && !targetPoseControlled)
{
if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X)
{
@@ -1205,7 +1205,7 @@ namespace Barotrauma
{
inWater = false;
headInWater = false;
RefreshFloorY(ignoreStairs: Stairs == null);
RefreshFloorY(deltaTime, ignoreStairs: Stairs == null);
}
//ragdoll isn't in any room -> it's in the water
else if (currentHull == null)
@@ -1217,7 +1217,7 @@ namespace Barotrauma
{
headInWater = false;
inWater = false;
RefreshFloorY(ignoreStairs: Stairs == null);
RefreshFloorY(deltaTime, ignoreStairs: Stairs == null);
if (currentHull.WaterPercentage > 0.001f)
{
(float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY();
@@ -1562,15 +1562,24 @@ namespace Barotrauma
lastFloorCheckPos = Vector2.Zero;
}
private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false)
// Force check floor y at least once a second so that we'll drop through gaps that we are standing upon.
private const float FloorYStaleTime = 1;
private float floorYCheckTimer;
private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false)
{
floorYCheckTimer -= deltaTime;
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
if (Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms)
if (floorYCheckTimer < 0 ||
lastFloorCheckIgnoreStairs != ignoreStairs ||
lastFloorCheckIgnorePlatforms != IgnorePlatforms ||
Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f)
{
floorY = GetFloorY(refBody.SimPosition, ignoreStairs);
lastFloorCheckPos = refBody.SimPosition;
lastFloorCheckIgnoreStairs = ignoreStairs;
lastFloorCheckIgnorePlatforms = IgnorePlatforms;
// Add some randomness to prevent all stationary characters doing the checks at the same frame.
floorYCheckTimer = FloorYStaleTime * Rand.Range(0.9f, 1.1f);
}
}
@@ -1854,6 +1863,7 @@ namespace Barotrauma
private bool collisionsDisabled;
private double lastObstacleRayCastTime;
protected void CheckDistFromCollider()
{
@@ -1861,15 +1871,28 @@ namespace Barotrauma
allowedDist = Math.Max(allowedDist, 1.0f);
float resetDist = allowedDist * 5.0f;
float obstacleCheckDist = 0.3f;
Vector2 diff = Collider.SimPosition - MainLimb.SimPosition;
float distSqrd = diff.LengthSquared();
if (distSqrd > resetDist * resetDist)
bool shouldReset = distSqrd > resetDist * resetDist;
if (!shouldReset && distSqrd > obstacleCheckDist * obstacleCheckDist)
{
if (Timing.TotalTime > lastObstacleRayCastTime + 1 &&
Submarine.PickBody(Collider.SimPosition, MainLimb.SimPosition, collisionCategory: Physics.CollisionWall) != null)
{
shouldReset = true;
lastObstacleRayCastTime = Timing.TotalTime;
}
}
if (shouldReset)
{
//ragdoll way too far, reset position
SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true);
}
if (distSqrd > allowedDist * allowedDist)
else if (distSqrd > allowedDist * allowedDist)
{
//ragdoll too far from the collider, disable collisions until it's close enough
//(in case the ragdoll has gotten stuck somewhere)
@@ -1891,7 +1914,7 @@ namespace Barotrauma
collisionsDisabled = false;
//force collision categories to be updated
prevCollisionCategory = Category.None;
}
}
}
partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos);
@@ -1068,7 +1068,7 @@ namespace Barotrauma
{
get
{
return SelectedItem == null || (SelectedItem.GetComponent<Controller>()?.AllowAiming ?? false);
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsIncapacitated && !IsRagdolled;
}
}
@@ -1131,6 +1131,7 @@ namespace Barotrauma
public HashSet<Identifier> MarkedAsLooted = new();
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public bool IsInPlayerSub => Submarine != null && Submarine.Info.IsPlayer;
public float AITurretPriority
{
@@ -2710,27 +2711,6 @@ namespace Barotrauma
CustomInteractHUDText = hudText;
}
private void TransformCursorPos()
{
if (Submarine == null)
{
//character is outside but cursor position inside
if (cursorPosition.Y > Level.Loaded.Size.Y)
{
var sub = Submarine.FindContaining(cursorPosition);
if (sub != null) cursorPosition += sub.Position;
}
}
else
{
//character is inside but cursor position is outside
if (cursorPosition.Y < Level.Loaded.Size.Y)
{
cursorPosition -= Submarine.Position;
}
}
}
public void SelectCharacter(Character character)
{
if (character == null || character == this) { return; }
@@ -4335,7 +4315,7 @@ namespace Barotrauma
if (statusEffect.type == ActionType.OnDamaged)
{
if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; }
if (statusEffect.OnlyPlayerTriggered)
if (statusEffect.OnlyWhenDamagedByPlayer)
{
if (LastAttacker == null || !LastAttacker.IsPlayer)
{
@@ -4393,6 +4373,10 @@ namespace Barotrauma
{
statusEffect.Apply(actionType, deltaTime, this, this);
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null)
{
statusEffect.Apply(actionType, deltaTime, this, CurrentHull);
}
}
if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
{
@@ -4670,6 +4654,13 @@ namespace Barotrauma
CharacterList.Remove(this);
foreach (var attachedProjectile in AttachedProjectiles.ToList())
{
attachedProjectile.Unstick();
}
Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
Latchers.Clear();
if (Inventory != null)
{
foreach (Item item in Inventory.AllItems)
@@ -4756,6 +4747,8 @@ namespace Barotrauma
}
#if SERVER
newItem.GetComponent<Terminal>()?.SyncHistory();
if (newItem.GetComponent<WifiComponent>() is WifiComponent wifiComponent) { newItem.CreateServerEvent(wifiComponent); }
if (newItem.GetComponent<GeneticMaterial>() is GeneticMaterial geneticMaterial) { newItem.CreateServerEvent(geneticMaterial); }
#endif
int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 });
if (!slotIndices.Any())
@@ -386,6 +386,8 @@ namespace Barotrauma
{
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects)
{
if (Strength <= periodicEffect.MinStrength) { continue; }
if (periodicEffect.MaxStrength > 0 && Strength > periodicEffect.MaxStrength) { continue; }
PeriodicEffectTimers[periodicEffect] -= deltaTime;
if (PeriodicEffectTimers[periodicEffect] <= 0.0f)
{
@@ -498,6 +500,13 @@ namespace Barotrauma
/// </summary>
public void SetStrength(float strength)
{
if (!MathUtils.IsValid(strength))
{
#if DEBUG
DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({strength})\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
_nonClampedStrength = strength;
_strength = _nonClampedStrength;
activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength);
@@ -730,13 +730,13 @@ namespace Barotrauma
/// <summary>
/// How high the strength has to be for the affliction icon to be shown with a health scanner
/// </summary>
public readonly float ShowInHealthScannerThreshold = 0.05f;
public readonly float ShowInHealthScannerThreshold;
/// <summary>
/// How strong the affliction needs to be before bots attempt to treat it.
/// Also effects when the affliction is shown in the suitable treatments list.
/// </summary>
public readonly float TreatmentThreshold = 5.0f;
public readonly float TreatmentThreshold;
/// <summary>
/// Bots will not try to treat the affliction if the character has any of these afflictions
@@ -847,7 +847,7 @@ namespace Barotrauma
{
foreach (var itemPrefab in ItemPrefab.Prefabs)
{
float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType));
float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType);
if (!MathUtils.NearlyEqual(suitability, 0.0f))
{
yield return new KeyValuePair<Identifier, float>(itemPrefab.Identifier, suitability);
@@ -915,7 +915,7 @@ namespace Barotrauma
ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold),
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f));
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f));
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
@@ -422,9 +423,13 @@ namespace Barotrauma
return strength;
}
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true)
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false)
{
if (!affliction.Prefab.IsBuff && Unkillable || Character.GodMode) { return; }
if (Character.GodMode) { return; }
if (!ignoreUnkillability)
{
if (!affliction.Prefab.IsBuff && Unkillable) { return; }
}
if (affliction.Prefab.LimbSpecific)
{
if (targetLimb == null)
@@ -455,11 +460,7 @@ namespace Barotrauma
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab.Identifier);
}
resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
if (resistance > 1f) { resistance = 1f; }
return resistance;
return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
}
public float GetStatValue(StatTypes statType)
@@ -990,7 +991,7 @@ namespace Barotrauma
#endif
}
private float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
{
float multiplier = 1.0f;
if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier))
@@ -1132,7 +1133,11 @@ namespace Barotrauma
strength = GetPredictedStrength(affliction, predictFutureDuration, limb);
}
if (strength <= affliction.Prefab.TreatmentThreshold) { continue; }
//other afflictions of the same type increase the "treatability"
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false);
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
if (ignoreHiddenAfflictions)
@@ -1149,13 +1154,20 @@ namespace Barotrauma
foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitability)
{
float suitability = treatment.Value * strength;
if (treatment.Value > strength)
{
//avoid using very effective meds on small injuries
float overtreatmentFactor = MathHelper.Clamp(treatment.Value / strength, 1.0f, 10.0f);
suitability /= overtreatmentFactor;
}
if (!treatmentSuitability.ContainsKey(treatment.Key))
{
treatmentSuitability[treatment.Key] = treatment.Value * strength;
treatmentSuitability[treatment.Key] = suitability;
}
else
{
treatmentSuitability[treatment.Key] += treatment.Value * strength;
treatmentSuitability[treatment.Key] += suitability;
}
minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability);
maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
@@ -1171,6 +1183,28 @@ namespace Barotrauma
}
}
/// <summary>
/// Returns the total strength of instances of the same affliction on all the characters limbs,
/// with a smaller weight given to the other afflictions on other limbs
/// </summary>
/// <param name="otherAfflictionMultiplier">Multiplier on the strengths of the afflictions on other limbs.</param>
/// <param name="includeSameAffliction">Should the strength of the provided affliction be included too?</param>
public float GetTotalAdjustedAfflictionStrength(Affliction affliction, float otherAfflictionMultiplier = 0.3f, bool includeSameAffliction = true)
{
float totalAfflictionStrength = includeSameAffliction ? affliction.Strength : 0;
if (affliction.Prefab.LimbSpecific)
{
foreach (Affliction otherAffliction in afflictions.Keys)
{
if (affliction.Prefab == otherAffliction.Prefab && affliction != otherAffliction)
{
totalAfflictionStrength += otherAffliction.Strength * otherAfflictionMultiplier;
}
}
}
return totalAfflictionStrength;
}
private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
public IEnumerable<Identifier> GetActiveAfflictionTags()
{
@@ -182,7 +182,9 @@ namespace Barotrauma
{
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)
{
DebugLogWhenFails = false
DebugLogWhenFails = false,
IsWaitOrder = true,
CloseEnough = 100
});
}
}
@@ -1226,7 +1226,7 @@ namespace Barotrauma
if (statusEffect.type == ActionType.OnDamaged)
{
if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; }
if (statusEffect.OnlyPlayerTriggered)
if (statusEffect.OnlyWhenDamagedByPlayer)
{
if (character.LastAttacker == null || !character.LastAttacker.IsPlayer)
{
@@ -231,12 +231,17 @@ namespace Barotrauma
protected void CreateSubParams()
{
SubParams.Clear();
var health = MainElement.GetChildElement("health");
if (health != null)
var healthElement = MainElement.GetChildElement("health");
if (healthElement != null)
{
Health = new HealthParams(health, this);
SubParams.Add(Health);
Health = new HealthParams(healthElement, this);
}
else
{
DebugConsole.ThrowError($"No health parameters defined for character \"{(SpeciesName)}\".");
Health = new HealthParams(null, this);
}
SubParams.Add(Health);
// TODO: support for multiple ai elements?
var ai = MainElement.GetChildElement("ai");
if (ai != null)
@@ -29,7 +29,7 @@ namespace Barotrauma.Abilities
nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true);
}
protected void ApplyEffectSpecific(Character targetCharacter)
protected void ApplyEffectSpecific(Character targetCharacter, Limb targetLimb = null)
{
//prevent an infinite loop if an effect triggers itself
//(e.g. a talent that triggers when an affliction is applied, and applies that same affliction)
@@ -66,6 +66,11 @@ namespace Barotrauma.Abilities
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) && targetLimb != null)
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetLimb);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.SetUser(Character);
@@ -99,7 +104,7 @@ namespace Barotrauma.Abilities
{
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter && !applyToSelf)
{
ApplyEffectSpecific(targetCharacter);
ApplyEffectSpecific(targetCharacter, targetLimb: (abilityObject as AbilityApplyTreatment)?.TargetLimb);
}
else
{
@@ -22,15 +22,13 @@
{
foreach (Identifier afflictionIdentifier in afflictionIdentifiers)
{
if (affliction.Identifier != afflictionIdentifier) { continue; }
affliction.Strength *= 1 + addedMultiplier;
if (affliction.Identifier != afflictionIdentifier) { continue; }
AfflictionPrefab afflictionPrefab = affliction.Prefab;
if (!replaceWith.IsEmpty)
{
if (AfflictionPrefab.Prefabs.TryGet(replaceWith, out AfflictionPrefab afflictionPrefab))
{
abilityAffliction.Affliction = new Affliction(afflictionPrefab, abilityAffliction.Affliction.Strength);
}
}
AfflictionPrefab.Prefabs.TryGet(replaceWith, out afflictionPrefab);
}
abilityAffliction.Affliction = new Affliction(afflictionPrefab, affliction.Strength * (1 + addedMultiplier));
}
}
else