v1.4.4.1 (Blood in the Water Update)

This commit is contained in:
Regalis11
2024-04-24 18:09:05 +03:00
parent 89b91d1c3e
commit ff1b8951a7
397 changed files with 15250 additions and 6479 deletions
@@ -294,6 +294,41 @@ namespace Barotrauma
return Priority;
}
/// <summary>
/// Get a normalized value representing how close the target position is.
/// The value is a rough estimation, where vertical movement is assumed to be more costly than horizontal.
/// </summary>
/// <param name="targetWorldPos">Position of the target</param>
/// <param name="verticalDistanceMultiplier">How much more costly vertical movement is than horizontal</param>
/// <param name="maxDistance">Maximum distance, after which the factor will reach it's minimum value (= anything beyond this point is "as far as it can be").</param>
/// <param name="factorAtMaxDistance">The factor at the maximum distance and beyond (= how "viable" very far-away targets should be considered).</param>
/// <param name="factorAtMinDistance">The factor at the minimum distance (= how viable a target that's 0 units a way is considered).</param>
public static float GetDistanceFactor(Vector2 selfPos, Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f)
{
float yDist = Math.Abs(selfPos.Y - targetWorldPos.Y);
yDist = yDist > 100 ? yDist * verticalDistanceMultiplier : 0;
float distance = Math.Abs(selfPos.X - targetWorldPos.X) + yDist;
float distanceFactor = MathHelper.Lerp(factorAtMinDistance, factorAtMaxDistance, MathUtils.InverseLerp(0, maxDistance, distance));
return
factorAtMinDistance > factorAtMaxDistance ?
MathHelper.Clamp(distanceFactor, factorAtMaxDistance, factorAtMinDistance) :
MathHelper.Clamp(distanceFactor, factorAtMinDistance, factorAtMaxDistance);
}
/// <summary>
/// Get a normalized value representing how close the target position is.
/// The value is a rough estimation, where vertical movement is assumed to be more costly than horizontal.
/// </summary>
/// <param name="targetWorldPos">Position of the target</param>
/// <param name="verticalDistanceMultiplier">How much more costly vertical movement is than horizontal</param>
/// <param name="maxDistance">Maximum distance, after which the factor will reach it's minimum value (= anything beyond this point is "as far as it can be").</param>
/// <param name="factorAtMaxDistance">The factor at the maximum distance and beyond (= how "viable" very far-away targets should be considered).</param>
/// <param name="factorAtMinDistance">The factor at the minimum distance (= how viable a target that's 0 units a way is considered).</param>
protected float GetDistanceFactor(Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f)
{
return GetDistanceFactor(character.WorldPosition, targetWorldPos, factorAtMaxDistance, verticalDistanceMultiplier, maxDistance, factorAtMinDistance);
}
private void UpdateDevotion(float deltaTime)
{
var currentObjective = objectiveManager.CurrentObjective;
@@ -463,7 +498,7 @@ namespace Barotrauma
{
hasBeenChecked = true;
CheckSubObjectives();
if (subObjectives.None() || ConcurrentObjectives && subObjectives.All(so => so is AIObjectiveGoTo))
if (subObjectives.None() || ConcurrentObjectives)
{
if (Check())
{
@@ -509,7 +544,7 @@ namespace Barotrauma
public virtual void SpeakAfterOrderReceived() { }
protected static bool CanEquip(Character character, Item item, bool allowWearing)
protected static bool CanPutInInventory(Character character, Item item, bool allowWearing)
{
if (item == null) { return false; }
bool canEquip = false;
@@ -550,6 +585,6 @@ namespace Barotrauma
return canEquip && character.Inventory.CanBePut(item);
}
protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing);
protected bool CanEquip(Item item, bool allowWearing) => CanPutInInventory(character, item, allowWearing);
}
}
@@ -21,6 +21,11 @@ namespace Barotrauma
private AIObjectiveDecontainItem decontainObjective;
private int itemIndex = 0;
/// <summary>
/// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
/// </summary>
public override bool ConcurrentObjectives => true;
public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -39,10 +44,8 @@ namespace Barotrauma
float distanceFactor = 0.9f;
if (!IsPriority && item.CurrentHull != character.CurrentHull)
{
float yDist = Math.Abs(character.WorldPosition.Y - item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist));
distanceFactor = GetDistanceFactor(item.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000,
factorAtMinDistance: 0.9f, factorAtMaxDistance: 0);
}
bool isSelected = character.HasItem(item);
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
@@ -116,7 +119,7 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (item.IgnoreByAI(character))
if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item))
{
Abandon = true;
}
@@ -56,8 +56,15 @@ namespace Barotrauma
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); }
if (target.CurrentHull.FireSources.Count > 0) { return false; }
// Don't clean up items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
foreach (Character c in Character.CharacterList)
{
if (c == character || !HumanAIController.IsActive(c)) { continue; }
if (c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c))
{
// Don't clean up items in rooms that have enemies inside.
return false;
}
}
return true;
}
@@ -89,9 +96,10 @@ namespace Barotrauma
IsItemInsideValidSubmarine(container, character) &&
!container.IsClaimedByBallastFlora;
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true, bool requireValidContainer = true, bool ignoreItemsMarkedForDeconstruction = true)
{
if (item == null) { return false; }
if (item.DontCleanUp) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
@@ -101,8 +109,9 @@ namespace Barotrauma
return false;
}
if (!allowUnloading) { return false; }
if (!IsValidContainer(item.Container, character)) { return false; }
if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; }
}
if (ignoreItemsMarkedForDeconstruction && Item.DeconstructItems.Contains(item)) { return false; }
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
@@ -121,11 +130,16 @@ namespace Barotrauma
return false;
}
}
if (item.GetComponent<Rope>() is { IsActive: true, Snapped: false })
{
// Don't clean up spears with an active rope component.
return false;
}
if (!checkInventory)
{
return true;
}
return CanEquip(character, item, allowWearing: false);
return CanPutInInventory(character, item, allowWearing: false);
}
public override void OnDeselected()
@@ -22,13 +22,13 @@ namespace Barotrauma
private readonly CombatMode initialMode;
private float checkWeaponsTimer;
private readonly float checkWeaponsInterval = 1;
private const float checkWeaponsInterval = 1;
private float ignoreWeaponTimer;
private readonly float ignoredWeaponsClearTime = 10;
private const float ignoredWeaponsClearTime = 10;
private readonly float goodWeaponPriority = 30;
private const float goodWeaponPriority = 30;
private readonly float arrestHoldFireTime = 8;
private const float arrestHoldFireTime = 8;
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
@@ -79,14 +79,17 @@ namespace Barotrauma
private bool canSeeTarget;
private float visibilityCheckTimer;
private readonly float visibilityCheckInterval = 0.2f;
private const float visibilityCheckInterval = 0.2f;
private float sqrDistance;
private readonly float maxDistance = 2000;
private readonly float distanceCheckInterval = 0.2f;
private const float maxDistance = 2000;
private const float distanceCheckInterval = 0.2f;
private float distanceTimer;
private const float closeDistanceThreshold = 300;
private const float floorHeightApproximate = 100;
public bool allowHoldFire;
public bool AllowHoldFire;
/// <summary>
/// Don't start using a weapon if this condition is true
@@ -95,26 +98,63 @@ namespace Barotrauma
public enum CombatMode
{
Defensive, // Use weapons against the enemy, but try to retreat to a safe place
Offensive, // Engage the enemy and keep attacking it
Arrest, // Try to arrest the enemy without using lethal weapons (stunning + handcuffs)
Retreat, // Run to a safe place without attacking the target
None // Don't use
/// <summary>
/// Use weapons against the enemy, but try to retreat to a safe place.
/// </summary>
Defensive,
/// <summary>
/// Engage the enemy and keep attacking it.
/// </summary>
Offensive,
/// <summary>
/// Try to arrest the enemy without using lethal weapons (stunning + handcuffs).
/// </summary>
Arrest,
/// <summary>
/// Attempt to retreat to a safe place. Unlike in the Defensive mode, the character won't try to attack the enemy.
/// </summary>
Retreat,
/// <summary>
/// Does nothing.
/// </summary>
None
}
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool IsOffensiveOrArrest => initialMode is CombatMode.Offensive or CombatMode.Arrest;
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator;
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private float AimSpeed => HumanAIController.AimSpeed;
private float AimAccuracy => HumanAIController.AimAccuracy;
private bool IsEnemyCloserThan(float margin) =>
Enemy != null && Enemy.CurrentHull != null &&
character.InWater && Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin ||
HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull) && Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X) < margin;
/// <summary>
/// This is just an approximation that attempts to take different rooms and floors into account.
/// It can be equal to a simple distance check, but when the target is nearby, we only use the horizontal axis.
/// It's used for checking whether the enemy is close in certain situations, not for checking the distance to the enemy in general.
/// </summary>
private bool IsEnemyClose(float margin)
{
if (Enemy == null) { return false; }
Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition;
if (character.CurrentHull != null && Enemy.CurrentHull != null && character.CurrentHull != Enemy.CurrentHull)
{
// Inside, not in the same hull with the enemy
if (Math.Abs(toEnemy.Y) > floorHeightApproximate)
{
// Different floor
return false;
}
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// Potentially visible and on the same floor -> use only the horizontal distance.
return Math.Abs(toEnemy.X) < margin;
}
}
// Outside or inside in the same hull -> use the normal distance check.
return Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin;
}
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
: base(character, objectiveManager, priorityModifier)
@@ -147,7 +187,7 @@ namespace Barotrauma
protected override float GetPriority()
{
if (Enemy == null)
if (Enemy == null || Enemy.Removed)
{
Priority = 0;
Abandon = true;
@@ -169,9 +209,9 @@ namespace Barotrauma
else
{
// 91-100
float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
float maxPriority = AIObjectiveManager.MaxObjectivePriority;
float priorityScale = maxPriority - minPriority;
const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
const float maxPriority = AIObjectiveManager.MaxObjectivePriority;
const 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))
@@ -208,12 +248,12 @@ namespace Barotrauma
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
}
bool isCurrentObjective = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
if (findSafety != null && isCurrentObjective)
bool isFightingIntruders = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
if (findSafety != null && isFightingIntruders)
{
findSafety.Priority = 0;
}
if (!AllowCoolDown && !character.IsOnPlayerTeam && !isCurrentObjective)
if (!AllowCoolDown && !character.IsOnPlayerTeam && !isFightingIntruders)
{
distanceTimer -= deltaTime;
if (distanceTimer < 0)
@@ -226,7 +266,7 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (character.Submarine == null || character.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
if (character.Submarine is not { TeamID: CharacterTeamType.FriendlyNPC })
{
// Can't lose the target in friendly outposts.
if (sqrDistance > maxDistance * maxDistance)
@@ -343,12 +383,15 @@ namespace Barotrauma
RemoveSubObjective(ref seekAmmunitionObjective);
return false;
}
bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest;
bool isAllowedToSeekWeapons = character.IsHostileEscortee || character.IsPrisoner || // Prisoners and terrorists etc are always allowed to seek new weapons.
(character.IsInFriendlySub // Other characters need to be on a friendly sub in order to "know" where the weapons are. This also prevents NPCs "stealing" player items.
&& IsOffensiveOrArrest // = Defensive or retreating AI shouldn't seek new weapons.
&& !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
if (checkWeaponsTimer < 0)
{
checkWeaponsTimer = checkWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
var allWeapons = FindWeaponsFromInventory();
HashSet<ItemComponent> allWeapons = FindWeaponsFromInventory();
while (allWeapons.Any())
{
Weapon = GetWeapon(allWeapons, out _weaponComponent);
@@ -369,14 +412,20 @@ namespace Barotrauma
// All good, the weapon is loaded
break;
}
if (Reload(seekAmmo: isAllowedToSeekWeapons))
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(closeDistanceThreshold);
if (Reload(seekAmmo: seekAmmo))
{
// All good, we can use the weapon.
break;
}
else if (seekAmmunitionObjective != null)
{
// Seeking ammo.
break;
}
else
{
// No ammo.
// No ammo and should not try to seek ammo.
allWeapons.Remove(WeaponComponent);
Weapon = null;
}
@@ -409,16 +458,16 @@ namespace Barotrauma
Mode = CombatMode.Retreat;
}
}
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority)))
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority && !IsEnemyClose(closeDistanceThreshold))))
{
// Poor weapon equipped -> try to find better.
RemoveSubObjective(ref seekAmmunitionObjective);
// No weapon or only a poor weapon equipped -> try to find better.
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref followTargetObjective);
TryAddSubObjective(ref seekWeaponObjective,
constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false)
{
AllowStealing = HumanAIController.IsMentallyUnstable,
AbortCondition = obj => IsEnemyClose(200),
EvaluateCombatPriority = false, // Use a custom formula instead
GetItemPriority = i =>
{
@@ -427,7 +476,39 @@ namespace Barotrauma
float priority = 0;
if (GetWeaponComponent(i) is ItemComponent ic)
{
priority = GetWeaponPriority(ic, prioritizeMelee: false, isCloseToEnemy: false, out _) / 100;
priority = GetWeaponPriority(ic, prioritizeMelee: false, canSeekAmmo: true, out _) / 100;
}
if (priority <= 0) { return 0; }
// Check that we are not running directly towards the enemy.
Vector2 toItem = i.WorldPosition - character.WorldPosition;
float range = HumanAIController.FindWeaponsRange;
if (range is > 0 and < float.PositiveInfinity)
{
// Y distance is irrelevant when we are on the same floor. If we are on a different floor, let's double it.
float yDiff = Math.Abs(toItem.Y) > floorHeightApproximate ? toItem.Y * 2 : 0;
Vector2 adjustedDiff = new Vector2(toItem.X, yDiff);
if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range))
{
// Too far -> not allowed to seek.
return 0;
}
}
Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition;
if (Math.Sign(toItem.X) == Math.Sign(toEnemy.X))
{
// Going towards the enemy -> reduce the priority.
priority *= 0.5f;
}
if (i.CurrentHull != null && !HumanAIController.VisibleHulls.Contains(i.CurrentHull))
{
if (Math.Abs(toItem.Y) > floorHeightApproximate && Math.Abs(toEnemy.Y) > floorHeightApproximate)
{
if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y))
{
// Different floor, at the direction of the enemy -> reduce the priority.
priority *= 0.75f;
}
}
}
return priority;
}
@@ -441,19 +522,19 @@ namespace Barotrauma
SpeakNoWeapons();
Mode = CombatMode.Retreat;
}
else
else if (!objectiveManager.HasActiveObjective<AIObjectiveFightIntruders>())
{
// Poor weapon equipped
Mode = CombatMode.Defensive;
}
});
}
}
else
else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
if (!CheckWeapon(seekAmmo: false))
{
Weapon = null;
RemoveSubObjective(ref seekAmmunitionObjective);
}
}
return Weapon != null;
@@ -504,10 +585,14 @@ namespace Barotrauma
item.GetComponent<RepairTool>() ??
item.GetComponent<Holdable>() as ItemComponent;
private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool isCloseToEnemy, out float lethalDmg)
/// <summary>
/// Normal range of combat priority is 0-100, but the value is not clamped.
/// </summary>
private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool canSeekAmmo, out float lethalDmg)
{
lethalDmg = -1;
float priority = weapon.CombatPriority;
if (priority <= 0) { return 0; }
if (weapon is RepairTool repairTool)
{
switch (repairTool.UsableIn)
@@ -531,9 +616,9 @@ namespace Barotrauma
}
if (weapon.IsEmpty(character))
{
if (weapon is RangedWeapon && isCloseToEnemy)
if (weapon is RangedWeapon && !canSeekAmmo)
{
// Ignore weapons that don't have any ammunition (-> Don't seek ammo).
// Ignore weapons that don't have any ammunition, when we are not allowed to seek more ammo.
return 0;
}
else
@@ -605,7 +690,45 @@ namespace Barotrauma
Attack attack = GetAttackDefinition(weapon);
priority = attack?.GetTotalDamage() ?? priority / 2;
}
// Reduce the priority of the weapon, if we don't have requires skills to use it.
float startPriority = priority;
var skillRequirementHints = weapon.Item.Prefab.SkillRequirementHints;
if (skillRequirementHints != null)
{
// If there are any skill requirement hints defined, let's use them.
// This should be the most accurate (manually defined) representation of the requirements (taking into account property conditionals etc).
foreach (SkillRequirementHint hint in skillRequirementHints)
{
float skillLevel = character.GetSkillLevel(hint.Skill);
float targetLevel = hint.Level;
priority = ReducePriority(priority, skillLevel, targetLevel);
}
}
else
{
// If no skill requirement hints are defined, let's rely on the required skill definition.
// This can be inaccurate in some cases (hmg, rifle), but in those cases there should be a skill requirement hint defined for the weapon.
foreach (Skill skill in weapon.RequiredSkills)
{
float skillLevel = character.GetSkillLevel(skill.Identifier);
// Skill multiplier is currently always 1, so it's not really needed, but that could change(?)
float targetLevel = skill.Level * weapon.GetSkillMultiplier();
priority = ReducePriority(priority, skillLevel, targetLevel);
}
}
// Don't allow to reduce more than half, because an assault rifle is still an assault rifle, even in untrained hands.
priority = Math.Max(priority, startPriority / 2);
return priority;
float ReducePriority(float prio, float skillLevel, float targetLevel)
{
float diff = targetLevel - skillLevel;
if (diff > 0)
{
prio -= diff;
}
return prio;
}
}
private float ApproximateStunDamage(ItemComponent weapon, Attack attack)
@@ -632,12 +755,12 @@ namespace Barotrauma
return attack.Stun + afflictionsStun + effectsStun;
}
private bool CanMeleeStunnerStun(ItemComponent weapon)
private static bool CanMeleeStunnerStun(ItemComponent weapon)
{
// If there's an item container that takes a battery,
// assume that it's required for the stun effect
// as we can't check the status effect conditions here.
var mobileBatteryTag = Tags.MobileBattery;
Identifier mobileBatteryTag = Tags.MobileBattery;
var containers = weapon.Item.Components.Where(ic =>
ic is ItemContainer container &&
container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
@@ -651,11 +774,11 @@ namespace Barotrauma
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
bool isCloseToEnemy = IsEnemyCloserThan(300);
bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character);
bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(closeDistanceThreshold);
foreach (var weapon in weaponList)
{
float priority = GetWeaponPriority(weapon, prioritizeMelee, isCloseToEnemy, out lethalDmg);
float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
if (priority > bestPriority)
{
weaponComponent = weapon;
@@ -678,7 +801,7 @@ namespace Barotrauma
}
isLethalWeapon = lethalDmg > 1;
}
if (allowHoldFire && !hasAimed && holdFireTimer <= 0)
if (AllowHoldFire && !hasAimed && holdFireTimer <= 0)
{
holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f);
}
@@ -699,15 +822,12 @@ namespace Barotrauma
private static Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
Attack attack = weapon switch
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
MeleeWeapon meleeWeapon => meleeWeapon.Attack,
RangedWeapon rangedWeapon => rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack,
_ => null
};
return attack;
}
@@ -726,7 +846,7 @@ namespace Barotrauma
return weapons;
}
private void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
private static void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
{
if (item == null) { return; }
foreach (var component in item.Components)
@@ -765,14 +885,13 @@ namespace Barotrauma
}
if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType))
{
//clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
character.ClearInput(InputType.Aim);
character.ClearInput(InputType.Shoot);
ClearInputs();
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(s => CharacterInventory.IsHandSlotType(s));
var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType);
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
SetReloadTime(WeaponComponent);
}
else
{
@@ -786,7 +905,7 @@ namespace Barotrauma
}
private float findHullTimer;
private readonly float findHullInterval = 1.0f;
private const float findHullInterval = 1.0f;
private void Retreat(float deltaTime)
{
@@ -796,6 +915,18 @@ namespace Barotrauma
}
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunitionObjective);
if (retreatTarget != null)
{
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// In the same hull with the enemy
if (retreatTarget == character.CurrentHull)
{
// Go elsewhere
retreatTarget = null;
}
}
}
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
{
RemoveSubObjective(ref retreatObjective);
@@ -809,7 +940,7 @@ namespace Barotrauma
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
return;
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
if (retreatTarget == null || retreatObjective is { CanBeCompleted: false })
{
if (findHullTimer > 0)
{
@@ -942,9 +1073,13 @@ namespace Barotrauma
if (!arrestingRegistered && followTargetObjective != null)
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 1000 :
WeaponComponent is MeleeWeapon mw ? mw.Range :
WeaponComponent is RepairTool rt ? rt.Range : 50;
WeaponComponent switch
{
RangedWeapon => 1000,
MeleeWeapon mw => mw.Range,
RepairTool rt => rt.Range,
_ => 50
};
}
}
@@ -976,9 +1111,8 @@ namespace Barotrauma
foreach (var item in Enemy.Inventory.AllItemsMod)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
item.HasTag(Tags.Weapon) ||
item.GetComponent<MeleeWeapon>() != null ||
item.GetComponent<RangedWeapon>() != null)
item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) ||
GetWeaponComponent(item) is { CombatPriority: > 0 })
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
@@ -1024,10 +1158,11 @@ namespace Barotrauma
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekWeaponObjective);
RemoveFollowTarget();
var itemContainer = Weapon.GetComponent<ItemContainer>();
TryAddSubObjective(ref seekAmmunitionObjective,
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
{
ItemCount = Weapon.GetComponent<ItemContainer>().Capacity * Weapon.GetComponent<ItemContainer>().MaxStackSize,
ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
checkInventory = false,
MoveWholeStack = true
},
@@ -1052,9 +1187,9 @@ namespace Barotrauma
// Eject empty ammo
HumanAIController.UnequipEmptyItems(Weapon);
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
foreach (RelatedItem requiredItem in WeaponComponent.RequiredItems[RelatedItem.RelationType.Contained])
{
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
ammunitionIdentifiers = requiredItem.Identifiers;
@@ -1075,12 +1210,14 @@ namespace Barotrauma
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
if (!container.Inventory.TryPutItem(ammunition, user: character))
if (container.Inventory.TryPutItem(ammunition, user: character))
{
if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
ClearInputs();
SetReloadTime(WeaponComponent);
}
else if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
}
}
@@ -1127,7 +1264,7 @@ namespace Barotrauma
}
if (Weapon.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
character.SetInput(InputType.Aim, hit: false, held: true);
}
hasAimed = true;
if (holdFireTimer > 0)
@@ -1194,23 +1331,17 @@ namespace Barotrauma
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
{
if (myBodies == null)
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody);
// Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
foreach (var body in pickedBodies)
{
Character target = null;
if (body.UserData is Character c)
Character target = body.UserData switch
{
target = c;
}
else if (body.UserData is Limb limb)
{
target = limb.character;
}
Character c => c,
Limb limb => limb.character,
_ => null
};
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
{
return;
@@ -1225,26 +1356,48 @@ namespace Barotrauma
{
// Never allow to attack characters with deadly weapons while trying to arrest.
if (Mode == CombatMode.Arrest && isLethalWeapon) { return; }
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
// If the weapon is just equipped, we can't shoot just yet.
if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
{
reloadTime = rangedWeapon.Reload;
}
}
if (WeaponComponent is MeleeWeapon mw)
{
if (!((HumanoidAnimController)character.AnimController).Crouching)
{
reloadTime = mw.Reload;
}
}
character.SetInput(InputType.Shoot, false, true);
character.SetInput(InputType.Shoot, hit: false, held: true);
Weapon.Use(deltaTime, user: character);
SetReloadTime(WeaponComponent);
}
private float GetReloadTime(ItemComponent weaponComponent)
{
float reloadTime = 0;
switch (weaponComponent)
{
case RangedWeapon rangedWeapon:
{
if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
{
reloadTime = rangedWeapon.Reload;
}
break;
}
case MeleeWeapon mw:
{
if (character.AnimController is HumanoidAnimController { Crouching: false })
{
reloadTime = mw.Reload;
}
break;
}
}
return reloadTime;
}
private void SetReloadTime(ItemComponent weaponComponent)
{
float reloadTime = GetReloadTime(weaponComponent);
reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
}
private void ClearInputs()
{
//clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
character.ClearInput(InputType.Aim);
character.ClearInput(InputType.Shoot);
}
private bool ShouldUnequipWeapon =>
Weapon != null &&
@@ -0,0 +1,116 @@
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveDeconstructItem : AIObjective
{
public override Identifier Identifier { get; set; } = "deconstruct item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
public override bool AllowInFriendlySubs => true;
public readonly Item Item;
private Deconstructor deconstructor;
private AIObjectiveDecontainItem decontainObjective;
public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
Item = item;
}
protected override void Act(float deltaTime)
{
if (subObjectives.Any()) { return; }
if (deconstructor == null)
{
deconstructor = FindDeconstructor();
if (deconstructor == null)
{
Abandon = true;
return;
}
}
TryAddSubObjective(ref decontainObjective,
constructor: () => new AIObjectiveDecontainItem(character, Item, objectiveManager,
sourceContainer: Item.Container?.GetComponent<ItemContainer>(), targetContainer: deconstructor.InputContainer, priorityModifier: PriorityModifier)
{
Equip = true,
RemoveExistingWhenNecessary = true
},
onCompleted: () =>
{
StartDeconstructor();
//make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost
if (deconstructor.Item.Submarine is { Info.IsOutpost: true })
{
HumanAIController.HandleRelocation(Item);
deconstructor.RelocateOutputToMainSub = true;
}
IsCompleted = true;
RemoveSubObjective(ref decontainObjective);
},
onAbandon: () =>
{
Abandon = true;
});
}
private Deconstructor FindDeconstructor()
{
Deconstructor closestDeconstructor = null;
float bestDistFactor = 0;
foreach (var otherItem in Item.ItemList)
{
var potentialDeconstructor = otherItem.GetComponent<Deconstructor>();
if (potentialDeconstructor?.InputContainer == null) { continue; }
if (!potentialDeconstructor.InputContainer.Inventory.CanBePut(Item)) { continue; }
if (!potentialDeconstructor.Item.HasAccess(character)) { continue; }
float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f);
if (distFactor > bestDistFactor)
{
closestDeconstructor = potentialDeconstructor;
bestDistFactor = distFactor;
}
}
return closestDeconstructor;
}
private void StartDeconstructor()
{
deconstructor.SetActive(active: true, user: character, createNetworkEvent: true);
}
protected override bool CheckObjectiveSpecific()
{
if (Item.IgnoreByAI(character))
{
Abandon = true;
}
else if (deconstructor != null && deconstructor.Item.IgnoreByAI(character))
{
Abandon = true;
}
return !Abandon && IsCompleted;
}
public override void Reset()
{
base.Reset();
decontainObjective = null;
}
public void DropTarget()
{
if (Item != null && character.HasItem(Item))
{
Item.Drop(character);
}
}
}
}
@@ -0,0 +1,122 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
namespace Barotrauma
{
class AIObjectiveDeconstructItems : AIObjectiveLoop<Item>
{
public override Identifier Identifier { get; set; } = "deconstruct items".ToIdentifier();
//Clear periodically, because we may ending up ignoring items when all deconstructors are full
protected override float IgnoreListClearInterval => 30;
public override bool AllowInFriendlySubs => true;
protected override int MaxTargets => 10;
private bool checkedDeconstructorExists;
public AIObjectiveDeconstructItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
}
public override void OnSelected()
{
base.OnSelected();
if (!checkedDeconstructorExists)
{
if (character.Submarine == null ||
Item.ItemList.None(it =>
it.GetComponent<Deconstructor>() != null &&
it.IsInteractable(character) &&
character.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true, allowDifferentTeam: true, allowDifferentType: true)))
{
character.Speak(TextManager.Get("orderdialogself.deconstructitem.nodeconstructor").Value, delay: 5.0f,
identifier: "nodeconstructor".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
Abandon = true;
}
checkedDeconstructorExists = true;
}
}
public override void Reset()
{
base.Reset();
checkedDeconstructorExists = false;
}
protected override float TargetEvaluation()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
{
return objectiveManager.GetOrderPriority(this);
}
return AIObjectiveManager.RunPriority - 0.5f;
}
protected override bool Filter(Item target)
{
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character, checkInventory: true))
{
return Objectives.ContainsKey(target) && AIObjectiveCleanupItems.IsItemInsideValidSubmarine(target, character);
}
if (target.CurrentHull.FireSources.Count > 0) { return false; }
foreach (Character c in Character.CharacterList)
{
if (c == character || !HumanAIController.IsActive(c)) { continue; }
if (c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c))
{
// Don't deconstruct items in rooms that have enemies inside.
return false;
}
else if (c.TeamID == character.TeamID && c.AIController is HumanAIController humanAi)
{
if (humanAi.ObjectiveManager.CurrentObjective is AIObjectiveDeconstructItem deconstruct && deconstruct.Item == target)
{
return false;
}
}
}
return true;
}
protected override IEnumerable<Item> GetList() => Item.DeconstructItems;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveDeconstructItem(item, character, objectiveManager, priorityModifier: PriorityModifier);
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveDeconstructItems, Item>(character, target);
private static bool IsValidTarget(Item item, Character character, bool checkInventory)
{
if (item == null) { return false; }
if (item.GetRootInventoryOwner() == character) { return true; }
return AIObjectiveCleanupItems.IsValidTarget(
item,
character,
checkInventory,
allowUnloading: true,
requireValidContainer: false,
ignoreItemsMarkedForDeconstruction: false);
}
public override void OnDeselected()
{
base.OnDeselected();
foreach (var subObjective in SubObjectives)
{
if (subObjective is AIObjectiveDeconstructItem deconstructObjective)
{
deconstructObjective.DropTarget();
}
}
}
}
}
@@ -39,6 +39,9 @@ namespace Barotrauma
/// </summary>
public bool DropIfFails { get; set; } = true;
/// <summary>
/// Should existing item(s) be removed from the targetContainer if the targetItem won't fit otherwise?
/// </summary>
public bool RemoveExistingWhenNecessary { get; set; }
public Func<Item, bool> RemoveExistingPredicate { get; set; }
public int? RemoveExistingMax { get; set; }
@@ -45,13 +45,18 @@ namespace Barotrauma
else
{
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull || HumanAIController.VisibleHulls.Contains(targetHull))
float distanceFactor = 1.0f;
if (targetHull != character.CurrentHull &&
!HumanAIController.VisibleHulls.Contains(targetHull))
{
distanceFactor = 1;
distanceFactor =
GetDistanceFactor(
new Vector2(character.WorldPosition.Y, characterY),
targetHull.WorldPosition,
verticalDistanceMultiplier: 3,
maxDistance: 5000,
factorAtMaxDistance: 0.1f);
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
if (severity > 0.75f && !isOrder &&
@@ -12,7 +12,7 @@ namespace Barotrauma
protected override float TargetUpdateTimeMultiplier => 0.2f;
public bool TargetCharactersInOtherSubs { get; set; }
public bool TargetCharactersInOtherSubs { get; init; }
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
@@ -33,20 +33,22 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(Character target)
{
AIObjectiveCombat.CombatMode combatMode = ShouldArrest(target, character) ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive;
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
AIObjectiveCombat.CombatMode combatMode = AIObjectiveCombat.CombatMode.Offensive;
if (character.IsOnPlayerTeam && target is { IsEscorted: true })
{
if (campaign.CurrentLocation is { IsFactionHostile: true })
// Try to arrest escorted characters, instead of killing them.
combatMode = AIObjectiveCombat.CombatMode.Arrest;
}
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode { CurrentLocation.IsFactionHostile: true })
{
combatObjective.holdFireCondition = () =>
{
combatObjective.holdFireCondition = () =>
{
//hold fire while the enemy is in the airlock (except if they've attacked us)
if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock");
};
character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f);
}
//hold fire while the enemy is in the airlock (except if they've attacked us)
if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock");
};
character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f);
}
return combatObjective;
}
@@ -77,10 +79,5 @@ namespace Barotrauma
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
return true;
}
public static bool ShouldArrest(Character target, Character character)
{
return target != null && target.IsEscorted && character.TeamID == CharacterTeamType.Team1;
}
}
}
@@ -33,22 +33,27 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true));
TrySetTargetItem(character.Inventory.FindItem(it => it.HasTag(gearTag) && IsSuitablePressureProtection(it, gearTag, character), true));
if (targetItem == null && gearTag == Tags.LightDivingGear)
{
TrySetTargetItem(character.Inventory.FindItemByTag(Tags.HeavyDivingGear, true));
TrySetTargetItem(character.Inventory.FindItem(
it => it.HasTag(Tags.HeavyDivingGear) && IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true));
}
if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
{
bool mustFindMorePressureProtection =
!objectiveManager.FailedToFindDivingGearForDepth &&
character.Inventory.FindItem(
it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null;
TryAddSubObjective(ref getDivingGear, () =>
{
if (targetItem == null && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
}
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
{
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
@@ -56,8 +61,42 @@ namespace Barotrauma
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
Wear = true
};
if (gearTag == Tags.HeavyDivingGear)
{
if (mustFindMorePressureProtection)
{
//if we're looking for a suit specifically because the current suit isn't enough,
//let's ignore unsuitable suits altogether...
getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character);
}
else
{
//...Otherwise it's fine to give a very small priority
//to inadequate suits (a suit not adequate for the depth is better than no suit)
getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f;
}
getItemObjective.GetItemPriority = it =>
{
if (IsSuitablePressureProtection(it, gearTag, character))
{
return 1000.0f;
}
else
{
//if we're looking for a suit specifically because the current suit isn't enough,
//let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority
//to inadequate suits (a suit not adequate for the depth is better than no suit)
return mustFindMorePressureProtection ? 0.0f : 1.0f;
}
};
}
return getItemObjective;
},
onAbandon: () => Abandon = true,
onAbandon: () =>
{
if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; }
Abandon = true;
},
onCompleted: () =>
{
RemoveSubObjective(ref getDivingGear);
@@ -160,6 +199,20 @@ namespace Barotrauma
}
}
public static bool IsSuitablePressureProtection(Item item, Identifier tag, Character character)
{
if (tag == Tags.HeavyDivingGear)
{
float realWorldDepth = Level.Loaded?.GetRealWorldDepth(character.WorldPosition.Y) ?? 0.0f;
if (item.GetComponent<Wearable>() is not { } wearable || wearable.PressureProtection < realWorldDepth + Steering.PressureWarningThreshold)
{
return false;
}
}
return true;
}
private bool IsSuitableContainedOxygenSource(Item item)
{
return
@@ -52,12 +52,26 @@ namespace Barotrauma
{
bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true);
if (isSuffocatingInDivingSuit ||
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)) ||
(!objectiveManager.HasActiveObjective<AIObjectiveFindDivingGear>() && IsSuffocatingWithoutDivingGear(character)))
if (isSuffocatingInDivingSuit || (!objectiveManager.HasActiveObjective<AIObjectiveFindDivingGear>() && IsSuffocatingWithoutDivingGear(character)))
{
Priority = AIObjectiveManager.MaxObjectivePriority;
}
else if (NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
{
if (objectiveManager.FailedToFindDivingGearForDepth &&
HumanAIController.HasDivingSuit(character, requireSuitablePressureProtection: false))
{
//we have a suit that's not suitable for the pressure,
//but we've failed to find a better one
// shit, not much we can do here, let's just allow the bot to get on with their current objective
Priority = 0;
}
else
{
Priority = AIObjectiveManager.MaxObjectivePriority;
}
}
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
{
@@ -259,7 +273,7 @@ namespace Barotrauma
bool inFriendlySub =
character.IsInFriendlySub ||
(character.IsEscorted && character.IsInPlayerSub);
if (cannotFindSafeHull && !inFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn))
if (cannotFindSafeHull && !inFriendlySub && character.IsOnPlayerTeam && objectiveManager.Objectives.None(o => o is AIObjectiveReturn))
{
if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab))
{
@@ -401,10 +415,7 @@ namespace Barotrauma
if (isCharacterInside)
{
hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - potentialHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
float distanceFactor = GetDistanceFactor(potentialHull.WorldPosition, factorAtMaxDistance: 0.9f);
hullSafety *= distanceFactor;
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
@@ -446,16 +457,13 @@ namespace Barotrauma
hullSafety = 100;
hullIsAirlock = true;
}
else if(!bestHullIsAirlock && potentialHull.LeadsOutside(character))
else if (!bestHullIsAirlock && potentialHull.LeadsOutside(character))
{
hullSafety = 100;
}
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
// Huge preference for closer targets
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance));
float distanceFactor = GetDistanceFactor(new Vector2(character.WorldPosition.X, characterY), potentialHull.WorldPosition, factorAtMaxDistance: 0.2f);
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
@@ -39,8 +39,8 @@ namespace Barotrauma
if (campaign.Map?.CurrentLocation?.Reputation is { } reputation)
{
return MathHelper.Lerp(
campaign.Settings.MaxStolenItemInspectionProbability,
campaign.Settings.MinStolenItemInspectionProbability,
campaign.Settings.PatdownProbabilityMax,
campaign.Settings.PatdownProbabilityMin,
reputation.NormalizedValue);
}
}
@@ -120,7 +120,7 @@ namespace Barotrauma
Abandon = true;
return;
}
if (weldingTool.OwnInventory == null && repairTool.requiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained))
if (weldingTool.OwnInventory == null && repairTool.RequiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained))
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no proper inventory");
@@ -159,13 +159,14 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (IdentifiersOrTags != null && !isDoneSeeking)
if (IdentifiersOrTags != null)
{
if (checkInventory)
{
if (CheckInventory())
{
isDoneSeeking = true;
itemCandidates.Clear();
}
}
if (!isDoneSeeking)
@@ -189,7 +190,14 @@ namespace Barotrauma
}
}
FindTargetItem();
if (!objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
}
if (targetItem == null)
{
if (isDoneSeeking)
{
HandlePotentialItems();
}
if (objectiveManager.CurrentOrder is not AIObjectiveGoTo)
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
@@ -201,20 +209,28 @@ namespace Barotrauma
Abandon = true;
return;
}
if (targetItem == null || targetItem.Removed)
bool ShouldAbort() => IdentifiersOrTags is null || isDoneSeeking && itemCandidates.None();
if (targetItem is null or { Removed: true })
{
if (ShouldAbort())
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
#endif
Abandon = true;
Abandon = true;
}
return;
}
else if (isDoneSeeking && moveToTarget == null)
if (moveToTarget is null)
{
if (ShouldAbort())
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red);
#endif
Abandon = true;
Abandon = true;
return;
}
return;
}
if (character.IsItemTakenBySomeoneElse(targetItem))
@@ -399,16 +415,8 @@ namespace Barotrauma
{
StopWatch.Restart();
}
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
if (!CheckPathForEachItem)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// 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.IsFollowOrder);
}
bool checkPath = CheckPathForEachItem;
float priority = objectiveManager.GetCurrentPriority();
bool checkPath = CheckPathForEachItem || priority >= AIObjectiveManager.RunPriority || ItemCount > 1;
// Reset if the character has switched subs.
if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true))
{
@@ -434,9 +442,9 @@ namespace Barotrauma
// Ignore items in the inventory when defined not to check it.
if (item.IsOwnedBy(character)) { continue; }
}
if (!AllowStealing)
if (!AllowStealing && character.IsOnPlayerTeam)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInCurrentOutpost) { continue; }
if (item.SpawnedInCurrentOutpost && !item.AllowStealing) { continue; }
}
if (!CheckItem(item)) { continue; }
if (item.Container != null)
@@ -454,11 +462,11 @@ namespace Barotrauma
if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; }
}
float itemPriority = item.Prefab.BotPriority;
if (itemPriority <= 0) { continue; }
if (GetItemPriority != null)
{
itemPriority *= GetItemPriority(item);
}
if (itemPriority <= 0) { continue; }
Entity rootInventoryOwner = item.GetRootInventoryOwner();
if (rootInventoryOwner is Item ownerItem)
{
@@ -474,11 +482,13 @@ namespace Barotrauma
}
}
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
float minDistFactor = EvaluateCombatPriority ? 0.1f : 0;
float distanceFactor = MathHelper.Lerp(1, minDistFactor, MathUtils.InverseLerp(100, 10000, dist));
float distanceFactor =
GetDistanceFactor(
itemPos,
verticalDistanceMultiplier: 5,
maxDistance: 10000,
factorAtMinDistance: 1.0f,
factorAtMaxDistance: EvaluateCombatPriority ? 0.1f : 0);
itemPriority *= distanceFactor;
if (EvaluateCombatPriority)
{
@@ -510,7 +520,7 @@ namespace Barotrauma
}
else
{
combatFactor = Math.Min(item.Components.Sum(ic => AIObjectiveCombat.GetLethalDamage(ic)) / 1000, 0.1f);
combatFactor = Math.Min(item.Components.Sum(AIObjectiveCombat.GetLethalDamage) / 1000, 0.1f);
}
itemPriority *= combatFactor;
}
@@ -518,10 +528,6 @@ 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 (EvaluateCombatPriority && itemPriority <= 0)
@@ -529,23 +535,27 @@ namespace Barotrauma
// Not good enough
continue;
}
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
if (checkPath)
{
itemCandidates.Add((item, itemPriority));
}
else
{
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
}
}
if (currentSearchIndex >= itemList.Count - 1)
{
isDoneSeeking = true;
}
if (checkedItems > 0)
{
if (isDoneSeeking && itemCandidates.Any())
if (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 (HumanAIController.DebugAI && StopWatch.ElapsedMilliseconds > 2)
{
string msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem?.Name ?? "NULL"} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}";
if (StopWatch.ElapsedMilliseconds > 5)
{
DebugConsole.ThrowError(msg);
@@ -557,60 +567,66 @@ namespace Barotrauma
}
}
}
if (isDoneSeeking)
}
private void HandlePotentialItems()
{
Debug.Assert(isDoneSeeking);
if (itemCandidates.Any())
{
if (PathSteering == null)
{
itemCandidates.Clear();
Abandon = true;
return;
}
if (itemCandidates.Any())
if (itemCandidates.FirstOrDefault() is var itemCandidate)
{
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)
{
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)
{
ItemPrefab prefab = FindItemToSpawn();
if (prefab == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
else
{
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
{
targetItem = spawnedItem;
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInCurrentOutpost = true;
}
});
}
// Remove the invalid candidates and continue on the next frame.
itemCandidates.Remove(itemCandidate);
}
else
{
// The path was valid -> we are done.
itemCandidates.Clear();
targetItem = itemCandidate.item;
moveToTarget = targetItem.GetRootInventoryOwner() ?? targetItem;
}
}
}
if (targetItem == null)
{
if (spawnItemIfNotFound)
{
ItemPrefab prefab = FindItemToSpawn();
if (prefab == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
else
{
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
{
targetItem = spawnedItem;
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInCurrentOutpost = true;
}
});
}
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
#endif
Abandon = true;
}
}
}
@@ -3,6 +3,7 @@ using Barotrauma.Extensions;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using System;
namespace Barotrauma
{
@@ -24,6 +25,12 @@ namespace Barotrauma
public bool CheckPathForEachItem { get; set; }
public bool RequireNonEmpty { get; set; }
public bool RequireAllItems { get; set; }
public bool RequireDivingSuitAdequate { get; set; }
/// <summary>
/// T1 = item to check, T2 = tag we're trying to find a suitable item for
/// </summary>
public Func<Item, Identifier, bool>? ItemFilter;
private readonly ImmutableArray<Identifier> gearTags;
private readonly ImmutableHashSet<Identifier> ignoredTags;
@@ -48,7 +55,8 @@ namespace Barotrauma
int count = gearTags.Count(t => t == tag);
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
var getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
AllowVariants = AllowVariants,
Wear = Wear,
@@ -58,29 +66,36 @@ namespace Barotrauma
CheckPathForEachItem = CheckPathForEachItem,
RequireNonEmpty = RequireNonEmpty,
ItemCount = count,
SpeakIfFails = RequireAllItems
},
onCompleted: () =>
SpeakIfFails = RequireAllItems,
};
if (ItemFilter != null)
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
achievedItems.Add(item);
}
},
onAbandon: () =>
getItem.ItemFilter = (Item it) => ItemFilter(it, tag);
}
return getItem;
},
onCompleted: () =>
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
achievedItems.Add(item);
}
},
onAbandon: () =>
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
}
subObjectivesCreated = true;
}
@@ -85,6 +85,8 @@ namespace Barotrauma
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
public bool FaceTargetOnCompleted { get; set; } = true;
public bool AlwaysUseEuclideanDistance { get; set; } = true;
/// <summary>
@@ -324,7 +326,7 @@ namespace Barotrauma
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
}
else if (tryToGetDivingGear)
{
@@ -346,26 +348,26 @@ namespace Barotrauma
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);
});
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));
@@ -450,10 +452,8 @@ namespace Barotrauma
{
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);
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(Tags.Scooter, allowBroken: false);
if (!shouldUseScooter)
{
float threshold = 500;
@@ -467,7 +467,7 @@ namespace Barotrauma
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
}
}
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
scooter = equippedScooters.FirstOrDefault();
@@ -477,23 +477,23 @@ namespace Barotrauma
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));
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem) && !character.Inventory.TryPutItem(leftHandItem, character, InvSlotType.Bag.ToEnumerable())) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem) && !character.Inventory.TryPutItem(rightHandItem, character, InvSlotType.Bag.ToEnumerable()));
if (!handsFull)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScooters, containedTag: Tags.MobileBattery, 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))
else if (HumanAIController.HasItem(character, Tags.Scooter, 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);
hasBattery = HumanAIController.HasItem(character, Tags.MobileBattery, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
@@ -511,7 +511,7 @@ namespace Barotrauma
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))
if (HumanAIController.HasItem(character, Tags.MobileBattery, 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))
@@ -811,16 +811,15 @@ namespace Barotrauma
private void StopMovement()
{
SteeringManager?.Reset();
if (Target != null)
if (FaceTargetOnCompleted && Target is Entity { Removed: false })
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
HumanAIController.FaceTarget(Target);
}
}
protected override void OnCompleted()
{
StopMovement();
HumanAIController.FaceTarget(Target);
if (Target is WayPoint { Ladders: null })
{
// Release ladders when ordered to wait at a spawnpoint.
@@ -454,14 +454,16 @@ namespace Barotrauma
{
targetHulls.Add(hull);
float weight = hull.RectWidth;
// Prefer rooms that are closer. Avoid rooms that are not in the same level.
// If the behavior is active, prefer rooms that are not close.
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = behavior == BehaviorType.Patrol ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(2500, 0, dist)) : MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist));
float distanceFactor = GetDistanceFactor(hull.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 2500,
factorAtMinDistance: 1, factorAtMaxDistance: 0);
if (behavior == BehaviorType.Patrol)
{
//invert when patrolling (= prefer travelling to far-away hulls)
distanceFactor = 1.0f - distanceFactor;
}
float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2));
weight *= distanceFactor * waterFactor;
System.Diagnostics.Debug.Assert(weight >= 0);
hullWeights.Add(weight);
}
}
@@ -193,7 +193,10 @@ namespace Barotrauma
if (yDist > 100) { dist += yDist * 5; }
dist += Math.Abs(character.WorldPosition.X - targetPos.X);
}
float distanceFactor = dist > 0.0f ? MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)) : 0.9f;
float distanceFactor =
GetDistanceFactor(targetItem.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000, factorAtMinDistance: 0.9f, factorAtMaxDistance: 0);
bool hasContainable = character.HasItem(targetItem);
float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100;
float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2);
@@ -67,6 +67,10 @@ namespace Barotrauma
}
private AIObjective currentOrder;
public AIObjective ForcedOrder { get; private set; }
/// <summary>
/// Includes orders.
/// </summary>
public AIObjective CurrentObjective { get; private set; }
public AIObjectiveManager(Character character)
@@ -104,6 +108,8 @@ namespace Barotrauma
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public bool FailedAutonomousObjectives { get; private set; }
public bool FailedToFindDivingGearForDepth;
private void ClearIgnored()
{
if (character.AIController is HumanAIController humanAi)
@@ -220,8 +226,11 @@ namespace Barotrauma
if (previousObjective == CurrentObjective) { return CurrentObjective; }
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
if (CurrentObjective != null)
{
CurrentObjective.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
}
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(character,
@@ -230,9 +239,14 @@ namespace Barotrauma
return CurrentObjective;
}
/// <summary>
/// Returns the highest priority of the current objective and its subobjectives.
/// </summary>
public float GetCurrentPriority()
{
return CurrentObjective == null ? 0.0f : CurrentObjective.Priority;
if (CurrentObjective == null) { return 0; }
float subObjectivePriority = CurrentObjective.SubObjectives.Any() ? CurrentObjective.SubObjectives.Max(so => so.Priority) : 0;
return Math.Max(CurrentObjective.Priority, subObjectivePriority);
}
public void UpdateObjectives(float deltaTime)
@@ -241,7 +255,7 @@ namespace Barotrauma
if (CurrentOrders.Any())
{
foreach(var order in CurrentOrders)
foreach (var order in CurrentOrders)
{
var orderObjective = order.Objective;
UpdateOrderObjective(orderObjective);
@@ -396,6 +410,9 @@ namespace Barotrauma
}
}
//reset this here so the bots can retry finding a better suit if it's needed for the new order
FailedToFindDivingGearForDepth = false;
var newCurrentObjective = CreateObjective(order);
if (newCurrentObjective != null)
{
@@ -592,6 +609,9 @@ namespace Barotrauma
case "loaditems":
newObjective = new AIObjectiveLoadItems(character, this, order.Option, order.GetTargetItems(order.Option), order.TargetEntity as Item, priorityModifier);
break;
case "deconstructitems":
newObjective = new AIObjectiveDeconstructItems(character, this, priorityModifier);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
@@ -613,6 +633,11 @@ namespace Barotrauma
return newObjective;
}
/// <summary>
/// Sets the order as dismissed, and enables the option to reissue the order on the crew list.
/// Note that this is not the same thing as just removing the order entirely!
/// </summary>
/// <param name="order"></param>
private void DismissSelf(Order order)
{
var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order.Identifier, order.Option));
@@ -651,13 +676,27 @@ namespace Barotrauma
return true;
}
/// <summary>
/// Only checks the current order. Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
/// <summary>
/// Checks the current objective (which can be an order too). Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
/// Return the first order whose objective is of the given type. Can return null.
/// </summary>
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T;
/// <summary>
/// Return the first order with the specified objective. Can return null.
/// </summary>
public Order GetOrder(AIObjective objective) => CurrentOrders.FirstOrDefault(o => o.Objective == objective);
public T GetLastActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
@@ -665,12 +704,12 @@ namespace Barotrauma
=> 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.
/// Returns all active objectives of the specific type.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective
{
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).OfType<T>();
}
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
@@ -211,6 +211,10 @@ namespace Barotrauma
return;
}
}
//the character shouldn't be grabbing anyone if it's trying to operate an item
character.SelectedCharacter = null;
if (target.CanBeSelected)
{
if (!character.IsClimbing && character.CanInteractWith(target.Item, out _, checkLinked: false))
@@ -87,13 +87,29 @@ namespace Barotrauma
AIObjectiveGetItems CreateObjectives(IEnumerable<Identifier> itemTags, bool requireAll)
{
AIObjectiveGetItems objectiveReference = null;
if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags)
if (!TryAddSubObjective(ref objectiveReference, () =>
{
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireNonEmpty = RequireNonEmpty,
RequireAllItems = requireAll
var getItems = new AIObjectiveGetItems(character, objectiveManager, itemTags)
{
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireNonEmpty = RequireNonEmpty,
RequireAllItems = requireAll
};
if (itemTags.Contains(Tags.HeavyDivingGear))
{
getItems.ItemFilter = (Item it, Identifier tag) =>
{
if (tag == Tags.HeavyDivingGear)
{
return AIObjectiveFindDivingGear.IsSuitablePressureProtection(it, tag, character);
}
return true;
};
}
return getItems;
},
onCompleted: () =>
{
@@ -64,10 +64,7 @@ namespace Barotrauma
float distanceFactor = 1;
if (!isPriority && Item.CurrentHull != character.CurrentHull)
{
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist));
distanceFactor = GetDistanceFactor(Item.WorldPosition, factorAtMaxDistance: 0.25f, verticalDistanceMultiplier: 5, maxDistance: 4000);
}
float requiredSuccessFactor = objectiveManager.HasOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100;
@@ -113,7 +110,7 @@ namespace Barotrauma
if (!repairable.HasRequiredItems(character, false))
{
//make sure we have all the items required to fix the target item
foreach (var kvp in repairable.requiredItems)
foreach (var kvp in repairable.RequiredItems)
{
foreach (RelatedItem requiredItem in kvp.Value)
{
@@ -140,7 +137,7 @@ namespace Barotrauma
}
if (repairTool != null)
{
if (repairTool.requiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems))
if (repairTool.RequiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems))
{
if (repairTool.Item.OwnInventory == null)
{
@@ -282,7 +279,7 @@ namespace Barotrauma
{
foreach (Repairable repairable in Item.Repairables)
{
foreach (var kvp in repairable.requiredItems)
foreach (var kvp in repairable.RequiredItems)
{
foreach (RelatedItem requiredItem in kvp.Value)
{
@@ -74,7 +74,7 @@ namespace Barotrauma
}
if (!RelevantSkill.IsEmpty)
{
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; }
if (item.Repairables.None(r => r.RequiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; }
}
return !HumanAIController.IsItemRepairedByAnother(item, out _);
}
@@ -278,52 +278,66 @@ namespace Barotrauma
float cprSuitability = Target.Oxygen < 0.0f ? -Target.Oxygen * 100.0f : 0.0f;
//find which treatments are the most suitable to treat the character's current condition
Target.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f);
//check if we already have a suitable treatment for any of the afflictions
float bestSuitability = 0.0f;
Item bestItem = null;
Affliction afflictionToTreat = null;
foreach (Affliction affliction in GetSortedAfflictions(Target))
{
if (affliction == null) { throw new Exception("Affliction was null"); }
if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); }
float bestSuitability = 0.0f;
Item bestItem = null;
foreach (KeyValuePair<Identifier, float> treatmentSuitability in affliction.Prefab.TreatmentSuitabilities)
//find which treatments are the most suitable to treat the character's current condition
Target.CharacterHealth.GetSuitableTreatments(
currentTreatmentSuitabilities,
limb: Target.CharacterHealth.GetAfflictionLimb(affliction),
user: character,
predictFutureDuration: 10.0f);
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) &&
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
float thisSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key];
if (thisSuitability <= 0) { continue; }
Item matchingItem = FindMedicalItem(character.Inventory, treatmentSuitability.Key);
//allow taking items from the target's inventory too if the target is unconscious
if (matchingItem == null && Target.IsIncapacitated)
{
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 && Target.IsIncapacitated)
{
matchingItem ??= Target.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true);
}
if (matchingItem != null)
{
bestItem = matchingItem;
bestSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key];
}
matchingItem = FindMedicalItem(Target.Inventory, treatmentSuitability.Key);
}
}
if (bestItem != null)
{
if (Target != character) { character.SelectCharacter(Target); }
ApplyTreatment(affliction, bestItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
if (matchingItem == null) { continue; }
//also check how suitable the treatment is for the specific affliction we're now checking
//we don't want to e.g. give fentanyl for oxygen low just because the character has burns on other limbs
//that would also be healed by it!
float suitabilityForThisAffliction = affliction.Prefab.GetTreatmentSuitability(matchingItem);
float totalSuitability = thisSuitability * suitabilityForThisAffliction;
if (matchingItem != null && totalSuitability > bestSuitability)
{
bestItem = matchingItem;
afflictionToTreat = affliction;
bestSuitability = totalSuitability;
}
}
}
if (bestItem != null && bestSuitability > cprSuitability)
{
if (Target != character) { character.SelectCharacter(Target); }
ApplyTreatment(afflictionToTreat, bestItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
}
// Find treatments outside of own inventory only if inside the own sub.
if (character.Submarine != null && character.Submarine.TeamID == character.TeamID)
{
//get "overall" suitability for no specific limb at this point
Target.CharacterHealth.GetSuitableTreatments(
currentTreatmentSuitabilities, user: character, predictFutureDuration: 10.0f);
//didn't have any suitable treatments available, try to find some medical items
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
{
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities.OrderByDescending(s => s.Value))
{
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab))
@@ -420,6 +434,28 @@ namespace Barotrauma
}
}
public static Item FindMedicalItem(Inventory inventory, Identifier itemIdentifier)
{
return FindMedicalItem(inventory, it => it.Prefab.Identifier == itemIdentifier);
}
public static Item FindMedicalItem(Inventory inventory, Func<Item, bool> predicate)
{
if (inventory == null) { return null; }
//prefer items not in a container
Item match = inventory.FindItem(predicate, recursive: false);
if (match != null) { return match; }
//start from the inventories with most slots
//= prefer taking items from things like toolbelts or doctor's uniforms, as opposed to e.g. autoinjectors which tend to have one or two slots
foreach (var potentialContainer in inventory.AllItems.OrderByDescending(it => it.OwnInventory?.Capacity ?? -1))
{
match = potentialContainer.OwnInventory?.FindItem(predicate, recursive: true);
if (match != null) { return match; }
}
return null;
}
private void SpeakCannotTreat()
{
LocalizedString msg = character == Target ?