Merge remote-tracking branch 'origin/master' into dev

# Conflicts:
#	Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs
#	Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs
#	Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs
#	Barotrauma/BarotraumaClient/LinuxClient.csproj
#	Barotrauma/BarotraumaClient/MacClient.csproj
#	Barotrauma/BarotraumaClient/WindowsClient.csproj
#	Barotrauma/BarotraumaServer/LinuxServer.csproj
#	Barotrauma/BarotraumaServer/MacServer.csproj
#	Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs
#	Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs
#	Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs
#	Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs
#	Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs
#	Barotrauma/BarotraumaServer/WindowsServer.csproj
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs
#	Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Enums.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs
#	Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs
#	Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs
#	Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs
#	Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs
#	Barotrauma/BarotraumaShared/changelog.txt
This commit is contained in:
Regalis11
2023-06-15 16:55:39 +03:00
238 changed files with 5395 additions and 3045 deletions
@@ -95,10 +95,7 @@ namespace Barotrauma
{
get
{
if (visibleHulls == null)
{
visibleHulls = Character.GetVisibleHulls();
}
visibleHulls ??= Character.GetVisibleHulls();
return visibleHulls;
}
private set
@@ -107,12 +104,26 @@ namespace Barotrauma
}
}
public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) =>
steeringManager is IndoorsSteeringManager pathSteering &&
pathSteering.CurrentPath != null &&
(!requireUnfinished || !pathSteering.CurrentPath.Finished) &&
!pathSteering.CurrentPath.Unreachable &&
(!requireNonDirty || !pathSteering.IsPathDirty);
/// <summary>
/// Is the current path valid, using the provided parameters.
/// </summary>
/// <param name="requireNonDirty"></param>
/// <param name="requireUnfinished"></param>
/// <param name="nodePredicate"></param>
/// <returns>When <paramref name="nodePredicate"/> is defined, returns false if any of the nodes fails to match the predicate.</returns>
public bool HasValidPath(bool requireNonDirty = true, bool requireUnfinished = true, Func<WayPoint, bool> nodePredicate = null)
{
if (SteeringManager is not IndoorsSteeringManager pathSteering) { return false; }
if (pathSteering.CurrentPath == null) { return false; }
if (pathSteering.CurrentPath.Unreachable) { return false; }
if (requireUnfinished && pathSteering.CurrentPath.Finished) { return false; }
if (requireNonDirty && pathSteering.IsPathDirty) { return false; }
if (nodePredicate != null)
{
return pathSteering.CurrentPath.Nodes.All(n => nodePredicate(n));
}
return true;
}
public bool IsCurrentPathNullOrUnreachable => IsCurrentPathUnreachable || steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath == null;
public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable;
@@ -251,7 +262,7 @@ namespace Barotrauma
}
private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false, IEnumerable<Identifier> targetTags = null)
{
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
@@ -265,23 +276,28 @@ namespace Barotrauma
}
else
{
var holdable = item.GetComponent<Holdable>();
if (holdable != null)
{
pickable = holdable;
}
// Not allowed to wear -> don't use the Wearable component even when it's found.
pickable = item.GetComponent<Holdable>();
}
if (item.ParentInventory is ItemInventory itemInventory)
{
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
}
if (equip)
if (equip && pickable != null)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
if (!wear)
{
if (slots != InvSlotType.RightHand && slots != InvSlotType.LeftHand && slots != (InvSlotType.RightHand | InvSlotType.LeftHand))
{
// Don't allow other than hand slots if not allowed to wear.
continue;
}
}
for (int i = 0; i < targetInventory.Capacity; i++)
{
if (targetInventory is CharacterInventory characterInventory)
@@ -294,7 +310,7 @@ namespace Barotrauma
var otherItem = targetInventory.GetItemAt(i);
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot))
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.AnySlot))
{
if (storeUnequipped && targetInventory.Owner == Character)
{
@@ -304,6 +320,11 @@ namespace Barotrauma
}
if (dropOtherIfCannotMove)
{
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
{
// Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory.
return false;
}
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
}
@@ -314,7 +335,7 @@ namespace Barotrauma
}
else
{
return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot);
return targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot);
}
}
@@ -339,7 +360,7 @@ namespace Barotrauma
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 (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
{
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
continue;
@@ -401,14 +422,9 @@ namespace Barotrauma
var door = gap.ConnectedDoor;
if (door != null)
{
if (!door.CanBeTraversed)
if (!pathSteering.CanAccessDoor(door))
{
if (!door.HasAccess(Character))
{
if (!canAttackDoors) { continue; }
// Treat doors that don't have access to like they were farther, because it will take time to break them.
multiplier = 5;
}
continue;
}
}
else
@@ -449,9 +465,10 @@ namespace Barotrauma
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared();
bool isClose = sqrDist < MathUtils.Pow2(100);
if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathNullOrUnreachable || IsCurrentPathFinished)
if (Character.CurrentHull == null || (isClose && !isClosedDoor) || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished)
{
// Very close to the target, outside, or at the end of the path -> try to steer through the gap
Character.ReleaseSecondaryItem();
SteeringManager.Reset();
pathSteering?.ResetPath();
Vector2 dir = Vector2.Normalize(diff);
@@ -369,7 +369,13 @@ namespace Barotrauma
}
else if (targetCharacter.AIController is EnemyAIController enemy)
{
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
if (enemy.PetBehavior != null && (PetBehavior != null || AIParams.HasTag("pet")))
{
// Pets see other pets as pets by default.
// Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below.
targetingTag = "pet";
}
else if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
{
targetingTag = "husk";
}
@@ -480,7 +486,7 @@ namespace Barotrauma
if (SelectedAiTarget?.Entity != null || EscapeTarget != null)
{
Entity t = SelectedAiTarget?.Entity ?? EscapeTarget;
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(requireNonDirty: true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath() ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left;
}
else
@@ -695,6 +701,9 @@ namespace Barotrauma
// Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group.
if (Character.IsSameSpeciesOrGroup(c)) { return false; }
if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; }
//don't try to attack targets in a sub that belongs to a different team
//(for example, targets in an outpost if we're in the main sub)
if (c.Submarine?.TeamID != Character.Submarine?.TeamID) { return false; }
if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
{
return a.Damage >= selectedTargetingParams.Threshold;
@@ -894,7 +903,7 @@ namespace Barotrauma
_previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
{
// Keep heading to the last known position of the target
var memory = GetTargetMemory(target, false);
var memory = GetTargetMemory(target);
if (memory != null)
{
var location = memory.Location;
@@ -981,7 +990,7 @@ namespace Barotrauma
}
else
{
PathSteering.SetPath(path);
PathSteering.SetPath(patrolTarget.SimPosition, path);
patrolTimerMargin = 0;
newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f);
searchingNewHull = false;
@@ -1088,13 +1097,13 @@ namespace Barotrauma
Character owner = GetOwner(item);
if (owner != null)
{
if (Character.IsFriendly(owner))
if (Character.IsFriendly(owner) || owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{
ResetAITarget();
State = AIState.Idle;
return;
}
else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
else
{
SelectedAiTarget = owner.AiTarget;
}
@@ -2186,7 +2195,7 @@ namespace Barotrauma
}
}
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true);
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true, keepAlive: true);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt;
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
@@ -2531,8 +2540,10 @@ namespace Barotrauma
if (Math.Abs(limbDiff.X) < itemBodyExtent &&
Math.Abs(limbDiff.Y) < Character.AnimController.Collider.GetMaxExtent() + Character.AnimController.ColliderHeightFromFloor)
{
Vector2 velocity = limbDiff;
if (limbDiff.LengthSquared() > 0.01f) { velocity = Vector2.Normalize(velocity); }
item.body.LinearVelocity *= 0.9f;
item.body.LinearVelocity -= limbDiff * 0.25f;
item.body.LinearVelocity -= velocity * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
@@ -2924,7 +2935,8 @@ namespace Barotrauma
}
}
}
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0)
//no need to eat if the character is already in full health (except if it's a pet - pets actually need to eat to stay alive, not just to regain health)
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0 && !Character.IsPet)
{
valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f);
}
@@ -3021,7 +3033,7 @@ namespace Barotrauma
//if the target is very close, the distance doesn't make much difference
// -> just ignore the distance and target whatever has the highest priority
dist = Math.Max(dist, 100.0f);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true, keepAlive: SelectedAiTarget != aiTarget);
if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null)
{
float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y;
@@ -3090,12 +3102,20 @@ namespace Barotrauma
if (aiTarget.Entity is Item i)
{
Character owner = GetOwner(i);
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (owner == Character) { continue; }
if (owner != null && (Character.IsFriendly(owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)))
if (owner != null)
{
continue;
if (owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)) { continue; }
if (Character.IsFriendly(owner))
{
// Don't target items that we own. This is a rare case, and almost entirely related to Humanhusks (in the vanilla game).
continue;
}
if (owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{
// ignore if owner is tagged to be explicitly ignored (Feign Death)
continue;
}
}
}
if (targetCharacter != null)
@@ -3418,7 +3438,7 @@ namespace Barotrauma
return false;
}
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound)
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound = false, bool keepAlive = false)
{
if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
{
@@ -3428,9 +3448,8 @@ namespace Barotrauma
targetMemories.Add(target, memory);
}
}
if (addIfNotFound)
if (keepAlive)
{
// Keep the memory alive.
memory.Priority = Math.Max(memory.Priority, minPriority);
}
return memory;
@@ -3446,7 +3465,7 @@ namespace Barotrauma
}
else if (CanPerceive(_selectedAiTarget, checkVisibility: false))
{
var memory = GetTargetMemory(_selectedAiTarget, false);
var memory = GetTargetMemory(_selectedAiTarget);
if (memory != null)
{
memory.Location = _selectedAiTarget.WorldPosition;
@@ -3643,7 +3662,11 @@ namespace Barotrauma
{
isStateChanged = true;
SetStateResetTimer();
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
if (!Character.IsPet || !target.IsHuman)
{
//don't turn pets hostile to all humans when attacked by one
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
}
if (target.IsHuman)
{
priority = GetTargetParams("human")?.Priority;
@@ -3934,7 +3957,7 @@ namespace Barotrauma
{
SteerAwayFromTheEnemy();
}
else if (canAttackDoors && HasValidPath(requireNonDirty: true, requireUnfinished: true))
else if (canAttackDoors && HasValidPath())
{
var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
@@ -10,7 +10,7 @@ namespace Barotrauma
{
partial class HumanAIController : AIController
{
public static bool debugai;
public static bool DebugAI;
public static bool DisableCrewAI;
private readonly AIObjectiveManager objectiveManager;
@@ -42,6 +42,8 @@ namespace Barotrauma
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>();
private readonly HashSet<Hull> dirtyHullSafetyCalculations = new HashSet<Hull>();
private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f;
private bool wasConscious;
@@ -55,6 +57,7 @@ namespace Barotrauma
private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5;
private float obstacleRaycastTimer;
private bool isBlocked;
private readonly float enemyCheckInterval = 0.2f;
private readonly float enemySpotDistanceOutside = 800;
@@ -92,7 +95,10 @@ namespace Barotrauma
private readonly SteeringManager outsideSteering, insideSteering;
public bool UseIndoorSteeringOutside { get; set; } = false;
/// <summary>
/// Waypoints that are not linked to a sub (e.g. main path).
/// </summary>
public bool UseOutsideWaypoints { get; private set; }
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
@@ -225,14 +231,15 @@ namespace Barotrauma
IgnoredItems.Clear();
}
bool IsCloseEnoughToTarget(float threshold, bool useTargetSub = true)
// Note: returns false when useTargetSub is 'true' and the target is outside (targetSub is 'null')
bool IsCloseEnoughToTarget(float threshold, bool targetSub = true)
{
Entity target = SelectedAiTarget?.Entity;
if (target == null)
{
return false;
}
if (useTargetSub)
if (targetSub)
{
if (target.Submarine is Submarine sub)
{
@@ -244,62 +251,71 @@ namespace Barotrauma
return false;
}
}
return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow(threshold, 2);
return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow2(threshold);
}
bool hasValidPath = HasValidPath();
if (Character.Submarine == null)
bool isOutside = Character.Submarine == null;
if (isOutside)
{
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
{
bool hasValidPath = HasValidPath();
isBlocked = false;
UseOutsideWaypoints = false;
obstacleRaycastTimer = obstacleRaycastIntervalLong;
if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity is ISpatialEntity target && target.Submarine == null || !IsCloseEnoughToTarget(2000, useTargetSub: false))
ISpatialEntity spatialTarget = SelectedAiTarget?.Entity ?? ObjectiveManager.GetLastActiveObjective<AIObjectiveGoTo>()?.Target;
if (spatialTarget != null && (spatialTarget.Submarine == null || !IsCloseEnoughToTarget(2000, targetSub: false)))
{
// If the target is behind a level wall, switch to the pathing to get around the obstacles.
ISpatialEntity spatialTarget = SelectedAiTarget?.Entity;
if (spatialTarget == null)
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
Vector2 rayEnd = spatialTarget.SimPosition;
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
{
var gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>();
spatialTarget = gotoObjective?.Target;
rayEnd += targetSub.SimPosition;
ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
}
if (spatialTarget == null)
var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
isBlocked = obstacle != null;
// Don't use outside waypoints when blocked by a sub, because we should use the waypoints linked to the sub instead.
UseOutsideWaypoints = isBlocked && (obstacle.UserData is not Submarine sub || sub.Info.IsRuin);
bool resetPath = false;
if (UseOutsideWaypoints)
{
UseIndoorSteeringOutside = false;
bool isUsingInsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine != null || n.Ruin != null);
if (isUsingInsideWaypoints)
{
resetPath = true;
}
}
else
{
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
Vector2 rayEnd = spatialTarget.SimPosition;
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
bool isUsingOutsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine == null && n.Ruin == null);
if (isUsingOutsideWaypoints)
{
rayEnd += targetSub.SimPosition;
ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
resetPath = true;
}
var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
UseIndoorSteeringOutside = obstacle != null;
}
if (resetPath)
{
PathSteering.ResetPath();
}
}
else
else if (hasValidPath)
{
UseIndoorSteeringOutside = false;
if (hasValidPath)
obstacleRaycastTimer = obstacleRaycastIntervalShort;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
{
obstacleRaycastTimer = obstacleRaycastIntervalShort;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
PathSteering.CurrentPath.Unreachable = true;
break;
}
PathSteering.CurrentPath.Unreachable = true;
break;
}
}
}
@@ -307,10 +323,11 @@ namespace Barotrauma
}
else
{
UseIndoorSteeringOutside = false;
UseOutsideWaypoints = false;
isBlocked = false;
}
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
if (isOutside || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
{
// Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
@@ -352,12 +369,13 @@ namespace Barotrauma
}
}
}
if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer))
bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
if (useInsideSteering)
{
if (steeringManager != insideSteering)
{
insideSteering.Reset();
PathSteering.ResetPath();
steeringManager = insideSteering;
}
if (IsCloseEnoughToTarget(maxSteeringBuffer))
@@ -420,6 +438,7 @@ namespace Barotrauma
foreach (Hull h in VisibleHulls)
{
PropagateHullSafety(Character, h);
dirtyHullSafetyCalculations.Remove(h);
}
}
else
@@ -427,18 +446,33 @@ namespace Barotrauma
foreach (Hull h in VisibleHulls)
{
RefreshHullSafety(h);
dirtyHullSafetyCalculations.Remove(h);
}
}
foreach (Hull h in dirtyHullSafetyCalculations)
{
RefreshHullSafety(h);
}
}
dirtyHullSafetyCalculations.Clear();
if (reportProblemsTimer <= 0.0f)
{
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
else
{
// Allows bots to heal targets autonomously while swimming outside of the sub.
if (AIObjectiveRescueAll.IsValidTarget(Character, Character))
{
AddTargets<AIObjectiveRescueAll, Character>(Character, Character);
}
}
reportProblemsTimer = reportProblemsInterval;
}
UpdateSpeaking();
SpeakAboutIssues();
UnequipUnnecessaryItems();
reactTimer = GetReactionTime();
}
@@ -590,7 +624,7 @@ namespace Barotrauma
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
Character.CurrentHull.IsWetRoom;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo { IsWaitOrder: true };
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
{
@@ -875,7 +909,7 @@ namespace Barotrauma
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
var rootContainer = container.Item.GetRootContainer() ?? container.Item;
var rootContainer = container.Item.RootContainer ?? container.Item;
if (rootContainer.GetComponent<Fabricator>() != null || rootContainer.GetComponent<Deconstructor>() != null) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
@@ -906,20 +940,40 @@ namespace Barotrauma
}
}))
{
suitableContainer = targetContainer;
return true;
if (targetContainer != null &&
character.AIController is HumanAIController humanAI &&
humanAI.PathSteering.PathFinder.FindPath(character.SimPosition, targetContainer.SimPosition, character.Submarine, errorMsgStr: $"FindSuitableContainer ({character.DisplayName})", nodeFilter: node => node.Waypoint.CurrentHull != null).Unreachable)
{
ignoredItems.Add(targetContainer);
itemIndex = 0;
return false;
}
else
{
suitableContainer = targetContainer;
return true;
}
}
return false;
}
private float draggedTimer;
private float refuseDraggingTimer;
private const float RefuseDraggingAfter = 10.0f;
/// <summary>
/// The bot breaks free if being dragged by a human player from another team for longer than this
/// </summary>
private const float RefuseDraggingThresholdHigh = 10.0f;
/// <summary>
/// If the RefuseDraggingDuration is active (the bot recently broke free of being dragged), the bot breaks free much faster
/// </summary>
private const float RefuseDraggingThresholdLow = 0.5f;
private const float RefuseDraggingDuration = 30.0f;
private void UpdateDragged(float deltaTime)
{
if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; }
if (Character.IsEscorted) { return; }
if (Character.LockHands) { return; }
//don't allow player characters who aren't in the same team to drag us for more than x seconds
if (Character.SelectedBy == null ||
@@ -931,8 +985,8 @@ namespace Barotrauma
}
draggedTimer += deltaTime;
if (draggedTimer > RefuseDraggingAfter ||
(draggedTimer > 0.5f && refuseDraggingTimer > 0.0f))
if (draggedTimer > RefuseDraggingThresholdHigh ||
(refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow))
{
draggedTimer = 0.0f;
refuseDraggingTimer = RefuseDraggingDuration;
@@ -945,7 +999,8 @@ namespace Barotrauma
{
Order newOrder = null;
Hull targetHull = null;
bool speak = Character.SpeechImpediment < 100;
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted;
if (Character.CurrentHull != null)
{
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
@@ -1044,25 +1099,21 @@ namespace Barotrauma
}
if (newOrder != null && speak)
{
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
if (!Character.IsEscorted)
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
{
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);
}
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(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
minDurationBetweenSimilar: 60.0f);
}
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);
#if SERVER
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
.WithTargetEntity(targetHull)
.WithOrderGiver(Character), "", null, Character));
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
.WithTargetEntity(targetHull)
.WithOrderGiver(Character), "", null, Character));
#endif
}
}
}
}
@@ -1093,21 +1144,33 @@ namespace Barotrauma
}
}
private void UpdateSpeaking()
private void SpeakAboutIssues()
{
if (!Character.IsOnPlayerTeam) { return; }
if (Character.SpeechImpediment >= 100) { return; }
if (Character.Oxygen < 20.0f)
float minDelay = 0.5f, maxDelay = 2f;
if (Character.Oxygen < CharacterHealth.InsufficientOxygenThreshold)
{
Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f);
string msgId = "DialogLowOxygen";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
if (Character.Bleeding > 2.0f)
if (Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !Character.IsMedic)
{
Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f);
string msgId = "DialogBleeding";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null)
if ((Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0) && !Character.IsProtectedFromPressure)
{
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f);
if (Character.PressureProtection > 0)
{
string msgId = "DialogInsufficientPressureProtection";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
else if (Character.CurrentHull?.DisplayName != null)
{
string msgId = "DialogPressure";
Character.Speak(TextManager.GetWithVariable(msgId, "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
}
}
@@ -1236,7 +1299,7 @@ namespace Barotrauma
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null;
if (isAccidental)
{
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)
if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold))
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
@@ -1402,7 +1465,7 @@ namespace Barotrauma
}
else
{
if (humanAI.ObjectiveManager.GetActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
{
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
@@ -1587,7 +1650,7 @@ namespace Barotrauma
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{
needsSuit = !Character.IsProtectedFromPressure;
needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
return needsAir || needsSuit;
}
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
@@ -1604,7 +1667,7 @@ namespace Barotrauma
/// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes));
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes));
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
@@ -1837,18 +1900,22 @@ namespace Barotrauma
private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
/// <summary>
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threads.
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threats.
/// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders.
/// There's now a cached value per each hull, which should prevent too frequent calculations.
/// </summary>
public static void PropagateHullSafety(Character character, Hull hull)
{
DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull));
DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
}
public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull);
private void RefreshHullSafety(Hull hull)
{
if (GetHullSafety(hull, Character, VisibleHulls) > HULL_SAFETY_THRESHOLD)
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : VisibleHulls;
float hullSafety = GetHullSafety(hull, Character, visibleHulls);
if (hullSafety > HULL_SAFETY_THRESHOLD)
{
UnsafeHulls.Remove(hull);
}
@@ -1916,7 +1983,7 @@ namespace Barotrauma
private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{
bool targetAdded = false;
DoForEachCrewMember(caller, humanAI =>
DoForEachBot(caller, humanAI =>
{
if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
var objective = humanAI.ObjectiveManager.GetObjective<T1>();
@@ -1933,7 +2000,7 @@ namespace Barotrauma
public static void RemoveTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{
DoForEachCrewMember(caller, humanAI =>
DoForEachBot(caller, humanAI =>
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
}
@@ -2108,7 +2175,7 @@ namespace Barotrauma
if (other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; }
return sameTeam || me.TeamID != CharacterTeamType.None;
}
else
{
@@ -2120,9 +2187,11 @@ namespace Barotrauma
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
{
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
//NPCs that allow some campaign interaction are not turned hostile by low reputation
if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; }
if (!npc.IsEscorted && npc.AIController is HumanAIController npcAI)
if (npc.AIController is HumanAIController npcAI)
{
return !npcAI.IsInHostileFaction();
}
@@ -2134,6 +2203,7 @@ namespace Barotrauma
public bool IsInHostileFaction()
{
if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; }
if (Character.IsEscorted) { return false; }
Identifier npcFaction = Character.Faction;
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
@@ -2145,8 +2215,7 @@ namespace Barotrauma
}
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
if (campaign.CurrentLocation is { IsFactionHostile: true })
{
return true;
}
@@ -2154,71 +2223,89 @@ namespace Barotrauma
return false;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious;
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
public static bool IsTrueForAllBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (FilterCrewMember(character, c))
if (!IsBotInTheCrew(character, c)) { continue; }
if (!predicate(c.AIController as HumanAIController))
{
if (!predicate(c.AIController as HumanAIController))
{
return false;
}
return false;
}
}
}
return true;
}
public static bool IsTrueForAnyCrewMember(Character character, Func<HumanAIController, bool> predicate)
public static bool IsTrueForAnyBotInTheCrew(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (FilterCrewMember(character, c))
if (!IsBotInTheCrew(character, c)) { continue; }
if (predicate(c.AIController as HumanAIController))
{
if (predicate(c.AIController as HumanAIController))
{
return true;
}
return true;
}
}
return false;
}
public static int CountCrew(Character character, Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false)
public static int CountBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate = null)
{
if (character == null) { return 0; }
int count = 0;
foreach (var other in Character.CharacterList)
{
if (onlyActive && !IsActive(other))
if (!IsBotInTheCrew(character, other)) { continue; }
if (predicate == null || predicate(other.AIController as HumanAIController))
{
continue;
}
if (onlyBots && other.IsPlayer)
{
continue;
}
if (FilterCrewMember(character, other))
{
if (predicate == null || predicate(other.AIController as HumanAIController))
{
count++;
}
count++;
}
}
return count;
}
public static void DoForEachCrewMember(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
/// <summary>
/// Including the player characters in the same team.
/// </summary>
public bool IsTrueForAnyCrewMember(Func<Character, bool> predicate, bool onlyActive = true, bool onlyConnectedSubs = false)
{
foreach (var c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (onlyActive && c.IsIncapacitated) { continue; }
if (onlyConnectedSubs)
{
if (Character.Submarine == null)
{
if (c.Submarine != null)
{
return false;
}
}
else if (c.Submarine != Character.Submarine && !Character.Submarine.GetConnectedSubs().Contains(c.Submarine))
{
return false;
}
}
if (predicate(c))
{
return true;
}
}
return false;
}
private static void DoForEachBot(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
{
if (character == null) { return; }
foreach (var c in Character.CharacterList)
{
if (FilterCrewMember(character, c) && CheckReportRange(character, c, range))
if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
{
action(c.AIController as HumanAIController);
}
@@ -2238,7 +2325,7 @@ namespace Barotrauma
}
}
private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self);
private static bool IsBotInTheCrew(Character self, Character other) => IsActive(other) && other.TeamID == self.TeamID && !other.IsIncapacitated && other.IsBot && other.AIController is HumanAIController;
public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
{
@@ -2283,10 +2370,9 @@ namespace Barotrauma
bool isOrder = IsOrderedToOperateThis(Character.AIController);
foreach (Character c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
if (c == Character) { continue; }
if (c.Removed) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
if (c.IsPlayer)
{
if (c.SelectedItem == target.Item)
@@ -2354,9 +2440,9 @@ namespace Barotrauma
bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
foreach (var c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
if (c == Character) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
other = c;
if (c.IsPlayer)
{
@@ -2370,7 +2456,7 @@ namespace Barotrauma
{
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
if (repairItemsObjective == null) { continue; }
if (!(repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is AIObjectiveRepairItem activeObjective) || activeObjective.Item != target)
if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
{
// Not targeting the same item.
continue;
@@ -2405,11 +2491,10 @@ namespace Barotrauma
}
#region Wrappers
public bool IsFriendly(Character other) => IsFriendly(Character, other);
public void DoForEachCrewMember(Action<HumanAIController> action) => DoForEachCrewMember(Character, action);
public bool IsTrueForAnyCrewMember(Func<HumanAIController, bool> predicate) => IsTrueForAnyCrewMember(Character, predicate);
public bool IsTrueForAllCrewMembers(Func<HumanAIController, bool> predicate) => IsTrueForAllCrewMembers(Character, predicate);
public int CountCrew(Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots);
public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam);
public bool IsTrueForAnyBotInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAnyBotInTheCrew(Character, predicate);
public bool IsTrueForAllBotsInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAllBotsInTheCrew(Character, predicate);
public int CountBotsInTheCrew(Func<HumanAIController, bool> predicate = null) => CountBotsInTheCrew(Character, predicate);
#endregion
}
}
@@ -22,7 +22,10 @@ namespace Barotrauma
private readonly Character character;
private Vector2 currentTarget;
/// <summary>
/// In sim units.
/// </summary>
private Vector2 currentTargetPos;
private float findPathTimer;
@@ -40,11 +43,6 @@ namespace Barotrauma
get { return pathFinder; }
}
public Vector2 CurrentTarget
{
get { return currentTarget; }
}
public bool IsPathDirty
{
get;
@@ -54,9 +52,9 @@ namespace Barotrauma
/// <summary>
/// Returns true if any node in the path is in stairs
/// </summary>
public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character);
public bool IsCurrentNodeLadder => GetCurrentLadder() != null;
public bool IsNextNodeLadder => GetNextLadder() != null;
@@ -64,14 +62,9 @@ namespace Barotrauma
{
get
{
if (currentPath == null) { return false; }
if (currentPath.CurrentNode == null) { return false; }
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
var currentLadder = GetCurrentLadder();
if (currentLadder == null) { return false; }
if (!currentLadder.Item.IsInteractable(character)) { return false; }
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
return currentLadder == GetNextLadder();
}
}
@@ -107,13 +100,10 @@ namespace Barotrauma
findPathTimer -= step;
}
public void SetPath(SteeringPath path)
public void SetPath(Vector2 targetPos, SteeringPath path)
{
currentTargetPos = targetPos;
currentPath = path;
if (path.Nodes.Any())
{
currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
}
findPathTimer = Math.Min(findPathTimer, 1.0f);
IsPathDirty = false;
}
@@ -136,66 +126,28 @@ namespace Barotrauma
steering += addition;
}
/// <summary>
/// Seeks the ladder from the next and next + 1 nodes.
/// </summary>
public Ladder GetNextLadder()
{
if (currentPath == null) { return null; }
if (currentPath.NextNode == null) { return null; }
if (currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character))
{
return currentPath.NextNode.Ladders;
}
else
{
int index = currentPath.CurrentIndex + 2;
if (currentPath.Nodes.Count > index)
{
var node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
{
return node.Ladders;
}
//if the next node is a hatch, check if the node after that is a ladder
else if (node.ConnectedDoor != null && node.ConnectedDoor.IsHorizontal)
{
index++;
if (currentPath.Nodes.Count > index)
{
node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
{
return node.Ladders;
}
}
}
public Ladder GetCurrentLadder() => GetLadder(currentPath?.CurrentNode);
}
return null;
public Ladder GetNextLadder() => GetLadder(currentPath?.NextNode);
private Ladder GetLadder(WayPoint wp)
{
if (wp?.Ladders?.Item is Item item && item.IsInteractable(character))
{
return wp.Ladders;
}
return null;
}
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished;
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null;
if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
{
Vector2 targetDiff = target - currentTarget;
if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null)
{
//target in a different sub than where the character is now
//take that into account when calculating if the target has moved
Submarine currentPathSub = currentPath?.CurrentNode?.Submarine;
if (currentPathSub == character.Submarine) { currentPathSub = currentPath?.Nodes.LastOrDefault()?.Submarine; }
if (currentPathSub != character.Submarine && targetDiff.LengthSquared() > 1 && currentPathSub != null)
{
Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition;
targetDiff += subDiff;
}
}
// If the target has moved, we need a new path.
// Different subs are already taken into account before setting the target.
// Triggers when either the target or we have changed subs, but only once (until the new path has been accepted).
Vector2 targetDiff = target - currentTargetPos;
if (targetDiff.LengthSquared() > 1)
{
needsNewPath = true;
@@ -205,15 +157,33 @@ namespace Barotrauma
if (needsNewPath || findPathTimer < -1.0f)
{
IsPathDirty = true;
if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp)
{
if (character.Submarine != null && wp.Ladders == null && wp.ConnectedDoor == null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
{
// Not moving -> need a new path.
needsNewPath = true;
}
if (character.Submarine == null && wp.CurrentHull != null)
{
// Current node inside, while we are outside
// -> Check that the current node is not too far (can happen e.g. if someone controls the character in the meanwhile)
float maxDist = 200;
if (Vector2.DistanceSquared(character.WorldPosition, wp.WorldPosition) > maxDist * maxDist)
{
needsNewPath = true;
}
}
}
if (findPathTimer < 0)
{
SkipCurrentPathNodes();
currentTarget = target;
currentTargetPos = target;
Vector2 currentPos = host.SimPosition;
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0;
bool useNewPath = needsNewPath;
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
@@ -234,6 +204,14 @@ namespace Barotrauma
useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
}
if (!useNewPath && !character.CanSeeTarget(currentPath.CurrentNode))
{
// If we are set to disregard the new path, ensure that we can actually see the current node of the old path,
// because it's possible that there's e.g. a closed door between us and the current node,
// and in that case we'd want to use the new path instead of the old.
// There's visibility checks in the pathfinder calls, so the new path should always be ok.
useNewPath = true;
}
bool IsIdenticalPath()
{
@@ -312,6 +290,7 @@ namespace Barotrauma
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius)
{
// TODO: might cause some edge cases -> do we need this?
diff.Y = 0.0f;
}
if (diff == Vector2.Zero) { return Vector2.Zero; }
@@ -328,12 +307,12 @@ namespace Barotrauma
}
if (currentPath.Finished)
{
Vector2 pos2 = host.SimPosition;
Vector2 hostPosition = host.SimPosition;
if (character != null && character.Submarine == null && CurrentPath.Nodes.Count > 0 && CurrentPath.Nodes.Last().Submarine != null)
{
pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition;
hostPosition -= CurrentPath.Nodes.Last().Submarine.SimPosition;
}
return currentTarget - pos2;
return currentTargetPos - hostPosition;
}
bool doorsChecked = false;
checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime());
@@ -353,14 +332,46 @@ namespace Barotrauma
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
Ladder currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder != null && !currentLadder.Item.IsInteractable(character))
{
currentLadder = null;
}
Ladder currentLadder = GetCurrentLadder();
Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder;
bool useLadders = canClimb && ladders != null && steering.LengthSquared() > 0.1f && (!isDiving || steering.Y > 1);
bool useLadders = canClimb && ladders != null;
var collider = character.AnimController.Collider;
Vector2 colliderSize = collider.GetSize();
if (useLadders)
{
if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
{
// If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders.
useLadders = false;
}
else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null)
{
Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos;
if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
{
// If the next node is horizontally farther from us than vertically, we don't want to start climbing.
useLadders = false;
}
}
else if (isDiving && steering.Y < 1)
{
// When diving, only use ladders to get upwards (towards the surface), otherwise we can just ignore them.
useLadders = false;
}
}
if (character.IsClimbing && !useLadders)
{
if (currentPath.IsAtEndNode && canClimb && ladders != null)
{
// Don't release the ladders when ending a path in ladders.
useLadders = true;
}
else
{
character.StopClimbing();
}
}
if (useLadders && character.SelectedSecondaryItem != ladders.Item)
{
if (character.CanInteractWith(ladders.Item))
@@ -380,56 +391,76 @@ namespace Barotrauma
}
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !useLadders)
{
character.StopClimbing();
}
if (character.IsClimbing && useLadders)
{
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
// Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
NextNode(!doorsChecked);
}
//at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f;
if (heightDiff < colliderSize)
else
{
float heightFromFloor = character.AnimController.GetHeightFromFloor();
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
bool nextLadderSameAsCurrent = currentLadder == nextLadder;
if (currentLadder != null && nextLadder != null)
{
character.StopClimbing();
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
else if (nextLadder != null && !nextLadderSameAsCurrent)
//at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderHeight = collider.Height / 2 + collider.Radius;
float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
if (heightDiff < colliderHeight * 1.25f)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
if (nextLadder != null && !nextLadderSameAsCurrent)
{
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
// Try to change the ladder (hatches between two submarines)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
{
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
{
NextNode(!doorsChecked);
}
}
}
bool isAboveFloor;
if (diff.Y < 0)
{
// When climbing down, let's use the collider bottom to prevent getting stuck at the bottom of the ladders.
float colliderBottom = character.AnimController.Collider.SimPosition.Y;
float floorY = character.AnimController.FloorY;
isAboveFloor = colliderBottom > floorY;
}
else
{
// When climbing up, let's use the lowest collider (feet).
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative,
// when a foot is still below the platform.
float heightFromFloor = character.AnimController.GetHeightFromFloor();
isAboveFloor = heightFromFloor > -0.1f;
}
if (isAboveFloor)
{
if (Math.Abs(diff.Y) < distanceMargin)
{
NextNode(!doorsChecked);
}
else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin)))
{
// Can't skip the node -> Release the ladders, because the next node is not on a ladder or it's horizontally too far.
character.StopClimbing();
}
}
}
if (!currentPath.IsAtEndNode && (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10))
else if (currentLadder != null && currentPath.NextNode != null)
{
NextNode(!doorsChecked);
}
}
else if (nextLadder != null)
{
//if the current node is below the character and the next one is above (or vice versa)
//and both are on ladders, we can skip directly to the next one
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
NextNode(!doorsChecked);
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
//if the current node is below the character and the next one is above (or vice versa)
//and both are on ladders, we can skip directly to the next one
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
NextNode(!doorsChecked);
}
}
}
return ConvertUnits.ToSimUnits(diff);
@@ -440,7 +471,6 @@ namespace Barotrauma
if (door == null || door.CanBeTraversed)
{
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
Vector2 colliderSize = collider.GetSize();
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
@@ -459,7 +489,6 @@ namespace Barotrauma
{
// Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
Vector2 velocity = collider.LinearVelocity;
// If the character is very short, it would fail to use the waypoint nodes because they are always too high.
// If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
@@ -486,9 +515,12 @@ namespace Barotrauma
}
}
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && (door == null || door.CanBeTraversed))
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow)
{
NextNode(!doorsChecked);
if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null))
{
NextNode(!doorsChecked);
}
}
}
if (currentPath.CurrentNode == null)
@@ -507,9 +539,9 @@ namespace Barotrauma
currentPath.SkipToNextNode();
}
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
public bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsBroken) { return true; }
if (door.CanBeTraversed) { return true; }
if (door.IsClosed)
{
if (!door.Item.IsInteractable(character)) { return false; }
@@ -605,10 +637,12 @@ namespace Barotrauma
{
//the node we're heading towards is the last one in the path, and at a door
//the door needs to be open for the character to reach the node
if (currentWaypoint.ConnectedDoor.LinkedGap != null)
if (currentWaypoint.ConnectedDoor.LinkedGap is Gap linkedGap)
{
// Keep the airlock doors closed, but not in ruins/wrecks
if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false } || currentWaypoint.Submarine == null || currentWaypoint.Submarine.Info.IsRuin || currentWaypoint.Submarine.Info.IsWreck)
if (currentWaypoint.Submarine == null ||
currentWaypoint.Submarine.Info is { IsPlayer: false } ||
!linkedGap.IsRoomToRoom ||
(linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false }))
{
shouldBeOpen = true;
door = currentWaypoint.ConnectedDoor;
@@ -213,7 +213,7 @@ namespace Barotrauma
{
foreach (Voronoi2.GraphEdge edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
{
Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos);
@@ -506,15 +506,31 @@ namespace Barotrauma
}
}
protected static bool CanEquip(Character character, Item item)
public virtual void SpeakAfterOrderReceived() { }
protected static bool CanEquip(Character character, Item item, bool allowWearing)
{
bool canEquip = item != null;
if (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any))
if (item == null) { return false; }
bool canEquip = false;
if (item.AllowedSlots.Contains(InvSlotType.Any))
{
if (character.Inventory.IsAnySlotAvailable(item))
{
canEquip = true;
}
}
if (!canEquip)
{
canEquip = false;
var inv = character.Inventory;
foreach (var allowedSlot in item.AllowedSlots)
{
if (!allowWearing)
{
if (!allowedSlot.HasFlag(InvSlotType.RightHand) && !allowedSlot.HasFlag(InvSlotType.LeftHand))
{
continue;
}
}
foreach (var slotType in inv.SlotTypes)
{
if (!allowedSlot.HasFlag(slotType)) { continue; }
@@ -530,18 +546,9 @@ namespace Barotrauma
}
}
}
return canEquip;
}
protected bool CheckItemIdentifiersOrTags(Item item, ImmutableHashSet<Identifier> identifiersOrTags)
{
if (identifiersOrTags.Contains(item.Prefab.Identifier)) { return true; }
foreach (var identifier in identifiersOrTags)
{
if (item.HasTag(identifier)) { return true; }
}
return false;
return canEquip && character.Inventory.CanBePut(item);
}
protected bool CanEquip(Item item) => CanEquip(character, item);
protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing);
}
}
@@ -64,7 +64,6 @@ namespace Barotrauma
if (subObjectives.Any()) { return; }
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{
itemIndex = 0;
if (suitableContainer != null)
{
bool equip = item.GetComponent<Holdable>() != null ||
@@ -112,10 +111,7 @@ namespace Barotrauma
Abandon = true;
}
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
protected override bool CheckObjectiveSpecific()
@@ -14,6 +14,10 @@ namespace Barotrauma
public readonly List<Item> prioritizedItems = new List<Item>();
public static readonly Identifier AllowCleanupTag = "allowcleanup".ToIdentifier();
protected override int MaxTargets => 100;
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -81,8 +85,8 @@ namespace Barotrauma
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
allowUnloading &&
container.HasTag(AllowCleanupTag) &&
container.HasAccess(character) &&
container.HasTag("allowcleanup") &&
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
container.GetComponent<ItemContainer>() != null &&
IsItemInsideValidSubmarine(container, character) &&
@@ -91,7 +95,6 @@ namespace Barotrauma
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
{
if (item == null) { return false; }
if (!item.HasAccess(character)) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
@@ -102,6 +105,7 @@ namespace Barotrauma
}
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
}
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
var wire = item.GetComponent<Wire>();
@@ -121,7 +125,7 @@ namespace Barotrauma
{
return true;
}
return CanEquip(character, item);
return CanEquip(character, item, allowWearing: false);
}
public override void OnDeselected()
@@ -170,13 +170,33 @@ namespace Barotrauma
return Priority;
}
}
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f);
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
if (Priority > 0)
if (TargetEliminated)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
Priority = 0;
}
else
{
// 91-100
float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
float maxPriority = AIObjectiveManager.MaxObjectivePriority;
float priorityScale = maxPriority - minPriority;
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
Priority = 0;
xDist /= 2;
yDist /= 2;
}
float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
float devotion = CumulatedDevotion / 100;
float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
if (Priority > 0)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
{
Priority = 0;
}
}
}
return Priority;
@@ -312,12 +332,10 @@ namespace Barotrauma
}
else
{
AskHelp();
Retreat(deltaTime);
}
break;
case CombatMode.Retreat:
AskHelp();
Retreat(deltaTime);
break;
default:
@@ -352,7 +370,7 @@ namespace Barotrauma
Weapon = null;
continue;
}
if (WeaponComponent.IsNotEmpty(character))
if (!WeaponComponent.IsEmpty(character))
{
// All good, the weapon is loaded
break;
@@ -470,7 +488,7 @@ namespace Barotrauma
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!WeaponComponent.IsNotEmpty(character))
if (WeaponComponent.IsEmpty(character))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
@@ -541,7 +559,7 @@ namespace Barotrauma
priority /= 2;
}
}
if (!weapon.IsNotEmpty(character))
if (weapon.IsEmpty(character))
{
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{
@@ -554,7 +572,6 @@ namespace Barotrauma
priority /= 2;
}
}
if (Enemy.Params.Health.StunImmunity)
{
if (weapon.Item.HasTag("stunner"))
@@ -750,7 +767,7 @@ namespace Barotrauma
private bool Equip()
{
if (character.LockHands) { return false; }
if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
if (WeaponComponent.IsEmpty(character))
{
return false;
}
@@ -783,6 +800,10 @@ namespace Barotrauma
private void Retreat(float deltaTime)
{
if (!Enemy.IsHuman)
{
SpeakRetreating();
}
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunitionObjective);
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
@@ -793,6 +814,7 @@ namespace Barotrauma
{
// Swim away
SteeringManager.Reset();
character.ReleaseSecondaryItem();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition));
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
return;
@@ -819,7 +841,8 @@ namespace Barotrauma
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager)
{
UsePathingOutside = false
UsePathingOutside = false,
SpeakIfFails = false
},
onAbandon: () =>
{
@@ -861,6 +884,7 @@ namespace Barotrauma
{
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
{
character.ReleaseSecondaryItem();
// Swim towards the target
SteeringManager.Reset();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10);
@@ -882,7 +906,8 @@ namespace Barotrauma
UsePathingOutside = false,
IgnoreIfTargetDead = true,
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false
AlwaysUseEuclideanDistance = false,
SpeakIfFails = false
},
onAbandon: () =>
{
@@ -966,14 +991,22 @@ namespace Barotrauma
item.GetComponent<RangedWeapon>() != null)
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot);
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
}
}
}
if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy))
//prefer using handcuffs already on the enemy's inventory
if (!HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems))
{
HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out matchingItems);
}
if (matchingItems.Any() &&
!Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
{
var handCuffs = matchingItems.First();
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true))
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
@@ -1028,54 +1061,43 @@ namespace Barotrauma
if (Weapon.OwnInventory == null) { return true; }
// Eject empty ammo
HumanAIController.UnequipEmptyItems(Weapon);
RelatedItem item = null;
Item ammunition = null;
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
ammunition = Weapon.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
if (ammunition != null)
{
// Ammunition still remaining
return true;
}
item = requiredItem;
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
ammunitionIdentifiers = requiredItem.Identifiers;
break;
}
}
else if (WeaponComponent is MeleeWeapon meleeWeapon)
{
ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
}
// No ammo
if (ammunition == null)
if (ammunitionIdentifiers != null)
{
if (ammunitionIdentifiers != null)
// Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
if (ammunition != null)
{
// Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
ammunition = character.Inventory.FindItem(i => CheckItemIdentifiersOrTags(i, ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
if (ammunition != null)
var container = Weapon.GetComponent<ItemContainer>();
if (!container.Inventory.TryPutItem(ammunition, user: character))
{
var container = Weapon.GetComponent<ItemContainer>();
if (!container.Inventory.TryPutItem(ammunition, null))
if (ammunition.ParentInventory == character.Inventory)
{
if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
ammunition.Drop(character);
}
}
}
}
if (WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
if (!WeaponComponent.IsEmpty(character))
{
return true;
}
else if (ammunition == null && !HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{
SeekAmmunition(ammunitionIdentifiers);
}
@@ -1270,7 +1292,7 @@ namespace Barotrauma
}
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30);
private void AskHelp() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
private void Speak(Identifier textIdentifier, float delay, float minDuration)
{
@@ -109,7 +109,7 @@ namespace Barotrauma
private bool CheckItem(Item item)
{
return CheckItemIdentifiersOrTags(item, itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character);
return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character);
}
protected override void Act(float deltaTime)
@@ -156,15 +156,15 @@ namespace Barotrauma
Inventory originalInventory = ItemToContain.ParentInventory;
var slots = originalInventory?.FindIndices(ItemToContain);
static bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain)
bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain)
{
if (targetSlot.HasValue)
{
return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: null);
return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: character);
}
else
{
return inventory.TryPutItem(itemToContain, user: null);
return inventory.TryPutItem(itemToContain, user: character);
}
}
@@ -198,11 +198,11 @@ namespace Barotrauma
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) ||
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
(container.Item.RootContainer?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach)
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.MaxReach)
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -66,7 +66,7 @@ namespace Barotrauma
else
{
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
}
return Priority;
@@ -28,7 +28,7 @@ namespace Barotrauma
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
// If there's any security officers onboard, leave fighting for them.
return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100;
return HumanAIController.IsTrueForAnyCrewMember(c => c.IsSecurity, onlyActive: true, onlyConnectedSubs: true) ? 0 : 100;
}
protected override AIObjective ObjectiveConstructor(Character target)
@@ -37,8 +37,7 @@ namespace Barotrauma
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
if (campaign.CurrentLocation is { IsFactionHostile: true })
{
combatObjective.holdFireCondition = () =>
{
@@ -30,7 +30,8 @@ namespace Barotrauma
public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier();
public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier();
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
protected override bool CheckObjectiveSpecific() =>
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
@@ -51,7 +52,7 @@ namespace Barotrauma
TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
}
if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) &&
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
{
TryAddSubObjective(ref getDivingGear, () =>
@@ -65,7 +66,7 @@ namespace Barotrauma
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes,
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
Wear = true
};
},
@@ -79,7 +80,7 @@ namespace Barotrauma
{
if (mask != targetItem)
{
character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot);
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
}
}
}
@@ -52,17 +52,14 @@ namespace Barotrauma
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
&& ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100;
&& ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
}
else
{
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
(HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
(needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) :
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))))
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
{
Priority = 100;
Priority = AIObjectiveManager.MaxObjectivePriority;
}
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
@@ -75,11 +72,11 @@ namespace Barotrauma
{
Priority = 0;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
// Boost the priority while seeking the diving gear
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100));
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority));
}
}
return Priority;
@@ -111,7 +108,7 @@ namespace Barotrauma
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
Priority -= priorityDecrease * deltaTime;
if (currenthullSafety >= 100)
if (currenthullSafety >= 100 && !character.IsLowInOxygen)
{
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
Priority = 0;
@@ -122,7 +119,7 @@ namespace Barotrauma
float dangerFactor = (100 - currenthullSafety) / 100;
Priority += dangerFactor * priorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
}
}
@@ -138,7 +135,7 @@ namespace Barotrauma
{
if (resetPriority) { return; }
var currentHull = character.CurrentHull;
bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0);
bool dangerousPressure = (currentHull == null || currentHull.LethalPressure > 0) && !character.IsProtectedFromPressure;
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
{
@@ -200,16 +197,11 @@ namespace Barotrauma
UpdateSimpleEscape(deltaTime);
return;
}
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = potentialSafeHull;
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _);
if (currentSafeHull == null)
{
currentSafeHull = previousSafeHull;
}
cannotFindSafeHull = currentSafeHull == null || NeedMoreDivingGear(currentSafeHull);
currentSafeHull ??= previousSafeHull;
if (currentSafeHull != null && currentSafeHull != currentHull)
{
if (goToObjective?.Target != currentSafeHull)
@@ -219,6 +211,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
SpeakIfFails = false,
AllowGoingOutside =
character.IsProtectedFromPressure ||
character.CurrentHull == null ||
@@ -300,6 +293,7 @@ namespace Barotrauma
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
{
character.ReleaseSecondaryItem();
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
@@ -349,7 +343,6 @@ namespace Barotrauma
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
//sort the hulls based on distance and which sub they're in
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
@@ -413,7 +406,7 @@ namespace Barotrauma
if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
{
// Don't allow to go outside if not already outside.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, potentialHull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
hullSafety = 0;
@@ -493,5 +486,18 @@ namespace Barotrauma
cannotFindDivingGear = false;
cannotFindSafeHull = false;
}
private bool NeedMoreDivingGear(Hull targetHull, float minOxygen = 0)
{
if (!HumanAIController.NeedsDivingGear(targetHull, out bool needsSuit)) { return false; }
if (needsSuit)
{
return !HumanAIController.HasDivingSuit(character, minOxygen);
}
else
{
return !HumanAIController.HasDivingGear(character, minOxygen);
}
}
}
}
@@ -37,40 +37,56 @@ namespace Barotrauma
{
Priority = 0;
Abandon = true;
return Priority;
}
else if (HumanAIController.IsTrueForAnyCrewMember(
other => other != HumanAIController &&
other.Character.IsBot &&
other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks &&
fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak)))
float coopMultiplier = 1;
foreach (var c in Character.CharacterList)
{
Priority = 0;
if (!HumanAIController.IsActive(c)) { continue; }
if (c.TeamID != character.TeamID) { continue; }
if (c == character) { continue; }
if (c.IsPlayer) { continue; }
if (c.AIController is HumanAIController otherAI )
{
if (otherAI.ObjectiveManager.GetFirstActiveObjective<AIObjectiveFixLeak>() is AIObjectiveFixLeak fixLeak)
{
if (fixLeak.Leak == Leak)
{
// Ignore leaks that others are already targeting
Priority = 0;
return Priority;
}
if (fixLeak.Leak.FlowTargetHull == Leak.FlowTargetHull)
{
// Reduce the priority of leaks that others should be targeting
coopMultiplier = 0.1f;
break;
}
}
}
}
float reduction = isPriority ? 1 : 2;
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction;
if (operateObjective != null && objectiveManager.GetFirstActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this)
{
// Prioritize leaks that we are already fixing
Priority = maxPriority;
}
else
{
float reduction = isPriority ? 1 : 2;
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction;
if (operateObjective != null && objectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this)
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull))
{
// Prioritize leaks that we are already fixing
Priority = maxPriority;
}
else
{
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull))
{
// Double the distance when the leak can be accessed from the current hull.
distanceFactor *= 2;
}
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
// Double the distance when the leak can be accessed from the current hull.
distanceFactor *= 2;
}
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier * coopMultiplier), 0, 1));
}
return Priority;
}
@@ -38,9 +38,9 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
int totalLeaks = Targets.Count();
int totalLeaks = Targets.Count;
if (totalLeaks == 0) { return 0; }
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine, onlyBots: true);
int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && c.Character.Submarine == character.Submarine);
bool anyFixers = otherFixers > 0;
if (objectiveManager.IsOrder(this))
{
@@ -52,7 +52,7 @@ namespace Barotrauma
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
int leaks = totalLeaks - secondaryLeaks;
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1;
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f))
{
// Enough fixers
return 0;
@@ -1,9 +1,11 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
namespace Barotrauma
{
@@ -31,14 +33,15 @@ namespace Barotrauma
private ISpatialEntity moveToTarget;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
private int currSearchIndex;
private int currentSearchIndex;
public ImmutableHashSet<Identifier> ignoredContainerIdentifiers;
public ImmutableHashSet<Identifier> ignoredIdentifiersOrTags;
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
private readonly bool checkInventory;
public static float DefaultReach = 100;
public const float DefaultReach = 100;
public const float MaxReach = 150;
public bool AllowToFindDivingGear { get; set; } = true;
public bool MustBeSpecificItem { get; set; }
@@ -76,7 +79,7 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
currentSearchIndex = 0;
Equip = equip;
originalTarget = targetItem;
this.targetItem = targetItem;
@@ -89,7 +92,7 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, IEnumerable<Identifier> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
currentSearchIndex = 0;
Equip = equip;
this.spawnItemIfNotFound = spawnItemIfNotFound;
this.checkInventory = checkInventory;
@@ -125,7 +128,7 @@ namespace Barotrauma
public static Func<PathNode, bool> CreateEndNodeFilter(ISpatialEntity targetEntity)
{
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach);
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(MaxReach);
}
private bool CheckInventory()
@@ -246,7 +249,7 @@ namespace Barotrauma
else
{
character.SelectCharacter(c);
canInteract = character.CanInteractWith(c, maxDist: DefaultReach);
canInteract = character.CanInteractWith(c);
character.DeselectCharacter();
}
}
@@ -268,7 +271,7 @@ namespace Barotrauma
Inventory itemInventory = targetItem.ParentInventory;
var slots = itemInventory?.FindIndices(targetItem);
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true))
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true, targetTags: IdentifiersOrTags))
{
if (TakeWholeStack && slots != null)
{
@@ -298,8 +301,11 @@ namespace Barotrauma
if (!Equip)
{
// Try equipping and wearing the item
Wear = true;
Equip = true;
if (!objectiveManager.HasActiveObjective<AIObjectiveCleanupItem>() && !objectiveManager.HasActiveObjective<AIObjectiveLoadItem>())
{
Wear = true;
}
return;
}
#if DEBUG
@@ -342,6 +348,10 @@ namespace Barotrauma
}
}
private Stopwatch sw;
private Stopwatch StopWatch => sw ??= new Stopwatch();
private readonly List<(Item item, float priority)> itemCandidates = new List<(Item, float)>();
private List<Item> itemList;
private void FindTargetItem()
{
if (IdentifiersOrTags == null)
@@ -349,13 +359,16 @@ namespace Barotrauma
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.", Color.Red);
DebugConsole.AddWarning($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.");
#endif
Abandon = true;
}
return;
}
if (HumanAIController.DebugAI)
{
StopWatch.Restart();
}
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
if (!CheckPathForEachItem)
{
@@ -363,15 +376,25 @@ namespace Barotrauma
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective);
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrder);
}
bool checkPath = CheckPathForEachItem;
bool hasCalledPathFinder = false;
int itemsPerFrame = (int)priority;
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
// Reset if the character has switched subs.
if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true))
{
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
currentSearchIndex = 0;
}
if (currentSearchIndex == 0)
{
itemCandidates.Clear();
itemList = character.Submarine.GetItems(alsoFromConnectedSubs: true);
}
int itemsPerFrame = (int)MathHelper.Lerp(30, 300, MathUtils.InverseLerp(10, 100, priority));
int checkedItems = 0;
for (int i = 0; i < itemsPerFrame && currentSearchIndex < itemList.Count; i++, currentSearchIndex++)
{
checkedItems++;
var item = itemList[currentSearchIndex];
Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine;
if (itemSub == null) { continue; }
Submarine mySub = character.Submarine;
@@ -395,8 +418,6 @@ namespace Barotrauma
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
}
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; }
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
if (item.ParentInventory is ItemInventory itemInventory)
{
@@ -411,11 +432,14 @@ namespace Barotrauma
if (rootInventoryOwner is Item ownerItem)
{
if (!ownerItem.IsInteractable(character)) { continue; }
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
//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)
if (ownerItem != item)
{
itemPriority *= 0.1f;
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
//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;
}
}
}
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
@@ -463,22 +487,69 @@ namespace Barotrauma
{
itemPriority *= item.Condition / item.MaxCondition;
}
if (checkPath)
{
itemCandidates.Add((item, itemPriority));
}
// Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
if (!hasCalledPathFinder && PathSteering != null && checkPath)
if (EvaluateCombatPriority && itemPriority <= 0)
{
hasCalledPathFinder = true;
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable) { continue; }
// Not good enough
continue;
}
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
}
if (currSearchIndex >= Item.ItemList.Count - 1)
if (currentSearchIndex >= itemList.Count - 1)
{
isDoneSeeking = true;
if (targetItem == null)
}
if (checkedItems > 0)
{
if (isDoneSeeking && itemCandidates.Any())
{
itemCandidates.Sort((x, y) => y.priority.CompareTo(x.priority));
}
if (HumanAIController.DebugAI && targetItem != null && StopWatch.ElapsedMilliseconds > 2)
{
var msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}";
if (StopWatch.ElapsedMilliseconds > 5)
{
DebugConsole.ThrowError(msg);
}
else
{
// An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue.
DebugConsole.AddWarning(msg);
}
}
}
if (isDoneSeeking)
{
if (PathSteering == null)
{
itemCandidates.Clear();
}
if (itemCandidates.Any())
{
if (itemCandidates.FirstOrDefault() is { } itemCandidate)
{
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
// Remove the invalid candidates and continue on the next frame.
itemCandidates.Remove(itemCandidate);
}
else
{
// The path was valid -> we are done.
itemCandidates.Clear();
}
}
}
if (targetItem == null && itemCandidates.None())
{
if (spawnItemIfNotFound)
{
@@ -569,11 +640,11 @@ namespace Barotrauma
{
if (!item.HasAccess(character)) { return false; }
if (ignoredItems.Contains(item)) { return false; };
if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; }
if (ignoredIdentifiersOrTags != null && item.HasIdentifierOrTags(ignoredIdentifiersOrTags)) { return false; }
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; }
return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; }
return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
}
public override void Reset()
@@ -591,7 +662,7 @@ namespace Barotrauma
targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false;
currSearchIndex = 0;
currentSearchIndex = 0;
currItemPriority = 0;
}
@@ -26,7 +26,8 @@ namespace Barotrauma
public Func<float> PriorityGetter;
public bool IsFollowOrderObjective;
public bool IsFollowOrder;
public bool IsWaitOrder;
public bool Mimic;
public bool SpeakIfFails { get; set; } = true;
@@ -59,7 +60,7 @@ namespace Barotrauma
{
get
{
if (IsFollowOrderObjective && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
if (IsFollowOrder && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
{
// Keep close when the target is going inside/outside
return minDistance;
@@ -220,15 +221,45 @@ namespace Barotrauma
}
}
Hull targetHull = GetTargetHull();
if (!IsFollowOrderObjective)
if (!IsFollowOrder)
{
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
&& PathSteering != null && PathSteering.CurrentPath != null
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull);
if (!objectiveManager.CurrentObjective.IgnoreUnsafeHulls)
{
Abandon = true;
if (HumanAIController.UnsafeHulls.Contains(targetHull))
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(targetHull);
}
else if (PathSteering?.CurrentPath != null)
{
foreach (WayPoint wp in PathSteering.CurrentPath.Nodes)
{
if (wp.CurrentHull == null) { continue; }
if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull))
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull);
}
}
}
}
if (isUnreachable)
{
SteeringManager.Reset();
if (PathSteering?.CurrentPath != null)
{
PathSteering.CurrentPath.Unreachable = true;
}
if (repeat)
{
SpeakCannotReach();
}
else
{
Abandon = true;
}
return;
}
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
@@ -250,6 +281,7 @@ namespace Barotrauma
if (repeat)
{
SpeakCannotReach();
return;
}
else
{
@@ -257,302 +289,290 @@ namespace Barotrauma
}
}
}
else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false))
else if (HumanAIController.HasValidPath(requireUnfinished: false))
{
waitUntilPathUnreachable = pathWaitingTime;
}
}
if (!Abandon)
if (Abandon) { return; }
if (getDivingGearIfNeeded)
{
if (getDivingGearIfNeeded)
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
if (Mimic && !character.IsImmuneToPressure)
{
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
if (Mimic && !character.IsImmuneToPressure)
if (HumanAIController.HasDivingSuit(followTarget))
{
if (HumanAIController.HasDivingSuit(followTarget))
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
tryToGetDivingGear = true;
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
tryToGetDivingGear = true;
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
if (repeat)
{
if (IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
if (needsDivingSuit)
{
if (character.CanSeeTarget(Target))
{
OnCompleted();
return;
}
}
}
}
float maxGapDistance = 500;
Character targetCharacter = Target as Character;
if (character.AnimController.InWater)
{
if (character.CurrentHull == null ||
IsFollowOrderObjective &&
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
{
if (seekGapsTimer > 0)
{
seekGapsTimer -= deltaTime;
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
if (!isRuins || !HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: true))
{
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (TargetGap != null)
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
// Check that nothing is blocking the way
Vector2 rayStart = character.SimPosition;
Vector2 rayEnd = TargetGap.SimPosition;
if (TargetGap.Submarine != null && character.Submarine == null)
{
rayStart -= TargetGap.Submarine.SimPosition;
}
else if (TargetGap.Submarine == null && character.Submarine != null)
{
rayEnd -= character.Submarine.SimPosition;
}
var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
if (closestBody != null)
{
TargetGap = null;
}
}
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
if (repeat && IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
{
if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder))
{
OnCompleted();
return;
}
}
}
float maxGapDistance = 500;
Character targetCharacter = Target as Character;
if (character.AnimController.InWater)
{
if (character.CurrentHull == null ||
IsFollowOrder &&
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
{
if (seekGapsTimer > 0)
{
seekGapsTimer -= deltaTime;
}
else
{
TargetGap = null;
}
if (TargetGap != null)
{
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
bool isEitherOneInside = isInside || Target.Submarine != null;
if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath()))
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
return;
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (TargetGap != null)
{
// Check that nothing is blocking the way
Vector2 rayStart = character.SimPosition;
Vector2 rayEnd = TargetGap.SimPosition;
if (TargetGap.Submarine != null && character.Submarine == null)
{
rayStart -= TargetGap.Submarine.SimPosition;
}
else if (TargetGap.Submarine == null && character.Submarine != null)
{
rayEnd -= character.Submarine.SimPosition;
}
var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
if (closestBody != null)
{
TargetGap = null;
}
}
}
else
{
TargetGap = null;
}
}
if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
Identifier scooterTag = "scooter".ToIdentifier();
Identifier batteryTag = "mobilebattery".ToIdentifier();
Item scooter = null;
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
if (!shouldUseScooter)
{
float threshold = 500;
if (isInside)
{
Vector2 diff = Target.WorldPosition - character.WorldPosition;
shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150;
}
else
{
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
}
}
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
scooter = equippedScooters.FirstOrDefault();
}
else if (shouldUseScooter)
{
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && character.Inventory.CheckIfAnySlotAvailable(leftHandItem, inWrongSlot: false) == -1) ||
(rightHandItem != null && character.Inventory.CheckIfAnySlotAvailable(rightHandItem, inWrongSlot: false) == -1);
if (!handsFull)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
{
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
// Equip only if we have a battery available
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
}
if (scooter != null && character.HasEquippedItem(scooter))
{
if (shouldUseScooter)
{
useScooter = true;
// Check the battery
if (scooter.ContainedItems.None(i => i.Condition > 0))
{
// Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.anySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
{
useScooter = false;
}
}
else
{
useScooter = false;
}
}
}
if (!useScooter)
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot);
}
}
}
else
{
checkScooterTimer -= deltaTime;
}
}
else
{
TargetGap = null;
useScooter = false;
checkScooterTimer = 0;
}
if (SteeringManager == PathSteering)
if (TargetGap != null)
{
Vector2 targetPos = character.GetRelativeSimPosition(Target);
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrder ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
{
nodeFilter = n => n.Waypoint.CurrentHull != null;
}
else if (!isInside && HumanAIController.UseIndoorSteeringOutside)
{
nodeFilter = n => n.Waypoint.Submarine == null;
}
if (!isInside && !UsePathingOutside)
{
PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
}
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
return;
}
else
{
PathSteering.SteeringSeek(targetPos, weight: 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter: endNodeFilter,
nodeFilter: nodeFilter,
checkVisiblity: Target is Item || Target is Character);
TargetGap = null;
}
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
}
if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
Identifier scooterTag = "scooter".ToIdentifier();
Identifier batteryTag = "mobilebattery".ToIdentifier();
Item scooter = null;
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
if (!shouldUseScooter)
{
if (useScooter)
float threshold = 500;
if (isInside)
{
UseScooter(Target.WorldPosition);
Vector2 diff = Target.WorldPosition - character.WorldPosition;
shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150;
}
else
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
}
}
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
scooter = equippedScooters.FirstOrDefault();
}
else if (shouldUseScooter)
{
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
if (!handsFull)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
// Equip only if we have a battery available
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
}
else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
if (scooter != null && character.HasEquippedItem(scooter))
{
UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition);
if (shouldUseScooter)
{
useScooter = true;
// Check the battery
if (scooter.ContainedItems.None(i => i.Condition > 0))
{
// Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
{
useScooter = false;
}
}
else
{
useScooter = false;
}
}
}
if (!useScooter)
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
}
}
}
else
{
checkScooterTimer -= deltaTime;
}
}
else
{
TargetGap = null;
useScooter = false;
checkScooterTimer = 0;
}
if (SteeringManager == PathSteering)
{
Vector2 targetPos = character.GetRelativeSimPosition(Target);
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
{
nodeFilter = n => n.Waypoint.CurrentHull != null;
}
else if (!isInside)
{
if (HumanAIController.UseOutsideWaypoints)
{
nodeFilter = n => n.Waypoint.Submarine == null;
}
else
{
nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null;
}
}
if (!isInside && !UsePathingOutside)
{
character.ReleaseSecondaryItem();
PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
}
}
else
{
PathSteering.SteeringSeek(targetPos, weight: 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter: endNodeFilter,
nodeFilter: nodeFilter,
checkVisiblity: Target is Item || Target is Character);
}
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
if (useScooter)
{
@@ -560,19 +580,41 @@ namespace Barotrauma
}
else
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
character.ReleaseSecondaryItem();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
}
}
}
else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
{
UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition);
}
}
else
{
if (useScooter)
{
UseScooter(Target.WorldPosition);
}
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
}
}
}
void UseScooter(Vector2 targetWorldPos)
{
if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; }
SteeringManager.Reset();
character.ReleaseSecondaryItem();
character.CursorPosition = targetWorldPos;
if (character.Submarine != null)
{
@@ -580,7 +622,7 @@ namespace Barotrauma
}
Vector2 diff = character.CursorPosition - character.Position;
Vector2 dir = Vector2.Normalize(diff);
if (character.CurrentHull == null && IsFollowOrderObjective)
if (character.CurrentHull == null && IsFollowOrder)
{
float sqrDist = diff.LengthSquared();
if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f))
@@ -659,7 +701,7 @@ namespace Barotrauma
{
if (gap.Open < 1) { continue; }
if (gap.Submarine == null) { continue; }
if (!IsFollowOrderObjective)
if (!IsFollowOrder)
{
if (gap.FlowTargetHull == null) { continue; }
if (gap.Submarine != Target.Submarine) { continue; }
@@ -772,6 +814,16 @@ namespace Barotrauma
{
StopMovement();
HumanAIController.FaceTarget(Target);
if (Target is WayPoint { Ladders: null })
{
// Release ladders when ordered to wait at a spawnpoint.
// This is a special case specifically meant for NPCs that spawn in outposts with a wait order.
// Otherwise they might keep holding to the ladders when the target is just next to it.
if (character.IsClimbing && character.AnimController.IsAboveFloor)
{
character.StopClimbing();
}
}
base.OnCompleted();
}
@@ -781,6 +833,10 @@ namespace Barotrauma
findDivingGear = null;
seekGapsTimer = 0;
TargetGap = null;
if (SteeringManager is IndoorsSteeringManager pathSteering)
{
pathSteering.ResetPath();
}
}
}
}
@@ -163,15 +163,22 @@ namespace Barotrauma
character.SelectedItem = null;
CleanupItems(deltaTime);
if (!character.IsClimbing)
{
CleanupItems(deltaTime);
}
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null && !IsForbidden(character.CurrentHull))
{
TargetHull = character.CurrentHull;
}
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid)
bool currentTargetIsInvalid =
currentTarget == null ||
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))
{
currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
@@ -251,7 +258,8 @@ namespace Barotrauma
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, character.Submarine, nodeFilter: node =>
Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe hulls on the way to the target
@@ -271,7 +279,7 @@ namespace Barotrauma
return;
}
character.AIController.SelectTarget(currentTarget.AiTarget);
PathSteering.SetPath(path);
PathSteering.SetPath(targetPos, path);
SetTargetTimerNormal();
searchingNewHull = false;
}
@@ -305,12 +313,8 @@ namespace Barotrauma
{
if (character.IsClimbing)
{
if (character.AnimController.GetHeightFromFloor() < 0.1f)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedSecondaryItem = null;
}
return;
PathSteering.Reset();
character.StopClimbing();
}
var currentHull = character.CurrentHull;
if (!character.AnimController.InWater && currentHull != null)
@@ -362,6 +366,7 @@ namespace Barotrauma
}
else
{
character.ReleaseSecondaryItem();
PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
}
return;
@@ -24,7 +24,7 @@ namespace Barotrauma
private ImmutableHashSet<Identifier> ValidContainableItemIdentifiers { get; }
private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
private int itemIndex = 0;
private int itemIndex;
private AIObjectiveDecontainItem decontainObjective;
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
private Item targetItem;
@@ -219,9 +219,8 @@ namespace Barotrauma
return Priority;
}
public override void Update(float deltaTime)
protected override void Act(float deltaTime)
{
base.Update(deltaTime);
if (targetItem == null)
{
if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority))
@@ -233,6 +232,7 @@ namespace Barotrauma
}
targetItem = item;
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
float GetPriority(Item item)
{
try
@@ -256,11 +256,7 @@ namespace Barotrauma
}
}
}
}
protected override void Act(float deltaTime)
{
if (targetItem != null)
else
{
if(decontainObjective == null && !IsValidContainable(targetItem))
{
@@ -290,10 +286,6 @@ namespace Barotrauma
Reset();
});
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
private bool IsValidContainable(Item item)
@@ -310,7 +302,7 @@ namespace Barotrauma
if (parentItem.HasTag("donttakeitems")) { return false; }
}
if (!item.HasAccess(character)) { return false; }
if (!character.HasItem(item) && !CanEquip(item)) { return false; }
if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; }
if (!ItemContainer.CanBeContained(item)) { return false; }
if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; }
if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full)
@@ -63,7 +63,7 @@ namespace Barotrauma
{
if (item == null || item.Removed) { return false; }
if (targetContainerTags.HasValue && !OrderPrefab.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; }
if (!(item.GetComponent<ItemContainer>() is ItemContainer container)) { return false; }
if ((item.GetComponent<ItemContainer>() is not ItemContainer container)) { return false; }
if (container.Inventory == null) { return false; }
if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; }
if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; }
@@ -48,6 +48,8 @@ namespace Barotrauma
protected virtual bool ResetWhenClearingIgnoreList => true;
protected virtual bool ForceOrderPriority => true;
protected virtual int MaxTargets => int.MaxValue;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
public override void Update(float deltaTime)
@@ -89,12 +91,7 @@ namespace Barotrauma
var target = objective.Key;
if (!Targets.Contains(target))
{
var subObjective = objective.Value;
if (CurrentSubObjective == subObjective)
{
CurrentSubObjective.Abandon = !CurrentSubObjective.IsCompleted;
}
subObjectives.Remove(subObjective);
subObjectives.Remove(objective.Value);
}
}
SyncRemovedObjectives(Objectives, GetList());
@@ -157,6 +154,11 @@ namespace Barotrauma
else
{
float max = AIObjectiveManager.LowestOrderPriority - 1;
if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character))
{
// Allow higher prio
max = AIObjectiveManager.EmergencyObjectivePriority;
}
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
@@ -188,6 +190,10 @@ namespace Barotrauma
if (!ignoreList.Contains(target))
{
Targets.Add(target);
if (Targets.Count > MaxTargets)
{
break;
}
}
}
}
@@ -20,6 +20,8 @@ namespace Barotrauma
MaxValue = 2
}
public const float MaxObjectivePriority = 100;
public const float EmergencyObjectivePriority = 90;
public const float HighestOrderPriority = 70;
public const float LowestOrderPriority = 60;
public const float RunPriority = 50;
@@ -125,11 +127,21 @@ namespace Barotrauma
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
}
var prevIdleObjective = GetObjective<AIObjectiveIdle>();
DelayedObjectives.Clear();
Objectives.Clear();
FailedAutonomousObjectives = false;
AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this));
var newIdleObjective = new AIObjectiveIdle(character, this);
if (prevIdleObjective != null)
{
newIdleObjective.TargetHull = prevIdleObjective.TargetHull;
newIdleObjective.Behavior = prevIdleObjective.Behavior;
prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t));
}
AddObjective(newIdleObjective);
int objectiveCount = Objectives.Count;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
@@ -155,7 +167,7 @@ namespace Barotrauma
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
}
}
}
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
@@ -410,12 +422,12 @@ namespace Barotrauma
newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = Rand.Range(80f, 100f),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == order.OrderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountBotsInTheCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == order.OrderGiver)) * Rand.Range(0.8f, 1f), 4),
ExtraDistanceOutsideSub = 100,
ExtraDistanceWhileSwimming = 100,
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
IsFollowOrderObjective = true,
IsFollowOrder = true,
Mimic = character.IsOnPlayerTeam,
DialogueIdentifier = "dialogcannotreachplace".ToIdentifier()
};
@@ -423,7 +435,11 @@ namespace Barotrauma
case "wait":
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = true
AllowGoingOutside = true,
IsWaitOrder = true,
DebugLogWhenFails = false,
SpeakIfFails = false,
CloseEnough = 100
};
break;
case "return":
@@ -633,10 +649,11 @@ namespace Barotrauma
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T;
/// <summary>
/// Returns the last active objective of the specific type.
/// </summary>
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
public T GetLastActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
public T GetFirstActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
@@ -122,7 +122,7 @@ namespace Barotrauma
else if (!isOrder)
{
var steering = component?.Item.GetComponent<Steering>();
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain)))
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true)))
{
// Ignore if already set to autopilot or if there's a captain onboard
Priority = 0;
@@ -204,7 +204,7 @@ namespace Barotrauma
}
if (operateTarget != null)
{
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
if (HumanAIController.IsTrueForAnyBotInTheCrew(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
{
// Another crew member is already targeting this entity (leak).
Abandon = true;
@@ -69,16 +69,6 @@ namespace Barotrauma
if (subObjective != null && subObjective.IsCompleted)
{
Priority = 0;
items.RemoveWhere(i => i == null || i.Removed || !i.IsOwnedBy(character));
if (items.None())
{
Abandon = true;
}
else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character))))
{
Reset();
}
}
return Priority;
}
@@ -162,25 +152,25 @@ namespace Barotrauma
};
}
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
onCompleted: () =>
{
if (KeepActiveWhenReady)
onCompleted: () =>
{
if (getSingleItemObjective != null)
if (KeepActiveWhenReady)
{
var item = getSingleItemObjective?.TargetItem;
if (item?.IsOwnedBy(character) != null)
if (getSingleItemObjective != null)
{
items.Add(item);
var item = getSingleItemObjective?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
}
else
{
IsCompleted = true;
}
},
onAbandon: () => Abandon = true))
else
{
IsCompleted = true;
}
},
onAbandon: () => Abandon = true))
{
Abandon = true;
}
@@ -102,7 +102,7 @@ namespace Barotrauma
// Don't stop fixing until completely done
return 100;
}
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>() && !c.Character.IsIncapacitated, onlyBots: true);
int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>());
int items = Targets.Count;
if (items == 0)
{
@@ -116,7 +116,7 @@ namespace Barotrauma
}
else
{
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f))
{
// Enough fixers
return 0;
@@ -293,6 +293,11 @@ namespace Barotrauma
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
//allow taking items from the target's inventory too if the target is unconscious
if (matchingItem == null && targetCharacter.IsIncapacitated)
{
matchingItem ??= targetCharacter.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true);
}
if (matchingItem != null)
{
bestItem = matchingItem;
@@ -379,24 +384,24 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
if (character != targetCharacter && character.IsOnPlayerTeam)
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
SpeakCannotTreat();
}
});
}
else if (cprSuitability <= 0)
{
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
Abandon = true;
SpeakCannotTreat();
}
}
}
else if (!targetCharacter.IsUnconscious)
{
//no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
Abandon = true;
//no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
SpeakCannotTreat();
return;
}
if (character != targetCharacter)
@@ -414,6 +419,14 @@ namespace Barotrauma
}
}
private void SpeakCannotTreat()
{
LocalizedString msg = character == targetCharacter ?
TextManager.Get("dialogcannottreatself") :
TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No);
character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
}
private void ApplyTreatment(Affliction affliction, Item item)
{
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
@@ -433,50 +446,36 @@ namespace Barotrauma
protected override float GetPriority()
{
if (!IsAllowed)
if (!IsAllowed || targetCharacter == null)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (character.CurrentHull == null)
if (character.CurrentHull != null)
{
if (!objectiveManager.HasOrder<AIObjectiveRescueAll>())
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
{
// Don't go into rooms that have enemies
Priority = 0;
Abandon = true;
return Priority;
}
}
else if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y);
if (character.Submarine?.Info is { IsRuin: false })
{
// Don't go into rooms that have enemies
Priority = 0;
Abandon = true;
return Priority;
verticalDistance *= 2;
}
if (targetCharacter == null)
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
if (character.CurrentHull != null && targetCharacter.CurrentHull == character.CurrentHull)
{
Priority = 0;
Abandon = true;
}
else
{
float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y);
if (character.Submarine?.Info is { IsRuin: false })
{
verticalDistance *= 2;
}
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
if (targetCharacter.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
distanceFactor = 1;
}
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
return Priority;
}
@@ -13,6 +13,8 @@ namespace Barotrauma
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
private readonly HashSet<Character> charactersWithMinorInjuries = new HashSet<Character>();
private const float vitalityThreshold = 75;
private const float vitalityThresholdForOrders = 90;
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
@@ -23,17 +25,34 @@ namespace Barotrauma
}
else
{
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? vitalityThresholdForOrders : vitalityThreshold;
}
}
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override bool Filter(Character target)
{
if (!IsValidTarget(target, character, requireTreatableAfflictions: false)) { return false; }
if (GetTreatableAfflictions(target).Any())
{
return true;
}
else
{
//the target might be at a low enough health to be considered a valid target,
//but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them
// -> make the bot speak to make it clear the bot intentionally ignores very minor injuries
if (!charactersWithMinorInjuries.Contains(character))
{
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
null, 1.0f, $"notreatableafflictions{target.Name}".ToIdentifier(), 10.0f);
charactersWithMinorInjuries.Add(character);
}
return false;
}
}
protected override IEnumerable<Character> GetList() => Character.CharacterList;
@@ -42,15 +61,16 @@ namespace Barotrauma
if (Targets.None()) { return 100; }
if (!objectiveManager.IsOrder(this))
{
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious))
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsMedic, onlyActive: true, onlyConnectedSubs: true))
{
// Don't do anything if there's a medic on board and we are not a medic
// Don't do anything if there's a medic on board actively treating and we are not a medic
return 100;
}
}
float worstCondition = Targets.Min(t => GetVitalityFactor(t));
if (Targets.Contains(character))
{
// Targeting self -> higher prio
if (character.Bleeding > 10)
{
// Enforce the highest priority when bleeding out.
@@ -67,7 +87,7 @@ namespace Barotrauma
float vitality = 100;
vitality -= character.Bleeding * 2;
vitality += Math.Min(character.Oxygen, 0);
foreach (Affliction affliction in GetTreatableAfflictions(character))
foreach (Affliction affliction in GetTreatableAfflictions(character, ignoreTreatmentThreshold: true))
{
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
@@ -83,12 +103,19 @@ namespace Barotrauma
return Math.Clamp(vitality, 0, 100);
}
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold = false)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (affliction.Prefab.IsBuff) { continue; }
if (!ignoreTreatmentThreshold)
{
//other afflictions of the same type increase the "treatability"
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
float totalAfflictionStrength = character.CharacterHealth.GetTotalAdjustedAfflictionStrength(affliction);
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
}
if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
@@ -101,7 +128,7 @@ namespace Barotrauma
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
public static bool IsValidTarget(Character target, Character character)
public static bool IsValidTarget(Character target, Character character, bool requireTreatableAfflictions = true)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target.IsInstigator) { return false; }
@@ -111,13 +138,13 @@ namespace Barotrauma
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target))
{
return false;
return false;
}
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
{
if (!character.IsMedic && target != character)
{
// Don't allow to treat others autonomously
// Don't allow to treat others autonomously, unless we are a medic
return false;
}
// Ignore unsafe hulls, unless ordered
@@ -126,6 +153,10 @@ namespace Barotrauma
return false;
}
}
if (requireTreatableAfflictions && GetTreatableAfflictions(target).None())
{
return false;
}
}
else
{
@@ -136,17 +167,17 @@ namespace Barotrauma
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; }
}
else
else if (target.Submarine != null)
{
return target.Submarine == null;
// We are outside, but the target is inside.
return false;
}
if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters
// Ignore all concious targets that are currently fighting, fleeing, or treating characters
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFixLeak>())
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
return false;
}
@@ -158,5 +189,11 @@ namespace Barotrauma
}
return character.GetDamageDoneByAttacker(target) <= 0;
}
public override void Reset()
{
base.Reset();
charactersWithMinorInjuries.Clear();
}
}
}
@@ -228,8 +228,8 @@ namespace Barotrauma
{
continue;
}
//optimization: node extremely close (< 1 m). If it's valid, choose it as the start node and skip the more exhaustive search for the closest one
if (node.TempDistance < 1.0f)
//optimization: node close enough. If it's valid, choose it as the start node and skip the more exhaustive search for the closest one
if (node.TempDistance < FarseerPhysics.ConvertUnits.ToSimUnits(AIObjectiveGetItem.DefaultReach))
{
if (IsValidStartNode(node))
{
@@ -1,5 +1,6 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,7 +8,7 @@ namespace Barotrauma
{
class ShipIssueWorkerOperateWeapons : ShipIssueWorkerItem
{
public override float RedundantIssueModifier => 0.65f;
public override float RedundantIssueModifier => 0.8f;
private readonly List<float> targetingImportances = new List<float>();
public override bool AllowEasySwitching => true;
@@ -17,43 +18,52 @@ namespace Barotrauma
float GetTargetingImportance(Entity entity)
{
float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition);
float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance * 0.5f);
if (TargetItem.Submarine != null)
if (currentDistanceToEnemy > Sonar.DefaultSonarRange) { return 0.0f; }
float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MaxImportance * 0.1f, MaxImportance * 0.5f);
if (TargetItem.Submarine != null && importance > 0.0f)
{
Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition;
Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition;
if (Vector2.Dot(dir, submarineDir) < 0)
if (TargetItemComponent is Turret turret)
{
//direction from the weapon to the target is opposite to the direction from the sub to the weapon
// = the turret is most likely on the wrong side of the sub, reduce importance
importance *= 0.1f;
if (!turret.CheckTurretAngle(entity.WorldPosition))
{
importance *= 0.1f;
}
}
else
{
Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition;
Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition;
if (Vector2.Dot(dir, submarineDir) < 0)
{
//direction from the weapon to the target is opposite to the direction from the sub to the weapon
// = the turret is most likely on the wrong side of the sub, reduce importance
importance *= 0.1f;
}
}
}
return importance;
}
public override void CalculateImportanceSpecific()
{
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot())
{
//operate (= recharge the turrets) with low priority if they're out of power
//if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority
Importance = ShipCommandManager.MinimumIssueThreshold * 1.05f;
return;
}
targetingImportances.Clear();
foreach (Character character in shipCommandManager.EnemyCharacters)
{
targetingImportances.Add(GetTargetingImportance(character));
}
// there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing
if (targetingImportances.Any(i => i > 0))
{
targetingImportances.Sort();
Importance = targetingImportances.TakeLast(3).Sum();
Importance = Math.Max(targetingImportances.TakeLast(3).Sum(), ShipCommandManager.MinimumIssueThreshold);
}
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot())
{
//operate (= recharge the turrets) with low priority if they're out of power
//if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority
Importance = Math.Max(ShipCommandManager.MinimumIssueThreshold / RedundantIssueModifier, Importance);
return;
}
}
}
@@ -256,7 +256,7 @@ namespace Barotrauma
float bestValue = 0f;
Character bestCharacter = null;
if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold)
if (mostImportantIssue != null && mostImportantIssue.Importance >= MinimumIssueThreshold)
{
IEnumerable<Character> bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrder, AlliedCharacters, character, true);
@@ -51,7 +51,7 @@ namespace Barotrauma
return;
}
if (!IsRemotePlayer && !(AIController is HumanAIController))
if (!IsRemotePlayer && AIController is not HumanAIController)
{
float characterDistSqr = GetDistanceSqrToClosestPlayer();
if (characterDistSqr > MathUtils.Pow2(Params.DisableDistance * 0.5f))
@@ -63,6 +63,10 @@ namespace Barotrauma
AnimController.SimplePhysicsEnabled = false;
}
}
else
{
AnimController.SimplePhysicsEnabled = false;
}
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (Controlled == this) { return; }
@@ -1,10 +1,10 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -1259,7 +1259,7 @@ namespace Barotrauma
if (ladder.Item.Prefab.Triggers.None())
{
character.SelectedSecondaryItem = null;
character.ReleaseSecondaryItem();
return;
}
@@ -1544,6 +1544,10 @@ namespace Barotrauma
target.AnimController.ResetPullJoints();
}
bool targetPoseControlled =
target.SelectedItem?.GetComponent<Controller>() is { ControlCharacterPose: true } ||
target.SelectedSecondaryItem?.GetComponent<Controller>() is { ControlCharacterPose: true };
if (IsClimbing)
{
//cannot drag up ladders if the character is conscious
@@ -1719,13 +1723,12 @@ namespace Barotrauma
targetForce = 5000.0f;
}
targetLimb.PullJointEnabled = true;
targetLimb.PullJointMaxForce = targetForce;
targetLimb.PullJointWorldAnchorB = targetAnchor;
targetLimb.Disabled = true;
if (diff.LengthSquared() > 0.1f)
if (!targetPoseControlled)
{
targetLimb.PullJointEnabled = true;
targetLimb.PullJointMaxForce = targetForce;
targetLimb.PullJointWorldAnchorB = targetAnchor;
targetLimb.Disabled = true;
target.AnimController.movement = -diff;
}
}
@@ -1751,7 +1754,7 @@ namespace Barotrauma
target.AnimController.IgnorePlatforms = IgnorePlatforms;
target.AnimController.TargetMovement = TargetMovement;
}
else if (target is AICharacter && target != Character.Controlled)
else if (target is AICharacter && target != Character.Controlled && !targetPoseControlled)
{
if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X)
{
@@ -78,6 +78,11 @@ namespace Barotrauma
}
}
/// <summary>
/// Attacks are used to deal damage to characters, structures and items.
/// They can be defined in the weapon components of the items or the limb definitions of the characters.
/// The limb attacks can also be used by the player, when they control a monster or have some appendage, like a husk stinger.
/// </summary>
partial class Attack : ISerializableEntity
{
[Serialize(AttackContext.Any, IsPropertySaveable.Yes, description: "The attack will be used only in this context."), Editable]
@@ -123,7 +128,7 @@ namespace Barotrauma
set => _damageRange = value;
}
[Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the minimum range required for the attack to hit."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float MinRange { get; private set; }
[Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)]
@@ -138,22 +143,22 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float CoolDownRandomFactor { get; private set; } = 0;
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "When set to true, causes the enemy AI to use the fast movement animations when the attack is on cooldown."), Editable]
public bool FullSpeedAfterAttack { get; private set; }
private float _structureDamage;
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to submarine walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
public float StructureDamage
{
get => _structureDamage * DamageMultiplier;
set => _structureDamage = value;
}
[Serialize(true, IsPropertySaveable.Yes), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "Whether or not damaging structures with the attack causes damage particles to emit."), Editable]
public bool EmitStructureDamageParticles { get; private set; }
private float _itemDamage;
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to items."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float ItemDamage
{
get =>_itemDamage * DamageMultiplier;
@@ -178,16 +183,16 @@ namespace Barotrauma
/// </summary>
public float ImpactMultiplier { get; set; } = 1;
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to level walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float LevelWallDamage { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Sets whether or not the attack is ranged or not."), Editable]
public bool Ranged { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description:"When enabled the attack will not be launched when there's a friendly character in the way. Only affects ranged attacks."), Editable]
public bool AvoidFriendlyFire { get; set; }
[Serialize(20f, IsPropertySaveable.Yes, description: "Only affects ranged attacks."), Editable]
[Serialize(20f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine how accurately the attack needs to be aimed for the attack to trigger. Only affects ranged attacks."), Editable]
public float RequiredAngle { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable]
@@ -205,16 +210,13 @@ namespace Barotrauma
[Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable]
public float SwayFrequency { get; set; }
/// <summary>
/// Legacy support. Use Afflictions.
/// </summary>
[Serialize(0.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No, description: "Legacy support. Use Afflictions.")]
public float Stun { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable]
public bool OnlyHumans { get; set; }
[Serialize("", IsPropertySaveable.Yes), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "List of limb indices to apply the force into."), Editable]
public string ApplyForceOnLimbs
{
get
@@ -240,20 +242,20 @@ namespace Barotrauma
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldStart { get; private set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldMiddle { get; private set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
public Vector2 RootForceWorldEnd { get; private set; }
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:""), Editable]
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:"Applied to the main limb. The transition smoothing of the applied force."), Editable]
public TransitionMode RootTransitionEasing { get; private set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)]
public float Torque { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Only apply the force once during the attacks lifetime."), Editable]
public bool ApplyForcesOnlyOnce { get; private set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
@@ -279,10 +281,10 @@ namespace Barotrauma
//public float StickChance { get; set; }
public float StickChance => 0f;
[Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the priority when selecting attacks. When random attacks are disabled on the character it is multiplied with distance to determine the which attack to use. Only attacks that are currently valid are taken into consideration when making the decision."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float Priority { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: ""), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Triggers the 'blink' animation on the attacking limbs when the attack executes. Used e.g. by abyss monsters to make their jaws close when attacking."), Editable]
public bool Blink { get; private set; }
public IEnumerable<StatusEffect> StatusEffects
@@ -298,7 +300,7 @@ namespace Barotrauma
private set;
} = new Dictionary<Identifier, SerializableProperty>();
//the indices of the limbs Force is applied on
//the indices of the limbs Force is applied on
//(if none, force is applied only to the limb the attack is attached to)
public readonly List<int> ForceOnLimbIndices = new List<int>();
@@ -309,6 +311,10 @@ namespace Barotrauma
/// </summary>
public List<PropertyConditional> Conditionals { get; private set; } = new List<PropertyConditional>();
/// <summary>
/// StatusEffects to apply when the attack triggers.
/// StatusEffect types of 'OnUse' are executed always, 'OnFailure' only when the attack doesn't deal damage and 'OnSuccess' executes when some damage is dealt.
/// </summary>
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
public void SetUser(Character user)
@@ -322,7 +328,7 @@ namespace Barotrauma
// used for talents/ability conditions
public Item SourceItem { get; set; }
public List<Affliction> GetMultipliedAfflictions(float multiplier)
{
List<Affliction> multipliedAfflictions = new List<Affliction>();
@@ -428,7 +434,13 @@ namespace Barotrauma
}
break;
case "conditional":
Conditionals.AddRange(PropertyConditional.FromXElement(subElement));
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
Conditionals.Add(new PropertyConditional(attribute));
}
}
break;
}
}
@@ -11,6 +11,7 @@ using FarseerPhysics.Dynamics;
using Barotrauma.Extensions;
using System.Collections.Immutable;
using Barotrauma.Abilities;
using System.Diagnostics;
#if SERVER
using System.Text;
#endif
@@ -964,6 +965,7 @@ namespace Barotrauma
/// The secondary selected item. It's an item other than a device (see <see cref="SelectedItem"/>), e.g. a ladder or a chair.
/// </summary>
public Item SelectedSecondaryItem { get; set; }
public void ReleaseSecondaryItem() => SelectedSecondaryItem = null;
/// <summary>
/// Has the characters selected a primary or a secondary item?
/// </summary>
@@ -1063,7 +1065,7 @@ namespace Barotrauma
{
get
{
return SelectedItem == null || (SelectedItem.GetComponent<Controller>()?.AllowAiming ?? false);
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsIncapacitated && !IsRagdolled;
}
}
@@ -1126,6 +1128,7 @@ namespace Barotrauma
public HashSet<Identifier> MarkedAsLooted = new();
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public bool IsInPlayerSub => Submarine != null && Submarine.Info.IsPlayer;
public float AITurretPriority
{
@@ -2432,6 +2435,8 @@ namespace Barotrauma
return true;
}
private Stopwatch sw;
private Stopwatch StopWatch => sw ??= new Stopwatch();
private float _selectedItemPriority;
private Item _foundItem;
/// <summary>
@@ -2445,14 +2450,20 @@ namespace Barotrauma
IEnumerable<Item> ignoredItems = null, IEnumerable<Identifier> ignoredContainerIdentifiers = null,
Func<Item, bool> customPredicate = null, Func<Item, float> customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null)
{
if (HumanAIController.DebugAI)
{
StopWatch.Restart();
}
if (itemIndex == 0)
{
_foundItem = null;
_selectedItemPriority = 0;
}
for (int i = 0; i < 10 && itemIndex < Item.ItemList.Count - 1; i++)
int itemsPerFrame = IsOnPlayerTeam ? 100 : 10;
int checkedItemCount = 0;
for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++)
{
itemIndex++;
checkedItemCount++;
var item = Item.ItemList[itemIndex];
if (!item.IsInteractable(this)) { continue; }
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
@@ -2492,7 +2503,21 @@ namespace Barotrauma
}
}
targetItem = _foundItem;
return itemIndex >= Item.ItemList.Count - 1;
bool completed = itemIndex >= Item.ItemList.Count - 1;
if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1)
{
var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
if (StopWatch.ElapsedMilliseconds > 5)
{
DebugConsole.ThrowError(msg);
}
else
{
// An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue.
DebugConsole.AddWarning(msg);
}
}
return completed;
}
public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null;
@@ -2512,7 +2537,7 @@ namespace Barotrauma
}
}
return checkVisibility ? CanSeeCharacter(c) : true;
return !checkVisibility || CanSeeCharacter(c);
}
public bool CanInteractWith(Item item, bool checkLinked = true)
@@ -2679,27 +2704,6 @@ namespace Barotrauma
CustomInteractHUDText = hudText;
}
private void TransformCursorPos()
{
if (Submarine == null)
{
//character is outside but cursor position inside
if (cursorPosition.Y > Level.Loaded.Size.Y)
{
var sub = Submarine.FindContainingInLocalCoordinates(cursorPosition);
if (sub != null) cursorPosition += sub.Position;
}
}
else
{
//character is inside but cursor position is outside
if (cursorPosition.Y < Level.Loaded.Size.Y)
{
cursorPosition -= Submarine.Position;
}
}
}
public void SelectCharacter(Character character)
{
if (character == null || character == this) { return; }
@@ -2863,7 +2867,7 @@ namespace Barotrauma
}
#endif
}
else if (!IsClimbing)
else
{
#if CLIENT
if (Controlled == this)
@@ -2906,14 +2910,14 @@ namespace Barotrauma
else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null && SelectedSecondaryItem.GetComponent<Ladder>() == null &&
(focusedItem == null || focusedItem == SelectedSecondaryItem || !selectInputSameAsDeselect))
{
SelectedSecondaryItem = null;
ReleaseSecondaryItem();
#if CLIENT
CharacterHealth.OpenHealthWindow = null;
#endif
}
else if (IsKeyHit(InputType.Health) && SelectedItem != null)
else if (IsKeyHit(InputType.Health) && (SelectedItem != null || SelectedSecondaryItem != null))
{
SelectedItem = null;
SelectedItem = SelectedSecondaryItem = null;
}
else if (focusedItem != null)
{
@@ -3246,7 +3250,7 @@ namespace Barotrauma
}
if (SelectedSecondaryItem != null && !CanInteractWith(SelectedSecondaryItem))
{
SelectedSecondaryItem = null;
ReleaseSecondaryItem();
}
if (!IsDead) { LockHands = false; }
@@ -3460,14 +3464,19 @@ namespace Barotrauma
despawnTimer += deltaTime * despawnPriority;
if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; }
if (IsHuman)
Identifier despawnContainerId =
IsHuman ?
"despawncontainer".ToIdentifier() :
Params.DespawnContainer;
if (!despawnContainerId.IsEmpty)
{
var containerPrefab =
ItemPrefab.Prefabs.Find(me => me.Tags.Contains("despawncontainer")) ??
MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ??
ItemPrefab.Prefabs.Find(me => me?.Tags != null && me.Tags.Contains(despawnContainerId)) ??
(MapEntityPrefab.FindByIdentifier("metalcrate".ToIdentifier()) as ItemPrefab);
if (containerPrefab == null)
{
DebugConsole.NewMessage("Could not spawn a container for a despawned character's items. No item with the tag \"despawncontainer\" or the identifier \"metalcrate\" found.", Color.Red);
DebugConsole.NewMessage($"Could not spawn a container for a despawned character's items. No item with the tag \"{despawnContainerId}\" or the identifier \"metalcrate\" found.", Color.Red);
}
else
{
@@ -3591,7 +3600,7 @@ namespace Barotrauma
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!(character.AIController is HumanAIController)) { continue; }
if (character.AIController is not HumanAIController) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
@@ -4562,11 +4571,6 @@ namespace Barotrauma
SetStun(0.0f, true);
isDead = false;
if (info != null)
{
info.CauseOfDeath = null;
}
foreach (LimbJoint joint in AnimController.LimbJoints)
{
var revoluteJoint = joint.revoluteJoint;
@@ -4590,7 +4594,10 @@ namespace Barotrauma
limb.IsSevered = false;
}
GameMain.GameSession?.ReviveCharacter(this);
if (GameMain.GameSession != null)
{
GameMain.GameSession.ReviveCharacter(this);
}
}
public override void Remove()
@@ -4627,6 +4634,13 @@ namespace Barotrauma
CharacterList.Remove(this);
foreach (var attachedProjectile in AttachedProjectiles.ToList())
{
attachedProjectile.Unstick();
}
Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
Latchers.Clear();
if (Inventory != null)
{
foreach (Item item in Inventory.AllItems)
@@ -4713,6 +4727,8 @@ namespace Barotrauma
}
#if SERVER
newItem.GetComponent<Terminal>()?.SyncHistory();
if (newItem.GetComponent<WifiComponent>() is WifiComponent wifiComponent) { newItem.CreateServerEvent(wifiComponent); }
if (newItem.GetComponent<GeneticMaterial>() is GeneticMaterial geneticMaterial) { newItem.CreateServerEvent(geneticMaterial); }
#endif
int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 });
if (!slotIndices.Any())
@@ -4966,6 +4982,8 @@ namespace Barotrauma
#region Talents
private readonly List<CharacterTalent> characterTalents = new List<CharacterTalent>();
public IReadOnlyCollection<CharacterTalent> CharacterTalents => characterTalents;
public void LoadTalents()
{
List<Identifier> toBeRemoved = null;
@@ -5062,7 +5080,7 @@ namespace Barotrauma
public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{
foreach (var characterTalent in characterTalents)
foreach (CharacterTalent characterTalent in CharacterTalents)
{
characterTalent.CheckTalent(abilityEffectType, abilityObject);
}
@@ -5358,7 +5376,7 @@ namespace Barotrauma
public void StopClimbing()
{
AnimController.StopClimbing();
SelectedSecondaryItem = null;
ReleaseSecondaryItem();
}
}
@@ -905,10 +905,19 @@ namespace Barotrauma
Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (talentIdentifier == Identifier.Empty) { continue; }
if (TalentPrefab.TalentPrefabs.TryGet(talentIdentifier, out TalentPrefab prefab))
{
foreach (TalentMigration migration in prefab.Migrations)
{
migration.TryApply(version, this);
}
}
UnlockedTalents.Add(talentIdentifier);
}
}
}
LoadHeadAttachments();
}
@@ -1886,6 +1895,21 @@ namespace Barotrauma
}
}
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
{
float statValue = GetSavedStatValue(statType, statIdentifier);
if (GameMain.NetworkMember is null) { return statValue; }
foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
{
int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier);
statValue = Math.Max(statValue, botStatValue);
}
return statValue;
}
public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
{
if (!SavedStatValues.ContainsKey(statType))
@@ -179,7 +179,7 @@ namespace Barotrauma
float currVitalityDecrease = MathHelper.Lerp(
currentEffect.MinVitalityDecrease,
currentEffect.MaxVitalityDecrease,
currentEffect.GetStrengthFactor(this));
currentEffect.GetStrengthFactor(strength));
if (currentEffect.MultiplyByMaxVitality)
{
@@ -386,6 +386,8 @@ namespace Barotrauma
{
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects)
{
if (Strength <= periodicEffect.MinStrength) { continue; }
if (periodicEffect.MaxStrength > 0 && Strength > periodicEffect.MaxStrength) { continue; }
PeriodicEffectTimers[periodicEffect] -= deltaTime;
if (PeriodicEffectTimers[periodicEffect] <= 0.0f)
{
@@ -495,6 +497,13 @@ namespace Barotrauma
/// </summary>
public void SetStrength(float strength)
{
if (!MathUtils.IsValid(strength))
{
#if DEBUG
DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({strength})\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
_nonClampedStrength = strength;
_strength = _nonClampedStrength;
activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength);
@@ -117,7 +117,7 @@ namespace Barotrauma
public readonly bool CauseSpeechImpediment;
/// <summary>
/// If not set to true, affected characters will no longer require air
/// If set to false, affected characters will no longer require air
/// once the affliction reaches the active stage.
/// </summary>
public readonly bool NeedsAir;
@@ -247,7 +247,7 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No, description:
"If set to true, MinVitalityDecrease and MaxVitalityDecrease represent a fraction of the affected character's maximum " +
"vilatily, with 1 meaning 100%, instead of the same amount for all species.")]
"vitality, with 1 meaning 100%, instead of the same amount for all species.")]
public bool MultiplyByMaxVitality { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's lowest strength.")]
@@ -457,16 +457,22 @@ namespace Barotrauma
/// Returns 0 if affliction.Strength is MinStrength,
/// 1 if affliction.Strength is MaxStrength
/// </summary>
public float GetStrengthFactor(Affliction affliction)
public float GetStrengthFactor(Affliction affliction) => GetStrengthFactor(affliction.Strength);
/// <summary>
/// Returns 0 if affliction.Strength is MinStrength,
/// 1 if affliction.Strength is MaxStrength
/// </summary>
public float GetStrengthFactor(float strength)
=> MathUtils.InverseLerp(
MinStrength,
MaxStrength,
affliction.Strength);
strength);
}
/// <summary>
/// Description element can be used to define descriptions for the affliction that are shown at specific conditions.
/// For example a description that only shows to other players or only at certain strength levels.
/// The description element can be used to define descriptions for the affliction which are shown under specific conditions;
/// for example a description that only shows to other players or only at certain strength levels.
/// </summary>
/// <doc>
/// <Field identifier="Text" type="string" defaultValue="&quot;&quot;">
@@ -692,8 +698,7 @@ namespace Barotrauma
public readonly bool ShowBarInHealthMenu;
/// <summary>
/// If set to true, this affliction's icon will be hidden from the HUD
/// after 5 seconds.
/// If set to true, this affliction's icon will be hidden from the HUD after 5 seconds.
/// </summary>
public readonly bool HideIconAfterDelay;
@@ -701,37 +706,37 @@ namespace Barotrauma
/// How high the strength has to be for the affliction to take effect
/// </summary>
public readonly float ActivationThreshold = 0.0f;
/// <summary>
/// How high the strength has to be for the affliction icon to be shown in the UI
/// </summary>
public readonly float ShowIconThreshold = 0.05f;
/// <summary>
/// How high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface
/// </summary>
public readonly float ShowIconToOthersThreshold = 0.05f;
/// <summary>
/// The maximum strength this affliction can have.
/// </summary>
public readonly float MaxStrength = 100.0f;
/// <summary>
/// The strength of the radiation grain effect to apply
/// when the strength of this affliction increases.
/// The strength of the radiation grain effect to apply when the strength of this affliction increases.
/// </summary>
public readonly float GrainBurst;
/// <summary>
/// How high the strength has to be for the affliction icon to be shown with a health scanner
/// </summary>
public readonly float ShowInHealthScannerThreshold = 0.05f;
public readonly float ShowInHealthScannerThreshold;
/// <summary>
/// How strong the affliction needs to be before bots attempt to treat it
/// How strong the affliction needs to be before bots attempt to treat it.
/// Also effects when the affliction is shown in the suitable treatments list.
/// </summary>
public readonly float TreatmentThreshold = 5.0f;
public readonly float TreatmentThreshold;
/// <summary>
/// Bots will not try to treat the affliction if the character has any of these afflictions
@@ -758,13 +763,12 @@ namespace Barotrauma
/// </summary>
public readonly float DamageOverlayAlpha;
/// <summary>
/// Steam achievement given when the controlled character receives the affliction
/// Steam achievement given when the controlled character receives the affliction.
/// </summary>
public readonly Identifier AchievementOnReceived;
/// <summary>
/// Steam achievement given when the affliction is removed from the controlled character
/// Steam achievement given when the affliction is removed from the controlled character.
/// </summary>
public readonly Identifier AchievementOnRemoved;
@@ -775,10 +779,8 @@ namespace Barotrauma
public readonly Color[] IconColors;
/// <summary>
/// If set to true and the affliction has an AfflictionOverlay element,
/// the overlay's opacity will be strictly proportional to its strength.
/// Otherwise, the overlay's opacity will be determined based on its
/// activation threshold and effects.
/// If set to true and the affliction has an AfflictionOverlay element, the overlay's opacity will be strictly proportional to its strength.
/// Otherwise, the overlay's opacity will be determined based on its activation threshold and effects.
/// </summary>
public readonly bool AfflictionOverlayAlphaIsLinear;
@@ -788,16 +790,18 @@ namespace Barotrauma
public readonly bool ResetBetweenRounds;
/// <summary>
/// Should damage particles be emitted when a character receives this affliction? Only relevant if the affliction is of the type "bleeding" or "damage".
/// Should damage particles be emitted when a character receives this affliction?
/// Only relevant if the affliction is of the type "bleeding" or "damage".
/// </summary>
public readonly bool DamageParticles;
/// <summary>
/// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target.
/// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile.
/// If the affliction is of type buff, the skill is increased only when the target is friendly.
/// If the affliction causes damage or is of the 'poison' or 'paralysis' type, the skill is increased only when the target is hostile.
/// If the affliction is of the 'buff' type, the skill is increased only when the target is friendly.
/// </summary>
public readonly float MedicalSkillGain;
/// <summary>
/// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target.
/// The skill is increased only when the target is hostile.
@@ -827,14 +831,13 @@ namespace Barotrauma
private readonly ConstructorInfo constructor;
/// <summary>
/// Icon that's used in UI to represent this affliction.
/// An icon thats used in the UI to represent this affliction.
/// </summary>
public readonly Sprite Icon;
/// <summary>
/// A sprite that covers the affected player's entire screen when this affliction is active.
/// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier
/// and MaxAfflictionOverlayAlphaMultiplier
/// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier and MaxAfflictionOverlayAlphaMultiplier
/// </summary>
public readonly Sprite AfflictionOverlay;
@@ -844,7 +847,7 @@ namespace Barotrauma
{
foreach (var itemPrefab in ItemPrefab.Prefabs)
{
float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType));
float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType);
if (!MathUtils.NearlyEqual(suitability, 0.0f))
{
yield return new KeyValuePair<Identifier, float>(itemPrefab.Identifier, suitability);
@@ -912,7 +915,7 @@ namespace Barotrauma
ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold),
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f));
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f));
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
@@ -417,9 +418,13 @@ namespace Barotrauma
return strength;
}
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true)
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false)
{
if (!affliction.Prefab.IsBuff && Unkillable || Character.GodMode) { return; }
if (Character.GodMode) { return; }
if (!ignoreUnkillability)
{
if (!affliction.Prefab.IsBuff && Unkillable) { return; }
}
if (affliction.Prefab.LimbSpecific)
{
if (targetLimb == null)
@@ -449,11 +454,7 @@ namespace Barotrauma
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab.Identifier);
}
resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
if (resistance > 1f) { resistance = 1f; }
return resistance;
return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
}
public float GetStatValue(StatTypes statType)
@@ -974,7 +975,7 @@ namespace Barotrauma
#endif
}
private float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
{
float multiplier = 1.0f;
if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier))
@@ -1116,7 +1117,11 @@ namespace Barotrauma
strength = GetPredictedStrength(affliction, predictFutureDuration, limb);
}
if (strength <= affliction.Prefab.TreatmentThreshold) { continue; }
//other afflictions of the same type increase the "treatability"
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false);
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
if (ignoreHiddenAfflictions)
@@ -1133,13 +1138,20 @@ namespace Barotrauma
foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitability)
{
float suitability = treatment.Value * strength;
if (treatment.Value > strength)
{
//avoid using very effective meds on small injuries
float overtreatmentFactor = MathHelper.Clamp(treatment.Value / strength, 1.0f, 10.0f);
suitability /= overtreatmentFactor;
}
if (!treatmentSuitability.ContainsKey(treatment.Key))
{
treatmentSuitability[treatment.Key] = treatment.Value * strength;
treatmentSuitability[treatment.Key] = suitability;
}
else
{
treatmentSuitability[treatment.Key] += treatment.Value * strength;
treatmentSuitability[treatment.Key] += suitability;
}
minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability);
maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
@@ -1155,6 +1167,28 @@ namespace Barotrauma
}
}
/// <summary>
/// Returns the total strength of instances of the same affliction on all the characters limbs,
/// with a smaller weight given to the other afflictions on other limbs
/// </summary>
/// <param name="otherAfflictionMultiplier">Multiplier on the strengths of the afflictions on other limbs.</param>
/// <param name="includeSameAffliction">Should the strength of the provided affliction be included too?</param>
public float GetTotalAdjustedAfflictionStrength(Affliction affliction, float otherAfflictionMultiplier = 0.3f, bool includeSameAffliction = true)
{
float totalAfflictionStrength = includeSameAffliction ? affliction.Strength : 0;
if (affliction.Prefab.LimbSpecific)
{
foreach (Affliction otherAffliction in afflictions.Keys)
{
if (affliction.Prefab == otherAffliction.Prefab && affliction != otherAffliction)
{
totalAfflictionStrength += otherAffliction.Strength * otherAfflictionMultiplier;
}
}
}
return totalAfflictionStrength;
}
private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
public IEnumerable<Identifier> GetActiveAfflictionTags()
{
@@ -182,7 +182,9 @@ namespace Barotrauma
{
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)
{
DebugLogWhenFails = false
DebugLogWhenFails = false,
IsWaitOrder = true,
CloseEnough = 100
});
}
}
@@ -119,6 +119,9 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable]
public float AISlowTurretPriority { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Identifier or tag of the item the character's items are placed inside when the character despawns."), Editable]
public Identifier DespawnContainer { get; private set; }
public readonly CharacterFile File;
public XDocument VariantFile { get; private set; }
@@ -228,12 +231,17 @@ namespace Barotrauma
protected void CreateSubParams()
{
SubParams.Clear();
var health = MainElement.GetChildElement("health");
if (health != null)
var healthElement = MainElement.GetChildElement("health");
if (healthElement != null)
{
Health = new HealthParams(health, this);
SubParams.Add(Health);
Health = new HealthParams(healthElement, this);
}
else
{
DebugConsole.ThrowError($"No health parameters defined for character \"{(SpeciesName)}\".");
Health = new HealthParams(null, this);
}
SubParams.Add(Health);
// TODO: support for multiple ai elements?
var ai = MainElement.GetChildElement("ai");
if (ai != null)
@@ -29,7 +29,7 @@ namespace Barotrauma.Abilities
nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true);
}
protected void ApplyEffectSpecific(Character targetCharacter)
protected void ApplyEffectSpecific(Character targetCharacter, Limb targetLimb = null)
{
//prevent an infinite loop if an effect triggers itself
//(e.g. a talent that triggers when an affliction is applied, and applies that same affliction)
@@ -66,6 +66,11 @@ namespace Barotrauma.Abilities
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) && targetLimb != null)
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetLimb);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.SetUser(Character);
@@ -99,7 +104,7 @@ namespace Barotrauma.Abilities
{
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter && !applyToSelf)
{
ApplyEffectSpecific(targetCharacter);
ApplyEffectSpecific(targetCharacter, targetLimb: (abilityObject as AbilityApplyTreatment)?.TargetLimb);
}
else
{
@@ -6,11 +6,13 @@ namespace Barotrauma.Abilities
{
private readonly ItemTalentStats stat;
private readonly float value;
private readonly bool stackable;
public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
stackable = abilityElement.GetAttributeBool("stackable", true);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
@@ -25,7 +27,7 @@ namespace Barotrauma.Abilities
{
if (abilityObject is not IAbilityItem ability) { return; }
ability.Item.StatManager.ApplyStat(stat, value, CharacterTalent);
ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
}
}
}
@@ -9,12 +9,14 @@ namespace Barotrauma.Abilities
private readonly ItemTalentStats stat;
private readonly float value;
private readonly ImmutableHashSet<Identifier> tags;
private readonly bool stackable;
public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
stackable = abilityElement.GetAttributeBool("stackable", true);
}
public override void InitializeAbility(bool addingFirstTime)
@@ -41,7 +43,7 @@ namespace Barotrauma.Abilities
{
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
{
item.StatManager.ApplyStat(stat, value, CharacterTalent);
item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
}
}
}
@@ -22,15 +22,13 @@
{
foreach (Identifier afflictionIdentifier in afflictionIdentifiers)
{
if (affliction.Identifier != afflictionIdentifier) { continue; }
affliction.Strength *= 1 + addedMultiplier;
if (affliction.Identifier != afflictionIdentifier) { continue; }
AfflictionPrefab afflictionPrefab = affliction.Prefab;
if (!replaceWith.IsEmpty)
{
if (AfflictionPrefab.Prefabs.TryGet(replaceWith, out AfflictionPrefab afflictionPrefab))
{
abilityAffliction.Affliction = new Affliction(afflictionPrefab, abilityAffliction.Affliction.Strength);
}
}
AfflictionPrefab.Prefabs.TryGet(replaceWith, out afflictionPrefab);
}
abilityAffliction.Affliction = new Affliction(afflictionPrefab, affliction.Strength * (1 + addedMultiplier));
}
}
else
@@ -217,6 +217,11 @@ namespace Barotrauma.Abilities
public static StatTypes ParseStatType(string statTypeString, string debugIdentifier)
{
//backwards compatibility
if (statTypeString.Equals("MedicalItemDurationMultiplier", StringComparison.OrdinalIgnoreCase))
{
statTypeString = "BuffItemApplyingMultiplier";
}
if (!Enum.TryParse(statTypeString, true, out StatTypes statType))
{
DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in CharacterTalent (" + debugIdentifier + ")");
@@ -71,6 +71,29 @@ namespace Barotrauma
}
}
private static readonly HashSet<Identifier> checkedNonStackableTalents = new();
/// <summary>
/// Checks talents for a given AbilityObject taking into account non-stackable talents.
/// </summary>
public static void CheckTalentsForCrew(IEnumerable<Character> crew, AbilityEffectType type, AbilityObject abilityObject)
{
checkedNonStackableTalents.Clear();
foreach (Character character in crew)
{
foreach (CharacterTalent characterTalent in character.CharacterTalents)
{
if (!characterTalent.Prefab.AbilityEffectsStackWithSameTalent)
{
if (checkedNonStackableTalents.Contains(characterTalent.Prefab.Identifier)) { continue; }
checkedNonStackableTalents.Add(characterTalent.Prefab.Identifier);
}
characterTalent.CheckTalent(type, abilityObject);
}
}
}
public void CheckTalent(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{
if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups))
@@ -0,0 +1,109 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
internal abstract class TalentMigration
{
private readonly Version version;
private delegate TalentMigration TalentMigrationCtor(Version version, ContentXElement element);
private static readonly Dictionary<Identifier, TalentMigrationCtor> migrationTemplates =
new()
{
[new Identifier("AddStat")] =
static (version, element) => new TalentMigrationAddStat(version, element),
[new Identifier("UpdateStatIdentifier")] =
static (version, element) => new TalentMigrationUpdateStatIdentifier(version, element)
};
public bool TryApply(Version savedVersion, CharacterInfo info)
{
if (version <= savedVersion) { return false; }
Apply(info);
return true;
}
protected abstract void Apply(CharacterInfo info);
protected TalentMigration(Version targetVersion)
{
version = targetVersion;
}
public static TalentMigration FromXML(ContentXElement element)
{
Version? version = XMLExtensions.GetAttributeVersion(element, "version", null);
if (version is null)
{
throw new Exception("Talent migration version not defined.");
}
Identifier name = element.Name.ToString().ToIdentifier();
if (!migrationTemplates.TryGetValue(name, out TalentMigrationCtor? ctor))
{
throw new Exception($"Unknown talent migration type: {name}.");
}
return ctor(version, element);
}
}
/// <summary>
/// Migration that adds a missing permanent stat to the character.
/// </summary>
internal sealed class TalentMigrationAddStat : TalentMigration
{
[Serialize(StatTypes.None, IsPropertySaveable.Yes)]
public StatTypes StatType { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier StatIdentifier { get; set; }
[Serialize(0f, IsPropertySaveable.Yes)]
public float Value { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool RemoveOnDeath { get; set; }
public TalentMigrationAddStat(Version targetVersion, ContentXElement element) : base(targetVersion)
=> SerializableProperty.DeserializeProperties(this, element);
protected override void Apply(CharacterInfo info)
{
info.ChangeSavedStatValue(StatType, Value, StatIdentifier, RemoveOnDeath);
}
}
/// <summary>
/// Migration that updates permanent stat identifiers.
/// </summary>
internal class TalentMigrationUpdateStatIdentifier : TalentMigration
{
[Serialize("", IsPropertySaveable.Yes, "The old identifier to update.")]
public Identifier Old { get; set; }
[Serialize("", IsPropertySaveable.Yes, "What to change the old identifier to.")]
public Identifier New { get; set; }
public TalentMigrationUpdateStatIdentifier(Version targetVersion, ContentXElement element) : base(targetVersion)
=> SerializableProperty.DeserializeProperties(this, element);
protected override void Apply(CharacterInfo info)
{
foreach (SavedStatValue statValue in info.SavedStatValues.Values.SelectMany(static s => s))
{
if (statValue.StatIdentifier != Old) { continue; }
statValue.StatIdentifier = New;
}
}
}
}
@@ -1,4 +1,7 @@
#if CLIENT
using System;
using System.Collections.Immutable;
using System.Xml.Linq;
#if CLIENT
using Microsoft.Xna.Framework;
#endif
@@ -12,6 +15,11 @@ namespace Barotrauma
public LocalizedString Description { get; private set; }
/// <summary>
/// When set to false the AbilityEffects of multiple of the same talent will not be checked and only the first one.
/// </summary>
public bool AbilityEffectsStackWithSameTalent;
public readonly Sprite Icon;
#if CLIENT
@@ -20,6 +28,8 @@ namespace Barotrauma
public static readonly PrefabCollection<TalentPrefab> TalentPrefabs = new PrefabCollection<TalentPrefab>();
public readonly ImmutableHashSet<TalentMigration> Migrations;
public ContentXElement ConfigElement
{
get;
@@ -32,6 +42,8 @@ namespace Barotrauma
DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value);
AbilityEffectsStackWithSameTalent = element.GetAttributeBool("abilityeffectsstackwithsametalent", true);
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
if (!nameIdentifier.IsEmpty)
{
@@ -48,6 +60,8 @@ namespace Barotrauma
: Option<Color>.None();
#endif
var migrations = ImmutableHashSet.CreateBuilder<TalentMigration>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -60,9 +74,25 @@ namespace Barotrauma
TextManager.ConstructDescription(ref tempDescription, subElement);
Description = tempDescription;
break;
case "migrations":
foreach (var migrationElement in subElement.Elements())
{
try
{
var migration = TalentMigration.FromXML(migrationElement);
migrations.Add(migration);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e);
}
}
break;
}
}
Migrations = migrations.ToImmutable();
if (element.GetAttribute("description") != null)
{
string description = element.GetAttributeString("description", string.Empty);
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
@@ -22,7 +23,7 @@ namespace Barotrauma
: string.Empty);
}
public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0);
public static readonly Version MinimumHashCompatibleVersion = new Version(1, 0, 13, 2);
public const string LocalModsDir = "LocalMods";
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
@@ -176,7 +177,7 @@ namespace Barotrauma
}
}
public Md5Hash CalculateHash(bool logging = false)
public Md5Hash CalculateHash(bool logging = false, string? name = null, string? modVersion = null)
{
using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.MD5);
@@ -203,7 +204,14 @@ namespace Barotrauma
break;
}
}
string selectedName = name ?? Name;
if (!selectedName.IsNullOrEmpty())
{
incrementalHash.AppendData(Encoding.UTF8.GetBytes(selectedName));
}
incrementalHash.AppendData(Encoding.UTF8.GetBytes(modVersion ?? ModVersion));
var md5Hash = Md5Hash.BytesAsHash(incrementalHash.GetHashAndReset());
if (logging)
{
@@ -120,7 +120,7 @@ namespace Barotrauma
{
if (contentPackages.AtLeast(2, cp2 => cp == cp2))
{
throw new InvalidOperationException($"Input contains duplicate packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})");
throw new InvalidOperationException($"There are duplicates in the list of selected content packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})");
}
}
}
@@ -1878,6 +1878,7 @@ namespace Barotrauma
commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true));
commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true));
commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("debugwiring", "Toggle the wiring debug mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false));
commands.Add(new Command("debugdrawlos", "Toggle the los debug drawing mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false));
@@ -104,7 +104,7 @@ namespace Barotrauma
/// </summary>
OnClose = 20,
/// <summary>
/// Executes when the entity spawns. Only valid for doors.
/// Executes when the entity spawns. Valid for items and characters.
/// </summary>
OnSpawn = 21,
/// <summary>
@@ -49,7 +49,7 @@ namespace Barotrauma
var limb = character.AnimController.GetLimb(LimbType);
if (Strength > 0.0f)
{
character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength));
character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true);
}
else if (Strength < 0.0f)
{
@@ -60,7 +60,7 @@ namespace Barotrauma
{
if (Strength > 0.0f)
{
character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength));
character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true);
}
else if (Strength < 0.0f)
{
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Barotrauma
{
class CheckSelectedItemAction : BinaryOptionAction
class CheckSelectedAction : BinaryOptionAction
{
public enum SelectedItemType { Primary, Secondary, Any };
@@ -16,7 +16,7 @@ namespace Barotrauma
[Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)]
public SelectedItemType ItemType { get; set; }
public CheckSelectedItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
public CheckSelectedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
@@ -34,7 +34,7 @@ namespace Barotrauma
}
if (character == null)
{
DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail.");
Error($"{nameof(CheckSelectedAction)} error: {GetEventName()} uses a {nameof(CheckSelectedAction)} but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail.");
return false;
}
if (!TargetTag.IsEmpty)
@@ -42,11 +42,16 @@ namespace Barotrauma
IEnumerable<Entity> targets = ParentEvent.GetTargets(TargetTag);
if (targets.None())
{
DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
Error($"{nameof(CheckSelectedAction)} error: {GetEventName()} uses a {nameof(CheckSelectedAction)} but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
return false;
}
foreach (var target in targets)
{
if (target is Character targetCharacter)
{
if (ItemType == SelectedItemType.Any && character.SelectedCharacter == targetCharacter) { return true; }
continue;
}
if (target is not Item targetItem)
{
continue;
@@ -79,6 +84,18 @@ namespace Barotrauma
_ => false
};
}
#if DEBUG
void Error(string errorMsg)
{
DebugConsole.ThrowError(errorMsg);
}
#else
void Error(string errorMsg)
{
DebugConsole.LogError(errorMsg);
}
#endif
}
private string GetEventName()
@@ -1,94 +0,0 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
{
class CheckSelectedAction : BinaryOptionAction
{
public enum SelectedItemType { Primary, Secondary, Any };
[Serialize("", IsPropertySaveable.Yes)]
public Identifier CharacterTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
[Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)]
public SelectedItemType ItemType { get; set; }
public CheckSelectedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
Character character = null;
if (!CharacterTag.IsEmpty)
{
foreach (var t in ParentEvent.GetTargets(CharacterTag))
{
if (t is Character c)
{
character = c;
break;
}
}
}
if (character == null)
{
DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail.");
return false;
}
if (!TargetTag.IsEmpty)
{
IEnumerable<Entity> targets = ParentEvent.GetTargets(TargetTag);
if (targets.None())
{
DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
return false;
}
foreach (var target in targets)
{
if (target is Character targetCharacter)
{
if (ItemType == SelectedItemType.Any && character.SelectedCharacter == targetCharacter) { return true; }
continue;
}
if (target is not Item targetItem)
{
continue;
}
if (IsSelected(targetItem))
{
return true;
}
}
return false;
bool IsSelected(Item item)
{
return ItemType switch
{
SelectedItemType.Any => character.IsAnySelectedItem(item),
SelectedItemType.Primary => character.SelectedItem == item,
SelectedItemType.Secondary => character.SelectedSecondaryItem == item,
_ => false
};
}
}
else
{
return ItemType switch
{
SelectedItemType.Any => !character.HasSelectedAnyItem,
SelectedItemType.Primary => character.SelectedItem == null,
SelectedItemType.Secondary => character.SelectedSecondaryItem == null,
_ => false
};
}
}
private string GetEventName()
{
return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event";
}
}
}
@@ -60,18 +60,42 @@ namespace Barotrauma
{
if (isFinished) { return; }
Identifier missionDebugId = (MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier);
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
Mission unlockedMission = null;
var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes);
var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes, mustAllowLocationTypeChanges: false);
if (unlockLocation == null && UnlockFurtherOnMap)
{
DebugConsole.NewMessage($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\" further on the map. Attempting to find a location earlier on the map...");
unlockLocation = FindUnlockLocation(MinLocationDistance, unlockFurtherOnMap: false, LocationTypes, mustAllowLocationTypeChanges: false);
}
if (unlockLocation == null && CreateLocationIfNotFound)
{
DebugConsole.NewMessage($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\". Attempting to change the type of an empty location to create a suitable location...");
//find an empty location at least 3 steps away, further on the map
var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable());
var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable(),
mustAllowLocationTypeChanges: true,
requireCorrectFaction: false);
if (emptyLocation == null)
{
DebugConsole.NewMessage($"Failed to find a suitable empty location further on the map. Attempting to find a location earlier on the map...");
emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: false, "none".ToIdentifier().ToEnumerable(),
mustAllowLocationTypeChanges: true,
requireCorrectFaction: false);
}
if (emptyLocation != null)
{
System.Diagnostics.Debug.Assert(!emptyLocation.LocationTypeChangesBlocked);
emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]);
unlockLocation = emptyLocation;
if (!RequiredFaction.IsEmpty)
{
emptyLocation.Faction = campaign.Factions.Find(f => f.Prefab.Identifier == RequiredFaction);
}
}
}
@@ -115,13 +139,13 @@ namespace Barotrauma
}
else
{
DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationTypes}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})");
DebugConsole.AddWarning($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\" (LocationType: {string.Join(", ", LocationTypes)}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})");
}
}
isFinished = true;
}
private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable<Identifier> locationTypes)
private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable<Identifier> locationTypes, bool mustAllowLocationTypeChanges, bool requireCorrectFaction = true)
{
var campaign = GameMain.GameSession.GameMode as CampaignMode;
if (LocationTypes.Length == 0 && minDistance <= 1)
@@ -140,7 +164,7 @@ namespace Barotrauma
foreach (var location in currentLocations)
{
checkedLocations.Add(location);
if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes))
if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes, mustAllowLocationTypeChanges, requireCorrectFaction))
{
return location;
}
@@ -160,9 +184,13 @@ namespace Barotrauma
return null;
}
private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable<Identifier> locationTypes)
private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable<Identifier> locationTypes, bool mustAllowLocationTypeChanges, bool requireCorrectFaction)
{
if (!RequiredFaction.IsEmpty)
if (mustAllowLocationTypeChanges && location.LocationTypeChangesBlocked)
{
return false;
}
if (requireCorrectFaction && !RequiredFaction.IsEmpty)
{
if (location.Faction?.Prefab.Identifier != RequiredFaction &&
location.SecondaryFaction?.Prefab.Identifier != RequiredFaction)
@@ -15,6 +15,8 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool AddToCrew { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool RemoveFromCrew { get; set; }
@@ -38,6 +40,8 @@ namespace Barotrauma
{
if (isFinished) { return; }
bool isPlayerTeam = TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2;
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
foreach (var npc in affectedNpcs)
{
@@ -49,9 +53,13 @@ namespace Barotrauma
if (idCard != null)
{
idCard.TeamID = TeamID;
if (isPlayerTeam)
{
idCard.SubmarineSpecificID = 0;
}
}
}
if (AddToCrew && (TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2))
if (AddToCrew && isPlayerTeam)
{
npc.Info.StartItemsGiven = true;
GameMain.GameSession.CrewManager.AddCharacter(npc);
@@ -46,7 +46,7 @@ namespace Barotrauma
var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f,
IsFollowOrderObjective = true
IsFollowOrder = true
};
humanAiController.ObjectiveManager.AddObjective(newObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
@@ -35,7 +35,9 @@ namespace Barotrauma
AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f,
SourceEventAction = this
SourceEventAction = this,
IsWaitOrder = true,
CloseEnough = 100
};
humanAiController.ObjectiveManager.AddObjective(gotoObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
@@ -225,7 +225,7 @@ namespace Barotrauma
}
}
while (QueuedEventsForNextRound.Count > 0 && QueuedEventsForNextRound.Dequeue() is Identifier id)
while (QueuedEventsForNextRound.TryDequeue(out var id))
{
var eventPrefab = EventSet.GetEventPrefab(id);
if (eventPrefab == null)
@@ -879,9 +879,9 @@ namespace Barotrauma
monsterStrength = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsIncapacitated || !character.Enabled || character.IsPet) { continue; }
if (character.IsIncapacitated || character.IsArrested || !character.Enabled || character.IsPet) { continue; }
if (character.AIController is EnemyAIController enemyAI)
if (character.AIController is EnemyAIController enemyAI)
{
if (!enemyAI.AIParams.StayInAbyss)
{
@@ -889,7 +889,7 @@ namespace Barotrauma
monsterStrength += enemyAI.CombatStrength;
}
if (character.CurrentHull?.Submarine?.Info != null &&
if (character.CurrentHull?.Submarine?.Info != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
{
@@ -902,10 +902,11 @@ namespace Barotrauma
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
enemyDanger += enemyAI.CombatStrength / 5000.0f;
}
}
}
else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1))
{
if (character.Submarine != null &&
if (character.Submarine != null &&
character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } &&
Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)
{
//we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character),
@@ -91,7 +91,7 @@ namespace Barotrauma
if (IsClient) { return; }
if (!swarmSpawned && level.CheckBeaconActive())
{
List<Submarine> connectedSubs = level.BeaconStation.GetConnectedSubs();
IEnumerable<Submarine> connectedSubs = level.BeaconStation.GetConnectedSubs();
foreach (Item item in Item.ItemList)
{
if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; }
@@ -320,7 +320,7 @@ namespace Barotrauma
private static bool IsItemDelivered(Item item)
{
if (item.Removed || item.Condition <= 0.0f || Submarine.MainSub == null) { return false; }
var submarine = item.Submarine ?? item.GetRootContainer()?.Submarine;
var submarine = item.Submarine ?? item.RootContainer?.Submarine;
return submarine == Submarine.MainSub || Submarine.MainSub.GetConnectedSubs().Contains(submarine);
}
}
@@ -1,3 +1,4 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
@@ -401,7 +401,7 @@ namespace Barotrauma
int reward = GetReward(sub);
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
CharacterTalent.CheckTalentsForCrew(crewCharacters, AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier);
crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
finalReward = (int)(reward * missionMoneyGainMultiplier.Value);
}
@@ -19,6 +19,8 @@ namespace Barotrauma
private float missionDifficulty;
private int alternateReward;
private Identifier factionIdentifier;
private Submarine enemySub;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
@@ -140,8 +142,8 @@ namespace Barotrauma
missionDifficulty = level?.Difficulty ?? 0;
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", Identifier.Empty);
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
@@ -170,11 +172,11 @@ namespace Barotrauma
submarineInfo = new SubmarineInfo(contentFile.Path.Value);
}
private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
{
return Math.Abs(levelDifficulty - preferredDifficulty + MathHelper.Lerp(-randomnessModifier, randomnessModifier, (float)rand.NextDouble()));
}
private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand)
private static int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand)
{
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount);
}
@@ -254,6 +256,7 @@ namespace Barotrauma
}
}
enemySub.ImmuneToBallastFlora = true;
enemySub.EnableFactionSpecificEntities(factionIdentifier);
}
private void InitPirates()
@@ -444,7 +447,7 @@ namespace Barotrauma
private bool CheckWinState() => !IsClient && characters.All(m => DeadOrCaptured(m));
private bool DeadOrCaptured(Character character)
private static bool DeadOrCaptured(Character character)
{
return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated;
}
@@ -16,8 +16,8 @@ namespace Barotrauma
public Item Item;
/// <summary>
/// Note that the integer values matter here: the state of the target can't go back to a smaller value,
/// and a larger or equal value than the <see href="RequiredRetrievalState">RequiredRetrievalState</see> means the item counts as retrieved
/// Note that the integer values matter here:
/// a larger or equal value than the <see href="RequiredRetrievalState">RequiredRetrievalState</see> means the item counts as retrieved
/// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub)
/// </summary>
public enum RetrievalState
@@ -167,6 +167,8 @@ namespace Barotrauma
private readonly List<Target> targets = new List<Target>();
public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved);
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
@@ -361,7 +363,7 @@ namespace Barotrauma
//make body dynamic when picked up
foreach (var target in targets)
{
var root = target.Item?.GetRootContainer() ?? target.Item;
var root = target.Item?.RootContainer ?? target.Item;
if (root == null) { continue; }
if (target.Item.ParentInventory != null && target.Item.body != null) { target.Item.body.FarseerBody.BodyType = BodyType.Dynamic; }
}
@@ -382,23 +384,46 @@ namespace Barotrauma
switch (target.State)
{
case Target.RetrievalState.None:
if (target.Interacted)
{
TrySetRetrievalState(Target.RetrievalState.Interact);
}
var root = target.Item?.GetRootContainer() ?? target.Item;
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
if (target.Interacted)
{
TrySetRetrievalState(Target.RetrievalState.Interact);
}
var root = target.Item?.RootContainer ?? target.Item;
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
}
}
break;
case Target.RetrievalState.PickedUp:
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine;
if (parentSub != null)
case Target.RetrievalState.RetrievedToSub:
{
if (parentSub.Info.Type == SubmarineType.Player || Level.IsLoadedFriendlyOutpost)
Entity rootInventoryOwner = target.Item.GetRootInventoryOwner();
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine;
bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player;
bool inPlayerInventory = false;
bool playerInFriendlySub = false;
if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1)
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
inPlayerInventory = true;
if (character.Submarine != null)
{
playerInFriendlySub =
character.IsInFriendlySub ||
(character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true });
}
}
if (inPlayerSub || (inPlayerInventory && playerInFriendlySub))
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
else
{
target.State = Target.RetrievalState.PickedUp;
}
}
break;
@@ -406,8 +431,8 @@ namespace Barotrauma
void TrySetRetrievalState(Target.RetrievalState retrievalState)
{
if (retrievalState < target.State) { return; }
bool wasRetrieved = false;
if (retrievalState < target.State || target.State == retrievalState) { return; }
bool wasRetrieved = target.Retrieved;
target.State = retrievalState;
//increment the mission state if the target became retrieved
if (!wasRetrieved && target.Retrieved) { State = i + 1; }
@@ -335,7 +335,7 @@ namespace Barotrauma
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false)
{
SpawnedInCurrentOutpost = validContainer.Key.Item.SpawnedInCurrentOutpost,
AllowStealing = validContainer.Key.Item.AllowStealing,
AllowStealing = validContainer.Key.Item.AllowStealing || validContainer.Key.Item.Prefab.AllowStealingContainedItems,
Quality = quality,
OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex,
OriginalContainerIndex =
@@ -416,7 +416,7 @@ namespace Barotrauma
}
}
#endif
return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities));
return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities)).ToList();
}
public static IReadOnlyCollection<Item> FindAllItemsOnPlayerAndSub(Character character)
@@ -440,7 +440,7 @@ namespace Barotrauma
if (!item.Components.All(static c => c is not Holdable { Attachable: true, Attached: true })) { return false; }
if (!item.Components.All(static c => c is not Wire w || w.Connections.All(static c => c is null))) { return false; }
if (!ItemAndAllContainersInteractable(item)) { return false; }
if (item.GetRootContainer() is { } rootContainer && rootContainer.HasTag("dontsellitems")) { return false; }
if (item.RootContainer is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; }
return true;
}).Distinct();
@@ -610,7 +610,7 @@ namespace Barotrauma
(itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo;
static void itemSpawned(PurchasedItem purchased, Item item)
{
Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine;
Submarine sub = item.Submarine ?? item.RootContainer?.Submarine;
if (sub != null)
{
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
@@ -15,7 +16,7 @@ namespace Barotrauma
/// <summary>
/// Maximum amount of reputation loss you can get from damaging outpost NPCs per round
/// </summary>
public const float MaxReputationLossFromNPCDamage = 10.0f;
public const float MaxReputationLossFromNPCDamage = 20.0f;
/// <summary>
/// Maximum amount of reputation loss you can get from damaging outpost walls per round
/// </summary>
@@ -71,45 +72,50 @@ namespace Barotrauma
public float GetReputationChangeMultiplier(float reputationChange)
{
if (reputationChange > 0f)
return reputationChange switch
{
float reputationGainMultiplier = 1f;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier, includeSaved: false);
reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0;
}
return reputationGainMultiplier;
}
else if (reputationChange < 0f)
> 0f => GetMultiplierForStatType(StatTypes.ReputationGainMultiplier, Identifier),
< 0f => GetMultiplierForStatType(StatTypes.ReputationLossMultiplier, Identifier),
_ => 1.0f
};
static float GetMultiplierForStatType(StatTypes statTypes, Identifier identifier)
{
float reputationLossMultiplier = 1f;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
float multiplier = 1f;
var crew = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (crew != null && crew.Any())
{
reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier, includeSaved: false);
reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0;
multiplier *= 1f + crew.Max(c => c.GetStatValue(statTypes, includeSaved: false));
multiplier *= 1f + crew.Max(c => c.Info?.GetSavedStatValue(statTypes, identifier) ?? 0);
}
return reputationLossMultiplier;
return multiplier;
}
return 1.0f;
}
public void AddReputation(float reputationChange, float maxReputationChangePerRound = float.MaxValue)
{
float currentValue = Value;
float currentReputationChange = currentValue - ReputationAtRoundStart;
if (Math.Abs(currentReputationChange) >= maxReputationChangePerRound &&
Math.Sign(currentReputationChange) == Math.Sign(reputationChange))
{
return;
float prevValue = Value;
//if we're already over the limit, do nothing (assuming the change is in the "same direction" that we've gone over the limit)
if (doesReputationChangeGoOverLimit(prevValue, reputationChange))
{
return;
}
float newValue = Value + reputationChange * GetReputationChangeMultiplier(reputationChange);
if (Math.Abs(newValue - ReputationAtRoundStart) > maxReputationChangePerRound &&
Math.Sign(newValue - currentValue) == Math.Sign(newValue - ReputationAtRoundStart))
if (doesReputationChangeGoOverLimit(newValue, newValue - prevValue))
{
newValue = ReputationAtRoundStart + maxReputationChangePerRound * Math.Sign(reputationChange);
}
Value = newValue;
bool doesReputationChangeGoOverLimit(float newValue, float change)
{
float totalReputationChange = newValue - ReputationAtRoundStart;
return
Math.Abs(totalReputationChange) > maxReputationChangePerRound &&
Math.Sign(totalReputationChange) == Math.Sign(change);
}
}
public readonly NamedEvent<Reputation> OnReputationValueChanged = new NamedEvent<Reputation>();
@@ -95,8 +95,6 @@ namespace Barotrauma
public SubmarineInfo PendingSubmarineSwitch;
public bool TransferItemsOnSubSwitch { get; set; }
public bool SwitchedSubsThisRound { get; private set; }
protected Map map;
public Map Map
{
@@ -129,6 +127,8 @@ namespace Barotrauma
}
}
public Location CurrentLocation => Map?.CurrentLocation;
public Wallet Bank;
public LevelData NextLevel
@@ -143,6 +143,8 @@ namespace Barotrauma
public virtual bool PurchasedLostShuttles { get; set; }
public virtual bool PurchasedItemRepairs { get; set; }
public bool DivingSuitWarningShown;
private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions)
{
if (GameMain.NetworkMember == null) { return true; }
@@ -291,7 +293,6 @@ namespace Barotrauma
PurchasedLostShuttlesInLatestSave = PurchasedLostShuttles = false;
var connectedSubs = Submarine.MainSub.GetConnectedSubs();
wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost);
SwitchedSubsThisRound = false;
}
public static int GetHullRepairCost()
@@ -739,9 +740,11 @@ namespace Barotrauma
//if there's a sub docked to the outpost, we can leave the level
if (Level.Loaded.StartOutpost.DockedTo.Any())
{
var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault();
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
foreach (var dockedSub in Level.Loaded.StartOutpost.DockedTo)
{
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { continue; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
}
}
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
@@ -777,9 +780,11 @@ namespace Barotrauma
//if there's a sub docked to the outpost, we can leave the level
if (Level.Loaded.EndOutpost.DockedTo.Any())
{
var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault();
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
foreach (var dockedSub in Level.Loaded.EndOutpost.DockedTo)
{
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { continue; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
}
}
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
@@ -931,6 +936,9 @@ namespace Barotrauma
CampaignMetadata.SetValue("campaign.endings".ToIdentifier(), loops + 1);
}
//no tutorials after finishing the campaign once
Settings.TutorialEnabled = false;
GameAnalyticsManager.AddProgressionEvent(
GameAnalyticsManager.ProgressionStatus.Complete,
Preset?.Identifier.Value ?? "none");
@@ -986,7 +994,7 @@ namespace Barotrauma
if (characterInfo == null) { return false; }
if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
{
if (GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation)
if (MathF.Round(GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation)
{
return false;
}
@@ -1205,26 +1213,15 @@ namespace Barotrauma
{
TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0);
TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0);
DivingSuitWarningShown = element.GetAttributeBool(nameof(DivingSuitWarningShown).ToLowerInvariant(), false);
}
protected XElement SaveStats()
{
return new XElement("stats",
new XAttribute(nameof(TotalPlayTime).ToLowerInvariant(), TotalPlayTime),
new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels));
}
protected void LoadEvents(XElement element)
{
TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0);
TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0);
}
protected XElement SaveEvents()
{
return new XElement("events",
new XAttribute(nameof(EventManager.QueuedEventsForNextRound).ToLowerInvariant(),
string.Join(',', GameMain.GameSession.EventManager.QueuedEventsForNextRound)));
new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels),
new XAttribute(nameof(DivingSuitWarningShown).ToLowerInvariant(), DivingSuitWarningShown));
}
public void LogState()
@@ -1310,7 +1307,6 @@ namespace Barotrauma
TransferItemsBetweenSubs();
}
RefreshOwnedSubmarines();
SwitchedSubsThisRound = true;
PendingSubmarineSwitch = null;
}
@@ -275,29 +275,35 @@ namespace Barotrauma
bool isSubmarineVisible(SubmarineInfo s)
=> !GameMain.NetworkMember.ServerSettings.HiddenSubs.Any(h
=> s.Name.Equals(h, StringComparison.OrdinalIgnoreCase));
List<SubmarineInfo> availableSubs =
SubmarineInfo.SavedSubmarines
var availableSubs = SubmarineInfo.SavedSubmarines;
#if CLIENT
if (GameMain.Client != null)
{
availableSubs = GameMain.Client.ServerSubmarines;
}
#endif
List<SubmarineInfo> campaignSubs =
availableSubs
.Where(s =>
s.IsCampaignCompatible
&& isSubmarineVisible(s))
.ToList();
if (!availableSubs.Any())
if (!campaignSubs.Any())
{
//None of the available subs were marked as campaign-compatible, just include all visible subs
availableSubs.AddRange(
SubmarineInfo.SavedSubmarines
.Where(isSubmarineVisible));
campaignSubs.AddRange(availableSubs.Where(isSubmarineVisible));
}
if (!availableSubs.Any())
if (!campaignSubs.Any())
{
//No subs are visible at all! Just make the selected one available
availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub);
campaignSubs.Add(GameMain.NetLobbyScreen.SelectedSub);
}
return availableSubs;
return campaignSubs;
}
private static void WriteItems(IWriteMessage msg, Dictionary<Identifier, List<PurchasedItem>> purchasedItems)
@@ -328,11 +328,11 @@ namespace Barotrauma
Campaign!.TransferItemsOnSubSwitch = transferItems;
}
public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
public bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
{
if (Campaign is null) { return; }
if (Campaign is null) { return false; }
int price = newSubmarine.GetPrice();
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return; }
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return false; }
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
@@ -341,6 +341,7 @@ namespace Barotrauma
(Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList);
#endif
}
return true;
}
public bool IsSubmarineOwned(SubmarineInfo query)
@@ -504,7 +505,8 @@ namespace Barotrauma
}
GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}");
#endif
if (GameMode is CampaignMode campaignMode)
var campaignMode = GameMode as CampaignMode;
if (campaignMode != null)
{
if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled)
{
@@ -532,7 +534,7 @@ namespace Barotrauma
}
#endif
#if CLIENT
if (GameMode is CampaignMode && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
if (campaignMode != null && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary;
if (existingRoundSummary?.ContinueButton != null)
@@ -575,6 +577,14 @@ namespace Barotrauma
HintManager.OnRoundStarted();
#endif
if (campaignMode is { DivingSuitWarningShown: false } &&
Level.Loaded != null && Level.Loaded.GetRealWorldDepth(0) > 4000)
{
#if CLIENT
CoroutineManager.Invoke(() => new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("hint.upgradedivingsuits")), delay: 5.0f);
#endif
campaignMode.DivingSuitWarningShown = true;
}
}
private void InitializeLevel(Level? level)
@@ -691,7 +701,7 @@ namespace Barotrauma
if (port.IsHorizontal || port.Docked) { continue; }
if (port.Item.Submarine == level.StartOutpost)
{
if (port.DockingTarget == null)
if (port.DockingTarget == null || (outPostPort != null && !outPostPort.MainDockingPort && port.MainDockingPort))
{
outPostPort = port;
}
@@ -972,7 +982,7 @@ namespace Barotrauma
Dictionary<ItemPrefab, int> submarineInventory = new Dictionary<ItemPrefab, int>();
foreach (Item item in Item.ItemList)
{
var rootContainer = item.GetRootContainer() ?? item;
var rootContainer = item.RootContainer ?? item;
if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; }
@@ -11,6 +11,9 @@ namespace Barotrauma
{
internal sealed partial class MedicalClinic
{
private const int RateLimitMaxRequests = 20,
RateLimitExpiry = 5;
public enum NetworkHeader
{
REQUEST_AFFLICTIONS,
@@ -24,7 +24,7 @@ namespace Barotrauma
}
public static readonly List<InvSlotType> anySlot = new List<InvSlotType>() { InvSlotType.Any };
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType>() { InvSlotType.Any };
protected bool[] IsEquipped;
@@ -85,6 +85,22 @@ namespace Barotrauma
InitProjSpecific(element);
var itemElements = element.Elements().Where(e => e.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase));
int itemCount = itemElements.Count();
if (itemCount > capacity)
{
DebugConsole.ThrowError($"Character \"{character.SpeciesName}\" is configured to spawn with more items than it has inventory capacity for.");
}
#if DEBUG
else if (itemCount > capacity - 2)
{
DebugConsole.ThrowError(
$"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " +
"This can cause issues with talents that spawn extra loot in monsters' inventories."
+ " Consider increasing the inventory size.");
}
#endif
if (!spawnInitialItems) { return; }
#if CLIENT
@@ -92,10 +108,8 @@ namespace Barotrauma
if (GameMain.Client != null) { return; }
#endif
foreach (var subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; }
foreach (var subElement in itemElements)
{
string itemIdentifier = subElement.GetAttributeString("identifier", "");
if (!ItemPrefab.Prefabs.TryGet(itemIdentifier, out var itemPrefab))
{
@@ -226,7 +240,7 @@ namespace Barotrauma
if (item.AllowedSlots.Contains(InvSlotType.Any))
{
var wearable = item.GetComponent<Wearable>();
if (wearable != null && !wearable.AutoEquipWhenFull && CheckIfAnySlotAvailable(item, false) == -1)
if (wearable != null && !wearable.AutoEquipWhenFull && !IsAnySlotAvailable(item))
{
return false;
}
@@ -336,7 +350,7 @@ namespace Barotrauma
//try to place the item in a LimbSlot.Any slot if that's allowed
if (allowedSlots.Contains(InvSlotType.Any) && item.AllowedSlots.Contains(InvSlotType.Any))
{
int freeIndex = CheckIfAnySlotAvailable(item, inWrongSlot);
int freeIndex = GetFreeAnySlot(item, inWrongSlot);
if (freeIndex > -1)
{
PutItem(item, freeIndex, user, true, createNetworkEvent);
@@ -393,7 +407,9 @@ namespace Barotrauma
return placedInSlot > -1;
}
public int CheckIfAnySlotAvailable(Item item, bool inWrongSlot)
public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1;
private int GetFreeAnySlot(Item item, bool inWrongSlot)
{
//attempt to stack first
for (int i = 0; i < capacity; i++)
@@ -1,13 +1,15 @@
using Barotrauma.Networking;
using Barotrauma.IO;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
#if CLIENT
using Barotrauma.Lights;
#endif
namespace Barotrauma.Items.Components
{
@@ -243,10 +245,12 @@ namespace Barotrauma.Items.Components
if (!target.item.Submarine.DockedTo.Contains(item.Submarine))
{
target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target);
target.item.Submarine.RefreshConnectedSubs();
}
if (!item.Submarine.DockedTo.Contains(target.item.Submarine))
{
item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this);
item.Submarine.RefreshConnectedSubs();
}
DockingTarget = target;
@@ -558,6 +562,7 @@ namespace Barotrauma.Items.Components
var subs = new Submarine[] { item.Submarine, DockingTarget.item.Submarine };
bodies = new Body[4];
RemoveConvexHulls();
if (DockingTarget.Door != null)
{
@@ -648,8 +653,10 @@ namespace Barotrauma.Items.Components
hullRects[i].X -= expand;
hullRects[i].Width += expand * 2;
hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
hulls[i] = new Hull(hullRects[i], subs[i]);
hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch";
hulls[i] = new Hull(hullRects[i], subs[i])
{
RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"
};
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
@@ -661,6 +668,15 @@ namespace Barotrauma.Items.Components
BodyType.Static);
}
}
#if CLIENT
for (int i = 0; i < 2; i++)
{
convexHulls[i] =
new ConvexHull(new Rectangle(
new Point((int)item.Position.X, item.Rect.Y - item.Rect.Height * i),
new Point((int)(DockingTarget.item.WorldPosition.X - item.WorldPosition.X), 0)), IsHorizontal, item);
}
#endif
if (rightHullDiff <= 100 && hulls[0].Submarine != null)
{
@@ -764,15 +780,17 @@ namespace Barotrauma.Items.Components
hullRects[1].Height += midHullDiff / 2 + 1;
}
int expand = 5;
for (int i = 0; i < 2; i++)
{
hullRects[i].Y += expand;
hullRects[i].Height += expand * 2;
hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
hulls[i] = new Hull(hullRects[i], subs[i]);
hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch";
hulls[i] = new Hull(hullRects[i], subs[i])
{
RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch",
AvoidStaying = true
};
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
@@ -784,6 +802,15 @@ namespace Barotrauma.Items.Components
BodyType.Static);
}
}
#if CLIENT
for (int i = 0; i < 2; i++)
{
convexHulls[i] =
new ConvexHull(new Rectangle(
new Point(item.Rect.X + item.Rect.Width * i, (int)item.Position.Y),
new Point(0, (int)(DockingTarget.item.WorldPosition.Y - item.WorldPosition.Y))), IsHorizontal, item);
}
#endif
if (midHullDiff <= 100 && hulls[0].Submarine != null)
{
@@ -822,6 +849,8 @@ namespace Barotrauma.Items.Components
}
}
partial void RemoveConvexHulls();
private void LinkHullsToGaps()
{
if (gap == null || hulls == null || hulls[0] == null || hulls[1] == null)
@@ -916,7 +945,9 @@ namespace Barotrauma.Items.Components
}
DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine);
DockingTarget.item.Submarine.RefreshConnectedSubs();
item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine);
item.Submarine.RefreshConnectedSubs();
if (Door != null && DockingTarget.Door != null)
{
@@ -976,6 +1007,8 @@ namespace Barotrauma.Items.Components
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
RemoveConvexHulls();
if (gap != null)
{
gap.Remove();
@@ -1091,6 +1124,7 @@ namespace Barotrauma.Items.Components
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
gap?.Remove(); gap = null;
RemoveConvexHulls();
overlaySprite?.Remove();
overlaySprite = null;
@@ -69,7 +69,7 @@ namespace Barotrauma.Items.Components
private bool isBroken;
public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck && !Impassable;
public bool CanBeTraversed => !Impassable && (IsBroken || IsOpen);
public bool IsBroken
{
@@ -186,13 +186,19 @@ namespace Barotrauma.Items.Components
{
get { return openState; }
set
{
{
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
#if CLIENT
float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
if (Math.Abs(lastConvexHullState - openState) * size < 5.0f) { return; }
UpdateConvexHulls();
lastConvexHullState = openState;
//refresh convex hulls if the body of the door has moved by 5 pixels,
//or if it becomes fully closed or fully open
if (Math.Abs(lastConvexHullState - openState) * size > 5.0f ||
(openState <= 0.0f && lastConvexHullState > 0.0f) ||
(openState >= 1.0f && lastConvexHullState < 1.0f))
{
UpdateConvexHulls();
lastConvexHullState = openState;
}
#endif
}
}
@@ -383,6 +389,8 @@ namespace Barotrauma.Items.Components
return;
}
bool isClosing = false;
if ((!IsStuck && !IsJammed) || !isOpen)
{
@@ -521,8 +529,11 @@ namespace Barotrauma.Items.Components
{
RefreshLinkedGap();
#if CLIENT
convexHull = new ConvexHull(Rectangle.Empty, !IsHorizontal, item);
if (Window != Rectangle.Empty) { convexHull2 = new ConvexHull(Rectangle.Empty, !IsHorizontal, item); }
convexHull = new ConvexHull(doorRect, IsHorizontal, item);
if (Window != Rectangle.Empty)
{
convexHull2 = new ConvexHull(doorRect, IsHorizontal, item);
}
UpdateConvexHulls();
#endif
}
@@ -556,6 +567,12 @@ namespace Barotrauma.Items.Components
gap.ConnectedDoor = null;
}
}
if (OutsideSubmarineFixture != null)
{
OutsideSubmarineFixture.Body.Remove(OutsideSubmarineFixture);
OutsideSubmarineFixture = null;
}
//no need to remove the gap if we're unloading the whole submarine
//otherwise the gap will be removed twice and cause console warnings
@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -96,11 +95,16 @@ namespace Barotrauma.Items.Components
if (selectedEffect != null)
{
targetCharacter = character;
ApplyStatusEffects(ActionType.OnWearing, 1.0f);
ApplyStatusEffects(ActionType.OnWearing, 1.0f, targetCharacter);
float selectedEffectStrength = GetCombinedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null) { affliction.Strength = selectedEffectStrength; }
if (affliction != null)
{
affliction.Strength = selectedEffectStrength;
//force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing
affliction.SetStrength(selectedEffectStrength);
}
#if SERVER
item.CreateServerEvent(this);
#endif
@@ -110,7 +114,12 @@ namespace Barotrauma.Items.Components
float selectedTaintedEffectStrength = GetCombinedTaintedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (affliction != null) { affliction.Strength = selectedTaintedEffectStrength; }
if (affliction != null)
{
affliction.Strength = selectedTaintedEffectStrength;
//force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing
affliction.SetStrength(selectedTaintedEffectStrength);
}
targetCharacter = character;
#if SERVER
item.CreateServerEvent(this);
@@ -127,7 +136,7 @@ namespace Barotrauma.Items.Components
base.Update(deltaTime, cam);
if (targetCharacter != null)
{
var rootContainer = item.GetRootContainer();
var rootContainer = item.RootContainer;
if (!targetCharacter.HasEquippedItem(item) &&
(rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface)))
{
@@ -220,7 +229,7 @@ namespace Barotrauma.Items.Components
return MathHelper.Clamp(probability, 0.0f, 1.0f);
}
private float GetTaintedProbabilityOnCombine(Character user)
private static float GetTaintedProbabilityOnCombine(Character user)
{
if (user == null) { return 1.0f; }
float probability = 1.0f - user.GetStatValue(StatTypes.GeneticMaterialTaintedProbabilityReductionOnCombine);
@@ -409,6 +409,7 @@ namespace Barotrauma.Items.Components
private int leafVariants;
private int[] flowerTiles;
[Serialize(100.0f, IsPropertySaveable.Yes)]
public float Health
{
get => health;
@@ -321,12 +321,12 @@ namespace Barotrauma.Items.Components
}
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
Drop(true, dropper);
Drop(true, dropper, setTransform);
}
private void Drop(bool dropConnectedWires, Character dropper)
private void Drop(bool dropConnectedWires, Character dropper, bool setTransform = true)
{
GetRope()?.Snap();
if (dropConnectedWires)
@@ -343,8 +343,11 @@ namespace Barotrauma.Items.Components
DeattachFromWall();
}
if (Pusher != null) { Pusher.Enabled = false; }
if (item.body != null) { item.body.Enabled = true; }
if (setTransform)
{
if (Pusher != null) { Pusher.Enabled = false; }
if (item.body != null) { item.body.Enabled = true; }
}
IsActive = false;
attachTargetCell = null;
@@ -357,7 +360,7 @@ namespace Barotrauma.Items.Components
item.Submarine = picker.Submarine;
if (item.body != null)
if (item.body != null && setTransform)
{
if (item.body.Removed)
{
@@ -599,6 +602,10 @@ namespace Barotrauma.Items.Components
throw new InvalidOperationException($"Tried to attach an item with no physics body to a wall ({item.Prefab.Identifier}).");
}
body.Enabled = false;
body.SetTransformIgnoreContacts(body.SimPosition, rotation: 0.0f);
item.body = null;
//outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub
if (item.CurrentHull == null && item.Submarine == null)
{
@@ -638,9 +645,6 @@ namespace Barotrauma.Items.Components
}
}
body.Enabled = false;
item.body = null;
DisplayMsg = prevMsg;
PickKey = prevPickKey;
requiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(prevRequiredItems);
@@ -700,7 +704,8 @@ namespace Barotrauma.Items.Components
}
Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true);
Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine;
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int maxAttachableCount = (int)character.Info.GetSavedStatValueWithBotsInMp(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int currentlyAttachedCount = Item.ItemList.Count(
i => i.Submarine == attachSubmarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier);
if (maxAttachableCount == 0)
@@ -811,7 +816,7 @@ namespace Barotrauma.Items.Components
foreach (var edge in cell.Edges)
{
if (!edge.IsSolid) { continue; }
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection))
{
attachPos = intersection;
edgeFound = true;
@@ -97,11 +97,18 @@ namespace Barotrauma.Items.Components
{
if (holdable != null && !holdable.Attached)
{
trigger.Enabled = false;
if (trigger != null)
{
trigger.Enabled = false;
}
IsActive = false;
}
else
{
if (trigger == null)
{
CreateTriggerBody();
}
if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f)
{
trigger.SetTransform(item.SimPosition, 0.0f);
@@ -123,12 +130,15 @@ namespace Barotrauma.Items.Components
{
holdable.PickingTime = float.MaxValue;
}
}
private void CreateTriggerBody()
{
System.Diagnostics.Debug.Assert(trigger == null, "LevelResource trigger already created!");
var body = item.body ?? holdable.Body;
if (body != null)
if (body != null && Attached)
{
trigger = new PhysicsBody(body.Width, body.Height, body.Radius,
trigger = new PhysicsBody(body.Width, body.Height, body.Radius,
body.Density,
BodyType.Static,
Physics.CollisionWall,
@@ -143,7 +153,6 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
if (trigger != null)
{
trigger.Remove();
@@ -170,9 +170,9 @@ namespace Barotrauma.Items.Components
return characterUsable || character == null;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
base.Drop(dropper);
base.Drop(dropper, setTransform);
hitting = false;
hitPos = 0.0f;
}
@@ -241,12 +241,9 @@ namespace Barotrauma.Items.Components
}
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
if (picker == null)
{
picker = dropper;
}
picker ??= dropper;
Vector2 bodyDropPos = Vector2.Zero;
@@ -255,8 +252,7 @@ namespace Barotrauma.Items.Components
if (item.ParentInventory != null && item.ParentInventory.Owner != null && !item.ParentInventory.Owner.Removed)
{
bodyDropPos = item.ParentInventory.Owner.SimPosition;
if (item.body != null) item.body.ResetDynamics();
item.body?.ResetDynamics();
}
}
else if (!picker.Removed)
@@ -270,7 +266,7 @@ namespace Barotrauma.Items.Components
picker = null;
}
if (item.body != null && !item.body.Enabled)
if (item.body != null && !item.body.Enabled && setTransform)
{
if (item.body.Removed)
{
@@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components
{
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
float spread = GetSpread(character) * Projectile.GetSpreadFromPool();
float spread = GetSpread(character) * projectile.GetSpreadFromPool();
var lastProjectile = LastProjectile;
if (lastProjectile != projectile)
@@ -277,7 +277,7 @@ namespace Barotrauma.Items.Components
{
Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool());
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * projectile.GetSpreadFromPool());
}
Item.RemoveContained(projectile.Item);
}
@@ -84,6 +84,9 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")]
public bool RepairMultiple { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Can the item repair multiple walls at once? Only relevant if RepairMultiple is true.")]
public bool RepairMultipleWalls { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")]
public bool RepairThroughHoles { get; set; }
@@ -383,6 +386,7 @@ namespace Barotrauma.Items.Components
//stop the ray if it already hit a door/wall and is now about to hit some other type of entity
if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; }
}
if (!RepairMultipleWalls && (bodyType == typeof(Structure) || (body.UserData as Item)?.GetComponent<Door>() != null)) { break; }
Character hitCharacter = null;
if (body.UserData is Limb limb)
@@ -56,9 +56,9 @@ namespace Barotrauma.Items.Components
return false;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
base.Drop(dropper);
base.Drop(dropper, setTransform);
throwState = ThrowState.None;
throwAngle = ThrowAngleStart;
Item.ResetWaterDragCoefficient();
@@ -237,7 +237,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")]
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not forced). Note that there's also a generic BotPriority for all item prefabs.")]
public float CombatPriority { get; private set; }
/// <summary>
@@ -435,7 +435,7 @@ namespace Barotrauma.Items.Components
}
/// <summary>a Character has dropped the item</summary>
public virtual void Drop(Character dropper) { }
public virtual void Drop(Character dropper, bool setTransform = true) { }
/// <returns>true if the operation was completed</returns>
public virtual bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
@@ -682,9 +682,10 @@ namespace Barotrauma.Items.Components
public virtual void FlipY(bool relativeToSub) { }
public bool IsNotEmpty(Character user, bool checkContainedItems = true) =>
HasRequiredContainedItems(user, addMessage: false) &&
(!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
/// <summary>
/// Shorthand for !HasRequiredContainedItems()
/// </summary>
public bool IsEmpty(Character user) => !HasRequiredContainedItems(user, addMessage: false);
public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null)
{
@@ -716,20 +717,50 @@ namespace Barotrauma.Items.Components
{
if (character.IsBot && item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (requiredItems.None()) { return true; }
if (character.Inventory != null)
if (requiredItems.Count == 0) { return true; }
if (character.Inventory != null && requiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List<RelatedItem> relatedItems))
{
foreach (Item item in character.Inventory.AllItems)
foreach (RelatedItem relatedItem in relatedItems)
{
if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item))))
foreach (Item otherItem in character.Inventory.AllItems)
{
return true;
}
if (relatedItem.MatchesItem(otherItem))
{
if (otherItem.GetComponent<IdCard>() is IdCard idCard)
{
if (!CheckIdCardAccess(relatedItem, idCard))
{
continue;
}
}
return true;
}
}
}
}
return false;
}
/// <summary>
/// Presumes that matching is already checked.
/// </summary>
private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard)
{
if (item.Submarine != null)
{
//id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
{
return false;
}
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
{
return false;
}
}
return true;
}
public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
if (requiredItems.None()) { return true; }
@@ -765,23 +796,14 @@ namespace Barotrauma.Items.Components
bool CheckItems(RelatedItem relatedItem, IEnumerable<Item> itemList)
{
bool Predicate(Item it)
bool Predicate(Item it)
{
if (it == null || it.Condition <= 0.0f || !relatedItem.MatchesItem(it)) { return false; }
if (item.Submarine != null)
if (it.GetComponent<IdCard>() is IdCard idCard)
{
var idCard = it.GetComponent<IdCard>();
if (idCard != null)
if (!CheckIdCardAccess(relatedItem, idCard))
{
//id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
{
return false;
}
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
{
return false;
}
return false;
}
}
return true;
@@ -20,11 +20,13 @@ namespace Barotrauma.Items.Components
{
public readonly int MaxStackSize;
public List<RelatedItem> ContainableItems;
public readonly bool AutoInject;
public SlotRestrictions(int maxStackSize, List<RelatedItem> containableItems)
public SlotRestrictions(int maxStackSize, List<RelatedItem> containableItems, bool autoInject)
{
MaxStackSize = maxStackSize;
ContainableItems = containableItems;
AutoInject = autoInject;
}
public bool MatchesItem(Item item)
@@ -269,7 +271,7 @@ namespace Barotrauma.Items.Components
List<SlotRestrictions> newSlotRestrictions = new List<SlotRestrictions>(totalCapacity);
for (int i = 0; i < capacity; i++)
{
newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems));
newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems, autoInject: false));
}
int subContainerIndex = capacity;
@@ -279,6 +281,7 @@ namespace Barotrauma.Items.Components
int subCapacity = subElement.GetAttributeInt("capacity", 1);
int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize);
bool autoInject = subElement.GetAttributeBool("autoinject", false);
var subContainableItems = new List<RelatedItem>();
foreach (var subSubElement in subElement.Elements())
@@ -298,7 +301,7 @@ namespace Barotrauma.Items.Components
for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++)
{
newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems));
newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems, autoInject));
}
subContainerIndex += subCapacity;
}
@@ -466,7 +469,7 @@ namespace Barotrauma.Items.Components
prevContainedItemPositions = item.Position;
}
if (AutoInject)
if (AutoInject || slotRestrictions.Any(s => s.AutoInject))
{
//normally autoinjection should delete the (medical) item, so it only gets applied once
//but in multiplayer clients aren't allowed to remove items themselves, so they may be able to trigger this dozens of times
@@ -480,7 +483,21 @@ namespace Barotrauma.Items.Components
ownerCharacter.HealthPercentage / 100f <= AutoInjectThreshold &&
ownerCharacter.HasEquippedItem(item))
{
foreach (Item item in Inventory.AllItemsMod)
if (AutoInject)
{
Inventory.AllItemsMod.ForEach(i => Inject(i));
}
else
{
for (int i = 0; i < slotRestrictions.Length; i++)
{
if (slotRestrictions[i].AutoInject)
{
Inventory.GetItemsAt(i).ForEachMod(i => Inject(i));
}
}
}
void Inject(Item item)
{
item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter, useTarget: ownerCharacter);
item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter, useTarget: ownerCharacter);
@@ -511,25 +528,28 @@ namespace Barotrauma.Items.Components
if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; }
StatusEffect effect = activeContainedItem.StatusEffect;
targets.Clear();
bool wearing = item.GetComponent<Wearable>() is Wearable { IsActive: true };
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects);
targets.AddRange(item.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects);
targets.AddRange(contained.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
{
effect.Apply(ActionType.OnContaining, deltaTime, item, character);
targets.Add(character);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
effect.AddNearbyTargets(item.WorldPosition, targets);
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
}
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
effect.Apply(ActionType.OnContaining, deltaTime, item, targets);
if (wearing) { effect.Apply(ActionType.OnWearing, deltaTime, item, targets); }
}
}
@@ -629,7 +649,7 @@ namespace Barotrauma.Items.Components
return false;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
IsActive = true;
SetContainedActive(false);
@@ -98,6 +98,20 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when another character has already selected it?")]
public bool AllowSelectingWhenSelectedByOther
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when a bot has already selected it?")]
public bool AllowSelectingWhenSelectedByBot
{
get;
set;
}
public bool ControlCharacterPose
{
get { return limbPositions.Count > 0; }
@@ -466,8 +480,18 @@ namespace Barotrauma.Items.Components
IsActive = false;
CancelUsing(user);
user = null;
return false;
}
else if (user.IsBot && !activator.IsBot)
{
if (AllowSelectingWhenSelectedByBot)
{
CancelUsing(user);
user = activator;
IsActive = true;
return true;
}
}
return AllowSelectingWhenSelectedByOther;
}
else
{
@@ -10,6 +10,18 @@ namespace Barotrauma.Items.Components
{
private float force;
/// <summary>
/// Latest signal the set_force connection received, used to set <see cref="targetForce"/> in the Update method.
/// We use a separate variable, because otherwise specific item update orders and sending multiple signals to set_force would lead to bugs:
/// targetForce could be set to 0, then a power grid might update as if the engine was off and mark the voltage of the grid as 1,
/// then another item could set the targetForce to 100 and make it run without power.
/// </summary>
private float? lastReceivedTargetForce;
/// <summary>
/// The amount of force the engine is aiming for (the actual force may be less than this,
/// depending on the amount of power, the condition of the engine or boosts from talents)
/// </summary>
private float targetForce;
private float maxForce;
@@ -58,7 +70,7 @@ namespace Barotrauma.Items.Components
public float CurrentVolume
{
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); }
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f))); }
}
public float CurrentBrokenVolume
@@ -110,7 +122,10 @@ namespace Barotrauma.Items.Components
hasPower = Voltage > MinVoltage;
}
if (lastReceivedTargetForce.HasValue)
{
targetForce = lastReceivedTargetForce.Value;
}
Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f);
if (Math.Abs(Force) > 1.0f)
{
@@ -254,7 +269,7 @@ namespace Barotrauma.Items.Components
if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float tempForce))
{
controlLockTimer = 0.1f;
targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
lastReceivedTargetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
User = signal.sender;
}
}
@@ -784,8 +784,10 @@ namespace Barotrauma.Items.Components
}
else
{
float condition1 = MathUtils.IsValid(item1.Condition) ? item1.Condition : 0;
float condition2 = MathUtils.IsValid(item2.Condition) ? item2.Condition : 0;
//prefer items in worse condition
return Math.Sign(item2.Condition - item1.Condition);
return Math.Sign(condition2 - condition1);
}
}
@@ -775,7 +775,6 @@ namespace Barotrauma.Items.Components
if (shutDown)
{
PowerOn = false;
AutoTemp = false;
TargetFissionRate = 0.0f;
TargetTurbineOutput = 0.0f;
unsentChanges = true;
@@ -68,7 +68,6 @@ namespace Barotrauma.Items.Components
public bool UseDirectionalPing => useDirectionalPing;
private bool useDirectionalPing = false;
private Vector2 pingDirection = new Vector2(1.0f, 0.0f);
private bool useMineralScanner;
private bool aiPingCheckPending;
@@ -133,6 +132,9 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool UseMineralScanner { get; set; }
public float Zoom
{
get { return zoom; }
@@ -366,7 +368,7 @@ namespace Barotrauma.Items.Components
bool isActive = msg.ReadBoolean();
bool directionalPing = useDirectionalPing;
float zoomT = zoom, pingDirectionT = 0.0f;
bool mineralScanner = useMineralScanner;
bool mineralScanner = UseMineralScanner;
if (isActive)
{
zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
@@ -391,13 +393,13 @@ namespace Barotrauma.Items.Components
float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, pingDirectionT);
pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
}
useMineralScanner = mineralScanner;
UseMineralScanner = mineralScanner;
#if CLIENT
zoomSlider.BarScroll = zoomT;
directionalModeSwitch.Selected = useDirectionalPing;
if (mineralScannerSwitch != null)
{
mineralScannerSwitch.Selected = useMineralScanner;
mineralScannerSwitch.Selected = UseMineralScanner;
}
#endif
}
@@ -418,7 +420,7 @@ namespace Barotrauma.Items.Components
float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8);
}
msg.WriteBoolean(useMineralScanner);
msg.WriteBoolean(UseMineralScanner);
}
}
}
@@ -444,7 +444,7 @@ namespace Barotrauma.Items.Components
if (connectedSubUpdateTimer <= 0.0f)
{
connectedSubs.Clear();
connectedSubs = controlledSub?.GetConnectedSubs();
connectedSubs.AddRange(controlledSub.GetConnectedSubs());
connectedSubUpdateTimer = ConnectedSubUpdateInterval;
}
@@ -535,7 +535,7 @@ namespace Barotrauma.Items.Components
foreach (GraphEdge edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection))
{
Vector2 diff = controlledSub.WorldPosition - intersection;
//far enough -> ignore
@@ -435,7 +435,7 @@ namespace Barotrauma.Items.Components
{
//other junction boxes don't need to receive the signal in the pass-through signal connections
//because we relay it straight to the connected items without going through the whole chain of junction boxes
if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; }
if (ic is PowerTransfer && ic is not RelayComponent) { continue; }
ic.ReceiveSignal(signal, recipient);
}
@@ -14,18 +14,18 @@ namespace Barotrauma.Items.Components
{
partial class Projectile : ItemComponent, IServerSerializable
{
const int SpreadCounterWrapAround = 256;
private static readonly ImmutableArray<float> spreadPool;
static Projectile()
{
MTRandom random = new MTRandom(0);
spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
spreadPool = Enumerable.Range(0, byte.MaxValue + 1).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
}
public static float GetSpreadFromPool()
public static byte SpreadCounter { get; private set; }
public static void ResetSpreadCounter()
{
return spreadPool[SpreadCounter];
SpreadCounter = 0;
}
struct HitscanResult
@@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components
private bool removePending;
public static byte SpreadCounter { get; private set; }
private byte spreadIndex;
//continuous collision detection is used while the projectile is moving faster than this
const float ContinuousCollisionThreshold = 5.0f;
@@ -299,7 +299,8 @@ namespace Barotrauma.Items.Components
return;
}
SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround);
spreadIndex = SpreadCounter;
SpreadCounter++;
InitProjSpecific(element);
}
@@ -328,6 +329,12 @@ namespace Barotrauma.Items.Components
originalCollisionTargets = item.body.CollidesWith;
}
public float GetSpreadFromPool()
{
spreadIndex = (byte)MathUtils.PositiveModulo(spreadIndex, spreadPool.Length);
return spreadPool[spreadIndex];
}
private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
{
if (Item.body == null) { return; }
@@ -380,7 +387,7 @@ namespace Barotrauma.Items.Components
{
#if SERVER
launchRot = rotation;
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1)));
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(spreadIndex - 1)));
#endif
}
}
@@ -396,7 +403,6 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < HitScanCount; i++)
{
float launchAngle;
if (StaticSpread)
{
launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread;
@@ -405,7 +411,7 @@ namespace Barotrauma.Items.Components
{
launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool());
}
SpreadCounter++;
spreadIndex++;
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
if (Hitscan)
@@ -703,7 +709,7 @@ namespace Barotrauma.Items.Components
return hits;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
Item.ResetWaterDragCoefficient();
if (dropper != null)
@@ -711,7 +717,7 @@ namespace Barotrauma.Items.Components
DisableProjectileCollisions();
Unstick();
}
base.Drop(dropper);
base.Drop(dropper, setTransform);
}
public override void Update(float deltaTime, Camera cam)
@@ -885,17 +891,8 @@ namespace Barotrauma.Items.Components
return false;
}
Vector2 normalizedVel;
Vector2 dir;
if (item.body.LinearVelocity.LengthSquared() < 0.001f)
{
normalizedVel = Vector2.Zero;
dir = contact.Manifold.LocalNormal;
}
else
{
normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity);
}
Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ?
contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity);
//do a raycast in the sub's coordinate space to see if it hit a structure
var wallBody = Submarine.PickBody(
@@ -904,7 +901,7 @@ namespace Barotrauma.Items.Components
collisionCategory: Physics.CollisionWall);
if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) &&
//ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction
Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0)
Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0)
{
target = wallBody.FixtureList.First();
if (hits.Contains(target.Body))
@@ -942,7 +939,7 @@ namespace Barotrauma.Items.Components
Character character = null;
if (target.Body.UserData is Submarine submarine && target.UserData is not Barotrauma.Item)
{
item.Move(-submarine.Position);
item.Move(-submarine.Position, ignoreContacts: false);
item.Submarine = submarine;
item.body.Submarine = submarine;
return !Hitscan;
@@ -339,18 +339,19 @@ namespace Barotrauma.Items.Components
if (Math.Abs(TargetPullForce) > 0.001f)
{
var targetBody = GetBodyToPull(target);
if (user != null && targetCharacter != null && !user.AnimController.InWater)
bool lerpForces = LerpForces;
if (!lerpForces && user != null && targetCharacter != null && !user.AnimController.InWater)
{
// Prevents rubberbanding horizontally when dragging a corpse.
if ((forceDir.X < 0) != (user.AnimController.Dir < 0))
{
forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f);
// Prevents rubberbanding horizontally when dragging a corpse.
lerpForces = true;
}
}
float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce;
float force = lerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce;
targetBody?.ApplyForce(-forceDir * force);
var targetRagdoll = targetCharacter?.AnimController;
if (targetRagdoll != null && (targetRagdoll.InWater || targetRagdoll.OnGround))
if (targetRagdoll?.Collider != null && (targetRagdoll.InWater || targetRagdoll.OnGround))
{
targetRagdoll.Collider.ApplyForce(-forceDir * force * 3);
}
@@ -29,7 +29,7 @@ namespace Barotrauma.Items.Components
private readonly Item item;
public readonly bool IsOutput;
public readonly List<StatusEffect> Effects;
public readonly List<(ushort wireId, int? connectionIndex)> LoadedWires;
@@ -40,6 +40,9 @@ namespace Barotrauma.Items.Components
//Priority in which power output will be handled - load is unaffected
public PowerPriority Priority = PowerPriority.Default;
public Signal LastSentSignal { get; private set; }
public Signal LastReceivedSignal {get; private set;}
public bool IsPower
{
get;
@@ -292,6 +295,7 @@ namespace Barotrauma.Items.Components
public void SendSignal(Signal signal)
{
LastSentSignal = signal;
enumeratingWires = true;
foreach (var wire in wires)
{
@@ -302,6 +306,10 @@ namespace Barotrauma.Items.Components
signal.source?.LastSentSignalRecipients.Add(recipient);
Connection connection = recipient;
connection.LastReceivedSignal = signal;
#if CLIENT
wire.RegisterSignal(signal, source: this);
#endif
foreach (ItemComponent ic in recipient.item.Components)
{
@@ -82,8 +82,10 @@ namespace Barotrauma.Items.Components
signalOut.SendDuration -= 1;
item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out");
if (signalOut.SendDuration <= 0)
{
signalQueue.Dequeue();
{
//check the queue isn't empty again, because sending the signal may empty it
//if this component is set to reset when it receives a signal and the signal is routed back to this component
signalQueue.TryDequeue(out _);
}
else
{
@@ -173,6 +173,8 @@ namespace Barotrauma.Items.Components
set
{
lightColor = value;
//reset previously received signal to force updating the color if we receive a set_color signal after the color has been modified manually
prevColorSignal = string.Empty;
#if CLIENT
if (Light != null)
{
@@ -249,6 +251,11 @@ namespace Barotrauma.Items.Components
base.OnItemLoaded();
SetLightSourceState(IsActive, lightBrightness);
turret = item.GetComponent<Turret>();
if (item.body != null)
{
item.body.FarseerBody.OnEnabled += CheckIfNeedsUpdate;
item.body.FarseerBody.OnDisabled += CheckIfNeedsUpdate;
}
#if CLIENT
Drawable = AlphaBlend && Light.LightSprite != null;
if (Screen.Selected.IsEditor)
@@ -277,15 +284,24 @@ namespace Barotrauma.Items.Components
return;
}
if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null &&
if ((item.body == null || !item.body.Enabled) &&
powerConsumption <= 0.0f && Parent == null && turret == null &&
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
if (item.body != null && !item.body.Enabled)
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
}
else
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
}
isOn = true;
SetLightSourceTransformProjSpecific();
base.IsActive = false;
isOn = true;
#if CLIENT
Light.ParentSub = item.Submarine;
#endif

Some files were not shown because too many files have changed in this diff Show More