(3dc4135ce) v0.9.5.1

This commit is contained in:
Regalis
2019-11-21 18:22:25 +01:00
parent b39922a074
commit 5c95c53118
287 changed files with 12655 additions and 5048 deletions
@@ -12,6 +12,16 @@ namespace Barotrauma
public abstract string DebugTag { get; }
public virtual bool ForceRun => false;
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool ReportFailures => true;
/// <summary>
/// Can there be multiple objective instaces of the same type? Currently multiple instances allowed only for main objectives and the subobjectives of objetive loops.
/// In theory, there could be multiple subobjectives of same type for concurrent objectives, but that would make things more complex -> potential issues
/// </summary>
public virtual bool AllowMultipleInstances => false;
/// <summary>
/// Run the main objective with all subobjectives concurrently?
@@ -19,19 +29,30 @@ namespace Barotrauma
/// </summary>
public virtual bool ConcurrentObjectives => false;
public virtual bool KeepDivingGearOn => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
public float Priority { get; set; }
public float PriorityModifier { get; private set; } = 1;
public readonly Character character;
public readonly AIObjectiveManager objectiveManager;
public string Option { get; protected set; }
public string Option { get; private set; }
protected bool abandon;
private bool _abandon;
public bool Abandon
{
get { return _abandon; }
set
{
_abandon = value;
if (_abandon)
{
OnAbandon();
}
}
}
/// <summary>
/// Can the objective be completed. That is, does the objective have failing subobjectives or other conditions that prevent it from completing.
/// </summary>
public virtual bool CanBeCompleted => !abandon && subObjectives.All(so => so.CanBeCompleted);
public virtual bool CanBeCompleted => !Abandon;
/// <summary>
/// When true, the objective is never completed, unless CanBeCompleted returns false.
@@ -39,71 +60,85 @@ namespace Barotrauma
public virtual bool IsLoop { get; set; }
public IEnumerable<AIObjective> SubObjectives => subObjectives;
private readonly List<AIObjective> all = new List<AIObjective>();
public IEnumerable<AIObjective> GetSubObjectivesRecursive(bool includingSelf = false)
{
all.Clear();
if (includingSelf)
{
all.Add(this);
}
foreach (var subObjective in subObjectives)
{
all.AddRange(subObjective.GetSubObjectivesRecursive(true));
}
return all;
}
public event Action Completed;
public event Action Abandoned;
public event Action Selected;
public event Action Deselected;
protected HumanAIController HumanAIController => character.AIController as HumanAIController;
protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering;
protected SteeringManager SteeringManager => HumanAIController.SteeringManager;
public AIObjective GetActiveObjective()
{
var subObjective = SubObjectives.FirstOrDefault();
return subObjective == null ? this : subObjective.GetActiveObjective();
}
public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
{
this.objectiveManager = objectiveManager;
this.character = character;
Option = option ?? string.Empty;
PriorityModifier = priorityModifier;
#if DEBUG
IsDuplicate(null);
#endif
}
/// <summary>
/// makes the character act according to the objective, or according to any subobjectives that
/// need to be completed before this one
/// Makes the character act according to the objective, or according to any subobjectives that need to be completed before this one
/// </summary>
public void TryComplete(float deltaTime)
{
if (isCompleted) { return; }
//if (Abandon && !IsLoop && subObjectives.None()) { return; }
if (CheckState()) { return; }
// Not ready -> act (can't do foreach because it's possible that the collection is modified in event callbacks.
for (int i = 0; i < subObjectives.Count; i++)
{
var subObjective = subObjectives[i];
if (subObjective.IsCompleted())
{
#if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.");
#endif
subObjective.OnCompleted();
subObjectives.Remove(subObjective);
}
else if (!subObjective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.");
#endif
subObjectives.Remove(subObjective);
}
subObjectives[i].TryComplete(deltaTime);
if (!ConcurrentObjectives) { return; }
}
foreach (AIObjective objective in subObjectives)
{
objective.TryComplete(deltaTime);
if (!ConcurrentObjectives)
{
return;
}
}
Act(deltaTime);
if (IsCompleted())
}
// TODO: check turret aioperate
public void AddSubObjective(AIObjective objective, bool addFirst = false)
{
var type = objective.GetType();
subObjectives.RemoveAll(o => o.GetType() == type);
if (addFirst)
{
OnCompleted();
subObjectives.Insert(0, objective);
}
else
{
subObjectives.Add(objective);
}
}
// TODO: go through AIOperate methods where subobjectives are added and ensure that they add the subobjectives correctly -> use TryAddSubObjective method instead?
public void AddSubObjective(AIObjective objective)
/// <summary>
/// This method allows multiple subobjectives of same type. Use with caution.
/// </summary>
public void AddSubObjectiveInQueue(AIObjective objective)
{
if (subObjectives.Any(o => o.IsDuplicate(objective))) { return; }
subObjectives.Add(objective);
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
}
public void RemoveSubObjective<T>(ref T objective) where T : AIObjective
@@ -120,6 +155,7 @@ namespace Barotrauma
public void SortSubObjectives()
{
if (!AllowSubObjectiveSorting) { return; }
if (subObjectives.None()) { return; }
subObjectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority()));
if (ConcurrentObjectives)
@@ -134,6 +170,8 @@ namespace Barotrauma
public virtual float GetPriority() => Priority * PriorityModifier;
public virtual bool IsDuplicate<T>(T otherObjective) where T : AIObjective => otherObjective.Option == Option;
public virtual void Update(float deltaTime)
{
if (objectiveManager.CurrentOrder == this)
@@ -150,8 +188,8 @@ namespace Barotrauma
}
}
Priority = MathHelper.Clamp(Priority, 0, 100);
subObjectives.ForEach(so => so.Update(deltaTime));
}
subObjectives.ForEach(so => so.Update(deltaTime));
}
/// <summary>
@@ -174,9 +212,9 @@ namespace Barotrauma
/// <summary>
/// Checks if the objective already is created and added in subobjectives. If not, creates it.
/// Handles objectives that cannot be completed. If the objective has been removed form the subobjectives, a null value is assigned to the reference.
/// Returns true if the objective was created.
/// Returns true if the objective was created and successfully added.
/// </summary>
protected bool TryAddSubObjective<T>(ref T objective, Func<T> constructor, Action onAbandon = null) where T : AIObjective
protected bool TryAddSubObjective<T>(ref T objective, Func<T> constructor, Action onCompleted = null, Action onAbandon = null) where T : AIObjective
{
if (objective != null)
{
@@ -184,11 +222,6 @@ namespace Barotrauma
// If the sub objective is removed -> it's either completed or impossible to complete.
if (!subObjectives.Contains(objective))
{
if (!objective.CanBeCompleted)
{
abandon = true;
onAbandon?.Invoke();
}
objective = null;
}
return false;
@@ -198,37 +231,122 @@ namespace Barotrauma
objective = constructor();
if (!subObjectives.Contains(objective))
{
AddSubObjective(objective);
if (objective.AllowMultipleInstances)
{
subObjectives.Add(objective);
}
else
{
AddSubObjective(objective);
}
if (onCompleted != null)
{
objective.Completed += onCompleted;
}
if (onAbandon != null)
{
objective.Abandoned += onAbandon;
}
return true;
}
return true;
#if DEBUG
DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace);
#endif
return false;
}
}
public virtual void OnSelected()
{
// Should we reset steering here?
//if (!ConcurrentObjectives)
//{
// SteeringManager.Reset();
//}
Reset();
Selected?.Invoke();
}
public virtual void OnDeselected()
{
Deselected?.Invoke();
}
protected virtual void OnCompleted()
{
Completed?.Invoke();
//if (Completed != null)
//{
// Completed();
// Completed = null;
//}
}
public virtual void Reset() { }
protected virtual void OnAbandon()
{
Abandoned?.Invoke();
}
public virtual void Reset()
{
isCompleted = false;
hasBeenChecked = false;
_abandon = false;
}
protected abstract void Act(float deltaTime);
public abstract bool IsCompleted();
private bool isCompleted;
private bool hasBeenChecked;
public abstract bool IsDuplicate(AIObjective otherObjective);
public bool IsCompleted
{
get
{
if (!hasBeenChecked)
{
CheckState();
}
return isCompleted;
}
protected set
{
isCompleted = value;
}
}
protected abstract bool Check();
private bool CheckState()
{
hasBeenChecked = true;
CheckSubObjectives();
if (subObjectives.None())
{
if (Check())
{
isCompleted = true;
OnCompleted();
}
}
return isCompleted;
}
private void CheckSubObjectives()
{
for (int i = 0; i < subObjectives.Count; i++)
{
var subObjective = subObjectives[i];
subObjective.CheckState();
if (subObjective.IsCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.", Color.LightGreen);
#endif
subObjectives.Remove(subObjective);
}
else if (!subObjective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.", Color.Red);
#endif
subObjectives.Remove(subObjective);
if (AbandonWhenCannotCompleteSubjectives)
{
Abandon = true;
}
}
}
}
}
}
@@ -15,11 +15,6 @@ namespace Barotrauma
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier)
: base(character, objectiveManager, priorityModifier, option) { }
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveChargeBatteries other && other.Option == Option;
}
protected override bool Filter(PowerContainer battery)
{
if (battery == null) { return false; }
@@ -29,15 +24,8 @@ namespace Barotrauma
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.ConditionPercentage <= 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; }
if (Option == "charge")
{
if (battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio - 0.01f) { return false; }
}
else
{
if (battery.RechargeRatio <= 0) { return false; }
}
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(battery)) { return false; }
return true;
}
@@ -67,8 +55,26 @@ namespace Barotrauma
return batteryList;
}
protected override AIObjective ObjectiveConstructor(PowerContainer battery)
=> new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier) { IsLoop = false };
private bool IsReady(PowerContainer battery)
{
if (battery.HasBeenTuned && character.CurrentOrder == null) { return true; }
if (Option == "charge")
{
return battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio;
}
else
{
return battery.RechargeRatio <= 0;
}
}
protected override AIObjective ObjectiveConstructor(PowerContainer battery) =>
new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
completionCondition = () => IsReady(battery)
};
protected override void OnObjectiveCompleted(AIObjective objective, PowerContainer target)
=> HumanAIController.RemoveTargets<AIObjectiveChargeBatteries, PowerContainer>(character, target);
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
{
@@ -12,9 +11,19 @@ namespace Barotrauma
{
public override string DebugTag => "combat";
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
private readonly CombatMode initialMode;
private float seekWeaponsTimer;
const float seekWeaponsInterval = 1;
private float ignoreWeaponTimer;
const float ignoredWeaponsClearTime = 10;
const float coolDown = 10.0f;
// Won't take the offensive with weapons that have lower priority than this
const float goodWeaponPriority = 30;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
@@ -48,9 +57,11 @@ namespace Barotrauma
}
public override bool ConcurrentObjectives => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
private readonly AIObjectiveFindSafety findSafety;
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
private readonly HashSet<Item> ignoredWeapons = new HashSet<Item>();
private AIObjectiveContainItem seekAmmunition;
private AIObjectiveGoTo retreatObjective;
@@ -79,7 +90,7 @@ namespace Barotrauma
if (findSafety != null)
{
findSafety.Priority = 0;
findSafety.unreachable.Clear();
HumanAIController.UnreachableHulls.Clear();
}
Mode = mode;
initialMode = Mode;
@@ -91,15 +102,19 @@ namespace Barotrauma
public override float GetPriority() => (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
public override bool IsDuplicate(AIObjective otherObjective)
public override void Update(float deltaTime)
{
if (!(otherObjective is AIObjectiveCombat objective)) return false;
return objective.Enemy == Enemy;
base.Update(deltaTime);
ignoreWeaponTimer -= deltaTime;
seekWeaponsTimer -= deltaTime;
if (ignoreWeaponTimer < 0)
{
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
}
}
public override void OnSelected() => Weapon = null;
public override bool IsCompleted()
protected override bool Check()
{
bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0);
if (completed)
@@ -122,18 +137,16 @@ namespace Barotrauma
{
coolDownTimer -= deltaTime;
}
if (abandon) { return; }
TryArm();
if (seekAmmunition == null || !subObjectives.Contains(seekAmmunition))
if (seekAmmunition == null)
{
if (!HoldPosition)
{
Move();
}
if (WeaponComponent != null)
if (TryArm())
{
OperateWeapon(deltaTime);
}
if (!HoldPosition && seekAmmunition == null)
{
Move();
}
}
}
@@ -153,42 +166,117 @@ namespace Barotrauma
}
}
private bool IsLoaded(ItemComponent weapon) => weapon.HasRequiredContainedItems(character, addMessage: false);
private bool TryArm()
{
if (character.LockHands) { return false; }
if (Weapon != null)
if (character.LockHands || Enemy == null)
{
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
Weapon = null;
return false;
}
if (seekWeaponsTimer < 0)
{
seekWeaponsTimer = seekWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
var allWeapons = GetAllWeapons().ToList();
while (allWeapons.Any())
{
Weapon = null;
}
else if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
{
// Seek ammunition only if cannot find a new weapon
if (!Reload(!HoldPosition, () => GetWeapon(out _) == null))
Weapon = GetWeapon(allWeapons, out _weaponComponent);
if (Weapon == null)
{
if (seekAmmunition != null && subObjectives.Contains(seekAmmunition))
{
return false;
}
else
// No weapons
break;
}
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
if (initialMode == CombatMode.Offensive)
{
// In the offensive mode, let's ignore weapons that cannot be used in the offensive mode
if (WeaponComponent.CombatPriority < goodWeaponPriority)
{
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
}
if (IsLoaded(WeaponComponent))
{
// All good, the weapon is loaded
break;
}
if (Reload(seekAmmo: false))
{
// All good, reloading successful
break;
}
else
{
// No ammo.
allWeapons.Remove(WeaponComponent);
Weapon = null;
}
}
if (Weapon == null)
{
// No weapon found with the conditions above. Try again, now let's try to seek ammunition too
Weapon = GetWeapon(out _weaponComponent);
if (Weapon != null)
{
if (!CheckWeapon(seekAmmo: true))
{
if (seekAmmunition != null)
{
// No loaded weapon, but we are trying to seek ammunition.
return false;
}
else
{
Weapon = null;
}
}
}
}
}
if (Weapon == null)
else
{
Weapon = GetWeapon(out _weaponComponent);
if (!CheckWeapon(seekAmmo: false))
{
Weapon = null;
}
}
if (Weapon == null)
{
Weapon = GetWeapon(out _weaponComponent, ignoreRequiredItems: true);
Mode = CombatMode.Retreat;
}
else
{
Mode = WeaponComponent.CombatPriority >= goodWeaponPriority ? initialMode : CombatMode.Defensive;
}
Mode = Weapon == null ? CombatMode.Retreat : initialMode;
return Weapon != null;
bool CheckWeapon(bool seekAmmo)
{
if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!IsLoaded(WeaponComponent))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
{
return false;
}
}
return true;
};
}
private void OperateWeapon(float deltaTime)
@@ -209,59 +297,72 @@ namespace Barotrauma
}
}
private Item GetWeapon(out ItemComponent weaponComponent, bool ignoreRequiredItems = false)
private Item GetWeapon(out ItemComponent weaponComponent)
{
weapons.Clear();
_weaponComponent = null;
foreach (var item in character.Inventory.Items)
{
if (item == null) { continue; }
SeekWeapons(item);
if (item.OwnInventory != null)
{
item.OwnInventory.Items.ForEach(i => SeekWeapons(i));
}
}
weaponComponent = weapons.OrderByDescending(w => w.CombatPriority).FirstOrDefault();
GetAllWeapons();
return GetWeapon(weapons, out weaponComponent);
}
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
{
weaponComponent = weaponList.OrderByDescending(w => CalculateWeaponPriority(w)).FirstOrDefault();
if (weaponComponent == null) { return null; }
if (weaponComponent.CombatPriority < 1) { return null; }
return weaponComponent.Item;
}
void SeekWeapons(Item item)
private float CalculateWeaponPriority(ItemComponent weapon)
{
float priority = weapon.CombatPriority;
// Halve the priority for weapons that don't have proper ammunition loaded.
if (!weapon.HasRequiredContainedItems(character, addMessage: false))
{
if (item == null) { return; }
foreach (var component in item.Components)
priority /= 2;
}
return priority;
}
private HashSet<ItemComponent> GetAllWeapons()
{
weapons.Clear();
foreach (var item in character.Inventory.Items)
{
if (item == null) { continue; }
if (ignoredWeapons.Contains(item)) { continue; }
SeekWeapons(item, weapons);
if (item.OwnInventory != null)
{
if (component is RangedWeapon rw)
item.OwnInventory.Items.ForEach(i => SeekWeapons(i, weapons));
}
}
return weapons;
}
private void SeekWeapons(Item item, ICollection<ItemComponent> weaponList)
{
if (item == null) { return; }
foreach (var component in item.Components)
{
if (component is RangedWeapon rw)
{
weaponList.Add(rw);
}
else if (component is MeleeWeapon mw)
{
weaponList.Add(mw);
}
else
{
var effects = component.statusEffectLists;
if (effects != null)
{
if (ignoreRequiredItems || rw.HasRequiredContainedItems(character, addMessage: false))
foreach (var statusEffects in effects.Values)
{
weapons.Add(rw);
}
}
else if (component is MeleeWeapon mw)
{
if (ignoreRequiredItems || mw.HasRequiredContainedItems(character, addMessage: false))
{
weapons.Add(mw);
}
}
else
{
var effects = component.statusEffectLists;
if (effects != null)
{
foreach (var statusEffects in effects.Values)
foreach (var statusEffect in statusEffects)
{
foreach (var statusEffect in statusEffects)
if (statusEffect.Afflictions.Any())
{
if (statusEffect.Afflictions.Any())
{
if (ignoreRequiredItems || component.HasRequiredContainedItems(character, addMessage: false))
{
weapons.Add(component);
}
}
weaponList.Add(component);
}
}
}
@@ -286,17 +387,14 @@ namespace Barotrauma
if (character.LockHands) { return false; }
if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
{
Mode = CombatMode.Retreat;
return false;
}
//if (!character.SelectedItems.Contains(Weapon))
if (!character.HasEquippedItem(Weapon))
{
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.FindAll(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
Weapon.Equip(character);
aimTimer = Rand.Range(0.5f, 1f);
}
else
@@ -323,13 +421,15 @@ namespace Barotrauma
}
if (character.CurrentHull != retreatTarget)
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true));
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref retreatObjective));
}
}
private void Engage()
{
if (character.LockHands)
if (character.LockHands || Enemy == null)
{
Mode = CombatMode.Retreat;
SteeringManager.Reset();
@@ -346,18 +446,18 @@ namespace Barotrauma
TryAddSubObjective(ref followTargetObjective,
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true)
{
AllowGoingOutside = true,
IgnoreIfTargetDead = true
},
onAbandon: () =>
{
Mode = CombatMode.Retreat;
Mode = CombatMode.Defensive;
SteeringManager.Reset();
RemoveSubObjective(ref followTargetObjective);
});
if (followTargetObjective != null && subObjectives.Contains(followTargetObjective))
if (followTargetObjective != null)
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 300 :
WeaponComponent is RangedWeapon ? 1000 :
WeaponComponent is MeleeWeapon mw ? mw.Range :
WeaponComponent is RepairTool rt ? rt.Range : 50;
}
@@ -377,29 +477,40 @@ namespace Barotrauma
targetItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
checkInventory = false
},
onCompleted: () => RemoveSubObjective(ref seekAmmunition),
onAbandon: () =>
{
Weapon = null;
Mode = CombatMode.Retreat;
SteeringManager.Reset();
RemoveSubObjective(ref seekAmmunition);
ignoredWeapons.Add(Weapon);
Weapon = null;
});
}
/// <summary>
/// Reloads the ammunition found in the inventory.
/// If seekAmmo is true and the condition is met or not provided, tries to get find the ammo elsewhere.
/// If seekAmmo is true, tries to get find the ammo elsewhere.
/// </summary>
private bool Reload(bool seekAmmo, Func<bool> condition = null)
private bool Reload(bool seekAmmo)
{
if (WeaponComponent == null) { return false; }
if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; }
var containedItems = Weapon.ContainedItems;
// Drop empty ammo
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0)
{
containedItem.Drop(character);
}
}
RelatedItem item = null;
Item ammunition = null;
string[] ammunitionIdentifiers = null;
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
ammunition = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
ammunition = containedItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
if (ammunition != null)
{
// Ammunition still remaining
@@ -411,20 +522,25 @@ namespace Barotrauma
// No ammo
if (ammunition == null)
{
var container = Weapon.GetComponent<ItemContainer>();
// Try reload ammunition in inventory
foreach (string identifier in ammunitionIdentifiers)
if (ammunitionIdentifiers != null)
{
foreach (var i in character.Inventory.Items)
// Try reload ammunition from inventory
ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0, true);
if (ammunition != null)
{
if (i == null) { continue; }
if (i.Prefab.Identifier == identifier || i.HasTag(identifier))
var container = Weapon.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
{
if (i.Condition > 0)
character.Inventory.RemoveItem(ammunition);
if (!container.Inventory.TryPutItem(ammunition, null))
{
container.Inventory.TryPutItem(ammunition, null);
ammunition.Drop(character);
}
}
else
{
container.Combine(ammunition, character);
}
}
}
}
@@ -432,12 +548,9 @@ namespace Barotrauma
{
return true;
}
else if (ammunition == null)
else if (ammunition == null && !HoldPosition && initialMode == CombatMode.Offensive && seekAmmo && ammunitionIdentifiers != null)
{
if (seekAmmo && ammunitionIdentifiers != null && (condition == null || condition()))
{
SeekAmmunition(ammunitionIdentifiers);
}
SeekAmmunition(ammunitionIdentifiers);
}
return false;
}
@@ -495,7 +608,8 @@ namespace Barotrauma
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall;
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories);
if (pickedBody != null)
{
@@ -512,7 +626,16 @@ namespace Barotrauma
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
aimTimer = Rand.Range(0.25f, 0.5f);
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
reloadTime = rangedWeapon.Reload;
}
if (WeaponComponent is MeleeWeapon mw)
{
reloadTime = mw.Reload;
}
aimTimer = reloadTime * Rand.Range(1f, 1.5f);
}
}
}
@@ -19,17 +19,28 @@ namespace Barotrauma
//can either be a tag or an identifier
public readonly string[] itemIdentifiers;
public readonly ItemContainer container;
public readonly Item item;
private AIObjectiveGetItem getItemObjective;
private AIObjectiveGoTo goToObjective;
private readonly HashSet<Item> containedItems = new HashSet<Item>();
public bool AllowToFindDivingGear { get; set; } = true;
public float ConditionLevel { get; set; }
public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.container = container;
this.item = item;
}
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { }
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base (character, objectiveManager, priorityModifier)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
@@ -40,17 +51,25 @@ namespace Barotrauma
this.container = container;
}
public override bool IsCompleted()
protected override bool Check()
{
int containedItemCount = 0;
foreach (Item item in container.Inventory.Items)
if (IsCompleted) { return true; }
if (item != null)
{
if (item != null && itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id)))
{
containedItemCount++;
}
return container.Inventory.Items.Contains(item);
}
else
{
int containedItemCount = 0;
foreach (Item i in container.Inventory.Items)
{
if (i != null && itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)))
{
containedItemCount++;
}
}
return containedItemCount >= targetItemCount;
}
return containedItemCount >= targetItemCount;
}
public override float GetPriority()
@@ -62,20 +81,58 @@ namespace Barotrauma
return 1.0f;
}
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage > ConditionLevel;
protected override void Act(float deltaTime)
{
//get the item that should be contained
Item itemToContain = null;
foreach (string identifier in itemIdentifiers)
Item itemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
if (itemToContain != null)
{
itemToContain = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier);
if (itemToContain != null && itemToContain.Condition > 0.0f) { break; }
}
if (itemToContain == null)
{
if (getItemObjective != null)
// Contain the item
if (itemToContain.ParentInventory == character.Inventory)
{
if (getItemObjective.IsCompleted())
character.Inventory.RemoveItem(itemToContain);
if (!container.Inventory.TryPutItem(itemToContain, null))
{
itemToContain.Drop(character);
Abandon = true;
}
}
else
{
if (character.CanInteractWith(container.Item, out _, checkLinked: false))
{
if (container.Combine(itemToContain, character))
{
IsCompleted = true;
}
else
{
Abandon = true;
}
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
}
else
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: false, checkInventory: checkInventory)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems,
AllowToFindDivingGear = this.AllowToFindDivingGear
}, onAbandon: () =>
{
Abandon = true;
}, onCompleted: () =>
{
if (getItemObjective.TargetItem != null)
{
@@ -83,55 +140,18 @@ namespace Barotrauma
}
else
{
// Reduce the target item count to prevent getting stuck here, if the target item for some reason is null, which shouldn't happen.
targetItemCount--;
if (container.Inventory.FindItem(i => CheckItem(i), recursive: false) != null)
{
IsCompleted = true;
}
else
{
Abandon = true;
}
}
getItemObjective = null;
}
else if (!getItemObjective.CanBeCompleted)
{
getItemObjective = null;
targetItemCount--;
}
}
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, checkInventory: checkInventory)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems
RemoveSubObjective(ref getItemObjective);
});
return;
}
if (container.Item.ParentInventory == character.Inventory)
{
character.Inventory.RemoveItem(itemToContain);
container.Inventory.TryPutItem(itemToContain, null);
}
else
{
if (!character.CanInteractWith(container.Item, out _, checkLinked: false))
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager));
return;
}
container.Combine(itemToContain, character);
}
}
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveContainItem objective)) { return false; }
if (objective.container != container) { return false; }
if (objective.itemIdentifiers.Length != itemIdentifiers.Length) { return false; }
for (int i = 0; i < itemIdentifiers.Length; i++)
{
if (objective.itemIdentifiers[i] != itemIdentifiers[i])
{
return false;
}
}
return true;
}
}
}
}
@@ -1,6 +1,7 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
@@ -12,24 +13,25 @@ namespace Barotrauma
//can either be a tag or an identifier
private readonly string[] itemIdentifiers;
private readonly ItemContainer container;
private readonly ItemContainer sourceContainer;
private ItemContainer targetContainer;
private readonly Item targetItem;
private AIObjectiveGoTo goToObjective;
private bool isCompleted;
private AIObjectiveContainItem containObjective;
public AIObjectiveDecontainItem(Character character, Item targetItem, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveDecontainItem(Character character, Item targetItem, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.targetItem = targetItem;
this.container = container;
this.sourceContainer = sourceContainer;
this.targetContainer = targetContainer;
}
public AIObjectiveDecontainItem(Character character, string itemIdentifier, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, sourceContainer, objectiveManager, targetContainer, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
@@ -37,10 +39,11 @@ namespace Barotrauma
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.container = container;
this.sourceContainer = sourceContainer;
this.targetContainer = targetContainer;
}
public override bool IsCompleted() => isCompleted;
protected override bool Check() => IsCompleted;
public override float GetPriority()
{
@@ -53,58 +56,50 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (isCompleted) { return; }
Item itemToDecontain = null;
//get the item that should be de-contained
if (targetItem == null)
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)), recursive: false);
if (itemToDecontain == null)
{
if (itemIdentifiers != null)
Abandon = true;
return;
}
if (targetContainer == null)
{
if (itemToDecontain.Container != sourceContainer.Item)
{
foreach (string identifier in itemIdentifiers)
{
itemToDecontain = container.Inventory.FindItemByIdentifier(identifier) ?? container.Inventory.FindItemByTag(identifier);
if (itemToDecontain != null) { break; }
}
IsCompleted = true;
return;
}
}
else
{
itemToDecontain = targetItem;
}
if (itemToDecontain == null || itemToDecontain.Container != container.Item) // Item not found or already de-contained, consider complete
{
isCompleted = true;
return;
}
if (itemToDecontain.OwnInventory != character.Inventory && itemToDecontain.ParentInventory != character.Inventory)
{
if (!character.CanInteractWith(container.Item, out _, checkLinked: false))
if (targetContainer.Inventory.Items.Contains(itemToDecontain))
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager));
IsCompleted = true;
return;
}
}
itemToDecontain.Drop(character);
isCompleted = true;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveDecontainItem decontainItem)) { return false; }
if (decontainItem.itemIdentifiers != null && itemIdentifiers != null)
if (goToObjective == null && !itemToDecontain.IsOwnedBy(character))
{
if (decontainItem.itemIdentifiers.Length != itemIdentifiers.Length) { return false; }
for (int i = 0; i < decontainItem.itemIdentifiers.Length; i++)
if (!character.CanInteractWith(sourceContainer.Item, out _, checkLinked: false))
{
if (decontainItem.itemIdentifiers[i] != itemIdentifiers[i]) { return false; }
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager),
onAbandon: () => Abandon = true);
return;
}
return true;
}
else if (decontainItem.itemIdentifiers == null && itemIdentifiers == null)
if (targetContainer != null)
{
return decontainItem.targetItem == targetItem;
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager) { GetItemPriority = this.GetItemPriority },
onCompleted: () => IsCompleted = true,
onAbandon: () => targetContainer = null);
}
else
{
itemToDecontain.Drop(character);
IsCompleted = true;
}
return false;
}
}
}
@@ -12,6 +12,7 @@ namespace Barotrauma
public override string DebugTag => "extinguish fire";
public override bool ForceRun => true;
public override bool ConcurrentObjectives => true;
public override bool KeepDivingGearOn => true;
private readonly Hull targetHull;
@@ -27,19 +28,23 @@ namespace Barotrauma
public override float GetPriority()
{
if (Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c))) { return 0; }
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 10000, dist));
if (!objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>()
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; }
float yDist = Math.Abs(character.WorldPosition.Y - 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)
{
distanceFactor = 1;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + severityFactor * distanceFactor, 0, 1));
}
public override bool IsCompleted() => targetHull.FireSources.None();
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveExtinguishFire otherExtinguishFire && otherExtinguishFire.targetHull == targetHull;
protected override bool Check() => targetHull.FireSources.None();
protected override void Act(float deltaTime)
{
@@ -58,9 +63,9 @@ namespace Barotrauma
if (extinguisher == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
#endif
abandon = true;
Abandon = true;
return;
}
foreach (FireSource fs in targetHull.FireSources)
@@ -119,7 +124,9 @@ namespace Barotrauma
if (move)
{
//go to the first firesource
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager));
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective));
}
break;
}
@@ -9,6 +9,7 @@ namespace Barotrauma
{
public override string DebugTag => "extinguish fires";
public override bool ForceRun => true;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
@@ -18,7 +19,6 @@ namespace Barotrauma
public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X);
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveExtinguishFires;
protected override IEnumerable<Hull> GetList() => Hull.hullList;
protected override AIObjective ObjectiveConstructor(Hull target)
@@ -11,12 +11,11 @@ namespace Barotrauma
{
public override string DebugTag => "fight intruders";
protected override float IgnoreListClearInterval => 30;
public virtual bool IgnoreUnsafeHulls => true;
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFightIntruders;
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
@@ -1,53 +1,56 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveFindDivingGear : AIObjective
{
public override string DebugTag => "find diving gear";
public override string DebugTag => $"find diving gear ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
private readonly string gearTag;
private readonly string fallbackTag;
private AIObjectiveGetItem getDivingGear;
private AIObjectiveContainItem getOxygen;
public override bool IsCompleted()
{
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
if (character.Inventory.SlotTypes[i] == InvSlotType.Any || character.Inventory.Items[i] == null) { continue; }
if (character.Inventory.Items[i].HasTag(gearTag))
{
var containedItems = character.Inventory.Items[i].ContainedItems;
if (containedItems == null) { continue; }
return containedItems.Any(it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f);
}
}
return false;
}
public static float lowOxygenThreshold = 10;
public override float GetPriority() => MathHelper.Clamp(100 - character.OxygenAvailable, 0, 100);
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFindDivingGear;
protected override bool Check() => HumanAIController.HasItem(character, gearTag, "oxygensource") || HumanAIController.HasItem(character, fallbackTag, "oxygensource");
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTag = needDivingSuit ? "divingsuit" : "diving";
gearTag = needDivingSuit ? "divingsuit" : "divingmask";
fallbackTag = needDivingSuit ? "divingsuit" : "diving";
}
protected override void Act(float deltaTime)
{
var item = character.Inventory.FindItemByTag(gearTag);
if (character.LockHands)
{
Abandon = true;
return;
}
var item = character.Inventory.FindItemByIdentifier(gearTag, true) ?? character.Inventory.FindItemByTag(gearTag, true);
if (item == null && fallbackTag != gearTag)
{
item = character.Inventory.FindItemByTag(fallbackTag, true);
}
if (item == null || !character.HasEquippedItem(item))
{
TryAddSubObjective(ref getDivingGear, () =>
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true);
});
if (item == null)
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
}
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { AllowToFindDivingGear = false };
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getDivingGear));
}
else
{
@@ -55,9 +58,9 @@ namespace Barotrauma
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory");
#endif
abandon = true;
Abandon = true;
return;
}
// Drop empty tanks
@@ -69,13 +72,54 @@ namespace Barotrauma
containedItem.Drop(character);
}
}
if (containedItems.None(it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f))
if (containedItems.None(it => it.HasTag("oxygensource") && it.Condition > lowOxygenThreshold))
{
TryAddSubObjective(ref getOxygen, () =>
var oxygenTank = character.Inventory.FindItemByTag("oxygensource", true);
if (oxygenTank != null)
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
return new AIObjectiveContainItem(character, new string[] { "oxygentank", "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager);
});
var container = item.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
{
character.Inventory.RemoveItem(oxygenTank);
if (!container.Inventory.TryPutItem(oxygenTank, null))
{
oxygenTank.Drop(character);
Abandon = true;
}
}
else
{
container.Combine(oxygenTank, character);
}
}
else
{
// Seek oxygen that has min 10% condition left
TryAddSubObjective(ref getOxygen, () =>
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
{
AllowToFindDivingGear = false,
ConditionLevel = lowOxygenThreshold
};
},
onAbandon: () =>
{
// Try to seek any oxygen sources
TryAddSubObjective(ref getOxygen, () =>
{
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
{
AllowToFindDivingGear = false,
ConditionLevel = 0
};
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getOxygen));
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
}
}
}
}
@@ -9,17 +9,18 @@ namespace Barotrauma
{
public override string DebugTag => "find safety";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool ConcurrentObjectives => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
// TODO: expose?
const float priorityIncrease = 100;
const float priorityDecrease = 10;
const float SearchHullInterval = 3.0f;
const float clearUnreachableInterval = 30;
public readonly HashSet<Hull> unreachable = new HashSet<Hull>();
private float currenthullSafety;
private float unreachableClearTimer;
private float searchHullTimer;
private AIObjectiveGoTo goToObjective;
@@ -27,21 +28,18 @@ namespace Barotrauma
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
public override bool IsCompleted() => false;
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFindSafety;
private bool resetPriority;
public override void Update(float deltaTime)
{
if (unreachableClearTimer > 0)
if (resetPriority)
{
unreachableClearTimer -= deltaTime;
}
else
{
unreachableClearTimer = clearUnreachableInterval;
unreachable.Clear();
Priority = 0;
resetPriority = false;
return;
}
if (character.CurrentHull == null)
{
@@ -49,7 +47,10 @@ namespace Barotrauma
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100;
return;
}
if (character.OxygenAvailable < CharacterHealth.LowOxygenThreshold) { Priority = 100; }
if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
{
Priority = 100;
}
currenthullSafety = HumanAIController.CurrentHullSafety;
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
@@ -61,7 +62,7 @@ namespace Barotrauma
Priority += dangerFactor * priorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted() && divingGearObjective.CanBeCompleted)
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
// Boost the priority while seeking the diving gear
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.OrderPriority + 20, 100));
@@ -72,32 +73,36 @@ namespace Barotrauma
private Hull previousSafeHull;
protected override void Act(float deltaTime)
{
var currentHull = character.AnimController.CurrentHull;
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull);
bool needsDivingSuit = needsDivingGear && (currentHull == null || currentHull.WaterPercentage > 90);
var currentHull = character.CurrentHull;
bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit);
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character);
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingMask(character);
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment)
if (needsEquipment && divingGearObjective == null && !character.LockHands)
{
TryAddSubObjective(ref divingGearObjective,
() => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => searchHullTimer = Math.Min(1, searchHullTimer));
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref divingGearObjective,
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () =>
{
searchHullTimer = Math.Min(1, searchHullTimer);
// Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
},
onCompleted: () =>
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
RemoveSubObjective(ref divingGearObjective);
});
}
else
else if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
{
if (divingGearObjective != null && divingGearObjective.IsCompleted())
{
// Reset the devotion.
Priority = 0;
divingGearObjective = null;
}
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
searchHullTimer = Math.Min(1, searchHullTimer);
@@ -108,7 +113,7 @@ namespace Barotrauma
}
else
{
searchHullTimer = SearchHullInterval;
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = FindBestHull();
if (currentSafeHull == null)
@@ -119,70 +124,80 @@ namespace Barotrauma
{
if (goToObjective?.Target != currentSafeHull)
{
goToObjective = null;
RemoveSubObjective(ref goToObjective);
}
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character)
},
onAbandon: () => unreachable.Add(goToObjective.Target as Hull));
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
},
onCompleted: () =>
{
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
}
RemoveSubObjective(ref goToObjective);
// If diving gear objective failed, let's reset it here.
RemoveSubObjective(ref divingGearObjective);
},
onAbandon: () =>
{
if (currentHull != null)
{
HumanAIController.UnreachableHulls.Add(goToObjective.Target as Hull);
}
RemoveSubObjective(ref goToObjective);
});
}
else
{
goToObjective = null;
RemoveSubObjective(ref goToObjective);
}
}
if (goToObjective != null)
if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
if (currentHull != null)
{
if (goToObjective.IsCompleted())
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero;
// TODO: optimize
foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources))
{
objectiveManager.GetObjective<AIObjectiveIdle>()?.Wander(deltaTime);
}
Priority = 0;
return;
}
if (currentHull == null) { return; }
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero;
// TODO: optimize
foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources))
{
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
foreach (Character enemy in Character.CharacterList)
{
if (enemy.IsDead || enemy.IsUnconscious || enemy.Removed || HumanAIController.IsFriendly(enemy)) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
if (escapeVel != Vector2.Zero)
{
float left = currentHull.Rect.X + 50;
float right = currentHull.Rect.Right - 50;
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
foreach (Character enemy in Character.CharacterList)
{
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
if (HumanAIController.IsFriendly(enemy) || !HumanAIController.IsActive(enemy)) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
else
if (escapeVel != Vector2.Zero)
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
float left = currentHull.Rect.X + 50;
float right = currentHull.Rect.Right - 50;
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
{
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
}
return;
}
}
else
{
Priority = 0;
objectiveManager.GetObjective<AIObjectiveIdle>()?.Wander(deltaTime);
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
@@ -195,24 +210,28 @@ namespace Barotrauma
if (hull.Submarine == null) { continue; }
if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (unreachable.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
float hullSafety = 0;
if (character.CurrentHull != null && character.Submarine != null)
{
// Inside
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
hullSafety = HumanAIController.GetHullSafety(hull, character);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y) * 2.0f;
hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
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)
if (hullSafety < bestValue) { continue; }
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
if (path.Unreachable)
// Don't allow to go outside if not already outside.
var path = character.CurrentHull != null ?
PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null) :
PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
if (path.Unreachable && character.CurrentHull != null)
{
unreachable.Add(hull);
HumanAIController.UnreachableHulls.Add(hull);
continue;
}
// Each unsafe node reduces the hull safety value.
@@ -11,10 +11,10 @@ namespace Barotrauma
{
public override string DebugTag => "fix leak";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public Gap Leak { get; private set; }
private AIObjectiveFindDivingGear findDivingGear;
private AIObjectiveGetItem getWeldingTool;
private AIObjectiveContainItem refuelObjective;
private AIObjectiveGoTo gotoObjective;
@@ -25,43 +25,30 @@ namespace Barotrauma
Leak = leak;
}
public override bool IsCompleted()
{
return Leak.Open <= 0.0f || Leak.Removed;
}
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
public override float GetPriority()
{
if (Leak.Open == 0.0f) { return 0.0f; }
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 10000, dist));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak);
if (Leak.Removed || Leak.Open <= 0) { return 0; }
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + severity * distanceFactor * PriorityModifier, 0, 1));
}
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveFixLeak fixLeak)) { return false; }
return fixLeak.Leak == Leak;
}
protected override void Act(float deltaTime)
{
if (!Leak.IsRoomToRoom)
{
if (!HumanAIController.HasDivingSuit(character))
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, true, objectiveManager));
return;
}
}
var weldingTool = character.Inventory.FindItemByTag("weldingtool");
var weldingTool = character.Inventory.FindItemByTag("weldingtool", true);
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingtool", objectiveManager, true));
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingtool", objectiveManager, true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getWeldingTool));
return;
}
else
@@ -70,9 +57,9 @@ namespace Barotrauma
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
#endif
abandon = true;
Abandon = true;
return;
}
// Drop empty tanks
@@ -84,9 +71,11 @@ namespace Barotrauma
containedItem.Drop(character);
}
}
if (containedItems.None(i => i.HasTag("weldingfueltank") && i.Condition > 0.0f))
if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfueltank", weldingTool.GetComponent<ItemContainer>(), objectiveManager));
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref refuelObjective));
return;
}
}
@@ -95,30 +84,55 @@ namespace Barotrauma
if (repairTool == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool");
#endif
abandon = true;
Abandon = true;
return;
}
Vector2 gapDiff = Leak.WorldPosition - character.WorldPosition;
Vector2 toLeak = Leak.WorldPosition - character.WorldPosition;
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(gapDiff.X) < 100 && gapDiff.Y < 0.0f && gapDiff.Y > -150)
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
{
HumanAIController.AnimController.Crouching = true;
}
// Use a greater reach, because the distance is calculated from the character to the leak, not from the item to the leak.
float reach = repairTool.Range + ((HumanoidAnimController)character.AnimController).ArmLength;
bool canOperate = gapDiff.LengthSquared() < reach * reach;
float reach = repairTool.Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
bool canOperate = toLeak.LengthSquared() < reach * reach;
if (canOperate)
{
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak));
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
if (Check()) { IsCompleted = true; }
else
{
// Failed to operate. Probably too far.
Abandon = true;
}
});
}
else
{
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager)
{
AllowGoingOutside = objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>(),
CloseEnough = reach
});
},
onAbandon: () =>
{
if (Check()) { IsCompleted = true; }
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > reach * reach * 2)
{
// Too far
Abandon = true;
}
else
{
// We are close, try again.
RemoveSubObjective(ref gotoObjective);
}
},
onCompleted: () => RemoveSubObjective(ref gotoObjective));
}
}
}
@@ -10,6 +10,8 @@ namespace Barotrauma
{
public override string DebugTag => "fix leaks";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
@@ -18,7 +20,7 @@ namespace Barotrauma
public static float GetLeakSeverity(Gap leak)
{
if (leak == null) { return 0; }
float sizeFactor = MathHelper.Lerp(1, 10, MathUtils.InverseLerp(0, 200, (leak.IsHorizontal ? leak.Rect.Width : leak.Rect.Height)));
float sizeFactor = MathHelper.Lerp(1, 10, MathUtils.InverseLerp(0, 200, leak.Size));
float severity = sizeFactor * leak.Open;
if (!leak.IsRoomToRoom)
{
@@ -32,7 +34,6 @@ namespace Barotrauma
}
}
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFixLeaks;
protected override float TargetEvaluation() => Targets.Max(t => GetLeakSeverity(t));
protected override IEnumerable<Gap> GetList() => Gap.GapList;
protected override AIObjective ObjectiveConstructor(Gap gap)
@@ -26,6 +26,8 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
public bool AllowToFindDivingGear { get; set; } = true;
public override float GetPriority()
{
if (objectiveManager.CurrentOrder == this)
@@ -35,7 +37,7 @@ namespace Barotrauma
return 1.0f;
}
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = false, float priorityModifier = 1)
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
@@ -43,10 +45,10 @@ namespace Barotrauma
this.targetItem = targetItem;
}
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = false, bool checkInventory = true, float priorityModifier = 1)
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, equip, checkInventory, priorityModifier) { }
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = false, bool checkInventory = true, float priorityModifier = 1)
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
@@ -65,32 +67,11 @@ namespace Barotrauma
private void CheckInventory()
{
if (itemIdentifiers == null) { return; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
var item = character.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.Condition > 0, recursive: true);
if (item != null)
{
if (character.Inventory.Items[i] == null || character.Inventory.Items[i].Condition <= 0.0f) { continue; }
if (itemIdentifiers.Any(id => character.Inventory.Items[i].Prefab.Identifier == id || character.Inventory.Items[i].HasTag(id)))
{
targetItem = character.Inventory.Items[i];
moveToTarget = targetItem;
currItemPriority = 100.0f;
break;
}
//check items inside items (tool inside a toolbox etc)
var containedItems = character.Inventory.Items[i].ContainedItems;
if (containedItems != null)
{
foreach (Item containedItem in containedItems)
{
if (containedItem == null || containedItem.Condition <= 0.0f) { continue; }
if (itemIdentifiers.Any(id => containedItem.Prefab.Identifier == id || containedItem.HasTag(id)))
{
targetItem = containedItem;
moveToTarget = character.Inventory.Items[i];
currItemPriority = 100.0f;
break;
}
}
}
targetItem = item;
moveToTarget = item.GetRootContainer() ?? item;
}
}
@@ -98,58 +79,91 @@ namespace Barotrauma
{
if (character.LockHands)
{
abandon = true;
Abandon = true;
return;
}
FindTargetItem();
if (targetItem == null || moveToTarget == null)
if (targetItem == null)
{
objectiveManager.GetObjective<AIObjectiveIdle>()?.Wander(deltaTime);
return;
FindTargetItem();
if (targetItem == null || moveToTarget == null)
{
if (targetItem != null && moveToTarget == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: Move to target is null!");
#endif
Abandon = true;
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
return;
}
}
if (character.IsItemTakenBySomeoneElse(targetItem))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's already equipped by someone else. Aborting.", Color.Yellow);
#endif
Abandon = true;
}
if (character.CanInteractWith(targetItem, out _, checkLinked: false))
{
if (IsTakenBySomeone(targetItem))
var pickable = targetItem.GetComponent<Pickable>();
if (pickable == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's equipped by someone else. Aborting.", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Target not pickable. Aborting.", Color.Yellow);
#endif
abandon = true;
Abandon = true;
return;
}
targetItem.TryInteract(character, forceSelectKey: true);
if (equip)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; }
targetSlot = i;
//slot free, continue
var otherItem = character.Inventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (character.Inventory.TryPutItem(otherItem, character, new List<InvSlotType>() { InvSlotType.Any })) { continue; }
//if everything else fails, simply drop the existing item
otherItem.Drop(character);
}
}
if (character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character))
{
IsCompleted = true;
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
Abandon = true;
}
}
else
{
int targetSlot = -1;
if (equip)
targetItem.ParentInventory.RemoveItem(targetItem);
if (character.Inventory.TryPutItem(targetItem, null, new List<InvSlotType>() { InvSlotType.Any }))
{
var pickable = targetItem.GetComponent<Pickable>();
if (pickable == null)
{
abandon = true;
return;
}
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; }
targetSlot = i;
//slot free, continue
if (character.Inventory.Items[i] == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List<InvSlotType>() { InvSlotType.Any })) { continue; }
//if everything else fails, simply drop the existing item
character.Inventory.Items[i].Drop(character);
}
}
IsCompleted = true;
}
targetItem.TryInteract(character, false, true);
if (targetSlot > -1 && !character.HasEquippedItem(targetItem))
else
{
character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character);
Abandon = true;
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
targetItem.Drop(character);
}
}
}
@@ -158,17 +172,16 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
//check if we're already looking for a diving gear
bool gettingDivingGear = (targetItem != null && targetItem.Prefab.Identifier == "divingsuit" || targetItem.HasTag("diving")) ||
(itemIdentifiers != null && (itemIdentifiers.Contains("diving") || itemIdentifiers.Contains("divingsuit")));
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: !gettingDivingGear);
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear);
},
onAbandon: () =>
{
targetItem = null;
moveToTarget = null;
ignoredItems.Add(targetItem);
});
RemoveSubObjective(ref goToObjective);
},
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
@@ -182,9 +195,9 @@ namespace Barotrauma
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item is was defined.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item was defined.", Color.Red);
#endif
abandon = true;
Abandon = true;
}
return;
}
@@ -194,26 +207,29 @@ namespace Barotrauma
var item = Item.ItemList[currSearchIndex];
if (ignoredItems.Contains(item)) { continue; }
if (item.Submarine == null) { continue; }
else if (item.Submarine.TeamID != character.TeamID) { continue; }
else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (item.CurrentHull == null || item.Condition <= 0.0f) { continue; }
if (itemIdentifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
if (IsTakenBySomeone(item)) { continue; }
float itemPriority = 0.0f;
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = 1;
if (GetItemPriority != null)
{
//ignore if the item has zero priority
itemPriority = GetItemPriority(item);
if (itemPriority <= 0.0f) { continue; }
}
Item rootContainer = item.GetRootContainer();
itemPriority -= Vector2.Distance((rootContainer ?? item).Position, character.Position) * 0.01f;
Vector2 itemPos = (rootContainer ?? 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 distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist));
itemPriority *= distanceFactor;
//ignore if the item has a lower priority than the currently selected one
if (moveToTarget != null && itemPriority < currItemPriority) { continue; }
if (itemPriority < currItemPriority) { continue; }
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootContainer ?? item;
@@ -222,82 +238,29 @@ namespace Barotrauma
if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
#endif
abandon = true;
Abandon = true;
}
}
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveGetItem getItem)) { return false; }
if (getItem.equip != equip) { return false; }
if (getItem.itemIdentifiers != null && itemIdentifiers != null)
{
if (getItem.itemIdentifiers.Length != itemIdentifiers.Length) { return false; }
for (int i = 0; i < getItem.itemIdentifiers.Length; i++)
{
if (getItem.itemIdentifiers[i] != itemIdentifiers[i]) { return false; }
}
return true;
}
else if (getItem.itemIdentifiers == null && itemIdentifiers == null)
{
return getItem.targetItem == targetItem;
}
return false;
}
public override bool IsCompleted()
protected override bool Check()
{
if (IsCompleted) { return true; }
if (targetItem != null)
{
return HasItem(targetItem);
return character.HasItem(targetItem, equip);
}
else if (itemIdentifiers != null)
{
foreach (string itemName in itemIdentifiers)
var matchingItem = character.Inventory.FindItem(i => !ignoredItems.Contains(i) && itemIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)), recursive: true);
if (matchingItem != null)
{
var matchingItem = character.Inventory.FindItemByTag(itemName) ?? character.Inventory.FindItemByIdentifier(itemName);
if (matchingItem != null && (!equip || character.HasEquippedItem(matchingItem)))
{
return true;
}
return !equip || character.HasEquippedItem(matchingItem);
}
return false;
}
return false;
}
private bool HasItem(Item item)
{
bool isEquipped = !equip || character.HasEquippedItem(item);
if (character.Inventory.Items.Contains(item) && isEquipped) { return true; }
if (!equip)
{
Item rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory)
{
return rootContainer.ParentInventory.Owner == character;
}
}
return false;
}
private bool IsTakenBySomeone(Item item)
{
//if the item is inside a character's inventory, don't steal it unless the character is dead
if (item.ParentInventory is CharacterInventory)
{
if (item.ParentInventory.Owner is Character owner && owner != character && !owner.IsDead) { return true; }
}
//if the item is inside an item, which is inside a character's inventory, don't steal it unless the character is dead
Item rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory)
{
if (rootContainer.ParentInventory.Owner is Character owner && owner != character && !owner.IsDead) { return true; }
}
return false;
}
}
}
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
@@ -9,7 +10,7 @@ namespace Barotrauma
public override string DebugTag => "go to";
private AIObjectiveFindDivingGear findDivingGear;
private bool repeat;
private readonly bool repeat;
//how long until the path to the target is declared unreachable
private float waitUntilPathUnreachable;
private bool getDivingGearIfNeeded;
@@ -21,13 +22,23 @@ namespace Barotrauma
public bool followControlledCharacter;
public bool mimic;
private float _closeEnough = 50;
/// <summary>
/// Display units
/// </summary>
public float CloseEnough { get; set; } = 50;
public float CloseEnough
{
get { return _closeEnough; }
set
{
_closeEnough = Math.Max(_closeEnough, value);
}
}
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public ISpatialEntity Target { get; private set; }
public override float GetPriority()
@@ -42,14 +53,18 @@ namespace Barotrauma
return 1.0f;
}
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1)
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
: base (character, objectiveManager, priorityModifier)
{
this.Target = target;
this.repeat = repeat;
waitUntilPathUnreachable = 3.0f;
this.getDivingGearIfNeeded = getDivingGearIfNeeded;
CalculateCloseEnough();
CloseEnough = closeEnough;
if (Target is Item i)
{
CloseEnough = Math.Max(CloseEnough, i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2);
}
}
protected override void Act(float deltaTime)
@@ -58,17 +73,17 @@ namespace Barotrauma
{
if (Character.Controlled == null)
{
abandon = true;
Abandon = true;
return;
}
Target = Character.Controlled;
}
if (Target == character)
{
// Wait
character.AIController.SteeringManager.Reset();
abandon = true;
return;
}
}
waitUntilPathUnreachable -= deltaTime;
if (!character.IsClimbing)
{
@@ -78,24 +93,37 @@ namespace Barotrauma
{
if (e.Removed)
{
abandon = true;
Abandon = true;
}
else
{
character.AIController.SelectTarget(e.AiTarget);
}
}
bool isInside = character.CurrentHull != null;
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
var targetHull = Target is Hull h ? h : Target is Item i ? i.CurrentHull : Target is Character c ? c.CurrentHull : character.CurrentHull;
if (!followControlledCharacter)
{
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
bool containsUnsafeNodes = HumanAIController.CurrentOrder == null && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
&& PathSteering != null && PathSteering.CurrentPath != null
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
{
Abandon = true;
SteeringManager.Reset();
return;
}
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
bool isInside = character.CurrentHull != null;
bool targetIsOutside = (Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (isInside && targetIsOutside && !AllowGoingOutside)
{
abandon = true;
Abandon = true;
}
else if (waitUntilPathUnreachable < 0)
{
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable)
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty)
{
if (repeat)
{
@@ -103,139 +131,147 @@ namespace Barotrauma
}
else
{
abandon = true;
Abandon = true;
}
}
}
if (abandon)
if (Abandon)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null)
if (objectiveManager.CurrentOrder != null && objectiveManager.CurrentOrder.ReportFailures)
{
character.Speak(TextManager.Get("DialogCannotReach"), identifier: "cannotreach", minDurationBetweenSimilar: 10.0f);
}
character.AIController.SteeringManager.Reset();
SteeringManager.Reset();
}
else
{
Vector2 currTargetSimPos = Vector2.Zero;
currTargetSimPos = Target.SimPosition;
// Take the sub position into account in the sim pos
if (SteeringManager != PathSteering && character.Submarine == null && Target.Submarine != null)
{
currTargetSimPos += Target.Submarine.SimPosition;
}
else if (character.Submarine != null && Target.Submarine == null)
{
currTargetSimPos -= character.Submarine.SimPosition;
}
else if (character.Submarine != Target.Submarine)
{
if (character.Submarine != null && Target.Submarine != null)
{
Vector2 diff = character.Submarine.SimPosition - Target.Submarine.SimPosition;
currTargetSimPos -= diff;
}
}
if (PathSteering != null)
{
PathSteering.startNodeFilter = startNodeFilter;
PathSteering.endNodeFilter = endNodeFilter;
}
SteeringManager.SteeringSeek(currTargetSimPos);
if (SteeringManager != PathSteering)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 1, heading: VectorExtensions.Forward(character.AnimController.Collider.Rotation));
}
if (getDivingGearIfNeeded)
if (getDivingGearIfNeeded && !character.LockHands)
{
Character followTarget = Target as Character;
bool needsDivingGear = HumanAIController.NeedsDivingGear(targetHull) || mimic && HumanAIController.HasDivingMask(followTarget);
bool needsDivingSuit = needsDivingGear && (targetHull == null || targetIsOutside || targetHull.WaterPercentage > 90) || mimic && HumanAIController.HasDivingSuit(followTarget);
bool needsDivingSuit = targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(character, targetHull, out needsDivingSuit);
if (!needsDivingGear && mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
{
needsDivingGear = true;
needsDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget))
{
needsDivingGear = true;
}
}
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character);
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingMask(character);
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager));
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
if (repeat && IsCloseEnough)
{
OnCompleted();
return;
}
if (SteeringManager == PathSteering)
{
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
{
nodeFilter = node => node.Waypoint.CurrentHull != null;
}
PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, startNodeFilter, endNodeFilter, nodeFilter);
}
else
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
}
if (!insideSteering)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 1);
}
}
}
private bool isCompleted;
public override bool IsCompleted()
private bool IsCloseEnough
{
// First check the distance
// Then the custom condition
// And finally check if can interact (heaviest)
if (isCompleted) { return true; }
if (Target == null)
{
abandon = true;
return false;
}
bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
if (repeat)
get
{
bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
if (closeEnough)
{
closeEnough = !(Target is Character) || Target is Character c && c.CurrentHull == character.CurrentHull;
}
if (closeEnough)
{
OnCompleted();
}
return closeEnough;
}
}
protected override bool Check()
{
if (IsCompleted) { return true; }
// First check the distance
// Then the custom condition
// And finally check if can interact (heaviest)
if (Target == null)
{
Abandon = true;
return false;
}
else if (closeEnough)
if (repeat)
{
if (requiredCondition == null || requiredCondition())
return false;
}
else
{
if (IsCloseEnough)
{
if (Target is Item item)
if (requiredCondition == null || requiredCondition())
{
if (character.CanInteractWith(item, out _, checkLinked: false)) { isCompleted = true; }
}
else if (Target is Character targetCharacter)
{
if (character.CanInteractWith(targetCharacter, CloseEnough)) { isCompleted = true; }
}
else
{
isCompleted = true;
if (Target is Item item)
{
if (character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; }
}
else if (Target is Character targetCharacter)
{
if (character.CanInteractWith(targetCharacter, CloseEnough)) { IsCompleted = true; }
}
else
{
IsCompleted = true;
}
}
}
}
return isCompleted;
return IsCompleted;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveGoTo objective)) { return false; }
return objective.Target == Target;
}
private void CalculateCloseEnough()
{
float interactionDistance = Target is Item i ? i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2 : 0;
CloseEnough = Math.Max(interactionDistance, CloseEnough);
}
protected override void OnCompleted()
private void StopMovement()
{
character.AIController.SteeringManager.Reset();
if (Target != null)
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
}
}
protected override void OnCompleted()
{
StopMovement();
HumanAIController.FaceTarget(Target);
base.OnCompleted();
}
}
@@ -11,12 +11,11 @@ namespace Barotrauma
{
public override string DebugTag => "idle";
const float WallAvoidDistance = 150.0f;
private readonly float newTargetIntervalMin = 5;
private readonly float newTargetIntervalMax = 15;
private readonly float standStillMin = 1;
private readonly float newTargetIntervalMin = 10;
private readonly float newTargetIntervalMax = 20;
private readonly float standStillMin = 2;
private readonly float standStillMax = 10;
private readonly float walkDurationMin = 3;
private readonly float walkDurationMin = 5;
private readonly float walkDurationMax = 10;
private Hull currentTarget;
@@ -36,7 +35,7 @@ namespace Barotrauma
walkDuration = Rand.Range(0.0f, 10.0f);
}
public override bool IsCompleted() => false;
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
@@ -93,8 +92,10 @@ namespace Barotrauma
if (currentTargetIsInvalid || currentTarget == null && HumanAIController.VisibleHulls.Any(h => IsForbidden(h)))
{
newTargetTimer = 0;
standStillTimer = 0;
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
//almost constantly when there's a small number of potential hulls to move to
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
//standStillTimer = 0.0f;
}
else if (character.IsClimbing)
{
@@ -102,7 +103,7 @@ namespace Barotrauma
{
newTargetTimer = 0;
}
else
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0)
{
// Don't allow new targets when climbing.
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
@@ -112,7 +113,7 @@ namespace Barotrauma
{
if (currentTarget == null)
{
newTargetTimer = 0;
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
}
}
if (newTargetTimer <= 0.0f)
@@ -127,24 +128,32 @@ namespace Barotrauma
else if (targetHulls.Count > 0)
{
//choose a random available hull
var randomHull = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullOK = !HumanAIController.UnsafeHulls.Contains(character.CurrentHull) && !IsForbidden(character.CurrentHull);
if (isCurrentHullOK)
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullAllowed = !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
// Only do this when the current hull is ok, because otherwise would block all paths from the current hull to the target hull.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, randomHull.SimPosition);
if (path.Unreachable || path.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull) || IsForbidden(n.CurrentHull)))
{
//can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(randomHull);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
return;
}
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; }
return true;
});
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
return;
}
currentTarget = randomHull;
searchingNewHull = false;
}
else
{
// Couldn't find a target for some reason -> reset
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
searchingNewHull = false;
}
@@ -156,7 +165,7 @@ namespace Barotrauma
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
@@ -174,20 +183,6 @@ namespace Barotrauma
if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
(PathSteering.CurrentPath.NextNode == null || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
{
if (!character.AnimController.InWater)
{
standStillTimer -= deltaTime;
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
PathSteering.Reset();
return;
}
if (standStillTimer < -walkDuration)
{
standStillTimer = Rand.Range(standStillMin, standStillMax);
}
}
Wander(deltaTime);
return;
}
@@ -195,64 +190,39 @@ namespace Barotrauma
if (currentTarget != null)
{
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition);
if (SteeringManager == PathSteering)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
else
{
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(currentTarget));
}
}
else
{
Wander(deltaTime);
}
}
public void Wander(float deltaTime)
{
if (character.IsClimbing) { return; }
//steer away from edges of the hull
var currentHull = character.CurrentHull;
if (currentHull != null)
{
float roomWidth = currentHull.Rect.Width;
if (roomWidth < WallAvoidDistance * 4)
{
PathSteering.Reset();
}
else
{
float leftDist = character.Position.X - currentHull.Rect.X;
float rightDist = currentHull.Rect.Right - character.Position.X;
if (leftDist < WallAvoidDistance && rightDist < WallAvoidDistance)
{
if (Math.Abs(rightDist - leftDist) > WallAvoidDistance / 2)
{
PathSteering.SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
}
else
{
PathSteering.Reset();
}
}
else if (leftDist < WallAvoidDistance)
{
float speed = (WallAvoidDistance - leftDist) / WallAvoidDistance;
PathSteering.SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
PathSteering.WanderAngle = 0.0f;
}
else if (rightDist < WallAvoidDistance)
{
float speed = (WallAvoidDistance - rightDist) / WallAvoidDistance;
PathSteering.SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
PathSteering.WanderAngle = MathHelper.Pi;
}
else
{
SteeringManager.SteeringWander();
}
}
}
else
{
SteeringManager.SteeringWander();
}
if (!character.AnimController.InWater)
{
//reset vertical steering to prevent dropping down from platforms etc
character.AIController.SteeringManager.ResetY();
standStillTimer -= deltaTime;
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
PathSteering.Reset();
return;
}
if (standStillTimer < -walkDuration)
{
standStillTimer = Rand.Range(standStillMin, standStillMax);
}
}
PathSteering.Wander(deltaTime);
}
private void FindTargetHulls()
@@ -280,8 +250,10 @@ namespace Barotrauma
targetHulls.Add(hull);
float weight = hull.Volume;
// Prefer rooms that are closer. Avoid rooms that are not in the same level.
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y) * 5.0f;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 2500, dist));
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 = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist));
weight *= distanceFactor;
hullWeights.Add(weight);
}
@@ -295,10 +267,5 @@ namespace Barotrauma
if (hullName == null) { return false; }
return hullName.Contains("ballast") || hullName.Contains("airlock");
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return (otherObjective is AIObjectiveIdle);
}
}
}
@@ -12,7 +12,7 @@ namespace Barotrauma
public Dictionary<T, AIObjective> Objectives { get; private set; } = new Dictionary<T, AIObjective>();
protected HashSet<T> ignoreList = new HashSet<T>();
private float ignoreListTimer;
private float targetUpdateTimer;
protected float targetUpdateTimer;
// By default, doesn't clear the list automatically
protected virtual float IgnoreListClearInterval => 0;
@@ -38,8 +38,11 @@ namespace Barotrauma
: base(character, objectiveManager, priorityModifier, option) { }
protected override void Act(float deltaTime) { }
public override bool IsCompleted() => false;
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowSubObjectiveSorting => true;
public override bool ReportFailures => false;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
@@ -69,18 +72,19 @@ namespace Barotrauma
foreach (var objective in Objectives)
{
var target = objective.Key;
if (!objective.Value.CanBeCompleted)
{
ignoreList.Add(target);
targetUpdateTimer = 0;
}
//if (!objective.Value.CanBeCompleted && !ignoreList.Contains(target))
//{
// // TODO: leaks that cannot be accessed from inside cause FixLeak objective to fail, but for some reason it's not ignored. Make sure that it is.
// ignoreList.Add(target);
// targetUpdateTimer = 0;
//}
if (!Targets.Contains(target))
{
subObjectives.Remove(objective.Value);
}
}
SyncRemovedObjectives(Objectives, GetList());
if (Objectives.None() && Targets.Any())
if (Objectives.None() && Targets.Any(t => !ignoreList.Contains(t)))
{
CreateObjectives();
}
@@ -91,6 +95,7 @@ namespace Barotrauma
public override void Reset()
{
base.Reset();
ignoreList.Clear();
ignoreListTimer = 0;
UpdateTargets();
@@ -98,6 +103,7 @@ namespace Barotrauma
public override float GetPriority()
{
if (character.LockHands) { return 0; }
if (character.Submarine == null) { return 0; }
if (Targets.None()) { return 0; }
// Allow the target value to be more than 100.
@@ -145,15 +151,26 @@ namespace Barotrauma
{
foreach (T target in Targets)
{
if (ignoreList.Contains(target)) { continue; }
if (!Objectives.TryGetValue(target, out AIObjective objective))
{
objective = ObjectiveConstructor(target);
objective.Completed += () => OnObjectiveCompleted(objective, target);
Objectives.Add(target, objective);
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
objective.Completed += () =>
{
Objectives.Remove(target);
OnObjectiveCompleted(objective, target);
};
objective.Abandoned += () =>
{
Objectives.Remove(target);
ignoreList.Add(target);
targetUpdateTimer = 0;
};
}
}
}
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -12,7 +13,7 @@ namespace Barotrauma
public const float OrderPriority = 70;
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 2;
public const float baseDevotion = 3;
public List<AIObjective> Objectives { get; private set; } = new List<AIObjective>();
@@ -35,7 +36,13 @@ namespace Barotrauma
public AIObjective CurrentOrder { get; private set; }
public AIObjective CurrentObjective { get; private set; }
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
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();
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
public AIObjectiveManager(Character character)
{
@@ -43,7 +50,7 @@ namespace Barotrauma
CreateAutonomousObjectives();
}
public void AddObjective(AIObjective objective)
public void AddObjective<T>(T objective) where T : AIObjective
{
if (objective == null)
{
@@ -52,21 +59,32 @@ namespace Barotrauma
#endif
return;
}
var duplicate = Objectives.Find(o => o.IsDuplicate(objective));
if (duplicate != null)
// Can't use the generic type, because it's possible that the user of this method uses the base type AIObjective.
// We need to get the highest type.
var type = objective.GetType();
if (objective.AllowMultipleInstances)
{
duplicate.Reset();
if (Objectives.FirstOrDefault(o => o.GetType() == type) is T existingObjective && existingObjective.IsDuplicate(objective))
{
Objectives.Remove(existingObjective);
}
}
else
{
Objectives.Add(objective);
Objectives.RemoveAll(o => o.GetType() == type);
}
Objectives.Add(objective);
}
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public void CreateAutonomousObjectives()
{
foreach (var delayedObjective in DelayedObjectives)
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
}
DelayedObjectives.Clear();
Objectives.Clear();
AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this));
@@ -82,8 +100,8 @@ namespace Barotrauma
matchingItems.RemoveAll(it => it.Submarine != character.Submarine);
var item = matchingItems.GetRandom();
var order = new Order(
orderPrefab,
item ?? character.CurrentHull as Entity,
orderPrefab,
item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType),
orderGiver: character);
if (order == null) { continue; }
@@ -94,15 +112,15 @@ namespace Barotrauma
objectiveCount++;
}
}
WaitTimer = Math.Max(WaitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
public void AddObjective(AIObjective objective, float delay, Action callback = null)
public void AddObjective<T>(T objective, float delay, Action callback = null) where T : AIObjective
{
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
#endif
return;
}
@@ -120,14 +138,7 @@ namespace Barotrauma
DelayedObjectives.Add(objective, coroutine);
}
public T GetObjective<T>() where T : AIObjective
{
foreach (AIObjective objective in Objectives)
{
if (objective is T) return (T)objective;
}
return null;
}
public T GetObjective<T>() where T : AIObjective => Objectives.FirstOrDefault(o => o is T) as T;
private AIObjective GetCurrentObjective()
{
@@ -143,6 +154,7 @@ namespace Barotrauma
}
if (previousObjective != CurrentObjective)
{
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().SetRandom();
}
@@ -165,17 +177,17 @@ namespace Barotrauma
for (int i = 0; i < Objectives.Count; i++)
{
var objective = Objectives[i];
if (objective.IsCompleted())
if (objective.IsCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"Removing objective {objective.DebugTag}, because it is completed.");
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightGreen);
#endif
Objectives.Remove(objective);
}
else if (!objective.CanBeCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"Removing objective {objective.DebugTag}, because it cannot be completed.");
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red);
#endif
Objectives.Remove(objective);
}
@@ -193,7 +205,7 @@ namespace Barotrauma
{
Objectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority()));
}
CurrentObjective?.SortSubObjectives();
GetCurrentObjective()?.SortSubObjectives();
}
public void DoCurrentObjective(float deltaTime)
@@ -260,7 +272,10 @@ namespace Barotrauma
newObjective = new AIObjectiveRescueAll(character, this, priorityModifier);
break;
case "repairsystems":
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier) { RequireAdequateSkills = option == "jobspecific" };
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier)
{
RequireAdequateSkills = option == "jobspecific"
};
break;
case "pumpwater":
newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier);
@@ -275,11 +290,21 @@ namespace Barotrauma
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition;
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) { IsLoop = true };
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override auto pilot unless it's an order by a player
Override = orderGiver == Character.Controlled || orderGiver.IsRemotePlayer
};
break;
default:
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) { IsLoop = true };
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override auto control unless it's an order by a player
Override = orderGiver == Character.Controlled || orderGiver.IsRemotePlayer
};
break;
}
return newObjective;
@@ -12,17 +12,20 @@ namespace Barotrauma
private ItemComponent component, controller;
private Entity operateTarget;
private bool isCompleted;
private bool requireEquip;
private bool useController;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
public bool Override { get; set; } = true;
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
public Entity OperateTarget => operateTarget;
public ItemComponent Component => component;
public Func<bool> completionCondition;
public override float GetPriority()
{
if (component.Item.ConditionPercentage <= 0) { return 0; }
@@ -32,7 +35,7 @@ namespace Barotrauma
}
if (component.Item.CurrentHull == null) { return 0; }
if (component.Item.CurrentHull.FireSources.Count > 0) { return 0; }
if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c))) { return 0; }
if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; }
float devotion = MathHelper.Min(10, Priority);
float value = devotion + AIObjectiveManager.OrderPriority * PriorityModifier;
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
@@ -57,21 +60,27 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
ItemComponent target = useController ? controller : component;
if (useController && controller == null)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
abandon = true;
Abandon = true;
return;
}
if (target.CanBeSelected)
{
if (character.CanInteractWith(target.Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(target.Item);
// Don't allow to operate an item that someone already operates, unless this objective is an order
if (objectiveManager.CurrentOrder != this && Character.CharacterList.Any(c => c.SelectedConstruction == target.Item && c != character && HumanAIController.IsFriendly(c)))
if (objectiveManager.CurrentOrder != this && Character.CharacterList.Any(c => c.SelectedConstruction == target.Item && c != character && HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
abandon = true;
// Don't abandon
return;
}
if (character.SelectedConstruction != target.Item)
@@ -80,12 +89,14 @@ namespace Barotrauma
}
if (component.AIOperate(deltaTime, character, this))
{
isCompleted = true;
IsCompleted = completionCondition == null || completionCondition();
}
}
else
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager));
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
else
@@ -93,12 +104,14 @@ namespace Barotrauma
if (component.Item.GetComponent<Pickable>() == null)
{
//controller/target can't be selected and the item cannot be picked -> objective can't be completed
abandon = true;
Abandon = true;
return;
}
else if (!character.Inventory.Items.Contains(component.Item))
{
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true));
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getItemObjective));
}
else
{
@@ -109,7 +122,7 @@ namespace Barotrauma
if (holdable == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveOperateItem failed - equipping item " + component.Item + " is required but the item has no Holdable component");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveOperateItem failed - equipping item " + component.Item + " is required but the item has no Holdable component");
#endif
return;
}
@@ -139,18 +152,12 @@ namespace Barotrauma
}
if (component.AIOperate(deltaTime, character, this))
{
isCompleted = true;
IsCompleted = completionCondition == null || completionCondition();
}
}
}
}
public override bool IsCompleted() => isCompleted && !IsLoop;
public override bool IsDuplicate(AIObjective otherObjective)
{
if (!(otherObjective is AIObjectiveOperateItem operateItem)) { return false; }
return (operateItem.component == component || otherObjective.Option == Option);
}
protected override bool Check() => IsCompleted && !IsLoop;
}
}
@@ -10,13 +10,14 @@ namespace Barotrauma
class AIObjectivePumpWater : AIObjectiveLoop<Pump>
{
public override string DebugTag => "pump water";
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
private IEnumerable<Pump> pumpList;
public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option) { }
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectivePumpWater && otherObjective.Option == Option;
protected override void FindTargets()
{
if (Option == null) { return; }
@@ -33,16 +34,8 @@ namespace Barotrauma
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; }
if (Option == "stoppumping")
{
if (!pump.IsActive || MathUtils.NearlyEqual(pump.FlowPercentage, 0)) { return false; }
}
else
{
if (!pump.Item.InWater) { return false; }
if (pump.IsActive && pump.FlowPercentage <= -99.9f) { return false; }
}
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(pump)) { return false; }
return true;
}
protected override IEnumerable<Pump> GetList()
@@ -67,8 +60,24 @@ namespace Barotrauma
}
}
private bool IsReady(Pump pump)
{
if (Option == "stoppumping")
{
return !pump.IsActive || MathUtils.NearlyEqual(pump.FlowPercentage, 0);
}
else
{
return !pump.Item.InWater || pump.IsActive && pump.FlowPercentage <= -99.9f;
}
}
protected override AIObjective ObjectiveConstructor(Pump pump)
=> new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false) { IsLoop = false };
=> new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false)
{
IsLoop = false,
completionCondition = () => IsReady(pump)
};
protected override void OnObjectiveCompleted(AIObjective objective, Pump target)
=> HumanAIController.RemoveTargets<AIObjectivePumpWater, Pump>(character, target);
@@ -10,6 +10,7 @@ namespace Barotrauma
class AIObjectiveRepairItem : AIObjective
{
public override string DebugTag => "repair item";
public override bool KeepDivingGearOn => true;
public Item Item { get; private set; }
@@ -18,6 +19,8 @@ namespace Barotrauma
private float previousCondition = -1;
private RepairTool repairTool;
private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent<Repairable>()?.CurrentFixer == character;
public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
Item = item;
@@ -28,41 +31,36 @@ namespace Barotrauma
// TODO: priority list?
// Ignore items that are being repaired by someone else.
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { return 0; }
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.5f, MathUtils.InverseLerp(0, 10000, dist));
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;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
if (Item.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float isSelected = character.SelectedConstruction == Item ? 50 : 0;
float isSelected = IsRepairing ? 50 : 0;
float devotion = (Math.Min(Priority, 10) + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
bool isCompleted = Item.IsFullCondition;
if (isCompleted && character.SelectedConstruction == Item)
{
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + damagePriority * distanceFactor * successFactor * PriorityModifier, 0, 1));
}
public override bool IsCompleted()
protected override bool Check()
{
bool isCompleted = Item.IsFullCondition;
if (isCompleted && character.SelectedConstruction == Item)
IsCompleted = Item.IsFullCondition;
if (IsCompleted && IsRepairing)
{
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
return isCompleted;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
return otherObjective is AIObjectiveRepairItem repairObjective && repairObjective.Item == Item;
return IsCompleted;
}
protected override void Act(float deltaTime)
{
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
foreach (Repairable repairable in Item.Repairables)
{
if (!repairable.HasRequiredItems(character, false))
@@ -72,14 +70,12 @@ namespace Barotrauma
{
foreach (RelatedItem requiredItem in kvp.Value)
{
AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true));
subObjectives.Add(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true));
}
}
return;
}
}
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
if (repairTool == null)
{
FindRepairTool();
@@ -90,9 +86,9 @@ namespace Barotrauma
if (containedItems == null)
{
#if DEBUG
DebugConsole.ThrowError("AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
#endif
abandon = true;
Abandon = true;
return;
}
// Drop empty tanks
@@ -115,12 +111,15 @@ namespace Barotrauma
if (fuel == null)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager));
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager),
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
}
}
if (character.CanInteractWith(Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(Item);
if (repairTool != null)
{
OperateRepairTool(deltaTime);
@@ -129,10 +128,10 @@ namespace Barotrauma
{
if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
{
// Someone else is repairing the target. Abandon the objective if the other is better at this then us.
abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
// Someone else is repairing the target. Abandon the objective if the other is better at this than us.
Abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
}
if (!abandon)
if (!Abandon)
{
if (character.SelectedConstruction != Item)
{
@@ -145,12 +144,15 @@ namespace Barotrauma
else if (Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
abandon = true;
character?.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
Abandon = true;
}
}
if (abandon)
if (Abandon)
{
if (IsRepairing)
{
character?.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
repairable.StopRepairing(character);
}
else
@@ -179,7 +181,14 @@ namespace Barotrauma
}
return objective;
},
onAbandon: () => character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f));
onAbandon: () =>
{
Abandon = true;
if (IsRepairing)
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
});
}
}
@@ -14,9 +14,12 @@ namespace Barotrauma
/// </summary>
public bool RequireAdequateSkills;
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
public override bool AllowMultipleInstances => true;
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveRepairItems repairItems && repairItems.RequireAdequateSkills == RequireAdequateSkills;
public override bool IsDuplicate<T>(T otherObjective) =>
(otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override void CreateObjectives()
{
@@ -28,7 +31,21 @@ namespace Barotrauma
{
objective = ObjectiveConstructor(item);
Objectives.Add(item, objective);
AddSubObjective(objective);
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
objective.Completed += () =>
{
Objectives.Remove(item);
OnObjectiveCompleted(objective, item);
};
objective.Abandoned += () =>
{
Objectives.Remove(item);
ignoreList.Add(item);
targetUpdateTimer = 0;
};
}
break;
}
@@ -40,7 +57,7 @@ namespace Barotrauma
if (!IsValidTarget(item, character)) { return false; }
if (item.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (!Objectives.ContainsKey(item))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.ShowRepairUIThreshold)) { return false; }
@@ -52,7 +69,7 @@ namespace Barotrauma
return true;
}
protected override float TargetEvaluation() => Targets.Max(t => 100 - t.ConditionPercentage);
protected override float TargetEvaluation() => Targets.Max(t => character.SelectedConstruction == t && t.ConditionPercentage < 100 ? 100 : 100 - t.ConditionPercentage);
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
@@ -10,12 +10,16 @@ namespace Barotrauma
{
public override string DebugTag => "rescue";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
const float TreatmentDelay = 0.5f;
const float CloseEnoughToTreat = 150.0f;
private readonly Character targetCharacter;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
private float treatmentTimer;
private Hull safeHull;
@@ -24,94 +28,79 @@ namespace Barotrauma
{
if (targetCharacter == null)
{
string errorMsg = "Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace;
string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
abandon = true;
return;
}
if (targetCharacter == character)
{
// TODO: enable healing self too
abandon = true;
Abandon = true;
return;
}
this.targetCharacter = targetCharacter;
}
public override bool IsDuplicate(AIObjective otherObjective)
{
AIObjectiveRescue rescueObjective = otherObjective as AIObjectiveRescue;
return rescueObjective != null && rescueObjective.targetCharacter == targetCharacter;
}
protected override void Act(float deltaTime)
{
if (targetCharacter == null || targetCharacter.Removed)
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Abandon = true;
return;
}
// Unconcious target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
if (targetCharacter != character)
{
if (character.SelectedCharacter != targetCharacter)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
// Unconcious target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
if (character.SelectedCharacter != targetCharacter)
{
if (goToObjective != null && goToObjective.Target != targetCharacter)
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
{
goToObjective = null;
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat },
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
}
else
{
character.SelectCharacter(targetCharacter);
}
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager));
}
else
{
character.SelectCharacter(targetCharacter);
}
}
else
{
// Drag the character into safety
if (goToObjective != null && goToObjective.Target == targetCharacter)
{
goToObjective = null;
}
if (safeHull == null)
{
var findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
if (findSafety == null)
// Drag the character into safety
if (safeHull == null)
{
// Ensure that we have the find safety objective (should always be the case)
findSafety = new AIObjectiveFindSafety(character, objectiveManager);
objectiveManager.AddObjective(findSafety);
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
}
if (character.CurrentHull != safeHull)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
}
safeHull = findSafety.FindBestHull(HumanAIController.VisibleHulls);
}
if (character.CurrentHull != safeHull)
{
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager));
}
}
}
if (subObjectives.Any()) { return; }
if (!character.CanInteractWith(targetCharacter))
if (targetCharacter != character && !character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref goToObjective);
// Go to the target and select it
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager));
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat },
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () => RemoveSubObjective(ref goToObjective));
}
else
{
// We can start applying treatment
if (character.SelectedCharacter != targetCharacter)
if (character != targetCharacter && character.SelectedCharacter != targetCharacter)
{
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
@@ -123,61 +112,59 @@ namespace Barotrauma
}
}
// TODO: consider optimizing a bit
private readonly List<string> suitableItemIdentifiers = new List<string>();
private readonly List<string> itemNameList = new List<string>();
private Dictionary<string, float> currentTreatmentSuitabilities = new Dictionary<string, float>();
private void GiveTreatment(float deltaTime)
{
if (treatmentTimer > 0.0f)
{
treatmentTimer -= deltaTime;
return;
}
treatmentTimer = TreatmentDelay;
var allAfflictions = targetCharacter.CharacterHealth.GetAllAfflictions()
.Where(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth) > 0)
.ToList();
//find which treatments are the most suitable to treat the character's current condition
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false);
allAfflictions.Sort((a1, a2) =>
{
return Math.Sign(a2.GetVitalityDecrease(targetCharacter.CharacterHealth) - a1.GetVitalityDecrease(targetCharacter.CharacterHealth));
});
var allAfflictions = GetVitalityReducingAfflictions(targetCharacter).OrderByDescending(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth));
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in allAfflictions)
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (treatmentSuitability.Value > 0.0f)
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key);
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
if (matchingItem == null) { continue; }
ApplyTreatment(affliction, matchingItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
return;
}
}
}
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//didn't have any suitable treatments available, try to find some medical items
HashSet<string> suitableItemIdentifiers = new HashSet<string>();
foreach (Affliction affliction in allAfflictions)
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
{
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<string, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (treatmentSuitability.Value > 0.0f)
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
{
if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
//only list the first 4 items
if (itemNameList.Count < 4)
{
itemNameList.Add(itemPrefab.Name);
}
}
}
}
if (suitableItemIdentifiers.Count > 0)
{
List<string> itemNameList = new List<string>();
foreach (string itemIdentifier in suitableItemIdentifiers)
{
if (MapEntityPrefab.Find(null, itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab)
{
itemNameList.Add(itemPrefab.Name);
}
//only list the first 4 items
if (itemNameList.Count >= 4) { break; }
}
if (itemNameList.Count > 0)
{
string itemListStr = "";
@@ -189,18 +176,24 @@ namespace Barotrauma
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
if (targetCharacter != character)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
}
character.DeselectCharacter();
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true),
onCompleted: () => RemoveSubObjective(ref getItemObjective),
onAbandon: () => RemoveSubObjective(ref getItemObjective));
}
character.DeselectCharacter();
AddSubObjective(new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true));
}
character.AnimController.Anim = AnimController.Animation.CPR;
}
private void ApplyTreatment(Affliction affliction, Item item)
{
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
@@ -224,43 +217,46 @@ namespace Barotrauma
}
}
public override bool IsCompleted()
protected override bool Check()
{
if (targetCharacter == null || targetCharacter.Removed)
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
abandon = true;
return true;
Abandon = true;
return false;
}
bool isCompleted = targetCharacter.Bleeding <= 0 && targetCharacter.Vitality / targetCharacter.MaxVitality > AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager);
if (isCompleted)
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
{
Abandon = true;
return false;
}
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) > AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager);
if (isCompleted && targetCharacter != character)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
}
return isCompleted || targetCharacter.IsDead;
return isCompleted;
}
public override float GetPriority()
{
if (targetCharacter == null) { return 0; }
if (targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
abandon = true;
return 0;
}
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(c)))
{
abandon = true;
return 0;
}
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f;
float distanceFactor = MathHelper.Lerp(1, 0.5f, MathUtils.InverseLerp(0, 10000, dist));
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetCharacter.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float vitalityFactor = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter);
float devotion = Math.Min(Priority, 10) / 100;
return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + vitalityFactor * distanceFactor, 0, 1));
}
public static IEnumerable<Affliction> GetVitalityReducingAfflictions(Character character) => character.CharacterHealth.GetAllAfflictions(a => a.GetVitalityDecrease(character.CharacterHealth) > 0);
}
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
@@ -7,9 +8,10 @@ namespace Barotrauma
{
public override string DebugTag => "rescue all";
public override bool ForceRun => true;
public override bool IgnoreUnsafeHulls => true;
private const float vitalityThreshold = 0.8f;
private const float vitalityThresholdForOrders = 0.95f;
private const float vitalityThreshold = 80;
private const float vitalityThresholdForOrders = 95;
public static float GetVitalityThreshold(AIObjectiveManager manager)
{
if (manager == null)
@@ -25,15 +27,13 @@ namespace Barotrauma
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveRescueAll;
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation() => Targets.Max(t => GetVitalityFactor(t)) * 100;
protected override float TargetEvaluation() => Targets.Max(t => GetVitalityFactor(t));
public static float GetVitalityFactor(Character character) => (character.MaxVitality - character.Vitality) / character.MaxVitality;
public static float GetVitalityFactor(Character character) => Math.Min(character.HealthPercentage - character.Bleeding - character.Bloodloss - Math.Min(character.Oxygen, 0), 100);
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
@@ -47,16 +47,18 @@ namespace Barotrauma
if (!HumanAIController.IsFriendly(character, target)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (target.Bleeding < 1 && target.Vitality / target.MaxVitality > GetVitalityThreshold(humanAI.ObjectiveManager)) { return false; }
if (GetVitalityFactor(target) > GetVitalityThreshold(humanAI.ObjectiveManager)) { return false; }
}
else
{
if (target.Bleeding < 1 && target.Vitality / target.MaxVitality > vitalityThreshold) { return false; }
if (GetVitalityFactor(target) > vitalityThreshold) { return false; }
}
if (target.Submarine == null || character.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
// Don't go into rooms that have enemies
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; }
return true;
}
}