Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2025-04-10 10:37:09 -03:00
296 changed files with 8420 additions and 2945 deletions
@@ -213,7 +213,8 @@ namespace Barotrauma
if (targetHull == null) { return false; }
if (maxDistance > 0)
{
if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; }
// Too far from the gap.
if (Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) > maxDistance * maxDistance) { return false; }
}
if (SteeringManager is IndoorsSteeringManager pathSteering)
{
@@ -325,7 +326,7 @@ namespace Barotrauma
}
if (dropOtherIfCannotMove)
{
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || (targetTags != null && otherItem.HasIdentifierOrTags(targetTags)))
{
bool switchingToBetterSuit =
targetTags != null &&
@@ -358,13 +359,23 @@ namespace Barotrauma
}
}
public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea);
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true, bool allowDestroying = false) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea, allowDestroying);
public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true, int? unequipMax = null) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea, unequipMax);
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
/// <param name="unequipMax">Optional max amount for items to be unequipped.</param>
public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true, bool allowDestroying = false, int? unequipMax = null) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea, allowDestroying, unequipMax);
public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea);
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true, bool allowDestroying = false) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea, allowDestroying);
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
/// <param name="unequipMax">Optional max amount for items to be unequipped.</param>
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, bool allowDestroying = false, int? unequipMax = null)
{
var inventory = parentItem.OwnInventory;
if (inventory == null || !inventory.Container.DrawInventory) { return; }
@@ -376,21 +387,36 @@ namespace Barotrauma
if (containedItem == null) { continue; }
if (predicate == null || predicate(containedItem))
{
if (avoidDroppingInSea && !character.IsInFriendlySub)
if (allowDestroying && GameMain.NetworkMember is not { IsClient: true } && character.AIController.HasInfiniteItemSpawns(containedItem.Prefab.Identifier))
{
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
{
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
continue;
}
Entity.Spawner?.AddItemToRemoveQueue(containedItem);
}
else
{
if (avoidDroppingInSea && !character.IsInFriendlySub)
{
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
{
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
continue;
}
}
containedItem.Drop(character);
}
containedItem.Drop(character);
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
}
}
}
}
public bool HasInfiniteItemSpawns(IEnumerable<Identifier> itemIdentifiers)
=> (Character.HumanPrefab?.InfiniteItems.Any(it => itemIdentifiers.Contains(it.Identifier) || it.Tags.Any(itemIdentifiers.Contains)) ?? false)
|| (Character.Info?.Job?.HasJobItem(jobItem => jobItem.Infinite && itemIdentifiers.Contains(jobItem.GetItemIdentifier(Character.TeamID, isPvPMode: GameMain.GameSession.GameMode is PvPMode))) ?? false);
public bool HasInfiniteItemSpawns(Identifier itemIdentifier)
=> (Character.HumanPrefab?.InfiniteItems.Any(it => it.Identifier == itemIdentifier || it.Tags.Contains(itemIdentifier)) ?? false)
|| (Character.Info?.Job?.HasJobItem(jobItem => jobItem.Infinite && jobItem.GetItemIdentifier(Character.TeamID, isPvPMode: GameMain.GameSession.GameMode is PvPMode) == itemIdentifier) ?? false);
public void ReequipUnequipped()
{
@@ -1128,9 +1128,9 @@ namespace Barotrauma
searchingNewHull = false;
}
}
if (patrolTarget != null && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable)
if (patrolTarget != null && pathSteering.CurrentPath is { Finished: false, Unreachable: false })
{
PathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n));
pathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: PatrolNodeFilter);
return;
}
}
@@ -1570,22 +1570,22 @@ namespace Barotrauma
Vector2 toTarget = attackWorldPos - attackLimbPos;
Vector2 toTargetOffset = toTarget;
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute
if (wallTarget != null && Character.Submarine == null)
if (Character.Submarine == null)
{
if (wallTarget.Structure.Submarine != null)
if (wallTarget != null)
{
Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity);
if (wallTarget.Structure.Submarine != null)
{
Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity);
toTargetOffset += margin;
}
}
else if (targetCharacter != null)
{
Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
toTargetOffset += margin;
}
}
else if (targetCharacter != null)
{
Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
toTargetOffset += margin;
}
else if (SelectedAiTarget.Entity is MapEntity e)
{
if (e.Submarine != null)
else if (SelectedAiTarget.Entity is MapEntity { Submarine: not null } e)
{
Vector2 margin = CalculateMargin(e.Submarine.Velocity);
toTargetOffset += margin;
@@ -1666,12 +1666,15 @@ namespace Barotrauma
// Crouch if the target is down (only humanoids), so that we can reach it.
if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2)
{
if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range)
if (targetCharacter?.CurrentHull is Hull targetHull && targetHull == Character.CurrentHull && toTarget.Y < 0)
{
humanoidAnimController.Crouch();
if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range)
{
humanoidAnimController.Crouch();
}
}
}
if (canAttack)
{
if (AttackLimb.attack.Ranged)
@@ -1706,6 +1709,10 @@ namespace Barotrauma
}
}
}
else if (wallTarget == null && Character.CurrentHull != null && targetCharacter != null)
{
canAttack = Submarine.PickBody(SimPosition, attackSimPos, collisionCategory: Physics.CollisionWall) == null;
}
}
}
Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null;
@@ -1720,6 +1727,8 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
// Note that this returns null when we don't currently use IndoorsSteeringManager, even if we are able to path!
// -> Don't use PathSteering, because that returns the reference even if we currently don't use it.
var pathSteering = SteeringManager as IndoorsSteeringManager;
if (AttackLimb != null && AttackLimb.attack.Retreat)
{
@@ -1727,58 +1736,71 @@ namespace Barotrauma
}
else
{
Vector2 steerPos = attackSimPos;
if (!Character.AnimController.SimplePhysicsEnabled)
{
// Offset so that we don't overshoot the movement
Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
steerPos += offset;
}
if (pathSteering != null)
{
if (pathSteering.CurrentPath != null)
// Attack doors
if (canAttackDoors && HasValidPath())
{
// Attack doors
if (canAttackDoors)
// If the target is in the same hull, there shouldn't be any doors blocking the path
if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
{
// If the target is in the same hull, there shouldn't be any doors blocking the path
if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door is { CanBeTraversed: false } && (!Character.IsInFriendlySub || !door.HasAccess(Character)))
{
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door is { CanBeTraversed: false } && (!Character.IsInFriendlySub || !door.HasAccess(Character)))
if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
{
if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
{
SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority);
State = AIState.Attack;
return;
}
SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority);
State = AIState.Attack;
return;
}
}
}
// When pursuing, we don't want to pursue too close
float max = 300;
float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max;
if (!canAttack || distance > margin)
}
// When pursuing, we don't want to pursue too close
float max = 300;
float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max;
if ((!canAttack || distance > margin) && !IsTryingToSteerThroughGap)
{
// Steer towards the target if in the same room and swimming
// Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering.
if (Character.CurrentHull != null &&
Character.Submarine != null && !Character.Submarine.Info.IsRuin &&
(Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))
{
// Steer towards the target if in the same room and swimming
// Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering.
if (Character.CurrentHull != null &&
Character.Submarine != null && !Character.Submarine.Info.IsRuin &&
(Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))
Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - myPos));
}
else
{
Func<PathNode, bool> nodeFilter = null;
float outsideNodePenalty = 0;
if (Character.CurrentHull != null && Character.IsInPlayerSub)
{
Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos));
// Prefer not to path back outside, if we are inside player sub.
// Fixes monsters sometimes pathing from the airlock to another airlock on the other side of the sub, because the path is technically cheaper than the path through the interiors.
outsideNodePenalty = 50;
}
else
{
pathSteering.SteeringSeek(steerPos, weight: 2,
minGapWidth: minGapSize,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null),
checkVisiblity: true);
pathSteering.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2,
minGapWidth: minGapSize,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null),
nodeFilter: nodeFilter,
checkVisiblity: true,
outsideNodePenalty: outsideNodePenalty);
if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
if (pathSteering.CurrentPath != null)
{
if (pathSteering.IsPathDirty)
{
if (Character.CurrentHull is Hull hull && hull.ConnectedGaps.Any(static g => !g.IsRoomToRoom && g.Open >= 1.0f && g.ConnectedDoor != null))
{
// Reset in the airlock, because otherwise the character may be too slow to change the steering and keep moving outside.
SteeringManager.Reset();
}
// Steer towards the target
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
}
else if (pathSteering.CurrentPath.Unreachable)
{
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
@@ -1787,39 +1809,42 @@ namespace Barotrauma
}
}
}
else if (!IsTryingToSteerThroughGap)
}
else if (!IsTryingToSteerThroughGap)
{
if (AttackLimb.attack.Ranged)
{
if (AttackLimb.attack.Ranged)
float dir = Character.AnimController.Dir;
if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin)
{
float dir = Character.AnimController.Dir;
if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin)
{
SteeringManager.Reset();
}
else
{
// Too close
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
SteeringManager.Reset();
}
else
{
// Close enough
SteeringManager.Reset();
// Too close
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
}
else
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
// Close enough
SteeringManager.Reset();
}
}
else
{
pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize);
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
}
}
else
{
Vector2 steerPos = attackSimPos;
if (!Character.AnimController.SimplePhysicsEnabled)
{
// Offset so that we don't overshoot the movement
Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
steerPos += offset;
}
// Sweeping and circling doesn't work well inside
if (Character.CurrentHull == null)
{
@@ -2232,7 +2257,7 @@ namespace Barotrauma
// Don't allow root motion attacks, if we are not on the same level with the target, because it can cause warping.
switch (target)
{
case Character targetCharacter when Character.CurrentHull != targetCharacter.CurrentHull || targetCharacter.IsKnockedDown:
case Character targetCharacter when Character.CurrentHull != targetCharacter.CurrentHull || targetCharacter.IsKnockedDownOrRagdolled:
case Item targetItem when Character.CurrentHull != targetItem.CurrentHull:
return false;
}
@@ -348,7 +348,7 @@ namespace Barotrauma
enemyCheckTimer -= deltaTime;
if (enemyCheckTimer < 0)
{
CheckEnemies();
SpotEnemies();
enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
}
}
@@ -443,7 +443,6 @@ namespace Barotrauma
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
else
{
@@ -467,31 +466,7 @@ namespace Barotrauma
bool run = !currentObjective.ForceWalk && (currentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority);
if (currentObjective is AIObjectiveGoTo goTo)
{
if (run && goTo == objectiveManager.ForcedOrder && goTo.IsWaitOrder && !Character.IsOnPlayerTeam)
{
// NPCs with a wait order don't run.
run = false;
}
else if (goTo.Target != null)
{
if (Character.CurrentHull == null)
{
run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300;
}
else
{
float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y;
if (Math.Abs(yDiff) > 100)
{
run = true;
}
else
{
float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X;
run = Math.Abs(xDiff) > 500;
}
}
}
run = goTo.ShouldRun(run);
}
//if someone is grabbing the bot and the bot isn't trying to run anywhere, let them keep dragging and "control" the bot
@@ -564,13 +539,15 @@ namespace Barotrauma
ShipCommandManager?.Update(deltaTime);
}
private void CheckEnemies()
private void SpotEnemies()
{
//already in combat, no need to check
if (objectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return; }
if (objectiveManager.HasActiveObjective<AIObjectiveCombat>()) { return; }
float closestDistance = 0;
Character closestEnemy = null;
bool shouldActOffensively = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != Character.Submarine) { continue; }
@@ -596,8 +573,8 @@ namespace Barotrauma
}
if (closestEnemy != null)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
}
AddCombatObjective(shouldActOffensively ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
}
}
private void UnequipUnnecessaryItems()
@@ -606,7 +583,7 @@ namespace Barotrauma
if (ObjectiveManager.CurrentObjective == null) { return; }
if (Character.CurrentHull == null) { return; }
bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, Tags.OxygenSource, out _, conditionPercentage: 1);
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveMoveItem>();
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
{
@@ -621,8 +598,8 @@ namespace Barotrauma
{
if (findItemState != FindItemState.OtherItem)
{
var decontain = ObjectiveManager.GetActiveObjectives<AIObjectiveDecontainItem>().LastOrDefault();
if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(Tags.HeavyDivingGear) &&
var moveItemObjective = ObjectiveManager.GetLastActiveObjective<AIObjectiveMoveItem>();
if (moveItemObjective is { TargetItem: not null } && moveItemObjective.TargetItem.HasTag(Tags.HeavyDivingGear) &&
ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
{
// Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker.
@@ -728,17 +705,17 @@ namespace Barotrauma
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
var moveItemObjective = new AIObjectiveMoveItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
{
DropIfFails = false
};
decontainObjective.Abandoned += () =>
moveItemObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
moveItemObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(moveItemObjective, addFirst: true);
return;
}
else
@@ -764,7 +741,7 @@ namespace Barotrauma
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
else if (findItemState is FindItemState.None or FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
@@ -773,14 +750,14 @@ namespace Barotrauma
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
var moveItemObjective = new AIObjectiveMoveItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
moveItemObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
moveItemObjective.Completed += ReequipUnequipped;
ObjectiveManager.CurrentObjective.AddSubObjective(moveItemObjective, addFirst: true);
return;
}
else
@@ -814,9 +791,9 @@ namespace Barotrauma
if (Character.TryPutItemInBag(item)) { continue; }
if (item.HasTag(Tags.Weapon))
{
// Don't decontain weapons, because it could be that we are holding a weapon that cannot be placed on back (if we have a toolbelt) nor in the any slot, such as an HMG.
// Don't store weapons in containers, because it could be that we are holding a weapon that cannot be placed on back (if we have a toolbelt) nor in any slot, such as an HMG.
// Could check that we only ignore weapons when we've had an order to find a weapon, but it could also be that we picked the weapon for self-defence, on ad-hoc basis.
// And I don't think it would make sense to decontain those weapons either.
// And I don't think it would make sense to move those weapons in containers either.
continue;
}
findItemState = FindItemState.OtherItem;
@@ -826,13 +803,13 @@ namespace Barotrauma
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
var moveItemObjective = new AIObjectiveMoveItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
moveItemObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
ObjectiveManager.CurrentObjective.AddSubObjective(moveItemObjective, addFirst: true);
return;
}
else
@@ -1046,12 +1023,12 @@ namespace Barotrauma
foreach (Character target in Character.CharacterList)
{
if (target.CurrentHull != hull || !target.Enabled || target.InDetectable) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false))
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, targetCharactersInOtherSubs: false))
{
if (!target.IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
if (AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
{
var orderPrefab = OrderPrefab.Prefabs["reportintruders"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
targetHull = hull;
if (target.IsEscorted)
{
@@ -1069,6 +1046,12 @@ namespace Barotrauma
}
}
}
if (Character.CombatAction == null && !isFighting)
{
// Immediately react to enemies when they are spotted. AIObjectiveFightIntruders and AIObjectiveFindSafety would make the bot react to the threats,
// but the reaction is delayed (and doesn't necessarily target this enemy), and in many cases the reaction would come only when the enemy attacks and triggers AIObjectiveCombat.
AddCombatObjective(ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive, target);
}
}
}
if (AIObjectiveExtinguishFires.IsValidTarget(hull, Character))
@@ -1076,14 +1059,14 @@ namespace Barotrauma
if (AddTargets<AIObjectiveExtinguishFires, Hull>(Character, hull) && newOrder == null)
{
var orderPrefab = OrderPrefab.Prefabs["reportfire"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
targetHull = hull;
}
}
if (IsBallastFloraNoticeable(Character, hull) && newOrder == null)
{
var orderPrefab = OrderPrefab.Prefabs["reportballastflora"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
targetHull = hull;
}
if (!isFighting)
@@ -1095,7 +1078,7 @@ namespace Barotrauma
if (AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap) && newOrder == null && !gap.IsRoomToRoom)
{
var orderPrefab = OrderPrefab.Prefabs["reportbreach"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
targetHull = hull;
}
}
@@ -1110,7 +1093,7 @@ namespace Barotrauma
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
targetHull = hull;
}
}
@@ -1135,20 +1118,19 @@ namespace Barotrauma
}
if (newOrder != null && speak)
{
string msg = newOrder.GetChatMessage(string.Empty, targetHull?.DisplayName?.Value ?? string.Empty, givingOrderToSelf: false);
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
minDurationBetweenSimilar: 60.0f);
Character.Speak(msg, ChatMessageType.Default, identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), minDurationBetweenSimilar: 60f);
}
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
Character.Speak(msg, messageType: ChatMessageType.Order);
#if SERVER
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
.WithTargetEntity(targetHull)
.WithOrderGiver(Character), "", null, Character));
.WithOrderGiver(Character), msg, targetCharacter: null, sender: Character));
#endif
}
}
@@ -1172,8 +1154,19 @@ namespace Barotrauma
public static void ReportProblem(Character reporter, Order order, Hull targetHull = null)
{
if (reporter == null || order == null) { return; }
var visibleHulls = targetHull is null ? new List<Hull>(reporter.GetVisibleHulls()) : new List<Hull> { targetHull };
foreach (var hull in visibleHulls)
if (targetHull == null)
{
foreach (var hull in reporter.GetVisibleHulls())
{
Report(hull);
}
}
else
{
Report(targetHull);
}
void Report(Hull hull)
{
PropagateHullSafety(reporter, hull);
RefreshTargets(reporter, order, hull);
@@ -1353,7 +1346,7 @@ namespace Barotrauma
// Inform other NPCs
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
{
if (GameMain.IsMultiplayer || !attacker.IsPlayer || Character.TeamID != attacker.TeamID)
if (!attacker.IsPlayer || Character.TeamID != attacker.TeamID)
{
InformOtherNPCs(cumulativeDamage);
}
@@ -1421,7 +1414,12 @@ namespace Barotrauma
if (otherCharacter.IsPlayer) { continue; }
if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; }
if (!otherHumanAI.IsFriendly(Character)) { continue; }
if (attacker.AIController is EnemyAIController enemyAI && otherHumanAI.IsFriendly(attacker))
if (otherHumanAI.objectiveManager.IsCurrentObjective<AIObjectiveCombat>() || otherHumanAI.objectiveManager.HasActiveObjective<AIObjectiveCombat>())
{
// Already in combat, don't change target (because we are not attacked by the enemy)
return;
}
if (attacker.AIController is EnemyAIController && otherHumanAI.IsFriendly(attacker))
{
// Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
continue;
@@ -1432,15 +1430,33 @@ namespace Barotrauma
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
if (!isWitnessing)
{
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
//don't react to the attack
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID || !CheckReportRange(Character, otherCharacter, ReportRange))
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID)
{
// Dead or in different team -> cannot report.
continue;
}
}
if (otherHumanAI.objectiveManager.HasOrders())
{
// Unless witnessing the attack, don't react, if have been ordered to do something.
// The combat objective would take a higher prio than the order.
continue;
}
if (!CheckReportRange(Character, otherCharacter, ReportRange))
{
// Not inside report range -> cannot report.
continue;
}
}
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing);
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
if (!isWitnessing)
{
if (combatMode is AIObjectiveCombat.CombatMode.Defensive or AIObjectiveCombat.CombatMode.Retreat)
{
// Ignore defensive and retreating behavior, unless witnessing the attack.
continue;
}
}
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 3.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
}
}
@@ -1476,12 +1492,8 @@ namespace Barotrauma
}
if (attacker.IsPlayer && c.TeamID == attacker.TeamID)
{
if (GameMain.IsSingleplayer || c.TeamID != attacker.TeamID)
{
// Bots in the player team never act aggressively in single player when attacked by the player
// In multiplayer, they react only to players attacking them or other crew members
return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
}
// Bots in the player team never act aggressively when attacked by the player
return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
}
if (c.Submarine == null || !c.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
@@ -1526,16 +1538,18 @@ namespace Barotrauma
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
currentCombatObjective.AllowHoldFire = false;
c.IsCriminal = true;
attacker.IsCriminal = true;
attacker.IsActingOffensively = true;
}
if (c.IsCriminal)
if (attacker.IsCriminal)
{
// Always react if the attacker has been misbehaving earlier.
cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold);
}
if (cumulativeDamage > majorDamageThreshold)
{
c.IsCriminal = true;
attacker.IsCriminal = true;
attacker.IsActingOffensively = true;
if (c.IsSecurity)
{
return AIObjectiveCombat.CombatMode.Offensive;
@@ -1547,6 +1561,7 @@ namespace Barotrauma
}
else if (cumulativeDamage > minorDamageThreshold)
{
attacker.IsActingOffensively = true;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
else
@@ -1611,7 +1626,6 @@ namespace Barotrauma
{
var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager)
{
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
AbortCondition = abortCondition,
AllowHoldFire = allowHoldFire,
SpeakWarnings = speakWarnings
@@ -1718,8 +1732,9 @@ namespace Barotrauma
}
return true;
}
public bool NeedsDivingGear(Hull hull, out bool needsSuit)
/// <param name="objectiveManager">Used for checking the objective.</param>
public bool NeedsDivingGear(Hull hull, out bool needsSuit, AIObjectiveManager objectiveManager = null)
{
needsSuit = false;
bool needsAir = Character.NeedsAir && Character.CharacterHealth.OxygenLowResistance < 1;
@@ -1728,7 +1743,11 @@ namespace Barotrauma
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{
needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
if (!Character.IsImmuneToPressure)
{
// Always require a diving suit when operating an item in a flooding room.
needsSuit = hull == null || hull.LethalPressure > 0 || objectiveManager?.CurrentOrder is AIObjectiveOperateItem operateItem && operateItem.GetTarget().Item.CurrentHull == hull;
}
return needsAir || needsSuit;
}
if (hull.WaterPercentage > 60 || (hull.IsWetRoom && hull.WaterPercentage > 10) || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
@@ -1863,6 +1882,7 @@ namespace Barotrauma
if (combatMode == AIObjectiveCombat.CombatMode.Offensive)
{
character.IsCriminal = true;
character.IsActingOffensively = true;
}
if (!TriggerSecurity(otherHumanAI, combatMode))
{
@@ -1903,6 +1923,11 @@ namespace Barotrauma
public static void ItemTaken(Item item, Character thief)
{
if (item == null || thief == null || item.GetComponent<LevelResource>() != null) { return; }
if (thief.IsBot && item.HasTag(AIObjectiveGetItem.AllowedItemsToTake))
{
// Bots are allowed to take diving gear or extinguishers, when they need them, without it being considered as stealing.
return;
}
bool someoneSpoke = false;
if (item.Illegitimate && item.GetRootInventoryOwner() is Character itemOwner && itemOwner != thief && itemOwner.TeamID == thief.TeamID)
{
@@ -2016,16 +2041,16 @@ namespace Barotrauma
/// 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>
public static void PropagateHullSafety(Character character, Hull hull)
private static void PropagateHullSafety(Character character, Hull hull)
{
DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
DoForEachBot(character, humanAi => humanAi.RefreshHullSafety(hull));
}
public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull);
private void RefreshHullSafety(Hull hull)
{
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : VisibleHulls;
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : null;
float hullSafety = GetHullSafety(hull, Character, visibleHulls);
if (hullSafety > HULL_SAFETY_THRESHOLD)
{
@@ -2037,7 +2062,7 @@ namespace Barotrauma
}
}
public static void RefreshTargets(Character character, Order order, Hull hull)
private static void RefreshTargets(Character character, Order order, Hull hull)
{
switch (order.Identifier.Value.ToLowerInvariant())
{
@@ -2068,7 +2093,7 @@ namespace Barotrauma
foreach (var enemy in Character.CharacterList)
{
if (enemy.CurrentHull != hull) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, false))
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, targetCharactersInOtherSubs: false))
{
AddTargets<AIObjectiveFightIntruders, Character>(character, enemy);
}
@@ -2147,7 +2172,7 @@ namespace Barotrauma
// Use the cached visible hulls
visibleHulls = VisibleHulls;
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires { Priority: > 0 } || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
@@ -2157,12 +2182,26 @@ namespace Barotrauma
}
return safety;
}
/// <summary>
/// Returns hull safety for the character without ignoring any threats.
/// Useful for example, when we need to calculate a safety value of the hull regardless of the protective equipment or buffs of the character.
/// No caching involved (always recalculated).
/// </summary>
public static float CalculateObjectiveHullSafety(Character character) => CalculateHullSafety(
hull: character.CurrentHull,
visibleHulls: character.AIController?.VisibleHulls ?? character.GetVisibleHulls(),
character,
ignoreEnemies: false, ignoreFire: false, ignoreWater: false, ignoreOxygen: false, ignorePressureProtection: true);
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false, bool ignorePressureProtection = false)
{
bool isProtectedFromPressure = character.IsProtectedFromPressure;
if (hull == null) { return isProtectedFromPressure ? 100 : 0; }
if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; }
if (!ignorePressureProtection)
{
bool isProtectedFromPressure = character.IsProtectedFromPressure;
if (hull == null) { return isProtectedFromPressure ? 100 : 0; }
if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; }
}
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
@@ -2181,42 +2220,56 @@ namespace Barotrauma
waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
}
}
if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
if (!ignoreOxygen)
{
oxygenFactor = 1;
}
if (isProtectedFromPressure)
{
waterFactor = 1;
if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
{
oxygenFactor = 1;
}
}
float fireFactor = 1;
if (!ignoreFire)
{
static float CalculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
// Even the smallest fire reduces the safety by 50%
float fire = visibleHulls?.Sum(CalculateFire) ?? CalculateFire(hull);
float fire = CalculateFire(hull) + hull.linkedTo.Sum(e => CalculateFire(e as Hull));
fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
float CalculateFire(Hull h)
{
if (h is not Hull) { return 0; }
bool isInDamageRange = h.FireSources.Any(fs => fs.IsInDamageRange(character, fs.DamageRange));
if (isInDamageRange) { return 1; }
// Even the smallest fire reduces the safety by 50%
return h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
}
}
float enemyFactor = 1;
if (!ignoreEnemies)
{
int enemyCount = 0;
float enemyCount = 0;
foreach (Character c in Character.CharacterList)
{
float countModifier = 1.0f;
if (c.CurrentHull == null) { continue; }
if (visibleHulls == null)
{
if (c.CurrentHull != hull) { continue; }
if (c.CurrentHull != hull && !c.CurrentHull.linkedTo.Contains(hull)) { continue; }
}
else
{
if (!visibleHulls.Contains(c.CurrentHull)) { continue; }
if (c.CurrentHull != hull && !c.CurrentHull.linkedTo.Contains(hull))
{
// Enemy in a visible room, but not in the same room -> a lower threat
countModifier = 0.25f;
}
}
if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed)
{
enemyCount++;
enemyCount += countModifier;
}
}
// The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages)
// The hull safety decreases 90% per enemy up to 100%,
// and 22.5% per each enemy in the visible, adjacent rooms
enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
}
float dangerousItemsFactor = 1f;
@@ -2236,7 +2289,7 @@ namespace Barotrauma
{
if (hull == null)
{
return CalculateHullSafety(hull, character, visibleHulls);
return CalculateHullSafety(null, character, visibleHulls);
}
if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
{
@@ -2254,7 +2307,7 @@ namespace Barotrauma
{
if (hull == null)
{
return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
return CalculateHullSafety(null, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
}
HullSafety hullSafety;
if (character.AIController is HumanAIController controller)
@@ -2279,11 +2332,14 @@ namespace Barotrauma
return hullSafety.safety;
}
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false, bool ignoreHuskDisguising = false)
{
if (other.IsHusk && !onlySameTeam)
if (onlySameTeam)
{
ignoreHuskDisguising = true;
}
if (other.IsHusk && !ignoreHuskDisguising)
{
// Disguised as husk
return me.IsDisguisedAsHusk;
}
else
@@ -2435,7 +2491,7 @@ namespace Barotrauma
private static void DoForEachBot(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
{
if (character == null) { return; }
foreach (var c in Character.CharacterList)
foreach (Character c in Character.CharacterList)
{
if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
{
@@ -119,10 +119,13 @@ namespace Barotrauma
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)
/// <summary>
/// </summary>
/// <param name="outsideNodePenalty">Additional cost applied to outside nodes. When the character is inside an extra cost of 100 is also automatically added for outside nodes, unless the character is protected from the pressure. Used for example to prevent monsters from preferring outside nodes when they already are inside.</param>
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, float outsideNodePenalty = 0)
{
// Have to use a variable here or resetting doesn't work.
Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity, outsideNodePenalty);
steering += addition;
}
@@ -139,9 +142,9 @@ namespace Barotrauma
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)
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, float outsideNodePenalty = 0)
{
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null;
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.IsAtEndNode || currentPath.CurrentNode == null;
if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
{
// If the target has moved, we need a new path.
@@ -182,7 +185,7 @@ namespace Barotrauma
Vector2 currentPos = host.SimPosition;
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility, outsideNodePenalty);
bool useNewPath = needsNewPath;
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
@@ -792,7 +795,7 @@ namespace Barotrauma
float? penalty = GetSingleNodePenalty(nextNode);
if (penalty == null) { return null; }
bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
if (!character.CanClimb)
if (!character.CanClimb && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null)
{
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) ||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
@@ -888,6 +891,7 @@ namespace Barotrauma
return penalty;
}
// TODO: Long and complex. Consider refactoring.
public static float smallRoomSize = 500;
public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true)
{
@@ -915,36 +919,92 @@ namespace Barotrauma
}
else
{
float leftDist = character.Position.X - currentHull.Rect.X;
float rightDist = currentHull.Rect.Right - character.Position.X;
if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
bool isVerySmallRoom = roomWidth < smallRoomSize;
Hull nextRoom = null;
if (!stayStillInTightSpace && isVerySmallRoom)
{
if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2)
float closestDistance = 0;
// Try to steer to the next room
foreach (Gap gap in currentHull.ConnectedGaps)
{
SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
return;
}
else if (stayStillInTightSpace)
{
Reset();
return;
if (gap.Open < 1.0f) { continue; }
float feetPos = ConvertUnits.ToDisplayUnits(character.AnimController.FloorY);
float gapTop = gap.Rect.Y;
float gapBottom = gap.Rect.Y - gap.Rect.Height;
const float margin = 25;
if (character.Position.Y > gapTop || feetPos < gapBottom - margin)
{
// If the character is above or below the gap, they can't walk through it.
continue;
}
Hull room = null;
foreach (var entity in gap.linkedTo)
{
if (entity is not Hull hull) { continue; }
if (hull.Submarine != character.Submarine) { continue; }
if (hull.Rect.Width < smallRoomSize) { continue; }
if (hull != currentHull)
{
room = hull;
break;
}
}
if (room == null) { continue; }
Vector2 toGap = gap.Position - character.Position;
float distance = Math.Abs(toGap.X);
if (nextRoom == null || distance < closestDistance)
{
closestDistance = distance;
nextRoom = room;
}
}
}
if (leftDist < wallAvoidDistance)
if (nextRoom != null)
{
float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
float toNextRoom = nextRoom.Position.X - character.Position.X;
SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(toNextRoom));
WanderAngle = 0.0f;
}
else if (rightDist < wallAvoidDistance)
{
float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
WanderAngle = MathHelper.Pi;
}
else
{
wander = true;
if (!stayStillInTightSpace && isVerySmallRoom)
{
// Reset regardless, because moving inside the room would look glitchy.
Reset();
return;
}
float leftDist = character.Position.X - currentHull.Rect.X;
float rightDist = currentHull.Rect.Right - character.Position.X;
if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
{
float diff = rightDist - leftDist;
if (Math.Abs(diff) > wallAvoidDistance / 2)
{
SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(diff));
return;
}
else if (stayStillInTightSpace)
{
Reset();
return;
}
}
if (leftDist < wallAvoidDistance)
{
float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
WanderAngle = 0.0f;
}
else if (rightDist < wallAvoidDistance)
{
float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
WanderAngle = MathHelper.Pi;
}
else
{
wander = true;
}
}
}
}
@@ -274,7 +274,7 @@ namespace Barotrauma
public bool IsIgnoredAtOutpost()
{
if (!IgnoreAtOutpost) { return false; }
if (!Level.IsLoadedFriendlyOutpost) { return false; }
if (!Level.IsLoadedFriendlyOutpost && GameMain.GameSession.GameMode is not TestGameMode) { return false; }
if (!character.IsOnPlayerTeam || character.IsFriendlyNPCTurnedHostile) { return false; }
if (character.Submarine?.Info == null) { return false; }
return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC;
@@ -48,19 +48,22 @@ namespace Barotrauma
protected override bool CheckObjectiveState() => false;
protected override float GetPriority()
{
if (character.IsClimbing)
{
// Target is climbing -> stop following the objective (soft abandon, without ignoring the target).
Priority = 0;
}
else if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
{
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
{
Priority = objectiveManager.GetOrderPriority(this);
}
else
{
Priority = AIObjectiveManager.LowestOrderPriority - 1;
if (HumanAIController.CurrentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.CalculateObjectiveHullSafety(Target) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
Priority = 0;
}
else
{
Priority = AIObjectiveManager.LowestOrderPriority - 1;
}
}
return Priority;
}
@@ -86,18 +89,18 @@ namespace Barotrauma
onCompleted: () =>
{
RemoveSubObjective(ref goToObjective);
if (character.IsClimbing)
if (character.IsClimbing || HumanAIController.CurrentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.CalculateObjectiveHullSafety(Target) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
// Shouldn't start inspecting characters when they climb, nor get here, because the priority should be 0,
// but if this still happens, we'll have to abandon the objective
// because it's not currently possible to hold to characters and ladders at the same time.
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
// Shouldn't start inspecting characters when they climb, but we can still get here, if they start climbing while we are moving at them.
// If that happens, let's abandon the objective, because it's not currently possible to hold to characters and ladders at the same time.
Abandon = true;
}
else
{
currentState = State.Inspect;
stolenItems.Clear();
Target.Inventory.FindAllItems(it => it.Illegitimate, recursive: true, stolenItems);
Target.Inventory.FindAllItems(it => IsItemIllegitimate(Target, it), recursive: true, stolenItems);
character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.criminal" : "dialogcheckstolenitems").Value);
}
},
@@ -190,11 +193,23 @@ namespace Barotrauma
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == Target);
if (stolenItemsOnCharacter.Any())
{
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
foreach (var stolenItem in stolenItemsOnCharacter)
if (Target.IsBot)
{
HumanAIController.ApplyStealingReputationLoss(stolenItem);
// Bots automatically comply and drop stolen items when being inspected.
foreach (Item item in stolenItemsOnCharacter)
{
item.Drop(Target);
}
character.Speak(TextManager.Get("dialogcheckstolenitems.comply").Value);
}
else
{
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
foreach (var stolenItem in stolenItemsOnCharacter)
{
HumanAIController.ApplyStealingReputationLoss(stolenItem);
}
}
}
else
@@ -242,5 +257,10 @@ namespace Barotrauma
currentWarnDelay = Target.IsCriminal ? CriminalWarnDelay : NormalWarnDelay;
warnTimer = currentWarnDelay;
}
/// <summary>
/// Checks for illegitimate item, ignoring handcuffs equipped on the owner.
/// </summary>
public static bool IsItemIllegitimate(Character owner, Item item) => item.Illegitimate && (!item.HasTag(Tags.HandLockerItem) || !owner.HasEquippedItem(item));
}
}
@@ -18,11 +18,11 @@ namespace Barotrauma
public bool IsPriority { get; set; }
private readonly List<Item> ignoredContainers = new List<Item>();
private AIObjectiveDecontainItem decontainObjective;
private AIObjectiveMoveItem moveItemObjective;
private int itemIndex = 0;
/// <summary>
/// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
/// Allows <see cref="moveItemObjective"/> to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
/// </summary>
protected override bool ConcurrentObjectives => true;
@@ -53,9 +53,9 @@ namespace Barotrauma
float reduction = IsPriority ? 1 : isSelected ? 2 : 3;
float max = AIObjectiveManager.LowestOrderPriority - reduction;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
if (decontainObjective == null)
if (moveItemObjective == null)
{
// Halve the priority until there's a decontain objective (a valid container was found).
// Halve the priority until there's a moveItemObjective (a valid container was found).
Priority /= 2;
}
}
@@ -79,7 +79,7 @@ namespace Barotrauma
s == InvSlotType.OuterClothes ||
s == InvSlotType.HealthInterface);
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
TryAddSubObjective(ref moveItemObjective, () => new AIObjectiveMoveItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{
Equip = equip,
TakeWholeStack = true,
@@ -99,7 +99,7 @@ namespace Barotrauma
{
HumanAIController.ReequipUnequipped();
}
if (decontainObjective != null && decontainObjective.ContainObjective != null && decontainObjective.ContainObjective.CanBeCompleted)
if (moveItemObjective is { ContainObjective.CanBeCompleted: true })
{
ignoredContainers.Add(suitableContainer);
}
@@ -144,7 +144,7 @@ namespace Barotrauma
base.Reset();
ignoredContainers.Clear();
itemIndex = 0;
decontainObjective = null;
moveItemObjective = null;
}
public void DropTarget()
@@ -4,15 +4,18 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using FarseerPhysics.Dynamics;
using static Barotrauma.AIObjectiveFindSafety;
using System.Collections.Immutable;
using System.Diagnostics;
using FarseerPhysics;
namespace Barotrauma
{
class AIObjectiveCombat : AIObjective
{
public override Identifier Identifier { get; set; } = "combat".ToIdentifier();
public override string DebugTag => $"{Identifier} ({Mode})";
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
@@ -35,10 +38,9 @@ namespace Barotrauma
private bool allowCooldown;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
private Item _weapon;
private Item Weapon
public Item Weapon
{
get { return _weapon; }
set
@@ -48,6 +50,7 @@ namespace Barotrauma
}
}
private ItemComponent _weaponComponent;
private bool hasValidRangedWeapon;
private ItemComponent WeaponComponent
{
get
@@ -87,7 +90,9 @@ namespace Barotrauma
private const float DistanceCheckInterval = 0.2f;
private float distanceTimer;
private const float CloseDistanceThreshold = 300;
private const float CloseDistance = 300;
private const float MeleeDistance = 125;
private const float TooCloseToShoot = 100;
private const float FloorHeightApproximate = 100;
public bool AllowHoldFire;
@@ -168,13 +173,7 @@ namespace Barotrauma
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = DefaultCoolDown)
: base(character, objectiveManager, priorityModifier)
{
if (mode == CombatMode.None)
{
#if DEBUG
DebugConsole.ThrowError("Combat mode == None");
#endif
return;
}
Debug.Assert(mode != CombatMode.None);
Enemy = enemy;
coolDownTimer = coolDown;
findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
@@ -229,6 +228,7 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
base.Update(deltaTime);
isAimBlocked = false;
ignoreWeaponTimer -= deltaTime;
checkWeaponsTimer -= deltaTime;
if (reloadTimer > 0)
@@ -331,68 +331,155 @@ namespace Barotrauma
pathBackTimer -= deltaTime;
}
}
if (standUpTimer > 0)
{
standUpTimer -= deltaTime;
}
else
{
// Crouch by default so that others can shoot from behind. Disabled when the line of sight is blocked and while moving.
allowCrouching = true;
}
if (HumanAIController.DebugAI)
{
BlockedPositions ??= new List<Vector2>();
BlockedPositions.Clear();
}
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
if (Mode != CombatMode.Retreat && TryArm())
{
OperateWeapon(deltaTime);
}
if (HoldPosition)
{
SteeringManager.Reset();
}
else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
isMoving = false;
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
Move(deltaTime);
}
}
}
private bool isMoving;
private void Move(float deltaTime)
{
switch (Mode)
if (Mode == CombatMode.Retreat)
{
case CombatMode.Offensive:
case CombatMode.Arrest:
Retreat(deltaTime);
}
else if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.CurrentOrder is AIObjectiveGoTo gotoObjective)
{
if (gotoObjective.IsWaitOrder && WeaponComponent is MeleeWeapon && IsEnemyClose(CloseDistance))
{
// Ordered to wait near the enemy with a melee weapon -> engage.
Engage(deltaTime);
break;
case CombatMode.Defensive:
if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
}
else
{
// Ordered to follow -> keep following.
if (!gotoObjective.IsCloseEnough)
{
if ((character.CurrentHull == null || character.CurrentHull == Enemy.CurrentHull) && sqrDistance < 200 * 200)
isMoving = true;
}
gotoObjective.FaceTargetOnCompleted = false;
gotoObjective.ForceAct(deltaTime);
if (!character.AnimController.InWater && IsEnemyClose(CloseDistance))
{
// If close to the enemy, face it, so that we can attack it.
HumanAIController.FaceTarget(Enemy);
HumanAIController.AutoFaceMovement = false;
if (!gotoObjective.ShouldRun(true))
{
Engage(deltaTime);
ForceWalk = true;
}
}
}
}
else
{
switch (Mode)
{
case CombatMode.Defensive:
Retreat(deltaTime);
break;
case CombatMode.Offensive when hasValidRangedWeapon && IsEnemyClose(CloseDistance):
// Too close to the enemy -> try to back off.
Hull currentHull = character.CurrentHull;
bool backOff = currentHull != null;
Vector2 escapeVel = Vector2.Zero;
if (backOff)
{
int previousEnemyDir = 0;
foreach (Character enemy in Character.CharacterList)
{
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsHandcuffed) { continue; }
if (enemy.CurrentHull == null) { continue; }
if (currentHull != enemy.CurrentHull && !currentHull.linkedTo.Contains(enemy.CurrentHull)) { continue; }
Vector2 dir = character.Position - enemy.Position;
int enemyDir = Math.Sign(dir.X);
if (enemyDir == 0)
{
// Exactly at the same pos.
if (previousEnemyDir != 0)
{
// Another enemy at either side -> Ignore this enemy.
continue;
}
else
{
// Just choose either direction.
enemyDir = Rand.Value() > 0.5f ? 1 : -1;
}
}
if (previousEnemyDir != 0 && enemyDir != previousEnemyDir)
{
// Don't back off when there are enemies in different directions, because that's doomed.
backOff = false;
break;
}
previousEnemyDir = enemyDir;
// This formula is slightly modified from AIObjectiveFindSafety.UpdateSimpleEscape().
float distMultiplier = MathHelper.Clamp(MeleeDistance / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(enemyDir * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
if (escapeVel == Vector2.Zero)
{
backOff = false;
}
if (backOff)
{
// Only move if we haven't reached the edge of the room.
float left = currentHull.Rect.X + 50;
float right = currentHull.Rect.Right - 50;
backOff = escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right;
}
}
if (backOff)
{
BackOff();
}
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();
}
Engage(deltaTime);
}
}
else
{
Retreat(deltaTime);
}
break;
case CombatMode.Retreat:
Retreat(deltaTime);
break;
default:
throw new NotImplementedException();
void BackOff()
{
RemoveFollowTarget();
isMoving = true;
if (!IsEnemyClose(MeleeDistance))
{
ForceWalk = true;
}
HumanAIController.FaceTarget(Enemy);
HumanAIController.AutoFaceMovement = false;
character.ReleaseSecondaryItem();
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
break;
default:
Engage(deltaTime);
break;
}
}
}
@@ -407,7 +494,8 @@ namespace Barotrauma
bool isAllowedToSeekWeapons = character.IsHostileEscortee || character.IsPrisoner || // Prisoners and terrorists etc are always allowed to seek new weapons.
(character.IsInFriendlySub // Other characters need to be on a friendly sub in order to "know" where the weapons are. This also prevents NPCs "stealing" player items.
&& IsOffensiveOrArrest // = Defensive or retreating AI shouldn't seek new weapons.
&& !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
&& !character.IsInstigator // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
&& objectiveManager.CurrentOrder is not AIObjectiveGoTo); // if ordered to wait/follow, shouldn't go seeking new weapons.
if (checkWeaponsTimer < 0)
{
checkWeaponsTimer = CheckWeaponsInterval;
@@ -433,7 +521,12 @@ namespace Barotrauma
// All good, the weapon is loaded
break;
}
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(CloseDistanceThreshold);
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null;
if (seekAmmo)
{
// Bots set to arrest the target are always allowed to seek (or spawn) more ammo, because otherwise they might not be able to stun the target and need to use lethal weapons.
seekAmmo = Mode == CombatMode.Arrest || !IsEnemyClose(CloseDistance);
}
if (Reload(seekAmmo: seekAmmo))
{
// All good, we can use the weapon.
@@ -479,7 +572,7 @@ namespace Barotrauma
Mode = CombatMode.Retreat;
}
}
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistanceThreshold))))
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistance))))
{
// No weapon or only a poor weapon equipped -> try to find better.
RemoveSubObjective(ref retreatObjective);
@@ -488,7 +581,7 @@ namespace Barotrauma
constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false)
{
AllowStealing = HumanAIController.IsMentallyUnstable,
AbortCondition = obj => IsEnemyClose(200),
AbortCondition = _ => IsEnemyClose(CloseDistance / 2),
EvaluateCombatPriority = false, // Use a custom formula instead
GetItemPriority = i =>
{
@@ -581,6 +674,12 @@ namespace Barotrauma
private void OperateWeapon(float deltaTime)
{
if (isMoving && character.IsClimbing)
{
// Don't climb and shoot at the same time, because it messes up the aiming.
ClearInputs();
return;
}
switch (Mode)
{
case CombatMode.Offensive:
@@ -789,17 +888,22 @@ namespace Barotrauma
return containers.None() || containers.Any(container =>
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
}
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
{
hasValidRangedWeapon = false;
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistanceThreshold);
foreach (var weapon in weaponList)
bool prioritizeMelee = IsEnemyClose(TooCloseToShoot) || EnemyAIController.IsLatchedTo(Enemy, character);
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistance);
foreach (ItemComponent weapon in weaponList)
{
float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
if (priority >= GoodWeaponPriority && weapon is RangedWeapon or RepairTool)
{
hasValidRangedWeapon = true;
}
if (priority > bestPriority)
{
weaponComponent = weapon;
@@ -947,6 +1051,7 @@ namespace Barotrauma
private void Retreat(float deltaTime)
{
isMoving = true;
if (!Enemy.IsHuman && !character.IsInFriendlySub)
{
// Only relevant when we are retreating from monsters and are not inside a friendly sub.
@@ -1044,6 +1149,7 @@ namespace Barotrauma
{
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
{
isMoving = true;
character.ReleaseSecondaryItem();
// Swim towards the target
SteeringManager.Reset();
@@ -1094,7 +1200,7 @@ namespace Barotrauma
}
});
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown && !arrestingRegistered)
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDownOrRagdolled && !arrestingRegistered)
{
bool hasHandCuffs = HumanAIController.HasItem(character, Tags.HandLockerItem, out _);
if (!hasHandCuffs && character.TeamID == CharacterTeamType.FriendlyNPC)
@@ -1119,12 +1225,20 @@ namespace Barotrauma
followTargetObjective.CloseEnough =
WeaponComponent switch
{
RangedWeapon => 1000,
RangedWeapon => isAimBlocked ? BlockedDistance : 1000,
MeleeWeapon mw => mw.Range,
RepairTool rt => rt.Range,
_ => 50
};
}
if (isAimBlocked)
{
ForceWalk = true;
}
if (!followTargetObjective.IsCloseEnough)
{
isMoving = true;
}
}
private void RemoveFollowTarget()
@@ -1142,19 +1256,23 @@ namespace Barotrauma
private void OnArrestTargetReached()
{
if (!Enemy.IsKnockedDown)
if (!Enemy.IsKnockedDownOrRagdolled)
{
RemoveFollowTarget();
return;
}
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
// Confiscate stolen goods and all weapons
foreach (var item in Enemy.Inventory.AllItemsMod)
{
// Ignore handcuffs already on the target.
if (item.HasTag(Tags.HandLockerItem) && Enemy.HasEquippedItem(item)) { continue; }
if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
AIObjectiveFindThieves.MarkTargetAsInspected(character);
bool confiscateItem = AIObjectiveCheckStolenItems.IsItemIllegitimate(Enemy, item);
if (!confiscateItem && Enemy.IsActingOffensively)
{
// Confiscate any weapons or items that can be used offensively.
confiscateItem = item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 };
}
if (confiscateItem)
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
@@ -1169,7 +1287,7 @@ namespace Barotrauma
}
if (matchingItems.Any() &&
!Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
!Enemy.IsUnconscious && Enemy.IsKnockedDownOrRagdolled && character.CanInteractWith(Enemy) && !Enemy.LockHands)
{
var handCuffs = matchingItems.First();
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
@@ -1202,7 +1320,8 @@ namespace Barotrauma
RemoveFollowTarget();
var itemContainer = Weapon.GetComponent<ItemContainer>();
TryAddSubObjective(ref seekAmmunitionObjective,
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager,
spawnItemIfNotFound: !character.IsOnPlayerTeam && character.AIController.HasInfiniteItemSpawns(ammunitionIdentifiers))
{
ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
checkInventory = false,
@@ -1224,10 +1343,9 @@ namespace Barotrauma
/// </summary>
private bool Reload(bool seekAmmo)
{
if (WeaponComponent == null) { return false; }
if (WeaponComponent == null) { return false; }
if (Weapon.OwnInventory == null) { return true; }
// Eject empty ammo
HumanAIController.UnequipEmptyItems(Weapon);
HumanAIController.UnequipEmptyItems(Weapon, allowDestroying: !character.IsOnPlayerTeam);
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
@@ -1267,7 +1385,7 @@ namespace Barotrauma
{
return true;
}
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
else if (IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{
// Inventory not drawn = it's not interactable
// If the weapon is empty and the inventory is inaccessible, it can't be reloaded
@@ -1277,6 +1395,20 @@ namespace Barotrauma
return false;
}
private bool isAimBlocked;
private float _blockedDistance;
private float BlockedDistance
{
get
{
if (_blockedDistance <= 0)
{
_blockedDistance = CloseDistance * Rand.Range(1.0f, 1.3f);
}
return _blockedDistance;
}
}
public List<Vector2> BlockedPositions;
private void Attack(float deltaTime)
{
character.CursorPosition = Enemy.WorldPosition;
@@ -1378,28 +1510,49 @@ namespace Barotrauma
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy),
ignoredBodies: character.AnimController.LimbBodies,
Physics.CollisionCharacter);
foreach (var body in pickedBodies)
{
Character target = body.UserData switch
if (body.UserData is Limb limb)
{
Character c => c,
Limb limb => limb.character,
_ => null
};
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
{
return;
Character target = limb.character;
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
{
// Blocked by a friendly target.
isAimBlocked = true;
if (HumanAIController.DebugAI)
{
BlockedPositions.Add(ConvertUnits.ToDisplayUnits(body.Position));
}
// Stand up, so that we might shoot past the friendlies that are crouching.
allowCrouching = false;
standUpTimer = StandUpCooldown;
return;
}
}
}
UseWeapon(deltaTime);
}
}
}
private bool allowCrouching;
private float standUpTimer;
private const float StandUpCooldown = 5;
private void UseWeapon(float deltaTime)
{
// Never allow friendly crew (bots) to attack with deadly weapons.
if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam && Enemy.IsOnPlayerTeam) { return; }
// Enable this to debug intentional friendly fire.
// if (isLethalWeapon && character.TeamID == Enemy.TeamID && character.IsOnPlayerTeam)
// {
// // Never allow friendly crew (bots) to attack with deadly weapons (this check should be redundant)
// Debugger.Break();
// return;
// }
if (allowCrouching && !isMoving && !character.AnimController.InWater && WeaponComponent is not MeleeWeapon)
{
HumanAIController.AnimController.Crouch();
}
character.SetInput(InputType.Shoot, hit: false, held: true);
Weapon.Use(deltaTime, user: character);
SetReloadTime(WeaponComponent);
@@ -1512,6 +1665,7 @@ namespace Barotrauma
hasAimed = false;
holdFireTimer = 0;
pathBackTimer = 0;
standUpTimer = 0;
isLethalWeapon = false;
canSeeTarget = false;
seekWeaponObjective = null;
@@ -48,6 +48,8 @@ namespace Barotrauma
public int? RemoveMax { get; set; }
public bool MoveWholeStack { get; set; }
public bool AllowStealing { get; set; }
private int _itemCount = 1;
public int ItemCount
@@ -147,11 +149,11 @@ namespace Barotrauma
if (RemoveExisting || (RemoveExistingWhenNecessary && !CanBePut(container.Inventory, TargetSlot, ItemToContain)))
{
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax);
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax, allowDestroying: spawnItemIfNotFound);
}
else if (RemoveEmpty)
{
HumanAIController.UnequipEmptyItems(container.Item);
HumanAIController.UnequipEmptyItems(container.Item, allowDestroying: spawnItemIfNotFound);
}
Inventory originalInventory = ItemToContain.ParentInventory;
var slots = originalInventory?.FindIndices(ItemToContain);
@@ -195,6 +197,7 @@ namespace Barotrauma
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
{
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) ||
@@ -231,7 +234,9 @@ namespace Barotrauma
return (RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)) && container.ShouldBeContained(potentialItem, out _);
},
ItemCount = ItemCount,
TakeWholeStack = MoveWholeStack
TakeWholeStack = MoveWholeStack,
ContainTarget = container,
AllowStealing = AllowStealing
}, onAbandon: () =>
{
Abandon = true;
@@ -14,7 +14,7 @@ namespace Barotrauma
private Deconstructor deconstructor;
private AIObjectiveDecontainItem decontainObjective;
private AIObjectiveMoveItem moveItemObjective;
private AIObjectiveGoTo gotoObjective;
public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
@@ -37,8 +37,8 @@ namespace Barotrauma
}
}
TryAddSubObjective(ref decontainObjective,
constructor: () => new AIObjectiveDecontainItem(character, Item, objectiveManager,
TryAddSubObjective(ref moveItemObjective,
constructor: () => new AIObjectiveMoveItem(character, Item, objectiveManager,
sourceContainer: Item.Container?.GetComponent<ItemContainer>(), targetContainer: deconstructor.InputContainer, priorityModifier: PriorityModifier)
{
Equip = true,
@@ -64,7 +64,7 @@ namespace Barotrauma
Abandon = true;
});
}
RemoveSubObjective(ref decontainObjective);
RemoveSubObjective(ref moveItemObjective);
},
onAbandon: () =>
{
@@ -125,7 +125,7 @@ namespace Barotrauma
public override void Reset()
{
base.Reset();
decontainObjective = null;
moveItemObjective = null;
}
public void DropTarget()
@@ -39,40 +39,39 @@ namespace Barotrauma
// Don't go into rooms with any enemies, unless it's an order
Priority = 0;
Abandon = true;
return Priority;
}
else
// Prioritize fires that currently damage the character.
bool inDamageRange = targetHull.FireSources.Any(fs => fs.IsInDamageRange(character, fs.DamageRange));
float severity = inDamageRange ? 1.0f : AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float distanceFactor = targetHull == character.CurrentHull ? 1.0f
: HumanAIController.VisibleHulls.Contains(targetHull) ? 0.75f : 0.0f;
if (distanceFactor <= 0.0f)
{
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float distanceFactor = 1.0f;
if (targetHull != character.CurrentHull &&
!HumanAIController.VisibleHulls.Contains(targetHull))
{
distanceFactor =
GetDistanceFactor(
new Vector2(character.WorldPosition.Y, characterY),
targetHull.WorldPosition,
verticalDistanceMultiplier: 3,
maxDistance: 5000,
factorAtMaxDistance: 0.1f);
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
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 to prevent casualities unless ordered to extinguish.
Priority = 0;
Abandon = true;
}
else
{
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
distanceFactor =
GetDistanceFactor(
new Vector2(character.WorldPosition.Y, characterY),
targetHull.WorldPosition,
verticalDistanceMultiplier: 3,
maxDistance: 5000,
factorAtMaxDistance: 0.1f);
}
if (!inDamageRange && severity > 0.75f && distanceFactor < 0.75f && !isOrder && character.IsOnPlayerTeam &&
targetHull.RoomName != null &&
!targetHull.RoomName.Contains("reactor", StringComparison.OrdinalIgnoreCase) &&
!targetHull.RoomName.Contains("engine", StringComparison.OrdinalIgnoreCase) &&
!targetHull.RoomName.Contains("command", StringComparison.OrdinalIgnoreCase))
{
// Bots in the player crew ignore severe fires that are not close to the target to prevent casualties unless ordered to extinguish.
Priority = 0;
Abandon = true;
return Priority;
}
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
return Priority;
}
@@ -81,16 +80,16 @@ namespace Barotrauma
private float sinTime;
protected override void Act(float deltaTime)
{
var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher".ToIdentifier());
var extinguisherItem = character.Inventory.FindItemByTag(Tags.FireExtinguisher);
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
{
TryAddSubObjective(ref getExtinguisherObjective, () =>
{
if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher".ToIdentifier(), allowBroken: false))
if (character.IsOnPlayerTeam && !character.HasEquippedItem(Tags.FireExtinguisher, allowBroken: false))
{
character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, "findextinguisher".ToIdentifier(), 30.0f);
character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, Tags.FireExtinguisher, 30.0f);
}
var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher".ToIdentifier(), objectiveManager, equip: true)
var getItemObjective = new AIObjectiveGetItem(character, Tags.FireExtinguisher, objectiveManager, equip: true)
{
AllowStealing = true,
// If the item is inside an unsafe hull, decrease the priority
@@ -124,7 +123,9 @@ namespace Barotrauma
break;
}
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X);
float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y);
// If fire source and the character are on the same level, it's better to ignore the y-axis (e.g. it doesn't matter if we stand or crouch), as the fire size is rectangular.
// If we'd do this while climbing, the character would often get too close to the fire.
float yDist = !character.IsClimbing && MathUtils.NearlyEqual(character.CurrentHull.WorldPosition.Y, targetHull.WorldPosition.Y) ? 0.0f : Math.Abs(character.CurrentHull.WorldPosition.Y - fs.WorldPosition.Y);
float dist = xDist + yDist;
bool inRange = dist < extinguisher.Range;
bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull);
@@ -153,8 +154,8 @@ namespace Barotrauma
{
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f)
{
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
TargetName = fs.Hull.DisplayName,
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachFire,
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
@@ -75,7 +75,7 @@ namespace Barotrauma
}
}
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsHandcuffed && target.IsKnockedDown) { return false; }
if (target.IsHandcuffed) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
return true;
}
@@ -21,7 +21,7 @@ namespace Barotrauma
private Item targetItem;
private int? oxygenSourceSlotIndex;
public const float MIN_OXYGEN = 10;
private const float MinOxygen = 10;
protected override bool CheckObjectiveState() =>
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
@@ -154,9 +154,10 @@ namespace Barotrauma
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
ConditionLevel = MIN_OXYGEN,
ConditionLevel = MinOxygen,
RemoveExistingWhenNecessary = true,
TargetSlot = oxygenSourceSlotIndex
TargetSlot = oxygenSourceSlotIndex,
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _)
};
if (container.HasSubContainers)
{
@@ -199,7 +200,7 @@ namespace Barotrauma
int ReportOxygenTankCount()
{
if (character.Submarine != Submarine.MainSub) { return 1; }
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1);
int remainingOxygenTanks = Submarine.MainSub?.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1) ?? 0;
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f);
@@ -227,7 +228,6 @@ namespace Barotrauma
return true;
}
private bool IsSuitableContainedOxygenSource(Item item)
{
return
@@ -259,7 +259,7 @@ namespace Barotrauma
// 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;
float minOxygen = character.IsInFriendlySub ? MinOxygen : min;
if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag(Tags.OxygenSource) && i.ConditionPercentage >= minOxygen))
{
// There's a valid oxygen tank in the inventory -> no need to swap the tank too early.
@@ -42,9 +42,9 @@ namespace Barotrauma
if (character.CurrentHull == null)
{
Priority = (
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
objectiveManager.CurrentOrder is AIObjectiveGoTo ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => (o is AIObjectiveCombat || o is AIObjectiveReturn) && o.Priority > 0))
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat or AIObjectiveReturn && o.Priority > 0))
&& ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
}
else
@@ -71,6 +71,11 @@ namespace Barotrauma
Priority = AIObjectiveManager.MaxObjectivePriority;
}
}
else if (objectiveManager.CurrentOrder is AIObjectiveGoTo { IsFollowOrder: true })
{
// Ordered to follow -> Don't flee from the enemies/fires (doesn't get here if we need more oxygen).
Priority = 0;
}
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
{
@@ -83,7 +88,7 @@ namespace Barotrauma
Priority = 0;
}
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
if (divingGearObjective is { IsCompleted: false, CanBeCompleted: true, Priority: > 0f })
{
// Boost the priority while seeking the diving gear
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority));
@@ -149,7 +154,13 @@ namespace Barotrauma
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
{
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit, objectiveManager);
if (character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine?.Info is { IsOutpost: true })
{
// In outposts, the NPCs don't try to use diving suits, because otherwise there's probably not enough for those trying to fix the leaks.
// This is not a hard rule: the bots may still grab a suit, unless they find a diving mask.
needsDivingSuit = false;
}
bool needsEquipment = shouldActOnSuffocation;
if (needsDivingSuit)
{
@@ -307,10 +318,10 @@ namespace Barotrauma
}
}
}
if (escapeVel != Vector2.Zero)
if (escapeVel != Vector2.Zero && character.CurrentHull is Hull currentHull)
{
float left = character.CurrentHull.Rect.X + 50;
float right = character.CurrentHull.Rect.Right - 50;
float left = currentHull.Rect.X + 50;
float right = currentHull.Rect.Right - 50;
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
{
@@ -441,7 +452,7 @@ namespace Barotrauma
hullSearchIndex = 0;
#if DEBUG
stopWatch.Stop();
DebugConsole.NewMessage($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms", debugOnly: true);
DebugConsole.Log($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms");
#endif
}
@@ -471,12 +482,47 @@ namespace Barotrauma
}
else
{
// Each unsafe node reduces the hull safety value.
// Ignore the current hull, because otherwise we couldn't find a path out.
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
hullSafety /= 1 + unsafeNodes;
// Check the path safety. Each unsafe node reduces the hull safety value.
Hull previousHull = null;
foreach (WayPoint node in path.Nodes)
{
Hull hull = node.CurrentHull;
if (hull == previousHull)
{
// Let's evaluate each hull only once. If we'd want to make this foolproof, we'd have to add the checked hulls to a list,
// yet in practice there shouldn't be a case where the path would get back to a hull once it has exited it.
continue;
}
previousHull = hull;
if (hull == character.CurrentHull)
{
// Ignore the current hull, because otherwise we couldn't find a path out.
continue;
}
if (HumanAIController.UnsafeHulls.Contains(hull))
{
// Compare safety of the node hull to the current hull safety.
float nodeHullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
if (nodeHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD && nodeHullSafety < HumanAIController.CurrentHullSafety)
{
// If the node hull is considered unsafe and less safe than the current hull, let's ignore the target.
hullSafety = 0;
break;
}
else
{
// Otherwise, each unsafe hull on the path reduces the safety of the target hull by 50% of their threat value.
float hullThreat = 100 - nodeHullSafety;
hullSafety -= hullThreat / 2;
if (hullSafety <= 0)
{
break;
}
}
}
}
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true))
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, includingConnectedSubs: true))
{
hullSafety /= 10;
}
@@ -83,7 +83,7 @@ namespace Barotrauma
{
return false;
}
if (!IsValidTarget(target, character)) { return false; }
if (!CheckTarget(target)) { return false; }
float inspectDist = target.IsCriminal ? CriminalInspectDistance : inspectDistance;
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > inspectDist * inspectDist) { return false; }
if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime))
@@ -145,26 +145,31 @@ namespace Barotrauma
// Might be e.g. sitting on a chair.
character.SelectedSecondaryItem = null;
}
foreach (var target in Character.CharacterList)
if (HumanAIController.CurrentHullSafety >= HumanAIController.HULL_SAFETY_THRESHOLD)
{
if (!IsValidTarget(target, character)) { continue; }
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
if (target.Inventory.AllItems.Any(it => it.Illegitimate && target.HasEquippedItem(it)) &&
character.CanSeeTarget(target, seeThroughWindows: true))
foreach (var target in Character.CharacterList)
{
AIObjectiveCheckStolenItems? existingObjective =
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
if (existingObjective == null)
if (!CheckTarget(target)) { continue; }
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
if (target.Inventory.AllItems.Any(it => target.HasEquippedItem(it) && AIObjectiveCheckStolenItems.IsItemIllegitimate(target, it)) && character.CanSeeTarget(target, seeThroughWindows: true))
{
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
lastInspectionTimes[target] = Timing.TotalTime;
if (HumanAIController.CalculateObjectiveHullSafety(target) >= HumanAIController.HULL_SAFETY_THRESHOLD)
{
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
AIObjectiveCheckStolenItems? existingObjective = objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
if (existingObjective == null)
{
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
lastInspectionTimes[target] = Timing.TotalTime;
}
}
}
}
}
checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval;
}
private bool IsValidTarget(Character target, Character character)
private bool CheckTarget(Character target)
{
if (target == null || target.Removed) { return false; }
if (target.IsIncapacitated) { return false; }
@@ -192,6 +197,16 @@ namespace Barotrauma
}
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
{
MarkTargetAsInspected(target);
}
/// <summary>
/// Marks the targets as being inspected for stolen items (e.g. while arresting the character),
/// meaning characters with this objective won't attempt to trigger an inspection in a while.
/// </summary>
/// <param name="target"></param>
public static void MarkTargetAsInspected(Character target)
{
lastInspectionTimes[target] = Timing.TotalTime;
}
@@ -194,7 +194,7 @@ namespace Barotrauma
{
UseDistanceRelativeToAimSourcePos = true,
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty,
DialogueIdentifier = Leak.FlowTargetHull != null ? AIObjectiveGoTo.DialogCannotReachLeak : Identifier.Empty,
TargetName = Leak.FlowTargetHull?.DisplayName,
requiredCondition = () =>
Leak.Submarine == character.Submarine &&
@@ -31,6 +31,10 @@ namespace Barotrauma
private Item targetItem;
private readonly Item originalTarget;
/// <summary>
/// ItemContainer the bot is trying to put the <see cref="TargetItem"/> into. Only set when the objective is a subobjective of a <see cref="AIObjectiveContainItem"/>.
/// </summary>
public ItemContainer ContainTarget;
private ISpatialEntity moveToTarget;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
@@ -76,6 +80,12 @@ namespace Barotrauma
}
public InvSlotType? EquipSlotType { get; set; }
/// <summary>
/// Tags of items that bots are allowed to take from outposts, when needed. For example when there's not enough oxygen in the room, or if they need to extinguish a fire.
/// The guards won't react if these items are taken by the bots.
/// </summary>
public static readonly Identifier[] AllowedItemsToTake = { Tags.OxygenSource, Tags.FireExtinguisher, Tags.LightDivingGear, Tags.HeavyDivingGear };
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -477,7 +487,17 @@ namespace Barotrauma
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
if (ownerItem != item.Container)
{
itemPriority *= 0.1f;
if (ContainTarget != null && ContainTarget.Item.Prefab.Identifier == item.Container.Prefab.Identifier)
{
// The item is identical to the item we are trying to contain the item to (e.g. trying to find an oxygen source to a mask -> allow to take oxygen sources from other masks)
// Reduce the priority just a tiny bit, so that we choose items that are not inside the items first.
// TODO: Doesn't solve the issue for items that are not the same type but that should be treated the same. E.g. diving mask and clown diving mask.
itemPriority = 0.95f;
}
else
{
itemPriority *= 0.1f;
}
}
}
}
@@ -645,6 +665,11 @@ namespace Barotrauma
if (prefab is not ItemPrefab itemPrefab) { continue; }
if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id)))
{
if (character.AIController.HasInfiniteItemSpawns(prefab.Identifier))
{
// If an item with infinite spawns is defined, let's use it.
return itemPrefab;
}
float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ?
itemPrefab.DefaultPrice.Price :
float.MaxValue;
@@ -95,7 +95,32 @@ namespace Barotrauma
protected override bool AllowOutsideSubmarine => AllowGoingOutside;
protected override bool AllowInAnySub => true;
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
/// <summary>
/// NPC line for when the NPC fails to find a path to a target.
/// Note that this line includes the tag [name], which needs to be replaced with the name of the target.
/// </summary>
public static readonly Identifier DialogCannotReachTarget = "dialogcannotreachtarget".ToIdentifier();
/// <summary>
/// Generic NPC line for when the NPC fails to find a path to some place/target.
/// </summary>
public static readonly Identifier DialogCannotReachPlace = "dialogcannotreachplace".ToIdentifier();
/// <summary>
/// NPC line for when the NPC fails to find a path to a patient they're trying to treat.
/// Note that this line includes the tag [name], which needs to be replaced with the name of the target.
/// </summary>
public static readonly Identifier DialogCannotReachPatient = "dialogcannotreachpatient".ToIdentifier();
/// <summary>
/// NPC line for when the NPC fails to find a path to a fire they're trying to extinguish.
/// Note that this line includes the tag [name], which needs to be replaced with the name of the room the NPC is trying to get to.
/// </summary>
public static readonly Identifier DialogCannotReachFire = "dialogcannotreachfire".ToIdentifier();
/// <summary>
/// NPC line for when the NPC fails to find a path to a leak they're trying to fix.
/// Note that this line includes the tag [name], which needs to be replaced with the name of the room the NPC is trying to get to.
/// </summary>
public static readonly Identifier DialogCannotReachLeak = "dialogcannotreachleak".ToIdentifier();
public Identifier DialogueIdentifier { get; set; } = DialogCannotReachPlace;
private readonly Identifier ExoSuitRefuel = "dialog.exosuit.refuel".ToIdentifier();
private readonly Identifier ExoSuitOutOfFuel = "dialog.exosuit.outoffuel".ToIdentifier();
@@ -116,12 +141,12 @@ namespace Barotrauma
Abandon = !isOrder;
return Priority;
}
if (Target == null || Target is Entity e && e.Removed)
if (Target is null or Entity { Removed: true })
{
Priority = 0;
Abandon = !isOrder;
}
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
if (IgnoreIfTargetDead && Target is Character { IsDead: true })
{
Priority = 0;
Abandon = !isOrder;
@@ -182,6 +207,17 @@ namespace Barotrauma
if (DialogueIdentifier == null) { return; }
if (!SpeakIfFails) { return; }
if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; }
if (TargetName == null && DialogueIdentifier == DialogCannotReachTarget)
{
#if DEBUG
DebugConsole.ThrowError(
$"Error in {nameof(SpeakCannotReach)}: "+
$"attempted to use a dialog line that mentions the target (dialogue identifier: {DialogueIdentifier}), but the name of the target ({(Target?.ToString() ?? "null")}) isn't set.");
#endif
DialogueIdentifier = DialogCannotReachPlace;
}
LocalizedString msg = TargetName == null ?
TextManager.Get(DialogueIdentifier) :
TextManager.GetWithVariable(DialogueIdentifier, "[name]".ToIdentifier(), TargetName, formatCapitals: Target is Character ? FormatCapitals.No : FormatCapitals.Yes);
@@ -342,34 +378,43 @@ namespace Barotrauma
}
}
if (Abandon) { return; }
if (getDivingGearIfNeeded)
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
Character followTarget = Target as Character;
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;
}
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
tryToGetDivingGear = true;
}
else if (tryToGetDivingGear)
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
}
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (!getDivingGearIfNeeded)
{
if (needsEquipment)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
// Don't try to reach the target without proper equipment.
Abandon = true;
return;
}
}
else
{
if (character.LockHands)
{
cantFindDivingGear = true;
@@ -794,6 +839,11 @@ namespace Barotrauma
// Going through a hatch
return false;
}
if (Target is Item targetItem && targetItem.GetComponent<Pickable>() == null)
{
// Targeting a static item, such as a reactor or a controller -> Don't complete, until we are no longer climbing.
return false;
}
}
}
if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater)
@@ -893,5 +943,35 @@ namespace Barotrauma
pathSteering.ResetPath();
}
}
public bool ShouldRun(bool run)
{
if (run && objectiveManager.ForcedOrder == this && IsWaitOrder && !character.IsOnPlayerTeam)
{
// NPCs with a wait order don't run.
run = false;
}
else if (Target != null)
{
if (character.CurrentHull == null)
{
run = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > 300 * 300;
}
else
{
float yDiff = Target.WorldPosition.Y - character.WorldPosition.Y;
if (Math.Abs(yDiff) > 100)
{
run = true;
}
else
{
float xDiff = Target.WorldPosition.X - character.WorldPosition.X;
run = Math.Abs(xDiff) > 500;
}
}
}
return run;
}
}
}
@@ -185,135 +185,162 @@ namespace Barotrauma
IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
if (behavior == BehaviorType.StayInHull && TargetHull != null && !currentTargetIsInvalid && !IsForbidden(TargetHull))
{
currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
if (stayInHull)
if (HumanAIController.UnsafeHulls.Contains(TargetHull))
{
Wander(deltaTime);
}
else if (currentTarget != null)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
// Ask to refresh, because otherwise we can't get back to the hull.
HumanAIController.AskToRecalculateHullSafety(TargetHull);
}
else
{
PathSteering.ResetPath();
PathSteering.Reset();
currentTarget = TargetHull;
NavigateTo(currentTarget);
return;
}
}
if (currentTarget != null && !currentTargetIsInvalid)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
{
if (currentTarget.Submarine.TeamID != character.TeamID)
{
currentTargetIsInvalid = true;
}
}
else
{
if (currentTarget.Submarine != character.Submarine)
{
currentTargetIsInvalid = true;
}
}
}
if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
{
if (newTargetTimer > timerMargin)
{
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
//almost constantly when there's a small number of potential hulls to move to
SetTargetTimerLow();
}
}
else if (character.IsClimbing)
{
if (currentTarget == null)
{
SetTargetTimerLow();
}
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
{
// Don't allow new targets when climbing straight up or down
SetTargetTimerHigh();
}
}
else if (character.AnimController.InWater)
{
if (currentTarget == null)
{
SetTargetTimerLow();
}
}
if (newTargetTimer <= 0.0f)
{
if (!searchingNewHull)
{
//find all available hulls first
searchingNewHull = true;
FindTargetHulls();
}
else if (targetHulls.Any())
{
//choose a random available hull
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);
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
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
return true;
//don't stop at ladders when idling
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
SetTargetTimerLow();
return;
}
character.AIController.SelectTarget(currentTarget.AiTarget);
PathSteering.SetPath(targetPos, path);
SetTargetTimerNormal();
searchingNewHull = false;
}
else
{
// Couldn't find a valid hull
SetTargetTimerHigh();
searchingNewHull = false;
}
}
newTargetTimer -= deltaTime;
if (currentTarget == null || PathSteering.CurrentPath == null)
{
Wander(deltaTime);
}
else
{
if (currentTarget != null && !currentTargetIsInvalid)
NavigateTo(currentTarget);
}
void NavigateTo(Hull target)
{
bool isAtTarget = character.CurrentHull == target && IsSteeringFinished();
if (isAtTarget)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
if (character.IsClimbing)
{
if (currentTarget.Submarine.TeamID != character.TeamID)
StopMoving();
if (character.AnimController.GetHeightFromFloor() < character.AnimController.ImpactTolerance / 2)
{
currentTargetIsInvalid = true;
character.StopClimbing();
}
}
else
{
if (currentTarget.Submarine != character.Submarine)
{
currentTargetIsInvalid = true;
}
Wander(deltaTime);
}
}
if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
else if (target != null)
{
if (newTargetTimer > timerMargin)
{
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
//almost constantly when there's a small number of potential hulls to move to
SetTargetTimerLow();
}
}
else if (character.IsClimbing)
{
if (currentTarget == null)
{
SetTargetTimerLow();
}
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
{
// Don't allow new targets when climbing straight up or down
SetTargetTimerHigh();
}
}
else if (character.AnimController.InWater)
{
if (currentTarget == null)
{
SetTargetTimerLow();
}
}
if (newTargetTimer <= 0.0f)
{
if (!searchingNewHull)
{
//find all available hulls first
searchingNewHull = true;
FindTargetHulls();
}
else if (targetHulls.Any())
{
//choose a random available hull
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);
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
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
return true;
//don't stop at ladders when idling
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
SetTargetTimerLow();
return;
}
character.AIController.SelectTarget(currentTarget.AiTarget);
PathSteering.SetPath(targetPos, path);
SetTargetTimerNormal();
searchingNewHull = false;
}
else
{
// Couldn't find a valid hull
SetTargetTimerHigh();
searchingNewHull = false;
}
}
newTargetTimer -= deltaTime;
if (!character.IsClimbing && (PathSteering == null || PathSteering.CurrentPath == null || IsSteeringFinished()))
{
Wander(deltaTime);
}
else if (currentTarget != null)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1,
nodeFilter: node => node.Waypoint.CurrentHull != null,
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
PathTo(target);
}
else
{
PathSteering.ResetPath();
PathSteering.Reset();
StopMoving();
}
}
void StopMoving()
{
SteeringManager.Reset();
PathSteering.ResetPath();
}
void PathTo(ISpatialEntity target)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(target), weight: 1,
nodeFilter: node => node.Waypoint.CurrentHull != null,
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
}
}
public void Wander(float deltaTime)
@@ -22,7 +22,7 @@ namespace Barotrauma
private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
private int itemIndex;
private AIObjectiveDecontainItem decontainObjective;
private AIObjectiveMoveItem moveItemObjective;
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
private Item targetItem;
private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable";
@@ -196,17 +196,17 @@ namespace Barotrauma
float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100;
float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2);
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
if (decontainObjective != null && targetItem.Container != Container)
if (moveItemObjective != null && targetItem.Container != Container)
{
if (!IsValidContainable(targetItem))
{
// Target is not valid anymore, abandon the objective
decontainObjective.Abandon = true;
moveItemObjective.Abandon = true;
}
else if (!ItemContainer.Inventory.CanBePut(targetItem) && ItemContainer.Inventory.AllItems.None(i => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition)))
{
// The container is full and there's no item that should be removed, abandon the objective
decontainObjective.Abandon = true;
moveItemObjective.Abandon = true;
}
}
if (ItemContainer.Inventory.IsFull())
@@ -257,26 +257,27 @@ namespace Barotrauma
}
else
{
if(decontainObjective == null && !IsValidContainable(targetItem))
if(moveItemObjective == null && !IsValidContainable(targetItem))
{
IgnoreTargetItem();
Reset();
return;
}
TryAddSubObjective(ref decontainObjective,
constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
TryAddSubObjective(ref moveItemObjective,
constructor: () => new AIObjectiveMoveItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
{
AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem),
AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier,
Equip = true,
RemoveExistingWhenNecessary = true,
RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
RemoveExistingMax = 1
RemoveExistingMax = 1,
AllowToFindDivingGear = objectiveManager.HasOrder<AIObjectiveLoadItems>()
},
onCompleted: () =>
{
IsCompleted = true;
RemoveSubObjective(ref decontainObjective);
RemoveSubObjective(ref moveItemObjective);
},
onAbandon: () =>
{
@@ -324,7 +325,7 @@ namespace Barotrauma
{
base.Reset();
// Don't reset the target item when resetting the objective because it affects priority calculations
decontainObjective = null;
moveItemObjective = null;
itemIndex = 0;
}
@@ -35,7 +35,7 @@ namespace Barotrauma
/// </summary>
public const float HighestOrderPriority = 70;
/// <summary>
/// Maximum priority of an order given to the character (rightmost order in the crew list)
/// Minimum priority of an order given to the character (rightmost order in the crew list)
/// </summary>
public const float LowestOrderPriority = 60;
/// <summary>
@@ -485,7 +485,7 @@ namespace Barotrauma
IgnoreIfTargetDead = true,
IsFollowOrder = true,
Mimic = character.IsOnPlayerTeam,
DialogueIdentifier = "dialogcannotreachplace".ToIdentifier()
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPlace
};
break;
case "wait":
@@ -724,6 +724,11 @@ namespace Barotrauma
/// </summary>
public bool HasObjectiveOrOrder<T>() where T : AIObjective => Objectives.Any(o => o is T) || HasOrder<T>();
/// <summary>
/// Returns the current objective or its currently active subobjective (first in chain), regadless of the type.
/// Note: Not recursive, and thus doesn't work for deeper hierarchy!
/// For seeking objectives of specific type and in a deep hierarchy, use <see cref="GetLastActiveObjective{T}"/> or with looping objectives <see cref="GetFirstActiveObjective{T}"/>
/// </summary>
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
@@ -740,7 +745,8 @@ namespace Barotrauma
/// Returns the last active objective of the specified objective type.
/// Should generally be used to get the active objective (or subobjective) of objectives that don't sort their subobjectives by priority (see <see cref="AIObjective.AllowSubObjectiveSorting"/>.
/// </summary>
/// <returns>The last active objective of the specified type if found.
/// <returns>
/// The last active objective of the specified type if found.
/// </returns>
public T GetLastActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
@@ -763,7 +769,12 @@ namespace Barotrauma
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).OfType<T>();
}
/// <summary>
/// Is the current objective or any of its subobjectives of the given type?
/// Useful for checking whether the bot has a certain type of objective active in the hierarchy.
/// </summary>
/// <returns>False for objectives and orders that are not currently active.</returns>
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
public bool IsOrder(AIObjective objective)
@@ -6,9 +6,9 @@ using System.Linq;
namespace Barotrauma
{
class AIObjectiveDecontainItem : AIObjective
class AIObjectiveMoveItem : AIObjective
{
public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier();
public override Identifier Identifier { get; set; } = "move item".ToIdentifier();
protected override bool AllowWhileHandcuffed => false;
public Func<Item, float> GetItemPriority;
@@ -47,8 +47,15 @@ namespace Barotrauma
public int? RemoveExistingMax { get; set; }
public string AbandonGetItemDialogueIdentifier { get; set; }
public Func<bool> AbandonGetItemDialogueCondition { get; set; }
/// <summary>
/// By default, finding diving gear is not allowed here, because it can cause unexpected behavior in most use cases.
/// E.g. bots equipping diving suits to clean up some items in flooded rooms.
/// Sometimes, at least when used in orders, we might want to allow this. See <see cref="AIObjectiveLoadItem"/>.
/// </summary>
public bool AllowToFindDivingGear { get; set; }
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
public AIObjectiveMoveItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.targetItem = targetItem;
@@ -56,10 +63,10 @@ namespace Barotrauma
this.targetContainer = targetContainer;
}
public AIObjectiveDecontainItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
public AIObjectiveMoveItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: this(character, new Identifier[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
public AIObjectiveMoveItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
@@ -75,16 +82,16 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
Item itemToDecontain =
Item itemToMove =
targetItem ??
sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI(character)), recursive: false);
if (itemToDecontain == null)
if (itemToMove == null)
{
Abandon = true;
return;
}
if (itemToDecontain.IgnoreByAI(character))
if (itemToMove.IgnoreByAI(character))
{
Abandon = true;
return;
@@ -96,19 +103,19 @@ namespace Barotrauma
Abandon = true;
return;
}
if (itemToDecontain.Container != sourceContainer.Item)
if (itemToMove.Container != sourceContainer.Item)
{
itemToDecontain.Drop(character);
itemToMove.Drop(character);
IsCompleted = true;
return;
}
}
else if (targetContainer.Inventory.Contains(itemToDecontain))
else if (targetContainer.Inventory.Contains(itemToMove))
{
IsCompleted = true;
return;
}
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
if (getItemObjective == null && !itemToMove.IsOwnedBy(character))
{
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip)
@@ -116,7 +123,8 @@ namespace Barotrauma
CannotFindDialogueCondition = AbandonGetItemDialogueCondition,
CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier,
SpeakIfFails = AbandonGetItemDialogueIdentifier != null,
TakeWholeStack = this.TakeWholeStack
TakeWholeStack = TakeWholeStack,
AllowToFindDivingGear = AllowToFindDivingGear
},
onAbandon: () => Abandon = true);
return;
@@ -124,7 +132,7 @@ namespace Barotrauma
if (targetContainer != null)
{
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
constructor: () => new AIObjectiveContainItem(character, itemToMove, targetContainer, objectiveManager)
{
MoveWholeStack = TakeWholeStack,
Equip = Equip,
@@ -133,14 +141,15 @@ namespace Barotrauma
RemoveExistingPredicate = RemoveExistingPredicate,
RemoveMax = RemoveExistingMax,
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = sourceContainer?.Item.Prefab.Identifier.ToEnumerable().ToImmutableHashSet()
ignoredContainerIdentifiers = sourceContainer?.Item.Prefab.Identifier.ToEnumerable().ToImmutableHashSet(),
AllowToFindDivingGear = AllowToFindDivingGear
},
onCompleted: () => IsCompleted = true,
onAbandon: () => Abandon = true);
}
else
{
itemToDecontain.Drop(character);
itemToMove.Drop(character);
IsCompleted = true;
}
}
@@ -1,5 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -89,6 +89,17 @@ namespace Barotrauma
Priority = 0;
return Priority;
}
Hull targetHull = targetItem.CurrentHull;
if (HumanAIController.UnsafeHulls.Contains(targetHull))
{
// Ignore the objective, if the target hull is dangerous.
Priority = 0;
if (isOrder && this == objectiveManager.CurrentObjective && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("dialogoperatetargetroomisunsafe", "[item]", targetItem.Name).Value, delay: 1.0f, identifier: "dialogoperatetargetroomisunsafe".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
}
return Priority;
}
var reactor = component.Item.GetComponent<Reactor>();
if (reactor != null)
{
@@ -137,10 +148,8 @@ namespace Barotrauma
}
if (targetItem.CurrentHull == null ||
targetItem.Submarine != character.Submarine && !isOrder ||
targetItem.CurrentHull.FireSources.Any() ||
IsItemOperatedByAnother(target) ||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))
|| component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character))
component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character))
{
Priority = 0;
}
@@ -246,6 +255,7 @@ namespace Barotrauma
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
TargetName = target.Item.Name,
endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item)
},
@@ -234,6 +234,7 @@ namespace Barotrauma
{
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
{
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
TargetName = Item.Name,
SpeakCannotReachCondition = () => isPriority
};
@@ -161,7 +161,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPatient,
TargetName = Target.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
@@ -197,13 +197,16 @@ namespace Barotrauma
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
safeHull = character.CurrentHull;
});
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager)
{
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPlace
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
safeHull = character.CurrentHull;
});
}
}
}
@@ -221,7 +224,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPatient,
TargetName = Target.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
@@ -20,7 +20,7 @@ namespace Barotrauma
Target = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
if (Target == null)
{
if (GameMain.GameSession.GameMode is not TestGameMode)
if (GameMain.GameSession?.GameMode is not TestGameMode)
{
DebugConsole.AddWarning($"({character.DisplayName}) No suitable return target found. Cannot return back to the main sub.");
}
@@ -170,7 +170,7 @@ namespace Barotrauma
private readonly List<PathNode> sortedNodes;
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)
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, float outsideNodePenalty = 0)
{
foreach (PathNode node in nodes)
{
@@ -325,7 +325,12 @@ namespace Barotrauma
#endif
return new SteeringPath(true);
}
return FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize);
float outsideNodeCostPenalty = outsideNodePenalty;
if (ApplyPenaltyToOutsideNodes)
{
outsideNodeCostPenalty += 100;
}
return FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize, outsideNodeCostPenalty);
bool IsValidStartNode(PathNode node) => IsValidNode(node, (isCharacter, start), startNodeFilter);
@@ -356,7 +361,7 @@ namespace Barotrauma
}
}
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "", float minGapSize = 0)
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "", float minGapSize = 0f, float outsideNodePenalty = 0f)
{
if (start == end)
{
@@ -398,50 +403,65 @@ namespace Barotrauma
for (int i = 0; i < currNode.connections.Count; i++)
{
PathNode nextNode = currNode.connections[i];
//a node that hasn't been searched yet
if (nextNode.state == 0)
{
nextNode.H = Vector2.Distance(nextNode.Position, end.Position);
float penalty = 0.0f;
if (GetNodePenalty != null)
switch (nextNode.state)
{
//a node that hasn't been searched yet
case 0:
{
float? nodePenalty = GetNodePenalty(currNode, nextNode);
if (nodePenalty == null)
nextNode.H = Vector2.Distance(nextNode.Position, end.Position);
float cost = CalculateNodeCost();
if (cost < float.PositiveInfinity)
{
nextNode.state = -1;
continue;
nextNode.G = cost;
nextNode.F = nextNode.G + nextNode.H;
nextNode.Parent = currNode;
nextNode.state = 1;
}
penalty = nodePenalty.Value;
else
{
// Set searched and invalid.
nextNode.state = -1;
}
break;
}
//node that has been searched
case 1 or -1:
{
float tempG = CalculateNodeCost();
//only use if this new route is better than the
//route the node was a part of
if (tempG < nextNode.G)
{
nextNode.G = tempG;
nextNode.F = nextNode.G + nextNode.H;
nextNode.Parent = currNode;
nextNode.state = 1;
}
break;
}
nextNode.G = currNode.G + currNode.distances[i] + penalty;
nextNode.F = nextNode.G + nextNode.H;
nextNode.Parent = currNode;
nextNode.state = 1;
}
//node that has been searched
else if (nextNode.state == 1 || nextNode.state == -1)
float CalculateNodeCost()
{
float tempG = currNode.G + currNode.distances[i];
float penalty = 0f;
if (GetNodePenalty != null)
{
float? nodePenalty = GetNodePenalty(currNode, nextNode);
if (nodePenalty == null) { continue; }
tempG += nodePenalty.Value;
if (nodePenalty.HasValue)
{
penalty += nodePenalty.Value;
}
else
{
return float.PositiveInfinity;
}
}
//only use if this new route is better than the
//route the node was a part of
if (tempG < nextNode.G)
if (currNode.Waypoint.CurrentHull == null)
{
nextNode.G = tempG;
nextNode.F = nextNode.G + nextNode.H;
nextNode.Parent = currNode;
nextNode.state = 1;
penalty += outsideNodePenalty;
}
return currNode.G + currNode.distances[i] + penalty;
}
}
}
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
@@ -160,7 +160,8 @@ namespace Barotrauma
aggregate += Items[i].Commonness;
if (aggregate >= r && Items[i].Prefab != null)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
//disabled to reduce the amount of data we collect through GA
//GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner?.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition);
break;
}
@@ -293,10 +294,15 @@ namespace Barotrauma
return false;
}
public bool CanPlayWith(Character player)
{
return AIController.Character.IsOnFriendlyTeam(player);
}
public void Play(Character player)
{
if (PlayTimer > 0.0f) { return; }
if (!AIController.Character.IsFriendly(player)) { return; }
if (!CanPlayWith(player)) { return; }
if (ToggleOwner)
{
Owner = Owner == player ? null : player;