(61d00a474) v0.9.7.1
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class AIObjective
|
||||
{
|
||||
public virtual float Devotion => AIObjectiveManager.baseDevotion;
|
||||
|
||||
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;
|
||||
|
||||
/// <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?
|
||||
/// If false, the main objective will continue only when all the subobjectives have been removed (done).
|
||||
/// </summary>
|
||||
public virtual bool ConcurrentObjectives => false;
|
||||
|
||||
public virtual bool KeepDivingGearOn => false;
|
||||
public virtual bool UnequipItems => false;
|
||||
|
||||
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
|
||||
private float _cumulatedDevotion;
|
||||
protected float CumulatedDevotion
|
||||
{
|
||||
get { return _cumulatedDevotion; }
|
||||
set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); }
|
||||
}
|
||||
|
||||
protected virtual float MaxDevotion => 10;
|
||||
|
||||
/// <summary>
|
||||
/// Final priority value after all calculations.
|
||||
/// </summary>
|
||||
public float Priority { get; set; }
|
||||
public float PriorityModifier { get; private set; } = 1;
|
||||
public readonly Character character;
|
||||
public readonly AIObjectiveManager objectiveManager;
|
||||
public string Option { get; private set; }
|
||||
|
||||
private bool _abandon;
|
||||
public bool Abandon
|
||||
{
|
||||
get { return _abandon; }
|
||||
set
|
||||
{
|
||||
_abandon = value;
|
||||
if (_abandon)
|
||||
{
|
||||
OnAbandon();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool CanBeCompleted => !Abandon;
|
||||
|
||||
/// <summary>
|
||||
/// When true, the objective is never completed, unless CanBeCompleted returns false.
|
||||
/// </summary>
|
||||
public virtual bool IsLoop { get; set; }
|
||||
public IEnumerable<AIObjective> SubObjectives => subObjectives;
|
||||
public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault();
|
||||
|
||||
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 = CurrentSubObjective;
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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++)
|
||||
{
|
||||
subObjectives[i].TryComplete(deltaTime);
|
||||
if (!ConcurrentObjectives) { return; }
|
||||
}
|
||||
Act(deltaTime);
|
||||
}
|
||||
|
||||
// TODO: check turret aioperate
|
||||
public void AddSubObjective(AIObjective objective, bool addFirst = false)
|
||||
{
|
||||
var type = objective.GetType();
|
||||
subObjectives.RemoveAll(o => o.GetType() == type);
|
||||
if (addFirst)
|
||||
{
|
||||
subObjectives.Insert(0, objective);
|
||||
}
|
||||
else
|
||||
{
|
||||
subObjectives.Add(objective);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method allows multiple subobjectives of same type. Use with caution.
|
||||
/// </summary>
|
||||
public void AddSubObjectiveInQueue(AIObjective objective)
|
||||
{
|
||||
if (!subObjectives.Contains(objective))
|
||||
{
|
||||
subObjectives.Add(objective);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveSubObjective<T>(ref T objective) where T : AIObjective
|
||||
{
|
||||
if (objective != null)
|
||||
{
|
||||
if (subObjectives.Contains(objective))
|
||||
{
|
||||
subObjectives.Remove(objective);
|
||||
}
|
||||
objective = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SortSubObjectives()
|
||||
{
|
||||
if (!AllowSubObjectiveSorting) { return; }
|
||||
if (subObjectives.None()) { return; }
|
||||
subObjectives.ForEach(so => so.GetPriority());
|
||||
subObjectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
|
||||
if (ConcurrentObjectives)
|
||||
{
|
||||
subObjectives.ForEach(so => so.SortSubObjectives());
|
||||
}
|
||||
else
|
||||
{
|
||||
subObjectives.First().SortSubObjectives();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
|
||||
/// </summary>
|
||||
public virtual float GetPriority()
|
||||
{
|
||||
Priority = CumulatedDevotion * PriorityModifier;
|
||||
return Priority;
|
||||
}
|
||||
|
||||
private void UpdateDevotion(float deltaTime)
|
||||
{
|
||||
var currentObjective = objectiveManager.CurrentObjective;
|
||||
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this)))
|
||||
{
|
||||
CumulatedDevotion += Devotion * PriorityModifier * deltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool IsDuplicate<T>(T otherObjective) where T : AIObjective => otherObjective.Option == Option;
|
||||
|
||||
public virtual void Update(float deltaTime)
|
||||
{
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
Priority = AIObjectiveManager.OrderPriority;
|
||||
}
|
||||
else if (objectiveManager.WaitTimer <= 0)
|
||||
{
|
||||
UpdateDevotion(deltaTime);
|
||||
}
|
||||
subObjectives.ForEach(so => so.Update(deltaTime));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the subobjectives in the given collection are removed from the subobjectives. And if so, removes it also from the dictionary.
|
||||
/// </summary>
|
||||
protected void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
|
||||
{
|
||||
foreach (T1 key in collection)
|
||||
{
|
||||
if (dictionary.TryGetValue(key, out T2 objective))
|
||||
{
|
||||
if (!subObjectives.Contains(objective))
|
||||
{
|
||||
dictionary.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 and successfully added.
|
||||
/// </summary>
|
||||
protected bool TryAddSubObjective<T>(ref T objective, Func<T> constructor, Action onCompleted = null, Action onAbandon = null) where T : AIObjective
|
||||
{
|
||||
if (objective != null)
|
||||
{
|
||||
// Sub objective already found, no need to do anything if it remains in the subobjectives
|
||||
// If the sub objective is removed -> it's either completed or impossible to complete.
|
||||
if (!subObjectives.Contains(objective))
|
||||
{
|
||||
objective = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
objective = constructor();
|
||||
if (!subObjectives.Contains(objective))
|
||||
{
|
||||
if (objective.AllowMultipleInstances)
|
||||
{
|
||||
subObjectives.Add(objective);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSubObjective(objective);
|
||||
}
|
||||
if (onCompleted != null)
|
||||
{
|
||||
objective.Completed += onCompleted;
|
||||
}
|
||||
if (onAbandon != null)
|
||||
{
|
||||
objective.Abandoned += onAbandon;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnSelected()
|
||||
{
|
||||
Reset();
|
||||
Selected?.Invoke();
|
||||
}
|
||||
|
||||
public virtual void OnDeselected()
|
||||
{
|
||||
CumulatedDevotion = 0;
|
||||
Deselected?.Invoke();
|
||||
}
|
||||
|
||||
protected virtual void OnCompleted()
|
||||
{
|
||||
Completed?.Invoke();
|
||||
}
|
||||
|
||||
protected virtual void OnAbandon()
|
||||
{
|
||||
Abandoned?.Invoke();
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
isCompleted = false;
|
||||
hasBeenChecked = false;
|
||||
_abandon = false;
|
||||
CumulatedDevotion = 0;
|
||||
}
|
||||
|
||||
protected abstract void Act(float deltaTime);
|
||||
|
||||
private bool isCompleted;
|
||||
private bool hasBeenChecked;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
|
||||
{
|
||||
public override string DebugTag => "charge batteries";
|
||||
public override bool UnequipItems => true;
|
||||
private IEnumerable<PowerContainer> batteryList;
|
||||
|
||||
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier)
|
||||
: base(character, objectiveManager, priorityModifier, option) { }
|
||||
|
||||
protected override bool Filter(PowerContainer battery)
|
||||
{
|
||||
if (battery == null) { return false; }
|
||||
var item = battery.Item;
|
||||
if (item.Submarine == null) { return false; }
|
||||
if (item.CurrentHull == null) { return false; }
|
||||
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) && HumanAIController.IsActive(c))) { return false; }
|
||||
if (IsReady(battery)) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
if (Option == "charge")
|
||||
{
|
||||
return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(PowerContainer.aiRechargeTargetRatio - t.RechargeRatio)));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
return Targets.Max(t => MathHelper.Lerp(0, 100, t.RechargeRatio));
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<PowerContainer> GetList()
|
||||
{
|
||||
if (batteryList == null)
|
||||
{
|
||||
if (character == null || character.Submarine == null)
|
||||
{
|
||||
return new PowerContainer[0];
|
||||
}
|
||||
batteryList = character.Submarine.GetItems(true).Select(i => i.GetComponent<PowerContainer>()).Where(b => b != null);
|
||||
}
|
||||
return batteryList;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+683
@@ -0,0 +1,683 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveCombat : AIObjective
|
||||
{
|
||||
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; }
|
||||
|
||||
private Item _weapon;
|
||||
private Item Weapon
|
||||
{
|
||||
get { return _weapon; }
|
||||
set
|
||||
{
|
||||
_weapon = value;
|
||||
_weaponComponent = null;
|
||||
RemoveSubObjective(ref seekAmmunition);
|
||||
}
|
||||
}
|
||||
private ItemComponent _weaponComponent;
|
||||
private ItemComponent WeaponComponent
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Weapon == null) { return null; }
|
||||
if (_weaponComponent == null)
|
||||
{
|
||||
_weaponComponent =
|
||||
Weapon.GetComponent<RangedWeapon>() as ItemComponent ??
|
||||
Weapon.GetComponent<MeleeWeapon>() as ItemComponent ??
|
||||
Weapon.GetComponent<RepairTool>() as ItemComponent;
|
||||
}
|
||||
return _weaponComponent;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
private AIObjectiveGoTo followTargetObjective;
|
||||
|
||||
private Hull retreatTarget;
|
||||
private float coolDownTimer;
|
||||
private IEnumerable<FarseerPhysics.Dynamics.Body> myBodies;
|
||||
private float aimTimer;
|
||||
|
||||
public enum CombatMode
|
||||
{
|
||||
Defensive,
|
||||
Offensive,
|
||||
Retreat
|
||||
}
|
||||
|
||||
public CombatMode Mode { get; private set; }
|
||||
|
||||
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
Enemy = enemy;
|
||||
coolDownTimer = coolDown;
|
||||
findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
|
||||
if (findSafety != null)
|
||||
{
|
||||
findSafety.Priority = 0;
|
||||
HumanAIController.UnreachableHulls.Clear();
|
||||
}
|
||||
Mode = mode;
|
||||
initialMode = Mode;
|
||||
if (Enemy == null)
|
||||
{
|
||||
Mode = CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
Priority = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
|
||||
return Priority;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
ignoreWeaponTimer -= deltaTime;
|
||||
seekWeaponsTimer -= deltaTime;
|
||||
if (ignoreWeaponTimer < 0)
|
||||
{
|
||||
ignoredWeapons.Clear();
|
||||
ignoreWeaponTimer = ignoredWeaponsClearTime;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
{
|
||||
bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0);
|
||||
if (completed)
|
||||
{
|
||||
if (objectiveManager.CurrentOrder == this && Enemy != null && Enemy.IsDead)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
|
||||
}
|
||||
if (Weapon != null)
|
||||
{
|
||||
Unequip();
|
||||
}
|
||||
}
|
||||
return completed;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (initialMode != CombatMode.Offensive)
|
||||
{
|
||||
coolDownTimer -= deltaTime;
|
||||
}
|
||||
if (seekAmmunition == null)
|
||||
{
|
||||
if (Mode != CombatMode.Retreat && TryArm() && Enemy != null && !Enemy.Removed)
|
||||
{
|
||||
OperateWeapon(deltaTime);
|
||||
}
|
||||
if (!HoldPosition && seekAmmunition == null)
|
||||
{
|
||||
Move();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Move()
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case CombatMode.Offensive:
|
||||
Engage();
|
||||
break;
|
||||
case CombatMode.Defensive:
|
||||
case CombatMode.Retreat:
|
||||
Retreat();
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsLoaded(ItemComponent weapon) => weapon.HasRequiredContainedItems(character, addMessage: false);
|
||||
|
||||
private bool TryArm()
|
||||
{
|
||||
if (character.LockHands || Enemy == 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 = GetWeapon(allWeapons, out _weaponComponent);
|
||||
if (Weapon == null)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!CheckWeapon(seekAmmo: false))
|
||||
{
|
||||
Weapon = null;
|
||||
}
|
||||
}
|
||||
if (Weapon == null)
|
||||
{
|
||||
Mode = CombatMode.Retreat;
|
||||
}
|
||||
else
|
||||
{
|
||||
Mode = WeaponComponent.CombatPriority >= goodWeaponPriority ? initialMode : CombatMode.Defensive;
|
||||
}
|
||||
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)
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case CombatMode.Offensive:
|
||||
case CombatMode.Defensive:
|
||||
if (Equip())
|
||||
{
|
||||
Attack(deltaTime);
|
||||
}
|
||||
break;
|
||||
case CombatMode.Retreat:
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private Item GetWeapon(out ItemComponent weaponComponent)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
foreach (var statusEffects in effects.Values)
|
||||
{
|
||||
foreach (var statusEffect in statusEffects)
|
||||
{
|
||||
if (statusEffect.Afflictions.Any())
|
||||
{
|
||||
weaponList.Add(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Unequip()
|
||||
{
|
||||
if (!character.LockHands && character.SelectedItems.Contains(Weapon))
|
||||
{
|
||||
if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
Weapon.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool Equip()
|
||||
{
|
||||
if (character.LockHands) { return false; }
|
||||
if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
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))
|
||||
{
|
||||
aimTimer = Rand.Range(0.5f, 1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
Weapon = null;
|
||||
Mode = CombatMode.Retreat;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Retreat()
|
||||
{
|
||||
RemoveSubObjective(ref followTargetObjective);
|
||||
RemoveSubObjective(ref seekAmmunition);
|
||||
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
|
||||
{
|
||||
retreatObjective = null;
|
||||
}
|
||||
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
|
||||
{
|
||||
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
|
||||
}
|
||||
if (retreatTarget != null && character.CurrentHull != retreatTarget)
|
||||
{
|
||||
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true),
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
|
||||
{
|
||||
// If in the same room with an enemy -> don't try to escape because we'd want to fight it
|
||||
SteeringManager.Reset();
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
// else abandon and fall back to find safety mode
|
||||
Abandon = true;
|
||||
}
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref retreatObjective));
|
||||
}
|
||||
}
|
||||
|
||||
private void Engage()
|
||||
{
|
||||
if (character.LockHands || Enemy == null)
|
||||
{
|
||||
Mode = CombatMode.Retreat;
|
||||
SteeringManager.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
retreatTarget = null;
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
RemoveSubObjective(ref seekAmmunition);
|
||||
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
|
||||
{
|
||||
followTargetObjective = null;
|
||||
}
|
||||
TryAddSubObjective(ref followTargetObjective,
|
||||
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true)
|
||||
{
|
||||
IgnoreIfTargetDead = true,
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = Enemy.DisplayName
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
Mode = CombatMode.Defensive;
|
||||
SteeringManager.Reset();
|
||||
RemoveSubObjective(ref followTargetObjective);
|
||||
});
|
||||
if (followTargetObjective != null)
|
||||
{
|
||||
followTargetObjective.CloseEnough =
|
||||
WeaponComponent is RangedWeapon ? 1000 :
|
||||
WeaponComponent is MeleeWeapon mw ? mw.Range :
|
||||
WeaponComponent is RepairTool rt ? rt.Range : 50;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeks for more ammunition. Creates a new subobjective.
|
||||
/// </summary>
|
||||
private void SeekAmmunition(string[] ammunitionIdentifiers)
|
||||
{
|
||||
retreatTarget = null;
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
RemoveSubObjective(ref followTargetObjective);
|
||||
TryAddSubObjective(ref seekAmmunition,
|
||||
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
|
||||
{
|
||||
targetItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
|
||||
checkInventory = false
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref seekAmmunition),
|
||||
onAbandon: () =>
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
RemoveSubObjective(ref seekAmmunition);
|
||||
ignoredWeapons.Add(Weapon);
|
||||
Weapon = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the ammunition found in the inventory.
|
||||
/// If seekAmmo is true, tries to get find the ammo elsewhere.
|
||||
/// </summary>
|
||||
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 && requiredItem.MatchesItem(it));
|
||||
if (ammunition != null)
|
||||
{
|
||||
// Ammunition still remaining
|
||||
return true;
|
||||
}
|
||||
item = requiredItem;
|
||||
ammunitionIdentifiers = requiredItem.Identifiers;
|
||||
}
|
||||
// No ammo
|
||||
if (ammunition == null)
|
||||
{
|
||||
if (ammunitionIdentifiers != null)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
var container = Weapon.GetComponent<ItemContainer>();
|
||||
if (container.Item.ParentInventory == character.Inventory)
|
||||
{
|
||||
if (!container.Inventory.CanBePut(ammunition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
character.Inventory.RemoveItem(ammunition);
|
||||
if (!container.Inventory.TryPutItem(ammunition, null))
|
||||
{
|
||||
ammunition.Drop(character);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
container.Combine(ammunition, character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (ammunition == null && !HoldPosition && initialMode == CombatMode.Offensive && seekAmmo && ammunitionIdentifiers != null)
|
||||
{
|
||||
SeekAmmunition(ammunitionIdentifiers);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Attack(float deltaTime)
|
||||
{
|
||||
float squaredDistance = Vector2.DistanceSquared(character.Position, Enemy.Position);
|
||||
character.CursorPosition = Enemy.Position;
|
||||
float engageDistance = 500;
|
||||
if (character.CurrentHull != Enemy.CurrentHull && squaredDistance > engageDistance * engageDistance) { return; }
|
||||
if (!character.CanSeeCharacter(Enemy)) { return; }
|
||||
if (Weapon.RequireAimToUse)
|
||||
{
|
||||
bool isOperatingButtons = false;
|
||||
if (SteeringManager == PathSteering)
|
||||
{
|
||||
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
|
||||
if (door != null && !door.IsOpen)
|
||||
{
|
||||
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
|
||||
}
|
||||
}
|
||||
if (!isOperatingButtons)
|
||||
{
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
}
|
||||
}
|
||||
bool isFacing = character.AnimController.Dir > 0 && Enemy.WorldPosition.X > character.WorldPosition.X || character.AnimController.Dir < 0 && Enemy.WorldPosition.X < character.WorldPosition.X;
|
||||
if (!isFacing)
|
||||
{
|
||||
aimTimer = Rand.Range(1f, 1.5f);
|
||||
}
|
||||
if (aimTimer > 0)
|
||||
{
|
||||
aimTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
if (WeaponComponent is MeleeWeapon meleeWeapon)
|
||||
{
|
||||
if (squaredDistance <= meleeWeapon.Range * meleeWeapon.Range)
|
||||
{
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Weapon.Use(deltaTime, character);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (WeaponComponent is RepairTool repairTool)
|
||||
{
|
||||
if (squaredDistance > repairTool.Range * repairTool.Range) { return; }
|
||||
}
|
||||
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4)
|
||||
{
|
||||
if (myBodies == null)
|
||||
{
|
||||
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
|
||||
}
|
||||
|
||||
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
|
||||
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories);
|
||||
if (pickedBody != null)
|
||||
{
|
||||
Character target = null;
|
||||
if (pickedBody.UserData is Character c)
|
||||
{
|
||||
target = c;
|
||||
}
|
||||
else if (pickedBody.UserData is Limb limb)
|
||||
{
|
||||
target = limb.character;
|
||||
}
|
||||
if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target)))
|
||||
{
|
||||
character.SetInput(InputType.Shoot, false, true);
|
||||
Weapon.Use(deltaTime, character);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//private float CalculateEnemyStrength()
|
||||
//{
|
||||
// float enemyStrength = 0;
|
||||
// AttackContext currentContext = character.GetAttackContext();
|
||||
// foreach (Limb limb in Enemy.AnimController.Limbs)
|
||||
// {
|
||||
// if (limb.attack == null) continue;
|
||||
// if (!limb.attack.IsValidContext(currentContext)) { continue; }
|
||||
// if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; }
|
||||
// enemyStrength += limb.attack.GetTotalDamage(false);
|
||||
// }
|
||||
// return enemyStrength;
|
||||
//}
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveContainItem: AIObjective
|
||||
{
|
||||
public override string DebugTag => "contain item";
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
|
||||
public int targetItemCount = 1;
|
||||
public string[] ignoredContainerIdentifiers;
|
||||
public bool checkInventory = true;
|
||||
|
||||
//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 bool Equip { get; set; }
|
||||
public bool RemoveEmpty { get; set; } = true;
|
||||
|
||||
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)
|
||||
{
|
||||
this.itemIdentifiers = itemIdentifiers;
|
||||
for (int i = 0; i < itemIdentifiers.Length; i++)
|
||||
{
|
||||
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
|
||||
}
|
||||
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (item != null)
|
||||
{
|
||||
return container.Inventory.Items.Contains(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
int containedItemCount = 0;
|
||||
foreach (Item i in container.Inventory.Items)
|
||||
{
|
||||
if (i != null && CheckItem(i))
|
||||
{
|
||||
containedItemCount++;
|
||||
}
|
||||
}
|
||||
return containedItemCount >= targetItemCount;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage > ConditionLevel;
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (container == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
Item itemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
|
||||
if (itemToContain != null)
|
||||
{
|
||||
if (character.CanInteractWith(container.Item, out _, checkLinked: false))
|
||||
{
|
||||
if (RemoveEmpty)
|
||||
{
|
||||
foreach (var emptyItem in container.Inventory.Items)
|
||||
{
|
||||
if (emptyItem == null) { continue; }
|
||||
if (emptyItem.Condition <= 0)
|
||||
{
|
||||
emptyItem.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Contain the item
|
||||
if (itemToContain.ParentInventory == character.Inventory)
|
||||
{
|
||||
if (!container.Inventory.CanBePut(itemToContain))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
character.Inventory.RemoveItem(itemToContain);
|
||||
if (container.Inventory.TryPutItem(itemToContain, null))
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
itemToContain.Drop(character);
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (container.Combine(itemToContain, character))
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = container.Item.Name
|
||||
},
|
||||
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: Equip, checkInventory: checkInventory)
|
||||
{
|
||||
GetItemPriority = GetItemPriority,
|
||||
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
|
||||
ignoredItems = containedItems,
|
||||
AllowToFindDivingGear = this.AllowToFindDivingGear
|
||||
}, onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
}, onCompleted: () =>
|
||||
{
|
||||
if (getItemObjective.TargetItem != null)
|
||||
{
|
||||
containedItems.Add(getItemObjective.TargetItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (container.Inventory.FindItem(i => CheckItem(i), recursive: false) != null)
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
RemoveSubObjective(ref getItemObjective);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveDecontainItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "decontain item";
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
|
||||
//can either be a tag or an identifier
|
||||
private readonly string[] itemIdentifiers;
|
||||
private readonly ItemContainer sourceContainer;
|
||||
private ItemContainer targetContainer;
|
||||
private readonly Item targetItem;
|
||||
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private AIObjectiveContainItem containObjective;
|
||||
|
||||
public bool Equip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true drops the item when containing the item fails.
|
||||
/// In both cases abandons the objective.
|
||||
/// Note that has no effect if the target container was not defined (always drops) -> completes when the item is dropped.
|
||||
/// </summary>
|
||||
public bool DropIfFailsToContain { get; set; } = true;
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.targetItem = targetItem;
|
||||
this.sourceContainer = sourceContainer;
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: this(character, new string[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.itemIdentifiers = itemIdentifiers;
|
||||
for (int i = 0; i < itemIdentifiers.Length; i++)
|
||||
{
|
||||
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
|
||||
}
|
||||
this.sourceContainer = sourceContainer;
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
protected override bool Check() => IsCompleted;
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)), recursive: false);
|
||||
if (itemToDecontain == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (targetContainer == null)
|
||||
{
|
||||
if (sourceContainer == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemToDecontain.Container != sourceContainer.Item)
|
||||
{
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (targetContainer.Inventory.Items.Contains(itemToDecontain))
|
||||
{
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (goToObjective == null && !itemToDecontain.IsOwnedBy(character))
|
||||
{
|
||||
if (sourceContainer == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (!character.CanInteractWith(sourceContainer.Item, out _, checkLinked: false))
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager)
|
||||
{
|
||||
// If the container changes, the item is no longer where it was
|
||||
abortCondition = () => itemToDecontain.Container != sourceContainer.Item,
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = sourceContainer.Item.Name
|
||||
},
|
||||
onAbandon: () => Abandon = true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (targetContainer != null)
|
||||
{
|
||||
TryAddSubObjective(ref containObjective,
|
||||
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
|
||||
{
|
||||
Equip = this.Equip,
|
||||
RemoveEmpty = false,
|
||||
GetItemPriority = this.GetItemPriority,
|
||||
ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null
|
||||
},
|
||||
onCompleted: () => IsCompleted = true,
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (DropIfFailsToContain)
|
||||
{
|
||||
itemToDecontain.Drop(character);
|
||||
}
|
||||
Abandon = true;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
itemToDecontain.Drop(character);
|
||||
IsCompleted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveExtinguishFire : AIObjective
|
||||
{
|
||||
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;
|
||||
|
||||
private AIObjectiveGetItem getExtinguisherObjective;
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
private float useExtinquisherTimer;
|
||||
|
||||
public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.targetHull = targetHull;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
if (!objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>()
|
||||
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
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 = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severityFactor * distanceFactor * PriorityModifier), 0, 1));
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool Check() => targetHull.FireSources.None();
|
||||
|
||||
private float sinTime;
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var extinguisherItem = character.Inventory.FindItemByIdentifier("fireextinguisher") ?? character.Inventory.FindItemByTag("fireextinguisher");
|
||||
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
|
||||
{
|
||||
TryAddSubObjective(ref getExtinguisherObjective, () =>
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
|
||||
return new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true)
|
||||
{
|
||||
// If the item is inside an unsafe hull, decrease the priority
|
||||
GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1
|
||||
};
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var extinguisher = extinguisherItem.GetComponent<RepairTool>();
|
||||
if (extinguisher == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
|
||||
#endif
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
foreach (FireSource fs in targetHull.FireSources)
|
||||
{
|
||||
bool inRange = fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range));
|
||||
bool move = !inRange || !HumanAIController.VisibleHulls.Contains(fs.Hull);
|
||||
if (inRange || useExtinquisherTimer > 0.0f)
|
||||
{
|
||||
useExtinquisherTimer += deltaTime;
|
||||
if (useExtinquisherTimer > 2.0f)
|
||||
{
|
||||
useExtinquisherTimer = 0.0f;
|
||||
}
|
||||
// Aim
|
||||
character.CursorPosition = fs.Position;
|
||||
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
|
||||
float dist = fromCharacterToFireSource.Length();
|
||||
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
|
||||
if (extinguisherItem.RequireAimToUse)
|
||||
{
|
||||
bool isOperatingButtons = false;
|
||||
if (SteeringManager == PathSteering)
|
||||
{
|
||||
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
|
||||
if (door != null && !door.IsOpen)
|
||||
{
|
||||
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
|
||||
}
|
||||
}
|
||||
if (!isOperatingButtons)
|
||||
{
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
}
|
||||
sinTime += deltaTime * 10;
|
||||
}
|
||||
character.SetInput(extinguisherItem.IsShootable ? InputType.Shoot : InputType.Use, false, true);
|
||||
extinguisher.Use(deltaTime, character);
|
||||
if (!targetHull.FireSources.Contains(fs))
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, true), null, 0, "putoutfire", 10.0f);
|
||||
}
|
||||
if (!character.CanSeeTarget(fs))
|
||||
{
|
||||
move = true;
|
||||
}
|
||||
}
|
||||
if (move)
|
||||
{
|
||||
//go to the first firesource
|
||||
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2)
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachfire",
|
||||
TargetName = fs.Hull.DisplayName
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref gotoObjective));
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SteeringManager.Reset();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveExtinguishFires : AIObjectiveLoop<Hull>
|
||||
{
|
||||
public override string DebugTag => "extinguish fires";
|
||||
public override bool ForceRun => true;
|
||||
|
||||
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
|
||||
|
||||
protected override float TargetEvaluation() => objectiveManager.CurrentObjective == this ? 100 : Targets.Sum(t => GetFireSeverity(t));
|
||||
|
||||
public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X);
|
||||
|
||||
protected override IEnumerable<Hull> GetList() => Hull.hullList;
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Hull target)
|
||||
=> new AIObjectiveExtinguishFire(character, target, objectiveManager, PriorityModifier);
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Hull target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveExtinguishFires, Hull>(character, target);
|
||||
|
||||
public static bool IsValidTarget(Hull hull, Character character)
|
||||
{
|
||||
if (hull == null) { return false; }
|
||||
if (hull.FireSources.None()) { return false; }
|
||||
if (hull.Submarine == null) { return false; }
|
||||
if (hull.Submarine.TeamID != character.TeamID) { return false; }
|
||||
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFightIntruders : AIObjectiveLoop<Character>
|
||||
{
|
||||
public override string DebugTag => "fight intruders";
|
||||
protected override float IgnoreListClearInterval => 30;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
|
||||
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Filter(Character target) => IsValidTarget(target, character);
|
||||
|
||||
protected override IEnumerable<Character> GetList() => Character.CharacterList;
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
// TODO: sorting criteria
|
||||
return 100;
|
||||
}
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Character target)
|
||||
=> new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveFightIntruders, Character>(character, target);
|
||||
|
||||
public static bool IsValidTarget(Character target, Character character)
|
||||
{
|
||||
if (target == null || target.IsDead || target.Removed) { return false; }
|
||||
if (target == character) { return false; }
|
||||
if (HumanAIController.IsFriendly(character, target)) { return false; }
|
||||
if (target.Submarine == null) { return false; }
|
||||
if (target.Submarine.TeamID != character.TeamID) { return false; }
|
||||
if (target.CurrentHull == null) { return false; }
|
||||
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFindDivingGear : AIObjective
|
||||
{
|
||||
public override string DebugTag => $"find diving gear ({gearTag})";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
|
||||
private readonly string gearTag;
|
||||
private readonly string fallbackTag;
|
||||
|
||||
private AIObjectiveGetItem getDivingGear;
|
||||
private AIObjectiveContainItem getOxygen;
|
||||
|
||||
public static float lowOxygenThreshold = 10;
|
||||
|
||||
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" : "divingmask";
|
||||
fallbackTag = needDivingSuit ? "divingsuit" : "diving";
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
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, () =>
|
||||
{
|
||||
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
|
||||
{
|
||||
var containedItems = item.ContainedItems;
|
||||
if (containedItems == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory");
|
||||
#endif
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
// Drop empty tanks
|
||||
foreach (Item containedItem in containedItems)
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
if (containedItems.None(it => it.HasTag("oxygensource") && it.Condition > lowOxygenThreshold))
|
||||
{
|
||||
var oxygenTank = character.Inventory.FindItemByTag("oxygensource", true);
|
||||
if (oxygenTank != null)
|
||||
{
|
||||
var container = item.GetComponent<ItemContainer>();
|
||||
if (container.Item.ParentInventory == character.Inventory)
|
||||
{
|
||||
if (!container.Inventory.CanBePut(oxygenTank))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFindSafety : AIObjective
|
||||
{
|
||||
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;
|
||||
|
||||
private float currenthullSafety;
|
||||
|
||||
private float searchHullTimer;
|
||||
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private AIObjectiveFindDivingGear divingGearObjective;
|
||||
|
||||
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Check() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
|
||||
private bool resetPriority;
|
||||
|
||||
public override float GetPriority() => Priority;
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (resetPriority)
|
||||
{
|
||||
Priority = 0;
|
||||
resetPriority = false;
|
||||
return;
|
||||
}
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
currenthullSafety = 0;
|
||||
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100;
|
||||
return;
|
||||
}
|
||||
if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
|
||||
{
|
||||
Priority = 100;
|
||||
}
|
||||
currenthullSafety = HumanAIController.CurrentHullSafety;
|
||||
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
Priority -= priorityDecrease * deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
float dangerFactor = (100 - currenthullSafety) / 100;
|
||||
Priority += dangerFactor * priorityIncrease * deltaTime;
|
||||
}
|
||||
Priority = MathHelper.Clamp(Priority, 0, 100);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
private Hull currentSafeHull;
|
||||
private Hull previousSafeHull;
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var currentHull = character.CurrentHull;
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit);
|
||||
bool needsEquipment = false;
|
||||
if (needsDivingSuit)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
|
||||
}
|
||||
else if (needsDivingGear)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
|
||||
}
|
||||
if (needsEquipment && divingGearObjective == null && !character.LockHands)
|
||||
{
|
||||
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 if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
|
||||
{
|
||||
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
searchHullTimer = Math.Min(1, searchHullTimer);
|
||||
}
|
||||
if (searchHullTimer > 0.0f)
|
||||
{
|
||||
searchHullTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
|
||||
previousSafeHull = currentSafeHull;
|
||||
currentSafeHull = FindBestHull();
|
||||
if (currentSafeHull == null)
|
||||
{
|
||||
currentSafeHull = previousSafeHull;
|
||||
}
|
||||
if (currentSafeHull != null && currentSafeHull != currentHull)
|
||||
{
|
||||
if (goToObjective?.Target != currentSafeHull)
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
}
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
|
||||
{
|
||||
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: () =>
|
||||
{
|
||||
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
|
||||
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
|
||||
if (currentHull != null && goToObjective != null)
|
||||
{
|
||||
if (goToObjective.Target is Hull hull)
|
||||
{
|
||||
HumanAIController.UnreachableHulls.Add(hull);
|
||||
}
|
||||
}
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
}
|
||||
}
|
||||
if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
|
||||
if (currentHull != null)
|
||||
{
|
||||
//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 (!HumanAIController.IsActive(enemy) || 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);
|
||||
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)
|
||||
{
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
|
||||
character.AIController.SteeringManager.Reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
public Hull FindBestHull(IEnumerable<Hull> ignoredHulls = null, bool allowChangingTheSubmarine = true)
|
||||
{
|
||||
Hull bestHull = null;
|
||||
float bestValue = 0;
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
if (hull.Submarine == null) { continue; }
|
||||
if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; }
|
||||
if (ignoredHulls != null && ignoredHulls.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, 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; }
|
||||
// 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)
|
||||
{
|
||||
HumanAIController.UnreachableHulls.Add(hull);
|
||||
continue;
|
||||
}
|
||||
// Each unsafe node reduces the hull safety value.
|
||||
// Ignore the current hull, because otherwise we couldn't find a path out.
|
||||
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
|
||||
hullSafety /= 1 + unsafeNodes;
|
||||
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true))
|
||||
{
|
||||
hullSafety /= 10;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Outside
|
||||
if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hullSafety = 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: could also target gaps that get us inside?
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.CurrentHull != hull && item.HasTag("airlock"))
|
||||
{
|
||||
hullSafety = 100;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: could we get a closest door to the outside and target the flowing hull if no airlock is found?
|
||||
// Huge preference for closer targets
|
||||
float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
|
||||
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
|
||||
hullSafety *= distanceFactor;
|
||||
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
|
||||
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC)
|
||||
{
|
||||
hullSafety /= 10;
|
||||
}
|
||||
}
|
||||
if (hullSafety > bestValue)
|
||||
{
|
||||
bestHull = hull;
|
||||
bestValue = hullSafety;
|
||||
}
|
||||
}
|
||||
return bestHull;
|
||||
}
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFixLeak : AIObjective
|
||||
{
|
||||
public override string DebugTag => "fix leak";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
|
||||
public Gap Leak { get; private set; }
|
||||
|
||||
private AIObjectiveGetItem getWeldingTool;
|
||||
private AIObjectiveContainItem refuelObjective;
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
private AIObjectiveOperateItem operateObjective;
|
||||
|
||||
public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base (character, objectiveManager, priorityModifier)
|
||||
{
|
||||
Leak = leak;
|
||||
}
|
||||
|
||||
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
if (Leak.Removed || Leak.Open <= 0)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
|
||||
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
|
||||
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
|
||||
float distanceFactor = 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 = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true);
|
||||
if (weldingTool == null)
|
||||
{
|
||||
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, true),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref getWeldingTool));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var containedItems = weldingTool.ContainedItems;
|
||||
if (containedItems == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
|
||||
#endif
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
// Drop empty tanks
|
||||
foreach (Item containedItem in containedItems)
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
|
||||
{
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref refuelObjective));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (subObjectives.Any()) { return; }
|
||||
var repairTool = weldingTool.GetComponent<RepairTool>();
|
||||
if (repairTool == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool");
|
||||
#endif
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
Vector2 toLeak = Leak.WorldPosition - character.WorldPosition;
|
||||
// TODO: use the collider size/reach?
|
||||
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
|
||||
{
|
||||
HumanAIController.AnimController.Crouching = true;
|
||||
}
|
||||
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),
|
||||
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 = !Leak.IsRoomToRoom && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() && HumanAIController.HasDivingSuit(character, conditionPercentage: 50),
|
||||
CloseEnough = reach,
|
||||
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
|
||||
TargetName = Leak.FlowTargetHull?.DisplayName
|
||||
},
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFixLeaks : AIObjectiveLoop<Gap>
|
||||
{
|
||||
public override string DebugTag => "fix leaks";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
|
||||
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Filter(Gap gap) => IsValidTarget(gap, character);
|
||||
|
||||
public static float GetLeakSeverity(Gap leak)
|
||||
{
|
||||
if (leak == null) { return 0; }
|
||||
float sizeFactor = MathHelper.Lerp(1, 10, MathUtils.InverseLerp(0, 200, leak.Size));
|
||||
float severity = sizeFactor * leak.Open;
|
||||
if (!leak.IsRoomToRoom)
|
||||
{
|
||||
severity *= 10;
|
||||
// If there is a leak in the outer walls, the severity cannot be lower than 10, no matter how small the leak
|
||||
return MathHelper.Clamp(severity, 10, 100);
|
||||
}
|
||||
else
|
||||
{
|
||||
return MathHelper.Min(severity, 100);
|
||||
}
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>(), onlyBots: true);
|
||||
int totalLeaks = Targets.Count();
|
||||
if (totalLeaks == 0) { return 0; }
|
||||
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
|
||||
int leaks = totalLeaks - secondaryLeaks;
|
||||
bool anyFixers = otherFixers > 0;
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1;
|
||||
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
|
||||
}
|
||||
else
|
||||
{
|
||||
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1;
|
||||
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
|
||||
{
|
||||
// Enough fixers
|
||||
return 0;
|
||||
}
|
||||
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<Gap> GetList() => Gap.GapList;
|
||||
protected override AIObjective ObjectiveConstructor(Gap gap)
|
||||
=> new AIObjectiveFixLeak(gap, character, objectiveManager, PriorityModifier);
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Gap target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveFixLeaks, Gap>(character, target);
|
||||
|
||||
public static bool IsValidTarget(Gap gap, Character character)
|
||||
{
|
||||
if (gap == null) { return false; }
|
||||
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
|
||||
if (gap.Submarine == null) { return false; }
|
||||
if (gap.Submarine.TeamID != character.TeamID) { return false; }
|
||||
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveGetItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "get item";
|
||||
|
||||
private readonly bool equip;
|
||||
public HashSet<Item> ignoredItems = new HashSet<Item>();
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
public Func<Item, bool> ItemFilter;
|
||||
public float TargetCondition { get; set; } = 1;
|
||||
|
||||
//can be either tags or identifiers
|
||||
private string[] itemIdentifiers;
|
||||
public IEnumerable<string> Identifiers => itemIdentifiers;
|
||||
private Item targetItem, moveToTarget, rootContainer;
|
||||
private bool isDoneSeeking;
|
||||
public Item TargetItem => targetItem;
|
||||
private int currSearchIndex;
|
||||
public string[] ignoredContainerIdentifiers;
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private float currItemPriority;
|
||||
private bool checkInventory;
|
||||
|
||||
public bool AllowToFindDivingGear { get; set; } = true;
|
||||
|
||||
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
currSearchIndex = -1;
|
||||
this.equip = equip;
|
||||
this.targetItem = targetItem;
|
||||
}
|
||||
|
||||
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 = true, bool checkInventory = true, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
currSearchIndex = -1;
|
||||
this.equip = equip;
|
||||
this.itemIdentifiers = itemIdentifiers;
|
||||
for (int i = 0; i < itemIdentifiers.Length; i++)
|
||||
{
|
||||
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
|
||||
}
|
||||
this.checkInventory = checkInventory;
|
||||
}
|
||||
|
||||
private bool CheckInventory()
|
||||
{
|
||||
if (itemIdentifiers == null) { return false; }
|
||||
var item = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
|
||||
if (item != null)
|
||||
{
|
||||
targetItem = item;
|
||||
rootContainer = item.GetRootContainer();
|
||||
moveToTarget = rootContainer ?? item;
|
||||
}
|
||||
return item != null;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (character.LockHands)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemIdentifiers != null && !isDoneSeeking)
|
||||
{
|
||||
if (checkInventory)
|
||||
{
|
||||
if (CheckInventory())
|
||||
{
|
||||
isDoneSeeking = true;
|
||||
}
|
||||
}
|
||||
if (!isDoneSeeking)
|
||||
{
|
||||
FindTargetItem();
|
||||
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (targetItem == null || targetItem.Removed)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
|
||||
#endif
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (character.IsItemTakenBySomeoneElse(targetItem))
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's already equipped by someone else.", Color.Yellow);
|
||||
#endif
|
||||
// Try again
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
if (character.CanInteractWith(targetItem, out _, checkLinked: false))
|
||||
{
|
||||
var pickable = targetItem.GetComponent<Pickable>();
|
||||
if (pickable == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Target not pickable. Aborting.", Color.Yellow);
|
||||
#endif
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
|
||||
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 (otherItem.AllowedSlots.Contains(InvSlotType.Any) &&
|
||||
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))
|
||||
{
|
||||
targetItem.Equip(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
|
||||
{
|
||||
if (character.Inventory.TryPutItem(targetItem, null, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Abandon = true;
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
constructor: () =>
|
||||
{
|
||||
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear)
|
||||
{
|
||||
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
|
||||
abortCondition = () => targetItem == null || targetItem.GetRootContainer() != rootContainer,
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = moveToTarget.Name
|
||||
};
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
ignoredItems.Add(targetItem);
|
||||
Reset();
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective));
|
||||
}
|
||||
}
|
||||
|
||||
private void FindTargetItem()
|
||||
{
|
||||
if (itemIdentifiers == null)
|
||||
{
|
||||
if (targetItem == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item was defined.", Color.Red);
|
||||
#endif
|
||||
Abandon = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++)
|
||||
{
|
||||
currSearchIndex++;
|
||||
var item = Item.ItemList[currSearchIndex];
|
||||
if (item.Submarine == null) { continue; }
|
||||
if (item.CurrentHull == null) { continue; }
|
||||
if (item.Submarine.TeamID != character.TeamID) { continue; }
|
||||
if (!CheckItem(item)) { continue; }
|
||||
if (ignoredContainerIdentifiers != null && item.Container != null)
|
||||
{
|
||||
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
|
||||
}
|
||||
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
|
||||
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
|
||||
float itemPriority = 1;
|
||||
if (GetItemPriority != null)
|
||||
{
|
||||
itemPriority = GetItemPriority(item);
|
||||
}
|
||||
Item rootContainer = item.GetRootContainer();
|
||||
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;
|
||||
itemPriority *= item.Condition / item.MaxCondition;
|
||||
//ignore if the item has a lower priority than the currently selected one
|
||||
if (itemPriority < currItemPriority) { continue; }
|
||||
currItemPriority = itemPriority;
|
||||
targetItem = item;
|
||||
moveToTarget = rootContainer ?? item;
|
||||
this.rootContainer = rootContainer;
|
||||
}
|
||||
if (currSearchIndex >= Item.ItemList.Count - 1)
|
||||
{
|
||||
isDoneSeeking = true;
|
||||
if (targetItem == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
|
||||
#endif
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (targetItem != null)
|
||||
{
|
||||
return character.HasItem(targetItem, equip);
|
||||
}
|
||||
else if (itemIdentifiers != null)
|
||||
{
|
||||
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
|
||||
if (matchingItem != null)
|
||||
{
|
||||
return !equip || character.HasEquippedItem(matchingItem);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool CheckItem(Item item)
|
||||
{
|
||||
if (ignoredItems.Contains(item)) { return false; };
|
||||
if (item.Condition < TargetCondition) { return false; }
|
||||
if (ItemFilter != null && !ItemFilter(item)) { return false; }
|
||||
return itemIdentifiers.Any(id => id == item.Prefab.Identifier || item.HasTag(id));
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
targetItem = null;
|
||||
moveToTarget = null;
|
||||
rootContainer = null;
|
||||
isDoneSeeking = false;
|
||||
currSearchIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveGoTo : AIObjective
|
||||
{
|
||||
public override string DebugTag => "go to";
|
||||
|
||||
private AIObjectiveFindDivingGear findDivingGear;
|
||||
private readonly bool repeat;
|
||||
//how long until the path to the target is declared unreachable
|
||||
private float waitUntilPathUnreachable;
|
||||
private bool getDivingGearIfNeeded;
|
||||
|
||||
/// <summary>
|
||||
/// Doesn't allow the objective to complete if this condition is false
|
||||
/// </summary>
|
||||
public Func<bool> requiredCondition;
|
||||
/// <summary>
|
||||
/// Aborts the objective when this condition is true
|
||||
/// </summary>
|
||||
public Func<bool> abortCondition;
|
||||
public Func<PathNode, bool> endNodeFilter;
|
||||
|
||||
public bool followControlledCharacter;
|
||||
public bool mimic;
|
||||
|
||||
private float _closeEnough = 50;
|
||||
/// <summary>
|
||||
/// Display units
|
||||
/// </summary>
|
||||
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 string DialogueIdentifier { get; set; }
|
||||
public string TargetName { get; set; }
|
||||
|
||||
public ISpatialEntity Target { get; private set; }
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
if (followControlledCharacter && Character.Controlled == null)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
if (Target is Entity e && e.Removed)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
return objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : Priority;
|
||||
}
|
||||
|
||||
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;
|
||||
CloseEnough = closeEnough;
|
||||
if (Target is Item i)
|
||||
{
|
||||
CloseEnough = Math.Max(CloseEnough, i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpeakCannotReach()
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow);
|
||||
#endif
|
||||
if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null)
|
||||
{
|
||||
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, true);
|
||||
if (msg != null)
|
||||
{
|
||||
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (followControlledCharacter)
|
||||
{
|
||||
if (Character.Controlled == null)
|
||||
{
|
||||
Abandon = true;
|
||||
SteeringManager.Reset();
|
||||
return;
|
||||
}
|
||||
Target = Character.Controlled;
|
||||
}
|
||||
if (Target == character)
|
||||
{
|
||||
// Wait
|
||||
character.AIController.SteeringManager.Reset();
|
||||
return;
|
||||
}
|
||||
waitUntilPathUnreachable -= deltaTime;
|
||||
if (!character.IsClimbing)
|
||||
{
|
||||
character.SelectedConstruction = null;
|
||||
}
|
||||
if (Target is Entity e)
|
||||
{
|
||||
if (e.Removed)
|
||||
{
|
||||
Abandon = true;
|
||||
SteeringManager.Reset();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SelectTarget(e.AiTarget);
|
||||
}
|
||||
}
|
||||
Hull targetHull = GetTargetHull();
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
else if (waitUntilPathUnreachable < 0)
|
||||
{
|
||||
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty)
|
||||
{
|
||||
if (repeat)
|
||||
{
|
||||
SpeakCannotReach();
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Abandon)
|
||||
{
|
||||
SpeakCannotReach();
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (getDivingGearIfNeeded && !character.LockHands)
|
||||
{
|
||||
Character followTarget = Target as Character;
|
||||
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, AIObjectiveFindDivingGear.lowOxygenThreshold);
|
||||
}
|
||||
else if (needsDivingGear)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
|
||||
}
|
||||
if (needsEquipment)
|
||||
{
|
||||
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, n =>
|
||||
{
|
||||
if (n.Waypoint.isObstructed) { return false; }
|
||||
return (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null);
|
||||
}, endNodeFilter, nodeFilter);
|
||||
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
|
||||
{
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Hull GetTargetHull()
|
||||
{
|
||||
if (Target is Hull h)
|
||||
{
|
||||
return h;
|
||||
}
|
||||
else if (Target is Item i)
|
||||
{
|
||||
return i.CurrentHull;
|
||||
}
|
||||
else if (Target is Character c)
|
||||
{
|
||||
return c.CurrentHull;
|
||||
}
|
||||
else if (Target is Gap g)
|
||||
{
|
||||
return g.FlowTargetHull;
|
||||
}
|
||||
else if (Target is WayPoint wp)
|
||||
{
|
||||
return wp.CurrentHull;
|
||||
}
|
||||
else if (Target is FireSource fs)
|
||||
{
|
||||
return fs.Hull;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsCloseEnough
|
||||
{
|
||||
get
|
||||
{
|
||||
if (character.IsClimbing && SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished)
|
||||
{
|
||||
// Still in ladders and the path is not finished -> don't release
|
||||
return false;
|
||||
}
|
||||
bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
|
||||
if (closeEnough)
|
||||
{
|
||||
closeEnough = !(Target is Character) || Target is Character c && c.CurrentHull == character.CurrentHull;
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (abortCondition != null && abortCondition())
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
if (repeat)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsCloseEnough)
|
||||
{
|
||||
if (requiredCondition == null || requiredCondition())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveIdle : AIObjective
|
||||
{
|
||||
public override string DebugTag => "idle";
|
||||
public override bool UnequipItems => true;
|
||||
|
||||
private readonly float newTargetIntervalMin = 10;
|
||||
private readonly float newTargetIntervalMax = 20;
|
||||
private readonly float standStillMin = 2;
|
||||
private readonly float standStillMax = 10;
|
||||
private readonly float walkDurationMin = 5;
|
||||
private readonly float walkDurationMax = 10;
|
||||
|
||||
private Hull currentTarget;
|
||||
private float newTargetTimer;
|
||||
|
||||
private bool searchingNewHull;
|
||||
|
||||
private float standStillTimer;
|
||||
private float walkDuration;
|
||||
|
||||
private readonly List<Hull> targetHulls = new List<Hull>(20);
|
||||
private readonly List<float> hullWeights = new List<float>(20);
|
||||
|
||||
public AIObjectiveIdle(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
standStillTimer = Rand.Range(-10.0f, 10.0f);
|
||||
walkDuration = Rand.Range(0.0f, 10.0f);
|
||||
}
|
||||
|
||||
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); }
|
||||
|
||||
private float randomTimer;
|
||||
private float randomUpdateInterval = 5;
|
||||
public float Random { get; private set; }
|
||||
|
||||
public void CalculatePriority()
|
||||
{
|
||||
Random = Rand.Range(0.5f, 1.5f);
|
||||
randomTimer = randomUpdateInterval;
|
||||
float max = Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
|
||||
float initiative = character.GetSkillLevel("initiative");
|
||||
Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
|
||||
}
|
||||
|
||||
public override float GetPriority() => Priority;
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (objectiveManager.CurrentObjective == this)
|
||||
{
|
||||
if (randomTimer > 0)
|
||||
{
|
||||
randomTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
CalculatePriority();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (PathSteering == null) { return; }
|
||||
|
||||
//don't keep dragging others when idling
|
||||
if (character.SelectedCharacter != null)
|
||||
{
|
||||
character.DeselectCharacter();
|
||||
}
|
||||
if (!character.IsClimbing)
|
||||
{
|
||||
character.SelectedConstruction = null;
|
||||
}
|
||||
|
||||
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
|
||||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
|
||||
|
||||
if (currentTargetIsInvalid || currentTarget == null && HumanAIController.VisibleHulls.Any(h => IsForbidden(h)))
|
||||
{
|
||||
//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)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
newTargetTimer = 0;
|
||||
}
|
||||
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
|
||||
{
|
||||
// Don't allow new targets when climbing straight up or down
|
||||
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
|
||||
}
|
||||
}
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
|
||||
}
|
||||
}
|
||||
if (newTargetTimer <= 0.0f)
|
||||
{
|
||||
if (!searchingNewHull)
|
||||
{
|
||||
//find all available hulls first
|
||||
FindTargetHulls();
|
||||
searchingNewHull = true;
|
||||
return;
|
||||
}
|
||||
else if (targetHulls.Count > 0)
|
||||
{
|
||||
//choose a random available hull
|
||||
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
|
||||
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;
|
||||
}
|
||||
searchingNewHull = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Couldn't find a target for some reason -> reset
|
||||
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
|
||||
searchingNewHull = false;
|
||||
}
|
||||
|
||||
if (currentTarget != null)
|
||||
{
|
||||
character.AIController.SelectTarget(currentTarget.AiTarget);
|
||||
string errorMsg = null;
|
||||
#if DEBUG
|
||||
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, nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
PathSteering.SetPath(path);
|
||||
}
|
||||
|
||||
newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax);
|
||||
}
|
||||
|
||||
newTargetTimer -= deltaTime;
|
||||
|
||||
//wander randomly
|
||||
// - if reached the end of the path
|
||||
// - if the target is unreachable
|
||||
// - if the path requires going outside
|
||||
if (!character.IsClimbing)
|
||||
{
|
||||
if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
|
||||
(PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
|
||||
{
|
||||
Wander(deltaTime);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTarget != null)
|
||||
{
|
||||
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; }
|
||||
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);
|
||||
}
|
||||
}
|
||||
PathSteering.Wander(deltaTime);
|
||||
}
|
||||
|
||||
private void FindTargetHulls()
|
||||
{
|
||||
targetHulls.Clear();
|
||||
hullWeights.Clear();
|
||||
foreach (var hull in Hull.hullList)
|
||||
{
|
||||
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
|
||||
if (hull.Submarine == null) { continue; }
|
||||
if (hull.Submarine.TeamID != character.TeamID) { continue; }
|
||||
// If the character is inside, only take connected hulls into account.
|
||||
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
|
||||
if (IsForbidden(hull)) { continue; }
|
||||
// Ignore hulls that are too low to stand inside
|
||||
if (character.AnimController is HumanoidAnimController animController)
|
||||
{
|
||||
if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!targetHulls.Contains(hull))
|
||||
{
|
||||
targetHulls.Add(hull);
|
||||
float weight = hull.Volume;
|
||||
// Prefer rooms that are closer. Avoid rooms that are not in the same level.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsForbidden(Hull hull)
|
||||
{
|
||||
if (hull == null) { return true; }
|
||||
string hullName = hull.RoomName;
|
||||
if (hullName == null) { return false; }
|
||||
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class AIObjectiveLoop<T> : AIObjective
|
||||
{
|
||||
public HashSet<T> Targets { get; private set; } = new HashSet<T>();
|
||||
public Dictionary<T, AIObjective> Objectives { get; private set; } = new Dictionary<T, AIObjective>();
|
||||
protected HashSet<T> ignoreList = new HashSet<T>();
|
||||
private float ignoreListTimer;
|
||||
protected float targetUpdateTimer;
|
||||
|
||||
private float syncTimer;
|
||||
private readonly float syncTime = 1;
|
||||
|
||||
// By default, doesn't clear the list automatically
|
||||
protected virtual float IgnoreListClearInterval => 0;
|
||||
|
||||
public HashSet<T> ReportedTargets { get; private set; } = new HashSet<T>();
|
||||
|
||||
public bool AddTarget(T target)
|
||||
{
|
||||
if (character.IsDead) { return false; }
|
||||
if (ReportedTargets.Contains(target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Filter(target))
|
||||
{
|
||||
ReportedTargets.Add(target);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public AIObjectiveLoop(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null)
|
||||
: base(character, objectiveManager, priorityModifier, option) { }
|
||||
|
||||
protected override void Act(float deltaTime) { }
|
||||
protected override bool Check() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool AllowSubObjectiveSorting => true;
|
||||
public virtual bool InverseTargetEvaluation => false;
|
||||
|
||||
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
if (IgnoreListClearInterval > 0)
|
||||
{
|
||||
if (ignoreListTimer > IgnoreListClearInterval)
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
ignoreListTimer += deltaTime;
|
||||
}
|
||||
}
|
||||
if (targetUpdateTimer < 0)
|
||||
{
|
||||
UpdateTargets();
|
||||
}
|
||||
else
|
||||
{
|
||||
targetUpdateTimer -= deltaTime;
|
||||
}
|
||||
if (syncTimer < 0)
|
||||
{
|
||||
syncTimer = syncTime * Rand.Range(0.9f, 1.1f);
|
||||
// Sync objectives, subobjectives and targets
|
||||
foreach (var objective in Objectives)
|
||||
{
|
||||
var target = objective.Key;
|
||||
if (!Targets.Contains(target))
|
||||
{
|
||||
subObjectives.Remove(objective.Value);
|
||||
}
|
||||
}
|
||||
SyncRemovedObjectives(Objectives, GetList());
|
||||
}
|
||||
else
|
||||
{
|
||||
syncTimer -= deltaTime;
|
||||
}
|
||||
if (Objectives.None() && Targets.Any(t => !ignoreList.Contains(t)))
|
||||
{
|
||||
CreateObjectives();
|
||||
}
|
||||
}
|
||||
|
||||
// the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
|
||||
private float SetTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1);
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
ignoreList.Clear();
|
||||
ignoreListTimer = 0;
|
||||
UpdateTargets();
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
if (character.LockHands || character.Submarine == null || Targets.None())
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Allow the target value to be more than 100.
|
||||
float targetValue = TargetEvaluation();
|
||||
if (InverseTargetEvaluation)
|
||||
{
|
||||
targetValue = 100 - targetValue;
|
||||
}
|
||||
var currentSubObjective = CurrentSubObjective;
|
||||
if (currentSubObjective != null && currentSubObjective.Priority > targetValue)
|
||||
{
|
||||
// If the priority is higher than the target value, let's just use it.
|
||||
// The priority calculation is more precise, but it takes into account things like distances,
|
||||
// so it's better not to use it if it's lower than the rougher targetValue.
|
||||
targetValue = Priority;
|
||||
}
|
||||
// If the target value is less than 1% of the max value, let's just treat it as zero.
|
||||
if (targetValue < 1)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
Priority = AIObjectiveManager.OrderPriority;
|
||||
}
|
||||
else
|
||||
{
|
||||
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
|
||||
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
|
||||
Priority = MathHelper.Lerp(0, max, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected void UpdateTargets()
|
||||
{
|
||||
SetTargetUpdateTimer();
|
||||
Targets.Clear();
|
||||
FindTargets();
|
||||
CreateObjectives();
|
||||
}
|
||||
|
||||
protected virtual void FindTargets()
|
||||
{
|
||||
foreach (T target in GetList())
|
||||
{
|
||||
// The bots always find targets when the objective is an order.
|
||||
if (objectiveManager.CurrentOrder != this)
|
||||
{
|
||||
// Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention.
|
||||
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater;
|
||||
if (!ignore && !ReportedTargets.Contains(target)) { continue; }
|
||||
}
|
||||
if (!Filter(target)) { continue; }
|
||||
if (!ignoreList.Contains(target))
|
||||
{
|
||||
Targets.Add(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CreateObjectives()
|
||||
{
|
||||
foreach (T target in Targets)
|
||||
{
|
||||
if (ignoreList.Contains(target)) { continue; }
|
||||
if (!Objectives.TryGetValue(target, out AIObjective objective))
|
||||
{
|
||||
objective = ObjectiveConstructor(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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void OnObjectiveCompleted(AIObjective objective, T target);
|
||||
|
||||
/// <summary>
|
||||
/// List of all possible items of the specified type. Used for filtering the removed objectives.
|
||||
/// </summary>
|
||||
protected abstract IEnumerable<T> GetList();
|
||||
|
||||
protected abstract float TargetEvaluation();
|
||||
|
||||
protected abstract AIObjective ObjectiveConstructor(T target);
|
||||
protected abstract bool Filter(T target);
|
||||
}
|
||||
}
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveManager
|
||||
{
|
||||
// TODO: expose
|
||||
public const float OrderPriority = 70;
|
||||
public const float RunPriority = 50;
|
||||
// Constantly increases the priority of the selected objective, unless overridden
|
||||
public const float baseDevotion = 3;
|
||||
|
||||
public List<AIObjective> Objectives { get; private set; } = new List<AIObjective>();
|
||||
|
||||
private readonly Character character;
|
||||
|
||||
|
||||
private float _waitTimer;
|
||||
/// <summary>
|
||||
/// When set above zero, the character will stand still doing nothing until the timer runs out. Does not affect orders, find safety or combat.
|
||||
/// </summary>
|
||||
public float WaitTimer
|
||||
{
|
||||
get { return _waitTimer; }
|
||||
set
|
||||
{
|
||||
_waitTimer = IsAllowedToWait() ? value : 0;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
/// <summary>
|
||||
/// Returns the last active objective of the specific type.
|
||||
/// </summary>
|
||||
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
|
||||
|
||||
/// <summary>
|
||||
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
|
||||
/// </summary>
|
||||
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
|
||||
|
||||
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
|
||||
|
||||
public AIObjectiveManager(Character character)
|
||||
{
|
||||
this.character = character;
|
||||
CreateAutonomousObjectives();
|
||||
}
|
||||
|
||||
public void AddObjective<T>(T objective) where T : AIObjective
|
||||
{
|
||||
if (objective == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
// 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)
|
||||
{
|
||||
if (Objectives.FirstOrDefault(o => o.GetType() == type) is T existingObjective && existingObjective.IsDuplicate(objective))
|
||||
{
|
||||
Objectives.Remove(existingObjective);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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));
|
||||
int objectiveCount = Objectives.Count;
|
||||
foreach (var automaticOrder in character.Info.Job.Prefab.AutomaticOrders)
|
||||
{
|
||||
var orderPrefab = Order.GetPrefab(automaticOrder.identifier);
|
||||
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); }
|
||||
// TODO: Similar code is used in CrewManager:815-> DRY
|
||||
var matchingItems = orderPrefab.ItemIdentifiers.Any() ?
|
||||
Item.ItemList.FindAll(it => orderPrefab.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(orderPrefab.ItemIdentifiers)) :
|
||||
Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == orderPrefab.ItemComponentType));
|
||||
matchingItems.RemoveAll(it => it.Submarine != character.Submarine);
|
||||
var item = matchingItems.GetRandom();
|
||||
var order = new Order(
|
||||
orderPrefab,
|
||||
item ?? character.CurrentHull as Entity,
|
||||
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType),
|
||||
orderGiver: character);
|
||||
if (order == null) { continue; }
|
||||
var objective = CreateObjective(order, automaticOrder.option, character, automaticOrder.priorityModifier);
|
||||
if (objective != null)
|
||||
{
|
||||
AddObjective(objective, delay: Rand.Value() / 2);
|
||||
objectiveCount++;
|
||||
}
|
||||
}
|
||||
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
|
||||
}
|
||||
|
||||
public void AddObjective<T>(T objective, float delay, Action callback = null) where T : AIObjective
|
||||
{
|
||||
if (objective == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine))
|
||||
{
|
||||
CoroutineManager.StopCoroutines(coroutine);
|
||||
DelayedObjectives.Remove(objective);
|
||||
}
|
||||
coroutine = CoroutineManager.InvokeAfter(() =>
|
||||
{
|
||||
//round ended before the coroutine finished
|
||||
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
|
||||
DelayedObjectives.Remove(objective);
|
||||
AddObjective(objective);
|
||||
callback?.Invoke();
|
||||
}, delay);
|
||||
DelayedObjectives.Add(objective, coroutine);
|
||||
}
|
||||
|
||||
public T GetObjective<T>() where T : AIObjective => Objectives.FirstOrDefault(o => o is T) as T;
|
||||
|
||||
private AIObjective GetCurrentObjective()
|
||||
{
|
||||
var previousObjective = CurrentObjective;
|
||||
var firstObjective = Objectives.FirstOrDefault();
|
||||
if (CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority)
|
||||
{
|
||||
CurrentObjective = CurrentOrder;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentObjective = firstObjective;
|
||||
}
|
||||
if (previousObjective != CurrentObjective)
|
||||
{
|
||||
previousObjective?.OnDeselected();
|
||||
CurrentObjective?.OnSelected();
|
||||
GetObjective<AIObjectiveIdle>().CalculatePriority();
|
||||
}
|
||||
return CurrentObjective;
|
||||
}
|
||||
|
||||
public float GetCurrentPriority()
|
||||
{
|
||||
return CurrentObjective == null ? 0.0f : CurrentObjective.Priority;
|
||||
}
|
||||
|
||||
public void UpdateObjectives(float deltaTime)
|
||||
{
|
||||
CurrentOrder?.Update(deltaTime);
|
||||
if (WaitTimer > 0)
|
||||
{
|
||||
WaitTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < Objectives.Count; i++)
|
||||
{
|
||||
var objective = Objectives[i];
|
||||
if (objective.IsCompleted)
|
||||
{
|
||||
#if DEBUG
|
||||
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($"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red);
|
||||
#endif
|
||||
Objectives.Remove(objective);
|
||||
}
|
||||
else if (objective != CurrentOrder)
|
||||
{
|
||||
objective.Update(deltaTime);
|
||||
}
|
||||
}
|
||||
GetCurrentObjective();
|
||||
}
|
||||
|
||||
public void SortObjectives()
|
||||
{
|
||||
if (Objectives.Any())
|
||||
{
|
||||
Objectives.ForEach(o => o.GetPriority());
|
||||
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
|
||||
}
|
||||
GetCurrentObjective()?.SortSubObjectives();
|
||||
}
|
||||
|
||||
public void DoCurrentObjective(float deltaTime)
|
||||
{
|
||||
if (WaitTimer <= 0)
|
||||
{
|
||||
CurrentObjective?.TryComplete(deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SteeringManager.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOrder(AIObjective objective)
|
||||
{
|
||||
CurrentOrder = objective;
|
||||
}
|
||||
|
||||
public void SetOrder(Order order, string option, Character orderGiver)
|
||||
{
|
||||
CurrentOrder = CreateObjective(order, option, orderGiver);
|
||||
if (CurrentOrder == null)
|
||||
{
|
||||
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
|
||||
CreateAutonomousObjectives();
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentOrder.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1)
|
||||
{
|
||||
if (order == null) { return null; }
|
||||
AIObjective newObjective;
|
||||
switch (order.Identifier.ToLowerInvariant())
|
||||
{
|
||||
case "follow":
|
||||
if (orderGiver == null) { return null; }
|
||||
newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
|
||||
{
|
||||
CloseEnough = 100,
|
||||
AllowGoingOutside = true,
|
||||
IgnoreIfTargetDead = true,
|
||||
followControlledCharacter = orderGiver == character,
|
||||
mimic = true,
|
||||
DialogueIdentifier = "dialogcannotreachplace"
|
||||
};
|
||||
break;
|
||||
case "wait":
|
||||
newObjective = new AIObjectiveGoTo(character, character, this, repeat: true, priorityModifier: priorityModifier)
|
||||
{
|
||||
AllowGoingOutside = character.CurrentHull == null
|
||||
};
|
||||
break;
|
||||
case "fixleaks":
|
||||
newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier);
|
||||
break;
|
||||
case "chargebatteries":
|
||||
newObjective = new AIObjectiveChargeBatteries(character, this, option, priorityModifier);
|
||||
break;
|
||||
case "rescue":
|
||||
newObjective = new AIObjectiveRescueAll(character, this, priorityModifier);
|
||||
break;
|
||||
case "repairsystems":
|
||||
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier)
|
||||
{
|
||||
RequireAdequateSkills = option == "jobspecific"
|
||||
};
|
||||
break;
|
||||
case "pumpwater":
|
||||
newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier);
|
||||
break;
|
||||
case "extinguishfires":
|
||||
newObjective = new AIObjectiveExtinguishFires(character, this, priorityModifier);
|
||||
break;
|
||||
case "fightintruders":
|
||||
newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier);
|
||||
break;
|
||||
case "steer":
|
||||
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,
|
||||
// Don't override unless it's an order by a player
|
||||
Override = orderGiver != null && orderGiver.IsPlayer
|
||||
};
|
||||
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,
|
||||
// Don't override unless it's an order by a player
|
||||
Override = orderGiver != null && orderGiver.IsPlayer
|
||||
};
|
||||
break;
|
||||
}
|
||||
return newObjective;
|
||||
}
|
||||
|
||||
private bool IsAllowedToWait()
|
||||
{
|
||||
if (CurrentOrder != null) { return false; }
|
||||
if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; }
|
||||
if (character.AnimController.InWater) { return false; }
|
||||
if (character.IsClimbing) { return false; }
|
||||
if (character.AIController is HumanAIController humanAI)
|
||||
{
|
||||
if (humanAI.UnsafeHulls.Contains(character.CurrentHull)) { return false; }
|
||||
}
|
||||
if (AIObjectiveIdle.IsForbidden(character.CurrentHull)) { return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveOperateItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "operate item";
|
||||
public override bool UnequipItems => true;
|
||||
|
||||
private ItemComponent component, controller;
|
||||
private Entity operateTarget;
|
||||
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 ItemComponent GetTarget() => useController ? controller : component;
|
||||
|
||||
public Func<bool> completionCondition;
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
if (component.Item.ConditionPercentage <= 0)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
Priority = AIObjectiveManager.OrderPriority;
|
||||
}
|
||||
if (component.Item.CurrentHull == null || component.Item.CurrentHull.FireSources.None() || IsOperatedByAnother(GetTarget()))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
|
||||
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
|
||||
Priority = MathHelper.Clamp(value, 0, max);
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1)
|
||||
: base (character, objectiveManager, priorityModifier, option)
|
||||
{
|
||||
this.component = item ?? throw new System.ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
|
||||
this.requireEquip = requireEquip;
|
||||
this.operateTarget = operateTarget;
|
||||
this.useController = useController;
|
||||
if (useController)
|
||||
{
|
||||
//try finding the controller with the simpler non-recursive method first
|
||||
controller =
|
||||
component.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
|
||||
component.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsOperatedByAnother(ItemComponent target)
|
||||
{
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
if (c == character) { continue; }
|
||||
if (!HumanAIController.IsFriendly(c)) { continue; }
|
||||
if (c.SelectedConstruction != target.Item) { continue; }
|
||||
// If the other character is player, don't try to operate
|
||||
if (c.IsRemotePlayer || Character.Controlled == c) { return true; }
|
||||
if (c.AIController is HumanAIController humanAi)
|
||||
{
|
||||
// If the other character is ordered to operate the item, let him do it
|
||||
if (humanAi.ObjectiveManager.IsCurrentOrder<AIObjectiveOperateItem>())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (target is Steering)
|
||||
{
|
||||
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
|
||||
return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm");
|
||||
}
|
||||
else
|
||||
{
|
||||
return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Shouldn't go here, unless we allow non-humans to operate items
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (character.LockHands)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
ItemComponent target = GetTarget();
|
||||
if (useController && controller == null)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
// Don't allow to operate an item that someone with a better skills already operates, unless this is an order
|
||||
if (objectiveManager.CurrentOrder != this && IsOperatedByAnother(target))
|
||||
{
|
||||
// Don't abandon
|
||||
return;
|
||||
}
|
||||
if (target.CanBeSelected)
|
||||
{
|
||||
if (character.CanInteractWith(target.Item, out _, checkLinked: false))
|
||||
{
|
||||
HumanAIController.FaceTarget(target.Item);
|
||||
if (character.SelectedConstruction != target.Item)
|
||||
{
|
||||
target.Item.TryInteract(character, false, true);
|
||||
}
|
||||
if (component.AIOperate(deltaTime, character, this))
|
||||
{
|
||||
IsCompleted = completionCondition == null || completionCondition();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = target.Item.Name
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
return;
|
||||
}
|
||||
else if (!character.Inventory.Items.Contains(component.Item))
|
||||
{
|
||||
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref getItemObjective));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (requireEquip && !character.HasEquippedItem(component.Item))
|
||||
{
|
||||
//the item has to be equipped before using it if it's holdable
|
||||
var holdable = component.Item.GetComponent<Holdable>();
|
||||
if (holdable == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveOperateItem failed - equipping item " + component.Item + " is required but the item has no Holdable component");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < character.Inventory.Capacity; i++)
|
||||
{
|
||||
if (character.Inventory.SlotTypes[i] == InvSlotType.Any || !holdable.AllowedSlots.Any(s => s.HasFlag(character.Inventory.SlotTypes[i])))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//equip slot already taken
|
||||
if (character.Inventory.Items[i] != null)
|
||||
{
|
||||
//try to put the item in an Any slot, and drop it if that fails
|
||||
if (!character.Inventory.Items[i].AllowedSlots.Contains(InvSlotType.Any) ||
|
||||
!character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
character.Inventory.Items[i].Drop(character);
|
||||
}
|
||||
}
|
||||
if (character.Inventory.TryPutItem(component.Item, i, true, false, character))
|
||||
{
|
||||
component.Item.Equip(character);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (component.AIOperate(deltaTime, character, this))
|
||||
{
|
||||
IsCompleted = completionCondition == null || completionCondition();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check() => IsCompleted && !IsLoop;
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectivePumpWater : AIObjectiveLoop<Pump>
|
||||
{
|
||||
public override string DebugTag => "pump water";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool UnequipItems => true;
|
||||
|
||||
private IEnumerable<Pump> pumpList;
|
||||
|
||||
public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier, option) { }
|
||||
|
||||
protected override void FindTargets()
|
||||
{
|
||||
if (Option == null) { return; }
|
||||
base.FindTargets();
|
||||
}
|
||||
|
||||
protected override bool Filter(Pump pump)
|
||||
{
|
||||
if (pump == null) { return false; }
|
||||
if (pump.Item.HasTag("ballast")) { return false; }
|
||||
if (pump.Item.Submarine == null) { return false; }
|
||||
if (pump.Item.CurrentHull == null) { return false; }
|
||||
if (pump.Item.Submarine.TeamID != character.TeamID) { return false; }
|
||||
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) && HumanAIController.IsActive(c))) { return false; }
|
||||
if (IsReady(pump)) { return false; }
|
||||
return true;
|
||||
}
|
||||
protected override IEnumerable<Pump> GetList()
|
||||
{
|
||||
if (pumpList == null)
|
||||
{
|
||||
if (character == null || character.Submarine == null) { return new Pump[0]; }
|
||||
pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent<Pump>()).Where(p => p != null);
|
||||
}
|
||||
return pumpList;
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
if (Option == "stoppumping")
|
||||
{
|
||||
return Targets.Max(t => MathHelper.Lerp(0, 100, Math.Abs(t.FlowPercentage / 100)));
|
||||
}
|
||||
else
|
||||
{
|
||||
return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(-t.FlowPercentage / 100)));
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
completionCondition = () => IsReady(pump)
|
||||
};
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Pump target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectivePumpWater, Pump>(character, target);
|
||||
}
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using FarseerPhysics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRepairItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "repair item";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
|
||||
public Item Item { get; private set; }
|
||||
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private AIObjectiveContainItem refuelObjective;
|
||||
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;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
// TODO: priority list?
|
||||
// Ignore items that are being repaired by someone else.
|
||||
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
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 = IsRepairing ? 50 : 0;
|
||||
float devotion = (CumulatedDevotion + isSelected) / 100;
|
||||
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (damagePriority * distanceFactor * successFactor * PriorityModifier), 0, 1));
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
{
|
||||
IsCompleted = Item.IsFullCondition;
|
||||
if (IsCompleted && IsRepairing)
|
||||
{
|
||||
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
|
||||
}
|
||||
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))
|
||||
{
|
||||
//make sure we have all the items required to fix the target item
|
||||
foreach (var kvp in repairable.requiredItems)
|
||||
{
|
||||
foreach (RelatedItem requiredItem in kvp.Value)
|
||||
{
|
||||
subObjectives.Add(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (repairTool == null)
|
||||
{
|
||||
FindRepairTool();
|
||||
}
|
||||
if (repairTool != null)
|
||||
{
|
||||
var containedItems = repairTool.Item.ContainedItems;
|
||||
if (containedItems == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
|
||||
#endif
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
// Drop empty tanks
|
||||
foreach (Item containedItem in containedItems)
|
||||
{
|
||||
if (containedItem == null) { continue; }
|
||||
if (containedItem.Condition <= 0.0f)
|
||||
{
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
}
|
||||
RelatedItem item = null;
|
||||
Item fuel = null;
|
||||
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
|
||||
{
|
||||
item = requiredItem;
|
||||
fuel = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
|
||||
if (fuel != null) { break; }
|
||||
}
|
||||
if (fuel == null)
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
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);
|
||||
}
|
||||
foreach (Repairable repairable in Item.Repairables)
|
||||
{
|
||||
if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
|
||||
{
|
||||
// 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 (character.SelectedConstruction != Item)
|
||||
{
|
||||
if (!Item.TryInteract(character, true, true))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
if (previousCondition == -1)
|
||||
{
|
||||
previousCondition = Item.Condition;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (Abandon)
|
||||
{
|
||||
if (IsRepairing)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
|
||||
}
|
||||
repairable.StopRepairing(character);
|
||||
}
|
||||
else if (repairable.CurrentFixer != character)
|
||||
{
|
||||
repairable.StartRepairing(character, Repairable.FixActions.Repair);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveSubObjective(ref refuelObjective);
|
||||
// If cannot reach the item, approach it.
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
constructor: () =>
|
||||
{
|
||||
previousCondition = -1;
|
||||
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
|
||||
{
|
||||
// Don't stop in ladders, because we can't interact with other items while holding the ladders.
|
||||
endNodeFilter = node => node.Waypoint.Ladders == null
|
||||
};
|
||||
if (repairTool != null)
|
||||
{
|
||||
objective.CloseEnough = repairTool.Range * 0.75f;
|
||||
}
|
||||
return objective;
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
if (IsRepairing)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void FindRepairTool()
|
||||
{
|
||||
foreach (Repairable repairable in Item.Repairables)
|
||||
{
|
||||
foreach (var kvp in repairable.requiredItems)
|
||||
{
|
||||
foreach (RelatedItem requiredItem in kvp.Value)
|
||||
{
|
||||
foreach (var item in character.Inventory.Items)
|
||||
{
|
||||
if (requiredItem.MatchesItem(item))
|
||||
{
|
||||
repairTool = item.GetComponent<RepairTool>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OperateRepairTool(float deltaTime)
|
||||
{
|
||||
character.CursorPosition = Item.Position;
|
||||
if (repairTool.Item.RequireAimToUse)
|
||||
{
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
}
|
||||
Vector2 fromToolToTarget = Item.Position - repairTool.Item.Position;
|
||||
if (fromToolToTarget.LengthSquared() < MathUtils.Pow(repairTool.Range / 2, 2))
|
||||
{
|
||||
// Too close -> steer away
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - Item.SimPosition) / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SteeringManager.Reset();
|
||||
}
|
||||
if (VectorExtensions.Angle(VectorExtensions.Forward(repairTool.Item.body.TransformedRotation), fromToolToTarget) < MathHelper.PiOver4)
|
||||
{
|
||||
repairTool.Use(deltaTime, character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRepairItems : AIObjectiveLoop<Item>
|
||||
{
|
||||
public override string DebugTag => "repair items";
|
||||
|
||||
/// <summary>
|
||||
/// Should the character only attempt to fix items they have the skills to fix, or any damaged item
|
||||
/// </summary>
|
||||
public bool RequireAdequateSkills;
|
||||
|
||||
public override bool AllowMultipleInstances => true;
|
||||
|
||||
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()
|
||||
{
|
||||
foreach (var item in Targets)
|
||||
{
|
||||
foreach (Repairable repairable in item.Repairables)
|
||||
{
|
||||
if (!Objectives.TryGetValue(item, out AIObjective objective))
|
||||
{
|
||||
objective = ObjectiveConstructor(item);
|
||||
Objectives.Add(item, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Filter(Item item)
|
||||
{
|
||||
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) && HumanAIController.IsActive(c))) { return false; }
|
||||
if (!Objectives.ContainsKey(item))
|
||||
{
|
||||
if (item != character.SelectedConstruction)
|
||||
{
|
||||
float condition = item.ConditionPercentage;
|
||||
if (item.Repairables.All(r => condition >= r.AIRepairThreshold)) { return false; }
|
||||
}
|
||||
}
|
||||
if (RequireAdequateSkills)
|
||||
{
|
||||
if (item.Repairables.Any(r => !r.HasRequiredSkills(character))) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
if (character.SelectedConstruction != null && Targets.Any(t => character.SelectedConstruction == t && t.ConditionPercentage < 100))
|
||||
{
|
||||
// Don't stop fixing until done
|
||||
return 100;
|
||||
}
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>(), onlyBots: true);
|
||||
int items = Targets.Count;
|
||||
bool anyFixers = otherFixers > 0;
|
||||
float ratio = anyFixers ? items / (float)otherFixers : 1;
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
|
||||
{
|
||||
// Enough fixers
|
||||
return 0;
|
||||
}
|
||||
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<Item> GetList() => Item.ItemList;
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Item item)
|
||||
=> new AIObjectiveRepairItem(character, item, objectiveManager, PriorityModifier);
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveRepairItems, Item>(character, target);
|
||||
|
||||
public static bool IsValidTarget(Item item, Character character)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (item.IsFullCondition) { return false; }
|
||||
if (item.CurrentHull == null) { return false; }
|
||||
if (item.Submarine == null) { return false; }
|
||||
if (item.Submarine.TeamID != character.TeamID) { return false; }
|
||||
if (item.Repairables.None()) { return false; }
|
||||
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRescue : AIObjective
|
||||
{
|
||||
public override string DebugTag => "rescue";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
|
||||
const float TreatmentDelay = 0.5f;
|
||||
|
||||
const float CloseEnoughToTreat = 100.0f;
|
||||
|
||||
private readonly Character targetCharacter;
|
||||
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private AIObjectiveGetItem getItemObjective;
|
||||
private float treatmentTimer;
|
||||
private Hull safeHull;
|
||||
|
||||
public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
if (targetCharacter == null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
this.targetCharacter = targetCharacter;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (targetCharacter.SelectedBy != null && targetCharacter.SelectedBy != character)
|
||||
{
|
||||
var otherCharacter = character.SelectedBy;
|
||||
if (otherCharacter != null)
|
||||
{
|
||||
// Someone else is rescuing/holding the target.
|
||||
Abandon = otherCharacter.IsPlayer || character.GetSkillLevel("medical") < otherCharacter.GetSkillLevel("medical");
|
||||
}
|
||||
}
|
||||
|
||||
if (targetCharacter != character)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient",
|
||||
TargetName = targetCharacter.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
Abandon = true;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
character.SelectCharacter(targetCharacter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Drag the character into safety
|
||||
if (safeHull == null)
|
||||
{
|
||||
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 = character.CurrentHull;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subObjectives.Any()) { return; }
|
||||
|
||||
if (targetCharacter != character && !character.CanInteractWith(targetCharacter))
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
// Go to the target and select it
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient",
|
||||
TargetName = targetCharacter.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
Abandon = true;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can start applying treatment
|
||||
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 }),
|
||||
null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f);
|
||||
|
||||
character.SelectCharacter(targetCharacter);
|
||||
}
|
||||
GiveTreatment(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
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 (!targetCharacter.IsPlayer)
|
||||
{
|
||||
// If the target is a bot, don't let it move
|
||||
targetCharacter.AIController.SteeringManager.Reset();
|
||||
}
|
||||
if (treatmentTimer > 0.0f)
|
||||
{
|
||||
treatmentTimer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
treatmentTimer = TreatmentDelay;
|
||||
|
||||
//find which treatments are the most suitable to treat the character's current condition
|
||||
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false);
|
||||
|
||||
//check if we already have a suitable treatment for any of the afflictions
|
||||
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
|
||||
{
|
||||
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
|
||||
{
|
||||
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f)
|
||||
{
|
||||
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
|
||||
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
|
||||
{
|
||||
itemNameList.Clear();
|
||||
suitableItemIdentifiers.Clear();
|
||||
foreach (KeyValuePair<string, float> treatmentSuitability in currentTreatmentSuitabilities)
|
||||
{
|
||||
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 (itemNameList.Count > 0)
|
||||
{
|
||||
string itemListStr = "";
|
||||
if (itemNameList.Count == 1)
|
||||
{
|
||||
itemListStr = itemNameList[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
if (character != targetCharacter)
|
||||
{
|
||||
character.AnimController.Anim = AnimController.Animation.CPR;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyTreatment(Affliction affliction, Item item)
|
||||
{
|
||||
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
|
||||
bool remove = false;
|
||||
foreach (ItemComponent ic in item.Components)
|
||||
{
|
||||
if (!ic.HasRequiredContainedItems(user: character, addMessage: false)) { continue; }
|
||||
#if CLIENT
|
||||
ic.PlaySound(ActionType.OnUse, character);
|
||||
#endif
|
||||
ic.WasUsed = true;
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb);
|
||||
if (ic.DeleteOnUse)
|
||||
{
|
||||
remove = true;
|
||||
}
|
||||
}
|
||||
if (remove)
|
||||
{
|
||||
Entity.Spawner?.AddToRemoveQueue(item);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
{
|
||||
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
// 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, character, targetCharacter);
|
||||
if (isCompleted && targetCharacter != character)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
|
||||
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
|
||||
}
|
||||
return isCompleted;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
{
|
||||
if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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.1f, MathUtils.InverseLerp(0, 5000, dist));
|
||||
if (targetCharacter.CurrentHull == character.CurrentHull)
|
||||
{
|
||||
distanceFactor = 1;
|
||||
}
|
||||
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRescueAll : AIObjectiveLoop<Character>
|
||||
{
|
||||
public override string DebugTag => "rescue all";
|
||||
public override bool ForceRun => true;
|
||||
public override bool InverseTargetEvaluation => true;
|
||||
|
||||
private const float vitalityThreshold = 80;
|
||||
private const float vitalityThresholdForOrders = 100;
|
||||
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
|
||||
{
|
||||
if (manager == null)
|
||||
{
|
||||
return vitalityThreshold;
|
||||
}
|
||||
else
|
||||
{
|
||||
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Filter(Character target) => IsValidTarget(target, character);
|
||||
|
||||
protected override IEnumerable<Character> GetList() => Character.CharacterList;
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
int otherRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRescueAll>(), onlyBots: true);
|
||||
int targetCount = Targets.Count;
|
||||
bool anyRescuers = otherRescuers > 0;
|
||||
float ratio = anyRescuers ? targetCount / (float)otherRescuers : 1;
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
return Targets.Min(t => GetVitalityFactor(t)) / ratio;
|
||||
}
|
||||
else
|
||||
{
|
||||
float multiplier = 1;
|
||||
if (anyRescuers)
|
||||
{
|
||||
float mySkill = character.GetSkillLevel("medical");
|
||||
int betterRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.Character.Info.Job.GetSkillLevel("medical") >= mySkill, onlyBots: true);
|
||||
if (targetCount / (float)betterRescuers <= 1)
|
||||
{
|
||||
// Enough rescuers
|
||||
return 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool foundOtherMedics = HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.Info.Job.Prefab.Identifier == "medicaldoctor");
|
||||
if (foundOtherMedics)
|
||||
{
|
||||
if (character.Info.Job.Prefab.Identifier != "medicaldoctor")
|
||||
{
|
||||
// Double the vitality factor -> less likely to take action
|
||||
multiplier = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Targets.Min(t => GetVitalityFactor(t)) / ratio * multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
public static float GetVitalityFactor(Character character)
|
||||
{
|
||||
float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0);
|
||||
return Math.Clamp(vitality, 0, 100);
|
||||
}
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Character target)
|
||||
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
|
||||
|
||||
public static bool IsValidTarget(Character target, Character character)
|
||||
{
|
||||
if (target == null || target.IsDead || target.Removed) { return false; }
|
||||
if (!HumanAIController.IsFriendly(character, target)) { return false; }
|
||||
if (character.AIController is HumanAIController humanAI)
|
||||
{
|
||||
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
|
||||
if (!humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveRescueAll>())
|
||||
{
|
||||
// Ignore unsafe hulls, unless ordered
|
||||
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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; }
|
||||
if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
|
||||
{
|
||||
// Ignore all concious targets that are currently fighting, fleeing or treating characters
|
||||
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
|
||||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
|
||||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user