v0.13.0.11

This commit is contained in:
Joonas Rikkonen
2021-04-22 17:33:08 +03:00
parent 0697d7fc64
commit 8bb31f2893
391 changed files with 17271 additions and 5949 deletions
@@ -68,7 +68,7 @@ namespace Barotrauma
if (_abandon)
{
#if DEBUG
if (HumanAIController.debugai && objectiveManager.CurrentOrder == this)
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
{
throw new Exception("Order abandoned!");
}
@@ -230,7 +230,7 @@ namespace Barotrauma
/// </summary>
public virtual float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
{
Priority = 0;
@@ -239,7 +239,7 @@ namespace Barotrauma
}
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
Priority = objectiveManager.GetOrderPriority(this);
}
else
{
@@ -261,7 +261,7 @@ namespace Barotrauma
public virtual void Update(float deltaTime)
{
if (objectiveManager.CurrentOrder != this && objectiveManager.WaitTimer <= 0)
if (!objectiveManager.IsOrder(this) && objectiveManager.WaitTimer <= 0)
{
UpdateDevotion(deltaTime);
}
@@ -430,7 +430,7 @@ namespace Barotrauma
subObjectives.Remove(subObjective);
if (AbandonWhenCannotCompleteSubjectives)
{
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
Reset();
}
@@ -64,7 +64,7 @@ namespace Barotrauma
private bool IsReady(PowerContainer battery)
{
if (battery.HasBeenTuned && character.CurrentOrder == null) { return true; }
if (battery.HasBeenTuned && character.IsDismissed) { return true; }
if (Option == "charge")
{
return battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio;
@@ -79,7 +79,7 @@ namespace Barotrauma
new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
Override = !character.IsDismissed,
completionCondition = () => IsReady(battery)
};
@@ -48,21 +48,35 @@ namespace Barotrauma
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
float devotion = (CumulatedDevotion + selectedBonus) / 100;
float reduction = IsPriority ? 1 : isSelected ? 2 : 3;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
float max = AIObjectiveManager.LowestOrderPriority - reduction;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
if (decontainObjective == null)
{
// Halve the priority until there's a decontain objective (a valid container was found).
Priority /= 2;
}
}
return Priority;
}
protected override void Act(float deltaTime)
{
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
if (item.IgnoreByAI)
{
Abandon = true;
return;
}
if (item.ParentInventory != null)
{
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrders()))
{
// Target was picked up or moved by someone.
Abandon = true;
return;
}
}
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{
itemIndex = 0;
@@ -79,6 +93,7 @@ namespace Barotrauma
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{
Equip = equip,
TakeWholeStack = true,
DropIfFails = true
},
onCompleted: () =>
@@ -125,5 +140,13 @@ namespace Barotrauma
itemIndex = 0;
decontainObjective = null;
}
public void DropTarget()
{
if (item != null && character.HasItem(item))
{
item.Drop(character);
}
}
}
}
@@ -2,6 +2,7 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System;
namespace Barotrauma
{
@@ -29,7 +30,21 @@ namespace Barotrauma
this.prioritizedItems.AddRange(prioritizedItems.Where(i => i != null));
}
protected override float TargetEvaluation() => Targets.Any() ? (objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : AIObjectiveManager.RunPriority - 1) : 0;
protected override float TargetEvaluation()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
{
float prio = objectiveManager.GetOrderPriority(this);
if (subObjectives.All(so => so.SubObjectives.None()))
{
// If none of the subobjectives have subobjectives, no valid container was found. In this case, let's reduce the priority below the run threshold.
prio = Math.Min(prio, AIObjectiveManager.RunPriority - 1);
}
return prio;
}
return AIObjectiveManager.RunPriority - 0.5f;
}
protected override bool Filter(Item target)
{
@@ -65,10 +80,10 @@ namespace Barotrauma
return true;
}
public static bool IsValidContainer(Item item, Character character) =>
!item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character);
public static bool IsValidContainer(Item item, Character character, bool allowUnloading = true) =>
!item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && allowUnloading && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character);
public static bool IsValidTarget(Item item, Character character, bool checkInventory)
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
{
if (item == null) { return false; }
if (item.IgnoreByAI) { return false; }
@@ -76,7 +91,7 @@ namespace Barotrauma
if (item.SpawnedInOutpost) { return false; }
if (item.ParentInventory != null)
{
if (item.Container == null || !IsValidContainer(item.Container, character)) { return false; }
if (item.Container == null || !IsValidContainer(item.Container, character, allowUnloading)) { return false; }
}
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
var pickable = item.GetComponent<Pickable>();
@@ -127,5 +142,17 @@ namespace Barotrauma
}
return canEquip;
}
public override void OnDeselected()
{
base.OnDeselected();
foreach (var subObjective in SubObjectives)
{
if (subObjective is AIObjectiveCleanupItem cleanUpObjective)
{
cleanUpObjective.DropTarget();
}
}
}
}
}
@@ -30,6 +30,7 @@ namespace Barotrauma
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
@@ -79,11 +80,18 @@ namespace Barotrauma
private float coolDownTimer;
private IEnumerable<Body> myBodies;
private float aimTimer;
private float reloadTimer;
private float spreadTimer;
private bool canSeeTarget;
private float visibilityCheckTimer;
private readonly float visibilityCheckInterval = 0.2f;
private float sqrDistance;
private readonly float maxDistance = 2000;
private readonly float distanceCheckInterval = 0.2f;
private float distanceTimer;
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
@@ -108,8 +116,12 @@ namespace Barotrauma
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool TargetEliminated => Enemy == null || Enemy.Removed || Enemy.IsUnconscious;
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious;
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private float AimSpeed => HumanAIController.AimSpeed;
private float AimAccuracy => HumanAIController.AimAccuracy;
private bool EnemyIsClose() => Enemy != null && character.CurrentHull != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
@@ -136,6 +148,8 @@ namespace Barotrauma
{
Mode = CombatMode.Retreat;
}
spreadTimer = Rand.Range(-10, 10);
HumanAIController.SortTimer = 0;
}
public override float GetPriority()
@@ -159,6 +173,10 @@ namespace Barotrauma
base.Update(deltaTime);
ignoreWeaponTimer -= deltaTime;
checkWeaponsTimer -= deltaTime;
if (reloadTimer > 0)
{
reloadTimer -= deltaTime;
}
if (ignoreWeaponTimer < 0)
{
ignoredWeapons.Clear();
@@ -168,17 +186,25 @@ namespace Barotrauma
{
findSafety.Priority = 0;
}
if (!character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
{
distanceTimer -= deltaTime;
if (distanceTimer < 0)
{
distanceTimer = distanceCheckInterval;
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
}
}
}
protected override bool Check()
{
if (IsOffensiveOrArrest && Mode != initialMode)
if (sqrDistance > maxDistance * maxDistance)
{
Abandon = true;
SteeringManager.Reset();
return false;
// The target escaped from us.
return true;
}
return IsEnemyDisabled || (!IsOffensiveOrArrest && coolDownTimer <= 0);
return IsEnemyDisabled || (AllowCoolDown && coolDownTimer <= 0);
}
protected override void Act(float deltaTime)
@@ -186,10 +212,9 @@ namespace Barotrauma
if (abortCondition != null && abortCondition())
{
Abandon = true;
SteeringManager.Reset();
return;
}
if (!IsOffensiveOrArrest)
if (AllowCoolDown)
{
coolDownTimer -= deltaTime;
}
@@ -199,7 +224,11 @@ namespace Barotrauma
{
OperateWeapon(deltaTime);
}
if (!HoldPosition && seekAmmunitionObjective == null && seekWeaponObjective == null)
if (HoldPosition)
{
SteeringManager.Reset();
}
else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
Move(deltaTime);
}
@@ -431,7 +460,7 @@ namespace Barotrauma
priority /= 2;
}
}
if (Enemy.Stun > 1)
if (Enemy.IsKnockedDown)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
@@ -621,7 +650,7 @@ namespace Barotrauma
var slots = Weapon.AllowedSlots.Where(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);
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
}
else
{
@@ -704,15 +733,12 @@ namespace Barotrauma
{
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = Enemy.DisplayName
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false
},
onAbandon: () =>
{
Abandon = true;
SteeringManager.Reset();
});
onAbandon: () => Abandon = true);
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.Stun > 2)
if (Mode == CombatMode.Arrest && (Enemy.Stun > 1 || Enemy.IsKnockedDown))
{
if (HumanAIController.HasItem(character, "handlocker", out _))
{
@@ -720,8 +746,8 @@ namespace Barotrauma
{
arrestingRegistered = true;
followTargetObjective.Completed += OnArrestTargetReached;
followTargetObjective.CloseEnough = 100;
}
followTargetObjective.CloseEnough = 100;
}
else
{
@@ -737,7 +763,7 @@ namespace Barotrauma
SteeringManager.Reset();
}
}
if (followTargetObjective != null)
if (!arrestingRegistered && followTargetObjective != null)
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 1000 :
@@ -760,7 +786,7 @@ namespace Barotrauma
private void OnArrestTargetReached()
{
if (HumanAIController.HasItem(character, "handlocker", out IEnumerable<Item> matchingItems) && Enemy.Stun > 0 && character.CanInteractWith(Enemy))
if (HumanAIController.HasItem(character, "handlocker", out IEnumerable<Item> matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy))
{
var handCuffs = matchingItems.First();
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true))
@@ -780,8 +806,8 @@ namespace Barotrauma
}
}
character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
IsCompleted = true;
}
IsCompleted = true;
}
/// <summary>
@@ -818,25 +844,7 @@ namespace Barotrauma
if (WeaponComponent == null) { return false; }
if (Weapon.OwnInventory == null) { return true; }
// Eject empty ammo
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
{
foreach (Item containedItem in Weapon.OwnInventory.AllItemsMod)
{
if (containedItem.Condition <= 0)
{
if (character.Submarine == null)
{
// If we are outside of main sub, try to put the ammo in the inventory instead dropping it in the sea.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
{
continue;
}
}
containedItem.Drop(character);
}
}
}
HumanAIController.UnequipEmptyItems(Weapon);
RelatedItem item = null;
Item ammunition = null;
string[] ammunitionIdentifiers = null;
@@ -869,22 +877,13 @@ namespace Barotrauma
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
if (!container.Inventory.TryPutItem(ammunition, null))
{
if (!container.Inventory.CanBePut(ammunition))
{
return false;
}
character.Inventory.RemoveItem(ammunition);
if (!container.Inventory.TryPutItem(ammunition, null))
if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
}
else
{
container.Combine(ammunition, character);
}
}
}
}
@@ -902,6 +901,15 @@ namespace Barotrauma
private void Attack(float deltaTime)
{
character.CursorPosition = Enemy.WorldPosition;
if (AimAccuracy < 1)
{
spreadTimer += deltaTime * Rand.Range(0.01f, 1f);
float shake = Rand.Range(0.95f, 1.05f);
float offsetAmount = (1 - AimAccuracy) * Rand.Range(300f, 500f);
float distanceFactor = MathUtils.InverseLerp(0, 1000 * 1000, sqrDistance);
float offset = (float)Math.Sin(spreadTimer * shake) * offsetAmount * distanceFactor;
character.CursorPosition += new Vector2(0, offset);
}
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
@@ -912,7 +920,11 @@ namespace Barotrauma
canSeeTarget = character.CanSeeTarget(Enemy);
visibilityCheckTimer = visibilityCheckInterval;
}
if (!canSeeTarget) { return; }
if (!canSeeTarget)
{
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
return;
}
if (Weapon.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
@@ -928,7 +940,15 @@ namespace Barotrauma
aimTimer -= deltaTime;
return;
}
if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 1) { return; }
if (reloadTimer > 0) { return; }
if (Mode == CombatMode.Arrest)
{
// If the target is arrested or if it's stunned and we can't lock the target up, consider the objective done.
if (Enemy.IsKnockedDown && !HumanAIController.HasItem(character, "handlocker", out _, requireEquipped: false) || HumanAIController.HasItem(Enemy, "handlocker", out _, requireEquipped: true))
{
IsCompleted = true;
}
}
if (holdFireCondition != null && holdFireCondition()) { return; }
float sqrDist = Vector2.DistanceSquared(character.Position, Enemy.Position);
if (WeaponComponent is MeleeWeapon meleeWeapon)
@@ -963,14 +983,12 @@ namespace Barotrauma
}
if (closeEnough)
{
SteeringManager.Reset();
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
UseWeapon(deltaTime);
}
else if (!character.IsFacing(Enemy.WorldPosition))
{
// Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around.
aimTimer = Rand.Range(1f, 1.5f);
aimTimer = Rand.Range(1f, 1.5f) / AimSpeed;
}
}
else
@@ -979,14 +997,15 @@ namespace Barotrauma
{
if (sqrDist > repairTool.Range * repairTool.Range) { return; }
}
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4)
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor)
{
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);
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true);
if (pickedBody != null)
{
Character target = null;
@@ -1000,31 +1019,62 @@ namespace Barotrauma
}
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);
UseWeapon(deltaTime);
}
}
}
}
}
private void UseWeapon(float deltaTime)
{
// Never allow to attack characters with deadly weapons while trying to arrest.
if (Mode == CombatMode.Arrest && isLethalWeapon) { return; }
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
// If the weapon is just equipped, we can't shoot just yet.
if (rangedWeapon.ReloadTimer <= 0)
{
reloadTime = rangedWeapon.Reload;
}
}
if (WeaponComponent is MeleeWeapon mw)
{
if (!((HumanoidAnimController)character.AnimController).Crouching)
{
reloadTime = mw.Reload;
}
}
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
}
private bool ShouldUnequipWeapon =>
Weapon != null &&
character.Submarine != null &&
character.Submarine.TeamID == character.TeamID &&
Character.CharacterList.None(c => c.Submarine == character.Submarine && HumanAIController.IsActive(c) && !HumanAIController.IsFriendly(character, c) && HumanAIController.VisibleHulls.Contains(c.CurrentHull));
protected override void OnCompleted()
{
base.OnCompleted();
if (Weapon != null)
if (ShouldUnequipWeapon)
{
Unequip();
}
SteeringManager.Reset();
}
protected override void OnAbandon()
{
base.OnAbandon();
if (ShouldUnequipWeapon)
{
Unequip();
}
SteeringManager.Reset();
}
public override void Reset()
@@ -34,6 +34,10 @@ namespace Barotrauma
public float ConditionLevel { get; set; } = 1;
public bool Equip { get; set; }
public bool RemoveEmpty { get; set; } = true;
public bool RemoveExisting { get; set; }
public bool MoveWholeStack { get; set; }
public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -102,47 +106,38 @@ namespace Barotrauma
}
if (character.CanInteractWith(container.Item, checkLinked: false))
{
if (RemoveEmpty && container.Inventory.AllItems.Any(it => it.Condition <= 0.0f))
if (RemoveExisting)
{
foreach (var emptyItem in container.Inventory.AllItemsMod)
{
if (emptyItem.Condition <= 0)
{
emptyItem.Drop(character);
}
}
HumanAIController.UnequipContainedItems(container.Item);
}
// Contain the item
if (ItemToContain.ParentInventory == character.Inventory)
else if (RemoveEmpty)
{
if (!container.Inventory.CanBePut(ItemToContain))
HumanAIController.UnequipEmptyItems(container.Item);
}
Inventory originalInventory = ItemToContain.ParentInventory;
var slots = originalInventory?.FindIndices(ItemToContain);
if (container.Inventory.TryPutItem(ItemToContain, null))
{
if (MoveWholeStack && slots != null)
{
Abandon = true;
}
else
{
character.Inventory.RemoveItem(ItemToContain);
if (container.Inventory.TryPutItem(ItemToContain, null))
foreach (int slot in slots)
{
IsCompleted = true;
}
else
{
ItemToContain.Drop(character);
Abandon = true;
foreach (Item item in originalInventory.GetItemsAt(slot).ToList())
{
container.Inventory.TryPutItem(item, null);
}
}
IsCompleted = true;
}
}
else
{
if (container.Combine(ItemToContain, character))
if (ItemToContain.ParentInventory == character.Inventory && character.Submarine == Submarine.MainSub)
{
IsCompleted = true;
}
else
{
Abandon = true;
ItemToContain.Drop(character);
}
Abandon = true;
}
}
else
@@ -151,7 +146,8 @@ namespace Barotrauma
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name,
abortCondition = () => !ItemToContain.IsOwnedBy(character)
abortCondition = obj => !ItemToContain.IsOwnedBy(character),
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -22,8 +22,13 @@ namespace Barotrauma
public AIObjectiveGetItem GetItemObjective => getItemObjective;
public AIObjectiveContainItem ContainObjective => containObjective;
public Item TargetItem => targetItem;
public ItemContainer TargetContainer => targetContainer;
public bool Equip { get; set; }
public bool TakeWholeStack { get; set; }
/// <summary>
/// If true drops the item when containing the item fails.
/// In both cases abandons the objective.
@@ -90,7 +95,7 @@ namespace Barotrauma
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
{
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip),
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) { TakeWholeStack = this.TakeWholeStack },
onAbandon: () => Abandon = true);
return;
}
@@ -99,6 +104,7 @@ namespace Barotrauma
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
{
MoveWholeStack = TakeWholeStack,
Equip = Equip,
RemoveEmpty = false,
GetItemPriority = GetItemPriority,
@@ -35,7 +35,7 @@ namespace Barotrauma
Abandon = true;
return Priority;
}
bool isOrder = objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>();
bool isOrder = objectiveManager.HasOrder<AIObjectiveExtinguishFires>();
if (!isOrder && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
// Don't go into rooms with any enemies, unless it's an order
@@ -78,7 +78,7 @@ namespace Barotrauma
{
TryAddSubObjective(ref getExtinguisherObjective, () =>
{
if (!character.HasEquippedItem("fireextinguisher", allowBroken: false))
if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher", allowBroken: false))
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
}
@@ -88,7 +88,7 @@ namespace Barotrauma
// If the item is inside an unsafe hull, decrease the priority
GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1
};
if (objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>())
if (objectiveManager.HasOrder<AIObjectiveExtinguishFires>())
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher"), null, 0.0f, "dialogcannotfindfireextinguisher", 10.0f);
};
@@ -42,6 +42,15 @@ namespace Barotrauma
if (hull.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(hull, includingConnectedSubs: true)) { return false; }
if (hull.BallastFlora != null) { return false; }
foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList)
{
if (ballastFlora.Parent?.Submarine != character.Submarine) { continue; }
if (ballastFlora.Branches.Any(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull))
{
return false;
}
}
return true;
}
}
@@ -10,6 +10,8 @@ namespace Barotrauma
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
protected override float TargetUpdateTimeMultiplier => 0.2f;
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
@@ -48,16 +50,14 @@ namespace Barotrauma
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target == null || target.Removed) { return false; }
if (target.IsDead || target.IsUnconscious) { 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 (character.Submarine == null) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null)
{
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
}
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
return true;
}
}
@@ -1,6 +1,7 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -37,11 +38,11 @@ namespace Barotrauma
return;
}
targetItem = character.Inventory.FindItemByTag(gearTag, true);
if (targetItem == null || !character.HasEquippedItem(targetItem))
if (targetItem == null || !character.HasEquippedItem(targetItem) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
{
TryAddSubObjective(ref getDivingGear, () =>
{
if (targetItem == null)
if (targetItem == null && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
}
@@ -57,46 +58,78 @@ namespace Barotrauma
}
else
{
if (!EjectEmptyTanks(character, targetItem, out var containedItems))
HumanAIController.UnequipContainedItems(targetItem, it => !it.HasTag("oxygensource"));
HumanAIController.UnequipEmptyItems(targetItem);
// Seek oxygen that has at least 10% condition left, if we are inside a friendly sub.
// The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
float min = character.Submarine != Submarine.MainSub ? 0.01f : MIN_OXYGEN;
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min))
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + targetItem + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
if (containedItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > MIN_OXYGEN))
{
// No valid oxygen source loaded.
// Seek oxygen that has min 10% condition left.
TryAddSubObjective(ref getOxygen, () =>
{
if (!HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 10))
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
if (HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: min))
{
character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f);
}
else
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
}
}
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
ConditionLevel = MIN_OXYGEN
ConditionLevel = MIN_OXYGEN,
RemoveExisting = true
};
},
onAbandon: () =>
{
// Try to seek any oxygen sources.
getOxygen = null;
int remainingTanks = ReportOxygenTankCount();
// Try to seek any oxygen sources, even if they have minimal amount of oxygen.
TryAddSubObjective(ref getOxygen, () =>
{
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true
AllowDangerousPressure = true,
RemoveExisting = true
};
},
onAbandon: () => Abandon = true,
onAbandon: () =>
{
Abandon = true;
if (remainingTanks > 0 && !HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 0.01f))
{
character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f);
}
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
onCompleted: () =>
{
RemoveSubObjective(ref getOxygen);
ReportOxygenTankCount();
});
int ReportOxygenTankCount()
{
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f);
}
else if (remainingOxygenTanks < 10)
{
character.Speak(TextManager.Get("DialogLowOnOxygenTanks"), null, 0.0f, "lowonoxygentanks", 30.0f);
}
return remainingOxygenTanks;
}
}
}
}
@@ -108,21 +141,7 @@ namespace Barotrauma
{
containedItems = target.OwnInventory?.AllItems;
if (containedItems == null) { return false; }
foreach (Item containedItem in target.OwnInventory.AllItemsMod)
{
if (containedItem.Condition <= 0.0f)
{
if (actor.Submarine == null)
{
// If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea.
if (actor.Inventory.TryPutItem(containedItem, actor, CharacterInventory.anySlot))
{
continue;
}
}
containedItem.Drop(actor);
}
}
AIController.UnequipEmptyItems(actor, target);
return true;
}
@@ -46,19 +46,27 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.Objectives.Any(o => o is AIObjectiveCombat)) && HumanAIController.HasDivingSuit(character) ? 0 : 100;
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.HasActiveObjective<AIObjectiveCombat>()) && HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
else
{
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
(needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN) :
!HumanAIController.HasDivingMask(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN)))
{
Priority = 100;
}
else if (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
{
// Ordered to follow/hold position inside a hostile sub -> ignore find safety unless we need to find a diving gear
Priority = 0;
}
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));
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100));
}
}
return Priority;
@@ -38,7 +38,7 @@ namespace Barotrauma
Priority = 0;
Abandon = true;
}
else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeak>()?.Leak == Leak))
else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeak>()?.Leak == Leak))
{
Priority = 0;
Abandon = true;
@@ -52,7 +52,7 @@ namespace Barotrauma
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float reduction = isPriority ? 1 : 2;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
float max = AIObjectiveManager.LowestOrderPriority - reduction;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
@@ -67,7 +67,7 @@ namespace Barotrauma
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onAbandon: () =>
{
if (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>())
if (character.IsOnPlayerTeam && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>())
{
character.Speak(TextManager.Get("dialogcannotfindweldingequipment"), null, 0.0f, "dialogcannotfindweldingequipment", 10.0f);
}
@@ -86,23 +86,34 @@ namespace Barotrauma
Abandon = true;
return;
}
// Drop empty tanks
if (weldingTool.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
HumanAIController.UnequipContainedItems(weldingTool, it => !it.HasTag("weldingfuel"));
HumanAIController.UnequipEmptyItems(weldingTool);
if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
foreach (Item containedItem in weldingTool.OwnInventory.AllItemsMod)
{
if (containedItem.Condition <= 0.0f)
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onAbandon: () =>
{
containedItem.Drop(character);
Abandon = true;
ReportWeldingFuelTankCount();
},
onCompleted: () =>
{
RemoveSubObjective(ref refuelObjective);
ReportWeldingFuelTankCount();
});
void ReportWeldingFuelTankCount()
{
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfWeldingFuel"), null, 0.0f, "outofweldingfuel", 30.0f);
}
else if (remainingOxygenTanks < 4)
{
character.Speak(TextManager.Get("DialogLowOnWeldingFuel"), null, 0.0f, "lowonweldingfuel", 30.0f);
}
}
}
if (weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref refuelObjective));
return;
}
}
@@ -42,7 +42,7 @@ namespace Barotrauma
if (totalLeaks == 0) { return 0; }
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
bool anyFixers = otherFixers > 0;
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1;
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
@@ -72,7 +72,11 @@ namespace Barotrauma
{
if (gap == null) { return false; }
// Don't fix a leak on a wall section set to be ignored
if (gap.ConnectedWall?.Sections?.Any(s => s.gap == gap && s.IgnoreByAI) ?? false) { return false; }
if (gap.ConnectedWall != null)
{
if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI)) { return false; }
if (gap.ConnectedWall.MaxHealth <= 0.0f) { return false; }
}
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
if (gap.Submarine == null || character.Submarine == null) { return false; }
// Don't allow going into another sub, unless it's connected and of the same team and type.
@@ -10,6 +10,8 @@ namespace Barotrauma
{
public override string DebugTag => "get item";
public override bool AbandonWhenCannotCompleteSubjectives => false;
private readonly bool equip;
public HashSet<Item> ignoredItems = new HashSet<Item>();
@@ -44,6 +46,8 @@ namespace Barotrauma
/// </summary>
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -191,8 +195,20 @@ namespace Barotrauma
return;
}
Inventory itemInventory = targetItem.ParentInventory;
var slots = itemInventory?.FindIndices(targetItem);
if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true))
{
if (TakeWholeStack && slots != null)
{
foreach (int slot in slots)
{
foreach (Item item in itemInventory.GetItemsAt(slot).ToList())
{
HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true);
}
}
}
IsCompleted = true;
}
else
@@ -211,9 +227,8 @@ namespace Barotrauma
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
{
// 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.GetRootInventoryOwner() != moveToTarget,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString()
abortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
SpeakIfFails = false
};
},
onAbandon: () =>
@@ -240,13 +255,18 @@ namespace Barotrauma
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item was defined.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Cannot find an 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++)
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
bool checkPath = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.followControlledCharacter);
bool hasCalledPathFinder = false;
int itemsPerFrame = (int)priority;
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
{
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
@@ -259,9 +279,13 @@ namespace Barotrauma
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; }
}
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
if (item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
if (item.Container.HasTag("donttakeitems")) { continue; }
if (ignoredContainerIdentifiers != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
}
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; }
@@ -287,8 +311,18 @@ namespace Barotrauma
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
// Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
if (!hasCalledPathFinder && PathSteering != null && checkPath)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
hasCalledPathFinder = true;
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable) { continue; }
}
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
@@ -303,7 +337,7 @@ namespace Barotrauma
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
@@ -322,8 +356,9 @@ namespace Barotrauma
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
#endif
SpeakCannotFind();
Abandon = true;
}
}
@@ -370,11 +405,48 @@ namespace Barotrauma
/// </summary>
private void ResetInternal()
{
goToObjective = null;
RemoveSubObjective(ref goToObjective);
targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false;
currSearchIndex = 0;
currItemPriority = 0;
}
protected override void OnAbandon()
{
base.OnAbandon();
if (moveToTarget == null) { return; }
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
#endif
}
private void SpeakCannotFind()
{
// TODO: Use the item name as the variable here.
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get("dialogcannotfinditem", true);
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f);
}
}
}
// TODO: remove?
private void SpeakCannotReach()
{
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString();
string msg = TargetName == null ? TextManager.Get("dialogcannotreachtarget", true) : TextManager.GetWithVariable("dialogcannotreachtarget", "[name]", TargetName, formatCapitals: !(moveToTarget is Character));
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotreachtarget", minDurationBetweenSimilar: 20.0f);
}
}
}
}
}
@@ -23,13 +23,14 @@ namespace Barotrauma
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
public Func<bool> abortCondition;
public Func<AIObjectiveGoTo, bool> abortCondition;
public Func<PathNode, bool> endNodeFilter;
public Func<float> priorityGetter;
public bool followControlledCharacter;
public bool mimic;
public bool SpeakIfFails { get; set; } = true;
public float extraDistanceWhileSwimming;
public float extraDistanceOutsideSub;
@@ -66,6 +67,8 @@ namespace Barotrauma
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
public bool AlwaysUseEuclideanDistance { get; set; } = true;
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
@@ -80,19 +83,14 @@ namespace Barotrauma
public override float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
return Priority;
}
if (followControlledCharacter && Character.Controlled == null)
{
Priority = 0;
Abandon = !isOrder;
}
if (Target is Entity e && e.Removed)
if (Target == null || Target is Entity e && e.Removed)
{
Priority = 0;
Abandon = !isOrder;
@@ -114,7 +112,7 @@ namespace Barotrauma
}
else
{
Priority = isOrder ? AIObjectiveManager.OrderPriority : 10;
Priority = isOrder ? objectiveManager.GetOrderPriority(this) : 10;
}
}
return Priority;
@@ -149,7 +147,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null)
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails)
{
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character));
if (msg != null)
@@ -163,13 +161,15 @@ namespace Barotrauma
{
if (followControlledCharacter)
{
if (Character.Controlled == null)
if (Character.Controlled != null && HumanAIController.IsFriendly(Character.Controlled))
{
Target = Character.Controlled;
}
if (Target == null)
{
Abandon = true;
SteeringManager.Reset();
return;
}
Target = Character.Controlled;
}
if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
{
@@ -187,7 +187,6 @@ namespace Barotrauma
if (e.Removed)
{
Abandon = true;
SteeringManager.Reset();
return;
}
else
@@ -199,7 +198,7 @@ namespace Barotrauma
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
bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
&& PathSteering != null && PathSteering.CurrentPath != null
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
@@ -249,16 +248,18 @@ namespace Barotrauma
}
}
bool needsEquipment = false;
float minOxygen = character.Submarine == null ? 0 : AIObjectiveFindDivingGear.MIN_OXYGEN;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (needsEquipment)
{
SteeringManager.Reset();
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
@@ -287,9 +288,14 @@ namespace Barotrauma
}
}
}
float maxGapDistance = 500;
Character targetCharacter = Target as Character;
if (character.AnimController.InWater)
{
if (character.CurrentHull == null)
if (character.CurrentHull == null ||
followControlledCharacter &&
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
{
if (seekGapsTimer > 0)
{
@@ -297,7 +303,7 @@ namespace Barotrauma
}
else
{
SeekGaps(maxDistance: 500);
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (TargetGap != null)
{
@@ -326,7 +332,7 @@ namespace Barotrauma
}
if (TargetGap != null)
{
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, TargetGap.FlowTargetHull.WorldPosition, deltaTime))
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, followControlledCharacter ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
return;
@@ -346,7 +352,7 @@ namespace Barotrauma
float closeEnough = 250;
float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition);
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic ||
(Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
(targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
@@ -527,17 +533,24 @@ namespace Barotrauma
{
Gap selectedGap = null;
float selectedDistance = -1;
Vector2 toTargetNormalized = Vector2.Normalize(Target.WorldPosition - character.WorldPosition);
foreach (Gap gap in Gap.GapList)
{
if (gap.Open < 1) { continue; }
if (gap.FlowTargetHull == null) { continue; }
if (gap.Submarine != Target.Submarine) { continue; }
float distance = Vector2.DistanceSquared(character.WorldPosition, gap.WorldPosition);
if (distance > maxDistance * maxDistance) { continue; }
if (selectedGap == null || distance < selectedDistance)
if (gap.Submarine == null) { continue; }
if (!followControlledCharacter)
{
if (gap.FlowTargetHull == null) { continue; }
if (gap.Submarine != Target.Submarine) { continue; }
}
Vector2 toGap = gap.WorldPosition - character.WorldPosition;
if (Vector2.Dot(Vector2.Normalize(toGap), toTargetNormalized) < 0) { continue; }
float squaredDistance = toGap.LengthSquared();
if (squaredDistance > maxDistance * maxDistance) { continue; }
if (selectedGap == null || squaredDistance < selectedDistance)
{
selectedGap = gap;
selectedDistance = distance;
selectedDistance = squaredDistance;
}
}
TargetGap = selectedGap;
@@ -554,6 +567,13 @@ namespace Barotrauma
//otherwise characters can let go of the ladders too soon once they're close enough to the target
if (PathSteering.CurrentPath.NextNode != null) { return false; }
}
if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater)
{
float yDiff = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y);
if (yDiff > CloseEnough) { return false; }
float xDiff = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X);
return xDiff <= CloseEnough;
}
return Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
}
}
@@ -569,7 +589,7 @@ namespace Barotrauma
Abandon = true;
return false;
}
if (abortCondition != null && abortCondition())
if (abortCondition != null && abortCondition(this))
{
Abandon = true;
return false;
@@ -617,7 +637,7 @@ namespace Barotrauma
private void StopMovement()
{
character.AIController.SteeringManager.Reset();
SteeringManager.Reset();
if (Target != null)
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
@@ -21,9 +21,9 @@ namespace Barotrauma
set
{
behavior = value;
if (behavior == BehaviorType.StayInHull && character.TeamID != CharacterTeamType.FriendlyNPC)
if (behavior == BehaviorType.StayInHull && TargetHull == null)
{
DebugConsole.NewMessage($"AIObjectiveIdle.BehaviorType.StayInHull is implemented only for outpost NPCs. Using passive behavior for {character.Name} ({character.Info.Job.Prefab.Identifier})", color: Color.Red);
DebugConsole.AddWarning($"Trying to set a character's behavior type to StayInHull, but target hull is not set. {character.Name} ({character.Info.Job.Prefab.Identifier})");
behavior = BehaviorType.Passive;
}
switch (behavior)
@@ -495,7 +495,7 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true) && !ignoredItems.Contains(item))
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
{
itemsToClean.Add(item);
}
@@ -540,5 +540,17 @@ namespace Barotrauma
ignoredItems.Clear();
autonomousObjectiveRetryTimer = 10;
}
public override void OnDeselected()
{
base.OnDeselected();
foreach (var subObjective in SubObjectives)
{
if (subObjective is AIObjectiveCleanupItem cleanUpObjective)
{
cleanUpObjective.DropTarget();
}
}
}
}
}
@@ -11,6 +11,7 @@ namespace Barotrauma
protected HashSet<T> ignoreList = new HashSet<T>();
private float ignoreListTimer;
protected float targetUpdateTimer;
protected virtual float TargetUpdateTimeMultiplier { get; } = 1;
private float syncTimer;
private readonly float syncTime = 1;
@@ -61,7 +62,7 @@ namespace Barotrauma
ignoreListTimer += deltaTime;
}
}
if (targetUpdateTimer < 0)
if (targetUpdateTimer <= 0)
{
UpdateTargets();
}
@@ -69,9 +70,9 @@ namespace Barotrauma
{
targetUpdateTimer -= deltaTime;
}
if (syncTimer < 0)
if (syncTimer <= 0)
{
syncTimer = syncTime * Rand.Range(0.9f, 1.1f);
syncTimer = Math.Min(syncTime * Rand.Range(0.9f, 1.1f), targetUpdateTimer);
// Sync objectives, subobjectives and targets
foreach (var objective in Objectives)
{
@@ -95,7 +96,7 @@ namespace Barotrauma
}
// 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);
private float CalculateTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1) * TargetUpdateTimeMultiplier;
public override void Reset()
{
@@ -139,13 +140,13 @@ namespace Barotrauma
}
else
{
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
Priority = ForceOrderPriority ? AIObjectiveManager.OrderPriority : targetValue;
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue;
}
else
{
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
float max = AIObjectiveManager.LowestOrderPriority - 1;
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
@@ -156,7 +157,7 @@ namespace Barotrauma
protected void UpdateTargets()
{
SetTargetUpdateTimer();
CalculateTargetUpdateTimer();
Targets.Clear();
FindTargets();
CreateObjectives();
@@ -167,7 +168,7 @@ namespace Barotrauma
foreach (T target in GetList())
{
// The bots always find targets when the objective is an order.
if (objectiveManager.CurrentOrder != this)
if (!objectiveManager.IsOrder(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;
@@ -1,6 +1,6 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Barotrauma.Networking; // used by the server
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -10,8 +10,8 @@ namespace Barotrauma
{
class AIObjectiveManager
{
// TODO: expose
public const float OrderPriority = 70;
public const float HighestOrderPriority = 70;
public const float LowestOrderPriority = 60;
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 5;
@@ -25,7 +25,6 @@ namespace Barotrauma
public HumanAIController HumanAIController => character.AIController as HumanAIController;
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.
@@ -39,26 +38,25 @@ namespace Barotrauma
}
}
public AIObjective CurrentOrder { get; private set; }
public List<OrderInfo> CurrentOrders { get; } = new List<OrderInfo>();
/// <summary>
/// The AIObjective in <see cref="CurrentOrders"/> with the highest <see cref="AIObjective.Priority"/>
/// </summary>
public AIObjective CurrentOrder
{
get
{
return ForcedOrder ?? currentOrder;
}
private set
{
currentOrder = value;
}
}
private AIObjective currentOrder;
public AIObjective ForcedOrder { 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;
@@ -134,7 +132,13 @@ namespace Barotrauma
}
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { continue; }
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
continue;
}
}
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
if (objective != null && objective.CanBeCompleted)
{
@@ -162,7 +166,11 @@ namespace Barotrauma
coroutine = CoroutineManager.InvokeAfter(() =>
{
//round ended before the coroutine finished
#if CLIENT
if (GameMain.GameSession == null || Level.Loaded == null && !(GameMain.GameSession.GameMode is TestGameMode)) { return; }
#else
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
#endif
DelayedObjectives.Remove(objective);
AddObjective(objective);
callback?.Invoke();
@@ -200,21 +208,34 @@ namespace Barotrauma
public void UpdateObjectives(float deltaTime)
{
if (CurrentOrder != null)
UpdateOrderObjective(ForcedOrder);
if (CurrentOrders.Any())
{
foreach(var order in CurrentOrders)
{
var orderObjective = order.Objective;
UpdateOrderObjective(orderObjective);
}
}
void UpdateOrderObjective(AIObjective orderObjective)
{
if (orderObjective == null) { return; }
#if DEBUG
// Note: don't automatically remove orders here. Removing orders needs to be done via dismissing.
if (CurrentOrder.IsCompleted)
if (orderObjective.IsCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red);
}
else if (!CurrentOrder.CanBeCompleted)
else if (!orderObjective.CanBeCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
}
#endif
CurrentOrder.Update(deltaTime);
orderObjective.Update(deltaTime);
}
if (WaitTimer > 0)
{
WaitTimer -= deltaTime;
@@ -248,7 +269,29 @@ namespace Barotrauma
public void SortObjectives()
{
CurrentOrder?.GetPriority();
ForcedOrder?.GetPriority();
AIObjective orderWithHighestPriority = null;
float highestPriority = 0;
foreach (var currentOrder in CurrentOrders)
{
var orderObjective = currentOrder.Objective;
if (orderObjective == null) { continue; }
orderObjective.GetPriority();
if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority)
{
orderWithHighestPriority = orderObjective;
highestPriority = orderObjective.Priority;
}
}
#if SERVER
if (orderWithHighestPriority != null && orderWithHighestPriority != currentOrder)
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.ObjectiveManagerOrderState });
}
#endif
CurrentOrder = orderWithHighestPriority;
for (int i = Objectives.Count - 1; i >= 0; i--)
{
Objectives[i].GetPriority();
@@ -257,6 +300,7 @@ namespace Barotrauma
{
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
}
GetCurrentObjective()?.SortSubObjectives();
}
@@ -272,13 +316,19 @@ namespace Barotrauma
}
}
public void SetOrder(AIObjective objective)
public void SetForcedOrder(AIObjective objective)
{
CurrentOrder = objective;
ForcedOrder = objective;
}
public void ClearForcedOrder()
{
ForcedOrder = null;
SortObjectives();
}
private CoroutineHandle speakRoutine;
public void SetOrder(Order order, string option, Character orderGiver, bool speak)
public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak)
{
if (character.IsDead)
{
@@ -289,8 +339,53 @@ namespace Barotrauma
#endif
}
ClearIgnored();
CurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
if (CurrentOrder == null)
if (order == null || order.Identifier == "dismissed")
{
if (!string.IsNullOrEmpty(option))
{
if (CurrentOrders.Any(o => o.MatchesDismissedOrder(option)))
{
var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(option));
CurrentOrders.Remove(dismissedOrderInfo);
}
}
else
{
CurrentOrders.Clear();
}
}
// Make sure the order priorities reflect those set by the player
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{
var currentOrder = CurrentOrders[i];
if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option))
{
CurrentOrders.RemoveAt(i);
continue;
}
var currentOrderInfo = character.GetCurrentOrder(currentOrder.Order, currentOrder.OrderOption);
if (currentOrderInfo.HasValue)
{
int currentPriority = currentOrderInfo.Value.ManualPriority;
if (currentOrder.ManualPriority != currentPriority)
{
CurrentOrders[i] = new OrderInfo(currentOrder, currentPriority);
}
}
else
{
CurrentOrders.RemoveAt(i);
}
}
var newCurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
if (newCurrentOrder != null)
{
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
}
if (!HasOrders())
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
@@ -298,56 +393,57 @@ namespace Barotrauma
else
{
// This should be redundant, because all the objectives are reset when they are selected as active.
CurrentOrder.Reset();
if (speak)
newCurrentOrder?.Reset();
if (speak && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
if (speakRoutine != null)
{
CoroutineManager.StopCoroutines(speakRoutine);
}
speakRoutine = CoroutineManager.InvokeAfter(() =>
{
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
if (CurrentOrder != null && character.SpeechImpediment < 100.0f)
{
if (CurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
{
character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
}
else if (CurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
{
character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
}
else if (CurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
{
character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
}
else if (CurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
{
character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
}
else if (CurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
{
character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
}
else if (CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
{
character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
}
else if (CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
{
character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
}
}
}, 3);
//if (speakRoutine != null)
//{
// CoroutineManager.StopCoroutines(speakRoutine);
//}
//speakRoutine = CoroutineManager.InvokeAfter(() =>
//{
// if (GameMain.GameSession == null || Level.Loaded == null) { return; }
// if (newCurrentOrder != null && character.SpeechImpediment < 100.0f)
// {
// if (newCurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
// }
// else if (newCurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
// }
// else if (newCurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
// }
// else if (newCurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
// }
// else if (newCurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
// }
// else if (newCurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
// }
// else if (newCurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
// }
// }
//}, 3);
}
}
}
public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1)
{
if (order == null) { return null; }
if (order == null || order.Identifier == "dismissed") { return null; }
AIObjective newObjective;
switch (order.Identifier.ToLowerInvariant())
{
@@ -360,7 +456,7 @@ namespace Barotrauma
extraDistanceWhileSwimming = 100,
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
followControlledCharacter = orderGiver == character,
followControlledCharacter = true,
mimic = true,
DialogueIdentifier = "dialogcannotreachplace"
};
@@ -430,7 +526,7 @@ namespace Barotrauma
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
Override = !character.IsDismissed,
completionCondition = () =>
{
if (float.TryParse(option, out float pct))
@@ -483,21 +579,9 @@ namespace Barotrauma
return newObjective;
}
private void DismissSelf()
{
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
{
GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), null, character);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), null, null, character, character));
#endif
}
private bool IsAllowedToWait()
{
if (CurrentOrder != null) { return false; }
if (HasOrders()) { return false; }
if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; }
if (character.AnimController.InWater) { return false; }
if (character.IsClimbing) { return false; }
@@ -508,5 +592,61 @@ namespace Barotrauma
if (AIObjectiveIdle.IsForbidden(character.CurrentHull)) { return false; }
return true;
}
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 bool IsOrder(AIObjective objective)
{
return objective == ForcedOrder || CurrentOrders.Any(o => o.Objective == objective);
}
public bool HasOrders()
{
return ForcedOrder != null || CurrentOrders.Any();
}
public bool HasOrder<T>() where T : AIObjective
{
return ForcedOrder is T || CurrentOrders.Any(o => o.Objective is T);
}
public float GetOrderPriority(AIObjective objective)
{
if (objective == ForcedOrder) { return HighestOrderPriority; }
var currentOrder = CurrentOrders.FirstOrDefault(o => o.Objective == objective);
if (currentOrder.Objective == null)
{
return HighestOrderPriority;
}
else if (currentOrder.ManualPriority > 0)
{
return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority));
}
#if DEBUG
DebugConsole.AddWarning("Error in order priority: shouldn't return 0!");
#endif
return 0;
}
public OrderInfo? GetCurrentOrderInfo()
{
if (currentOrder == null) { return null; }
return CurrentOrders.FirstOrDefault(o => o.Objective == CurrentOrder);
}
}
}
@@ -36,7 +36,7 @@ namespace Barotrauma
public override float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed || character.LockHands)
{
Priority = 0;
@@ -51,7 +51,7 @@ namespace Barotrauma
{
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
Priority = objectiveManager.GetOrderPriority(this);
}
ItemComponent target = GetTarget();
Item targetItem = target?.Item;
@@ -69,10 +69,9 @@ namespace Barotrauma
{
if (!isOrder)
{
if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC ||
HumanAIController.IsTrueForAnyCrewMember(c =>
c.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() == target))
if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC)
{
// The reactor was previously operated by a player -> ignore.
Priority = 0;
return Priority;
}
@@ -89,11 +88,15 @@ namespace Barotrauma
case "powerup":
// Check that we don't already have another order that is targeting the same item.
// Without this the autonomous objective will tell the bot to turn the reactor on again.
if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target && operateOrder.Option != Option)
if (IsAnotherOrderTargetingSameItem(objectiveManager.ForcedOrder) || objectiveManager.CurrentOrders.Any(o => IsAnotherOrderTargetingSameItem(o.Objective)))
{
Priority = 0;
return Priority;
}
bool IsAnotherOrderTargetingSameItem(AIObjective objective)
{
return objective is AIObjectiveOperateItem operateObjective && operateObjective != this && operateObjective.GetTarget() == target && operateObjective.Option != Option;
}
break;
}
}
@@ -108,14 +111,23 @@ namespace Barotrauma
}
else
{
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
float max = isOrder ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1;
if (!isOrder && reactor != null && reactor.PowerOn && Option == "powerup")
if (isOrder)
{
// Decrease the priority when targeting a reactor that is already on.
value /= 2;
float max = objectiveManager.GetOrderPriority(this);
float value = CumulatedDevotion + (max * PriorityModifier);
Priority = MathHelper.Clamp(value, 0, max);
}
else
{
float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier);
float max = AIObjectiveManager.LowestOrderPriority - 1;
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && Option == "powerup")
{
// Decrease the priority when targeting a reactor that is already on.
value /= 2;
}
Priority = MathHelper.Clamp(value, 0, max);
}
Priority = MathHelper.Clamp(value, 0, max);
}
}
return Priority;
@@ -154,15 +166,18 @@ namespace Barotrauma
ItemComponent target = GetTarget();
if (useController && controller == null)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
}
Abandon = true;
return;
}
if (operateTarget != null)
{
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
{
// Another crew member is already targeting this entity.
// Another crew member is already targeting this entity (leak).
Abandon = true;
return;
}
@@ -59,13 +59,13 @@ namespace Barotrauma
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist));
}
float requiredSuccessFactor = objectiveManager.IsCurrentOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
float requiredSuccessFactor = objectiveManager.HasOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100;
bool isSelected = IsRepairing();
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
float devotion = (CumulatedDevotion + selectedBonus) / 100;
float reduction = isPriority ? 1 : isSelected ? 2 : 3;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
float max = AIObjectiveManager.LowestOrderPriority - reduction;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
@@ -74,7 +74,7 @@ namespace Barotrauma
protected override bool Check()
{
IsCompleted = Item.IsFullCondition;
if (IsCompleted && IsRepairing())
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
@@ -97,7 +97,10 @@ namespace Barotrauma
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true);
if (objectiveManager.IsCurrentOrder<AIObjectiveRepairItems>())
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f);
if (character.IsOnPlayerTeam)
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f);
}
}
subObjectives.Add(getItemObjective);
}
@@ -119,27 +122,8 @@ namespace Barotrauma
Abandon = true;
return;
}
// Eject empty tanks
if (repairTool.Item.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
{
foreach (Item containedItem in repairTool.Item.OwnInventory.AllItemsMod)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
if (character.Submarine == null)
{
// If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
{
continue;
}
}
containedItem.Drop(character);
}
}
}
HumanAIController.UnequipContainedItems(repairTool.Item, it => !it.HasTag("weldingfuel"));
HumanAIController.UnequipEmptyItems(repairTool.Item);
RelatedItem item = null;
Item fuel = null;
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
@@ -193,7 +177,7 @@ namespace Barotrauma
}
if (Abandon)
{
if (IsRepairing())
if (character.IsOnPlayerTeam && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
@@ -228,7 +212,7 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
if (IsRepairing())
if (character.IsOnPlayerTeam && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
@@ -104,7 +104,7 @@ namespace Barotrauma
}
bool anyFixers = otherFixers > 0;
float ratio = anyFixers ? items / (float)otherFixers : 1;
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
return Targets.Sum(t => 100 - t.ConditionPercentage);
}
@@ -153,6 +153,8 @@ namespace Barotrauma
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null || character.Submarine == null) { return false; }
//player crew ignores items in outposts
if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; }
if (item.Repairables.None()) { return false; }
return true;
@@ -78,14 +78,14 @@ namespace Barotrauma
// Check if the character needs more oxygen
if (!ignoreOxygen && character.SelectedCharacter == targetCharacter || character.CanInteractWith(targetCharacter))
{
// Replace empty oxygen tank
// First remove empty tanks
// Replace empty oxygen and welding fuel.
if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out IEnumerable<Item> suits, requireEquipped: true))
{
Item suit = suits.FirstOrDefault();
if (suit != null)
{
AIObjectiveFindDivingGear.EjectEmptyTanks(character, suit, out _);
AIController.UnequipEmptyItems(character, suit);
AIController.UnequipContainedItems(character, suit, it => it.HasTag("weldingfuel"));
}
}
else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
@@ -93,7 +93,8 @@ namespace Barotrauma
Item mask = masks.FirstOrDefault();
if (mask != null)
{
AIObjectiveFindDivingGear.EjectEmptyTanks(character, mask, out _);
AIController.UnequipEmptyItems(character, mask);
AIController.UnequipContainedItems(character, mask, it => it.HasTag("weldingfuel"));
}
}
bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0;
@@ -322,7 +323,7 @@ namespace Barotrauma
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
if (targetCharacter != character)
if (targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
@@ -336,7 +337,10 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
if (character != targetCharacter && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
}
});
}
}
@@ -383,8 +387,10 @@ namespace Barotrauma
Abandon = true;
return false;
}
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character)
bool isCompleted =
AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) ||
targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold);
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
@@ -25,8 +25,8 @@ namespace Barotrauma
{
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
// On the other hand, if the bots too eagerly heal characters when it's not nevessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
}
}
@@ -40,7 +40,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (Targets.None()) { return 100; }
if (objectiveManager.CurrentOrder != this)
if (!objectiveManager.IsOrder(this))
{
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious))
{
@@ -82,8 +82,12 @@ namespace Barotrauma
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
if (!humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveRescueAll>())
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target) ||
target.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold))
{
return false;
}
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
{
if (!character.IsMedic && target != character)
{