v1.4.4.1 (Blood in the Water Update)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -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;
|
||||
}
|
||||
|
||||
+19
-5
@@ -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()
|
||||
|
||||
+259
-106
@@ -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 &&
|
||||
|
||||
+116
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+122
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -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; }
|
||||
|
||||
+11
-6
@@ -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 &&
|
||||
|
||||
+15
-18
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+57
-4
@@ -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
|
||||
|
||||
+21
-13
@@ -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
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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");
|
||||
|
||||
+97
-81
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-22
@@ -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;
|
||||
}
|
||||
|
||||
+33
-34
@@ -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.
|
||||
|
||||
+8
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -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);
|
||||
|
||||
+46
-7
@@ -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);
|
||||
|
||||
+4
@@ -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))
|
||||
|
||||
+22
-6
@@ -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: () =>
|
||||
{
|
||||
|
||||
+4
-7
@@ -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)
|
||||
{
|
||||
|
||||
+1
-1
@@ -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 _);
|
||||
}
|
||||
|
||||
+67
-31
@@ -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 ?
|
||||
|
||||
Reference in New Issue
Block a user