Unstable 1.1.14.0

This commit is contained in:
Markus Isberg
2023-10-02 16:43:54 +03:00
parent 94f5a93a0c
commit cf8f0de659
606 changed files with 21906 additions and 11456 deletions
@@ -40,6 +40,7 @@ namespace Barotrauma
public virtual bool AllowOutsideSubmarine => false;
public virtual bool AllowInFriendlySubs => false;
public virtual bool AllowInAnySub => false;
public virtual bool AllowWhileHandcuffed => true;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
@@ -246,32 +247,43 @@ namespace Barotrauma
{
get
{
if (IgnoreAtOutpost && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
return false;
}
}
if (!AllowWhileHandcuffed && character.LockHands) { return false; }
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
// Evaluate ignored at outpost first, because it has higher priority than AllowInAnySub or AllowInFriendlySubs.
if (IsIgnoredAtOutpost()) { return false; }
if (AllowInAnySub) { return true; }
if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; }
return character.Submarine.TeamID == character.TeamID ||
character.Submarine.TeamID == character.OriginalTeamID ||
character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID || sub.TeamID == character.OriginalTeamID);
return character.Submarine.TeamID == character.TeamID || character.Submarine.TeamID == character.OriginalTeamID;
}
}
/// <summary>
/// Returns true only when at a friendly outpost and when the order is set to be ignored there.
/// Note that even if this returns false, the objective can be disallowed, because AllowInFriendlySubs is false.
/// </summary>
public bool IsIgnoredAtOutpost()
{
if (!IgnoreAtOutpost) { return false; }
if (!Level.IsLoadedFriendlyOutpost) { return false; }
if (!character.IsOnPlayerTeam) { return false; }
if (character.Submarine?.Info == null) { return false; }
return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC;
}
protected void HandleNonAllowed()
{
Priority = 0;
Abandon = !IsIgnoredAtOutpost();
}
protected virtual float GetPriority()
{
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
if (isOrder)
if (objectiveManager.IsOrder(this))
{
Priority = objectiveManager.GetOrderPriority(this);
}
@@ -446,7 +458,7 @@ namespace Barotrauma
}
}
protected virtual bool Check()
private bool Check()
{
if (AbortCondition != null && AbortCondition(this))
{
@@ -19,6 +19,7 @@ namespace Barotrauma
protected override bool Filter(PowerContainer battery)
{
if (battery == null) { return false; }
if (battery.OutputDisabled) { return false; }
var item = battery.Item;
if (item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
@@ -12,6 +12,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public override bool AllowWhileHandcuffed => false;
public readonly Item item;
public bool IsPriority { get; set; }
@@ -30,8 +31,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
else
@@ -119,18 +119,21 @@ namespace Barotrauma
if (item.IgnoreByAI(character))
{
Abandon = true;
return false;
}
if (item.ParentInventory != null)
else if (item.ParentInventory != null)
{
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder<AIObjectiveCleanupItems>()))
if (!objectiveManager.HasOrder<AIObjectiveCleanupItems>())
{
// Don't allow taking items from containers in the idle state.
Abandon = true;
}
else if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character))
{
// Target was picked up or moved by someone.
Abandon = true;
return false;
}
}
return IsCompleted;
return !Abandon && IsCompleted;
}
public override void Reset()
@@ -14,8 +14,6 @@ namespace Barotrauma
public readonly List<Item> prioritizedItems = new List<Item>();
public static readonly Identifier AllowCleanupTag = "allowcleanup".ToIdentifier();
protected override int MaxTargets => 100;
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1)
@@ -83,9 +81,8 @@ namespace Barotrauma
return true;
}
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
allowUnloading &&
container.HasTag(AllowCleanupTag) &&
public static bool IsValidContainer(Item container, Character character) =>
container.HasTag(Tags.AllowCleanup) &&
container.HasAccess(character) &&
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
container.GetComponent<ItemContainer>() != null &&
@@ -103,15 +100,18 @@ namespace Barotrauma
// In a character inventory
return false;
}
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
if (!allowUnloading) { return false; }
if (!IsValidContainer(item.Container, character)) { return false; }
}
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
//something (e.g. a pet) was eating the item within the last second - don't clean up
if (item.LastEatenTime > Timing.TotalTimeUnpaused - 1.0) { return false; }
var wire = item.GetComponent<Wire>();
if (wire != null)
{
if (wire.Connections.Any()) { return false; }
if (wire.Connections.Any(c => c != null)) { return false; }
}
else
{
@@ -46,7 +46,6 @@ namespace Barotrauma
_weapon = value;
_weaponComponent = null;
hasAimed = false;
RemoveSubObjective(ref seekAmmunitionObjective);
}
}
private ItemComponent _weaponComponent;
@@ -55,14 +54,7 @@ namespace Barotrauma
get
{
if (Weapon == null) { return null; }
if (_weaponComponent == null)
{
_weaponComponent =
Weapon.GetComponent<RangedWeapon>() ??
Weapon.GetComponent<MeleeWeapon>() ??
Weapon.GetComponent<RepairTool>() as ItemComponent;
}
return _weaponComponent;
return _weaponComponent ?? GetWeaponComponent(Weapon);
}
}
@@ -280,13 +272,13 @@ namespace Barotrauma
}
break;
case CombatMode.Arrest:
if (HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out _, requireEquipped: true))
if (HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out _, requireEquipped: true))
{
IsCompleted = true;
}
else if (Enemy.IsKnockedDown &&
!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>() &&
!HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _, requireEquipped: false))
!HumanAIController.HasItem(character, Tags.HandLockerItem, out _, requireEquipped: false))
{
IsCompleted = true;
}
@@ -348,8 +340,10 @@ namespace Barotrauma
if (character.LockHands || Enemy == null)
{
Weapon = null;
RemoveSubObjective(ref seekAmmunitionObjective);
return false;
}
bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest;
if (checkWeaponsTimer < 0)
{
checkWeaponsTimer = checkWeaponsInterval;
@@ -375,7 +369,7 @@ namespace Barotrauma
// All good, the weapon is loaded
break;
}
if (Reload(seekAmmo: false))
if (Reload(seekAmmo: isAllowedToSeekWeapons))
{
// All good, we can use the weapon.
break;
@@ -407,7 +401,6 @@ namespace Barotrauma
}
}
}
bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest;
if (!isAllowedToSeekWeapons)
{
if (WeaponComponent == null)
@@ -416,7 +409,7 @@ namespace Barotrauma
Mode = CombatMode.Retreat;
}
}
else if (seekAmmunitionObjective == null && (WeaponComponent == null || WeaponComponent.CombatPriority < goodWeaponPriority))
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority)))
{
// Poor weapon equipped -> try to find better.
RemoveSubObjective(ref seekAmmunitionObjective);
@@ -431,27 +424,10 @@ namespace Barotrauma
{
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
if (i.IsOwnedBy(character)) { return 0; }
var mw = i.GetComponent<MeleeWeapon>();
var rw = i.GetComponent<RangedWeapon>();
float priority = 0;
if (mw != null)
if (GetWeaponComponent(i) is ItemComponent ic)
{
priority = mw.CombatPriority / 100;
}
else if (rw != null)
{
priority = rw.CombatPriority / 100;
}
if (i.HasTag("stunner"))
{
if (Mode == CombatMode.Arrest)
{
priority *= 2;
}
else
{
priority /= 2;
}
priority = GetWeaponPriority(ic, prioritizeMelee: false, isCloseToEnemy: false, out _) / 100;
}
return priority;
}
@@ -477,6 +453,7 @@ namespace Barotrauma
if (!CheckWeapon(seekAmmo: false))
{
Weapon = null;
RemoveSubObjective(ref seekAmmunitionObjective);
}
}
return Weapon != null;
@@ -521,111 +498,164 @@ namespace Barotrauma
private Item FindWeapon(out ItemComponent weaponComponent) => GetWeapon(FindWeaponsFromInventory(), out weaponComponent);
private static ItemComponent GetWeaponComponent(Item item) =>
item.GetComponent<MeleeWeapon>() ??
item.GetComponent<RangedWeapon>() ??
item.GetComponent<RepairTool>() ??
item.GetComponent<Holdable>() as ItemComponent;
private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool isCloseToEnemy, out float lethalDmg)
{
lethalDmg = -1;
float priority = weapon.CombatPriority;
if (weapon is RepairTool repairTool)
{
switch (repairTool.UsableIn)
{
case RepairTool.UseEnvironment.Air:
if (character.InWater) { return 0; }
break;
case RepairTool.UseEnvironment.Water:
if (!character.InWater) { return 0; }
break;
case RepairTool.UseEnvironment.None:
return 0;
case RepairTool.UseEnvironment.Both:
default:
break;
}
}
if (prioritizeMelee && weapon is MeleeWeapon)
{
priority *= 5;
}
if (weapon.IsEmpty(character))
{
if (weapon is RangedWeapon && isCloseToEnemy)
{
// Ignore weapons that don't have any ammunition (-> Don't seek ammo).
return 0;
}
else
{
// Reduce the priority for weapons that don't have proper ammunition loaded.
if (character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType))
{
// Yet prefer the equipped weapon.
priority *= 0.75f;
}
else
{
priority *= 0.5f;
}
}
}
if (Enemy.Params.Health.StunImmunity)
{
if (weapon.Item.HasTag(Tags.StunnerItem))
{
priority /= 2;
}
}
else if (Enemy.IsKnockedDown)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float max = lethalDmg + 1;
if (weapon.Item.HasTag(Tags.StunnerItem))
{
priority = max;
}
else
{
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max);
}
}
}
else if (Mode == CombatMode.Arrest)
{
// Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons.
if (weapon.Item.HasTag(Tags.StunnerItem))
{
priority *= 5;
}
else
{
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
if (diff < 0)
{
priority /= 2;
}
}
}
}
else if (weapon is MeleeWeapon && weapon.Item.HasTag(Tags.StunnerItem) && (Enemy.Params.Health.StunImmunity || !CanMeleeStunnerStun(weapon)))
{
// Cannot do stun damage -> use the melee damage to determine the priority.
Attack attack = GetAttackDefinition(weapon);
priority = attack?.GetTotalDamage() ?? priority / 2;
}
return priority;
}
private float ApproximateStunDamage(ItemComponent weapon, Attack attack)
{
// Try to reduce the priority using the actual damage values and status effects.
// This is an approximation, because we can't check the status effect conditions here.
// The result might be incorrect if there is a high stun effect that's only applied in certain conditions.
var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character));
if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List<StatusEffect> hitEffects))
{
statusEffects = statusEffects.Concat(hitEffects);
}
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0);
float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
{
float stunAmount = 0;
var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType);
if (stunAffliction != null)
{
stunAmount = stunAffliction.Strength;
}
return stunAmount;
});
return attack.Stun + afflictionsStun + effectsStun;
}
private bool CanMeleeStunnerStun(ItemComponent weapon)
{
// If there's an item container that takes a battery,
// assume that it's required for the stun effect
// as we can't check the status effect conditions here.
var mobileBatteryTag = Tags.MobileBattery;
var containers = weapon.Item.Components.Where(ic =>
ic is ItemContainer container &&
container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
// If there's no such container, assume that the melee weapon can stun without a battery.
return containers.None() || containers.Any(container =>
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
}
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
{
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
bool isAllowedToSeekWeapons = !IsEnemyCloserThan(300);
bool isCloseToEnemy = IsEnemyCloserThan(300);
bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character);
foreach (var weapon in weaponList)
{
float priority = weapon.CombatPriority;
if (weapon is RepairTool repairTool)
{
switch (repairTool.UsableIn)
{
case RepairTool.UseEnvironment.Air:
if (character.InWater) { continue; }
break;
case RepairTool.UseEnvironment.Water:
if (!character.InWater) { continue; }
break;
case RepairTool.UseEnvironment.None:
continue;
case RepairTool.UseEnvironment.Both:
default:
break;
}
}
if (prioritizeMelee)
{
if (weapon is MeleeWeapon)
{
priority *= 5;
}
else
{
priority /= 2;
}
}
if (weapon.IsEmpty(character))
{
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{
// Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo).
continue;
}
else
{
// Halve the priority for weapons that don't have proper ammunition loaded.
priority /= 2;
}
}
if (Enemy.Params.Health.StunImmunity)
{
if (weapon.Item.HasTag("stunner"))
{
priority /= 2;
}
}
else if (Enemy.IsKnockedDown)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float max = lethalDmg + 1;
if (weapon.Item.HasTag("stunner"))
{
priority = max;
}
else
{
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max);
}
}
}
else if (Mode == CombatMode.Arrest)
{
// Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons.
if (weapon.Item.HasTag("stunner"))
{
priority *= 2;
}
else
{
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
if (diff < 0)
{
priority /= 2;
}
}
}
}
else if (weapon is MeleeWeapon && weapon.Item.HasTag("stunner") && !CanMeleeStunnerStun(weapon))
{
Attack attack = GetAttackDefinition(weapon);
priority = attack?.GetTotalDamage() ?? priority / 2;
}
float priority = GetWeaponPriority(weapon, prioritizeMelee, isCloseToEnemy, out lethalDmg);
if (priority > bestPriority)
{
weaponComponent = weapon;
@@ -636,7 +666,7 @@ namespace Barotrauma
if (bestPriority < 1) { return null; }
if (Mode == CombatMode.Arrest)
{
if (weaponComponent.Item.HasTag("stunner"))
if (weaponComponent.Item.HasTag(Tags.StunnerItem))
{
isLethalWeapon = false;
}
@@ -654,44 +684,6 @@ namespace Barotrauma
}
}
return weaponComponent.Item;
float ApproximateStunDamage(ItemComponent weapon, Attack attack)
{
// Try to reduce the priority using the actual damage values and status effects.
// This is an approximation, because we can't check the status effect conditions here.
// The result might be incorrect if there is a high stun effect that's only applied in certain conditions.
var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character));
if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List<StatusEffect> hitEffects))
{
statusEffects = statusEffects.Concat(hitEffects);
}
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0);
float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
{
float stunAmount = 0;
var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType);
if (stunAffliction != null)
{
stunAmount = stunAffliction.Strength;
}
return stunAmount;
});
return attack.Stun + afflictionsStun + effectsStun;
}
bool CanMeleeStunnerStun(ItemComponent weapon)
{
// If there's an item container that takes a battery,
// assume that it's required for the stun effect
// as we can't check the status effect conditions here.
var mobileBatteryTag = "mobilebattery".ToIdentifier();
var containers = weapon.Item.Components.Where(ic =>
ic is ItemContainer container &&
container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
// If there's no such container, assume that the melee weapon can stun without a battery.
return containers.None() || containers.Any(container =>
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
}
}
public static float GetLethalDamage(ItemComponent weapon)
@@ -771,13 +763,13 @@ namespace Barotrauma
{
return false;
}
if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType))
if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType))
{
//clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
character.ClearInput(InputType.Aim);
character.ClearInput(InputType.Shoot);
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
var slots = Weapon.AllowedSlots.Where(s => CharacterInventory.IsHandSlotType(s));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
@@ -791,8 +783,6 @@ namespace Barotrauma
}
}
return true;
static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
}
private float findHullTimer;
@@ -926,7 +916,7 @@ namespace Barotrauma
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown)
{
if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _))
if (HumanAIController.HasItem(character, Tags.HandLockerItem, out _))
{
if (!arrestingRegistered)
{
@@ -986,7 +976,7 @@ namespace Barotrauma
foreach (var item in Enemy.Inventory.AllItemsMod)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
item.HasTag("weapon") ||
item.HasTag(Tags.Weapon) ||
item.GetComponent<MeleeWeapon>() != null ||
item.GetComponent<RangedWeapon>() != null)
{
@@ -997,9 +987,9 @@ namespace Barotrauma
}
//prefer using handcuffs already on the enemy's inventory
if (!HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems))
if (!HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out IEnumerable<Item> matchingItems))
{
HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out matchingItems);
HumanAIController.HasItem(character, Tags.HandLockerItem, out matchingItems);
}
if (matchingItems.Any() &&
@@ -1079,7 +1069,7 @@ namespace Barotrauma
if (ammunitionIdentifiers != null)
{
// Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
if (ammunition != null)
{
@@ -1205,7 +1195,7 @@ namespace Barotrauma
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
// Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
foreach (var body in pickedBodies)
{
Character target = null;
@@ -1248,7 +1238,7 @@ namespace Barotrauma
}
}
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
Weapon.Use(deltaTime, user: character);
reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
}
@@ -1265,7 +1255,7 @@ namespace Barotrauma
{
Unequip();
}
SteeringManager.Reset();
SteeringManager?.Reset();
}
protected override void OnAbandon()
@@ -1275,7 +1265,7 @@ namespace Barotrauma
{
Unequip();
}
SteeringManager.Reset();
SteeringManager?.Reset();
}
public override void Reset()
@@ -11,6 +11,7 @@ namespace Barotrauma
class AIObjectiveContainItem: AIObjective
{
public override Identifier Identifier { get; set; } = "contain item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
public Func<Item, float> GetItemPriority;
@@ -109,7 +110,7 @@ namespace Barotrauma
private bool CheckItem(Item item)
{
return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character);
return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character) && container.ShouldBeContained(item, out _);
}
protected override void Act(float deltaTime)
@@ -226,7 +227,10 @@ namespace Barotrauma
AllowToFindDivingGear = AllowToFindDivingGear,
AllowDangerousPressure = AllowDangerousPressure,
TargetCondition = ConditionLevel,
ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem),
ItemFilter = (Item potentialItem) =>
{
return (RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)) && container.ShouldBeContained(potentialItem, out _);
},
ItemCount = ItemCount,
TakeWholeStack = MoveWholeStack
}, onAbandon: () =>
@@ -9,11 +9,12 @@ namespace Barotrauma
class AIObjectiveDecontainItem : AIObjective
{
public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
public Func<Item, float> GetItemPriority;
//can either be a tag or an identifier
private readonly string[] itemIdentifiers;
private readonly Identifier[] itemIdentifiers;
private readonly ItemContainer sourceContainer;
private readonly ItemContainer targetContainer;
private readonly Item targetItem;
@@ -52,16 +53,16 @@ namespace Barotrauma
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, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: this(character, new Identifier[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
public AIObjectiveDecontainItem(Character character, Identifier[] 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();
itemIdentifiers[i] = itemIdentifiers[i];
}
this.sourceContainer = sourceContainer;
this.targetContainer = targetContainer;
@@ -88,7 +88,7 @@ namespace Barotrauma
escapeProgress += Rand.Range(2, 5);
if (escapeProgress > 15)
{
Item handcuffs = character.Inventory.FindItemByTag("handlocker".ToIdentifier());
Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem);
if (handcuffs != null)
{
handcuffs.Drop(character);
@@ -15,6 +15,8 @@ namespace Barotrauma
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
private readonly Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective;
@@ -30,8 +32,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
bool isOrder = objectiveManager.HasOrder<AIObjectiveExtinguishFires>();
@@ -176,19 +177,19 @@ namespace Barotrauma
getExtinguisherObjective = null;
gotoObjective = null;
sinTime = 0;
SteeringManager.Reset();
SteeringManager?.Reset();
}
protected override void OnCompleted()
{
base.OnCompleted();
SteeringManager.Reset();
SteeringManager?.Reset();
}
protected override void OnAbandon()
{
base.OnAbandon();
SteeringManager.Reset();
SteeringManager?.Reset();
}
}
}
@@ -12,6 +12,7 @@ namespace Barotrauma
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowWhileHandcuffed => false;
private readonly Identifier gearTag;
@@ -22,34 +23,20 @@ namespace Barotrauma
public const float MIN_OXYGEN = 10;
public static readonly Identifier HEAVY_DIVING_GEAR = "deepdiving".ToIdentifier();
public static readonly Identifier LIGHT_DIVING_GEAR = "lightdiving".ToIdentifier();
/// <summary>
/// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear)
/// </summary>
public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier();
public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier();
protected override bool CheckObjectiveSpecific() =>
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTag = needsDivingSuit ? HEAVY_DIVING_GEAR : LIGHT_DIVING_GEAR;
gearTag = needsDivingSuit ? Tags.HeavyDivingGear : Tags.LightDivingGear;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true));
if (targetItem == null && gearTag == LIGHT_DIVING_GEAR)
if (targetItem == null && gearTag == Tags.LightDivingGear)
{
TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
TrySetTargetItem(character.Inventory.FindItemByTag(Tags.HeavyDivingGear, true));
}
if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
@@ -74,7 +61,7 @@ namespace Barotrauma
onCompleted: () =>
{
RemoveSubObjective(ref getDivingGear);
if (gearTag == HEAVY_DIVING_GEAR && HumanAIController.HasItem(character, LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
{
foreach (Item mask in masks)
{
@@ -95,10 +82,10 @@ namespace Barotrauma
{
if (character.IsOnPlayerTeam)
{
if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min))
if (HumanAIController.HasItem(character, Tags.OxygenSource, out _, conditionPercentage: min))
{
character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f);
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min, recursive: true).Count == 1)
if (character.Inventory.FindAllItems(i => i.HasTag(Tags.OxygenSource) && i.Condition > min, recursive: true).Count == 1)
{
character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f);
}
@@ -109,7 +96,7 @@ namespace Barotrauma
}
}
var container = targetItem.GetComponent<ItemContainer>();
var objective = new AIObjectiveContainItem(character, OXYGEN_SOURCE, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
var objective = new AIObjectiveContainItem(character, Tags.OxygenSource, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
@@ -119,7 +106,7 @@ namespace Barotrauma
};
if (container.HasSubContainers)
{
objective.TargetSlot = container.FindSuitableSubContainerIndex(OXYGEN_SOURCE);
objective.TargetSlot = container.FindSuitableSubContainerIndex(Tags.OxygenSource);
}
// Only remove the oxygen source being replaced
objective.RemoveExistingPredicate = i => objective.IsInTargetSlot(i);
@@ -132,7 +119,7 @@ namespace Barotrauma
// 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)
return new AIObjectiveContainItem(character, Tags.OxygenSource, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
@@ -142,7 +129,7 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
if (remainingTanks > 0 && !HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: 0.01f))
if (remainingTanks > 0 && !HumanAIController.HasItem(character, Tags.OxygenSource, out _, conditionPercentage: 0.01f))
{
character.Speak(TextManager.Get("dialogcantfindtoxygen").Value, null, 0, "cantfindoxygen".ToIdentifier(), 30.0f);
}
@@ -158,7 +145,7 @@ namespace Barotrauma
int ReportOxygenTankCount()
{
if (character.Submarine != Submarine.MainSub) { return 1; }
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 1);
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f);
@@ -177,7 +164,7 @@ namespace Barotrauma
{
return
item != null &&
item.HasTag(OXYGEN_SOURCE) &&
item.HasTag(Tags.OxygenSource) &&
item.Condition > 0 &&
(oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value));
}
@@ -188,7 +175,7 @@ namespace Barotrauma
targetItem = item;
if (targetItem != null)
{
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(OXYGEN_SOURCE);
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(Tags.OxygenSource);
}
else
{
@@ -212,7 +199,7 @@ namespace Barotrauma
// 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 = 0.01f;
float minOxygen = character.IsInFriendlySub ? MIN_OXYGEN : min;
if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag("oxygensource") && i.ConditionPercentage >= minOxygen))
if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag(Tags.OxygenSource) && i.ConditionPercentage >= minOxygen))
{
// There's a valid oxygen tank in the inventory -> no need to swap the tank too early.
minOxygen = min;
@@ -40,24 +40,21 @@ namespace Barotrauma
protected override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (character.CurrentHull == null)
{
Priority = (
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
objectiveManager.Objectives.Any(o => (o is AIObjectiveCombat || o is AIObjectiveReturn) && o.Priority > 0))
&& ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
}
else
{
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true);
if (isSuffocatingInDivingSuit ||
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)) ||
(!objectiveManager.HasActiveObjective<AIObjectiveFindDivingGear>() && IsSuffocatingWithoutDivingGear(character)))
{
Priority = AIObjectiveManager.MaxObjectivePriority;
}
@@ -215,7 +212,7 @@ namespace Barotrauma
AllowGoingOutside =
character.IsProtectedFromPressure ||
character.CurrentHull == null ||
character.CurrentHull.IsTaggedAirlock() ||
character.CurrentHull.IsAirlock ||
character.CurrentHull.LeadsOutside(character)
},
onCompleted: () =>
@@ -258,6 +255,13 @@ namespace Barotrauma
}
if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
UpdateSimpleEscape(deltaTime);
if (cannotFindSafeHull && !character.IsInFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn))
{
if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab))
{
objectiveManager.AddObjective(new AIObjectiveReturn(character, character, objectiveManager));
}
}
}
}
@@ -433,8 +437,7 @@ namespace Barotrauma
}
else
{
// TODO: could also target gaps that get us inside?
if (potentialHull.IsTaggedAirlock())
if (potentialHull.IsAirlock)
{
hullSafety = 100;
hullIsAirlock = true;
@@ -12,7 +12,9 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInFriendlySubs => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
public Gap Leak { get; private set; }
@@ -35,8 +37,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
float coopMultiplier = 1;
@@ -94,6 +95,7 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
var weldingTool = character.Inventory.FindItemByTag("weldingequipment".ToIdentifier(), true);
var repairTool = weldingTool?.GetComponent<RepairTool>();
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment".ToIdentifier(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
@@ -110,17 +112,25 @@ namespace Barotrauma
}
else
{
if (weldingTool.OwnInventory == null)
if (repairTool == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no RepairTool component but is tagged as a welding tool");
#endif
Abandon = true;
return;
}
if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
if (weldingTool.OwnInventory == null && repairTool.requiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel".ToIdentifier(), weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no proper inventory");
#endif
Abandon = true;
return;
}
if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag(Tags.WeldingFuel) && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, Tags.WeldingFuel, weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
RemoveExisting = true
},
@@ -138,7 +148,7 @@ namespace Barotrauma
void ReportWeldingFuelTankCount()
{
if (character.Submarine != Submarine.MainSub) { return; }
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1);
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.WeldingFuel) && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfWeldingFuel").Value, null, 0.0f, "outofweldingfuel".ToIdentifier(), 30.0f);
@@ -152,15 +162,6 @@ namespace Barotrauma
}
}
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.AnimController.AimSourceWorldPos;
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
@@ -200,7 +201,8 @@ namespace Barotrauma
Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))),
endNodeFilter = IsSuitableEndNode,
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
SpeakCannotReachCondition = () => !CheckObjectiveSpecific()
// Only report about contextual targets.
SpeakCannotReachCondition = () => isPriority && !CheckObjectiveSpecific()
},
onAbandon: () =>
{
@@ -9,7 +9,8 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
public override bool AllowInFriendlySubs => true;
private Hull PrioritizedHull { get; set; }
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Hull prioritizedHull = null) : base(character, objectiveManager, priorityModifier)
@@ -15,6 +15,7 @@ namespace Barotrauma
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowMultipleInstances => true;
public override bool AllowWhileHandcuffed => false;
public HashSet<Item> ignoredItems = new HashSet<Item>();
@@ -158,11 +159,6 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (IdentifiersOrTags != null && !isDoneSeeking)
{
if (checkInventory)
@@ -271,15 +267,49 @@ namespace Barotrauma
Inventory itemInventory = targetItem.ParentInventory;
var slots = itemInventory?.FindIndices(targetItem);
var droppedStack = TargetItem.DroppedStack.ToList();
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true, targetTags: IdentifiersOrTags))
{
if (TakeWholeStack && slots != null)
if (TakeWholeStack)
{
foreach (int slot in slots)
//taking the whole stack in this context means "as many items that can fit in one of the bot's slots",
//and the stack means either a stack of items in an inventory slot or a "dropped stack"
//so we need a bit of extra logic here
int maxStackSize = 0;
int takenItemCount = 1;
for (int i = 0; i < character.Inventory.Capacity; i++)
{
foreach (Item item in itemInventory.GetItemsAt(slot).ToList())
maxStackSize = Math.Max(maxStackSize, character.Inventory.HowManyCanBePut(targetItem.Prefab, i, condition: null));
}
if (slots != null)
{
foreach (int slot in slots)
{
HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true);
foreach (Item item in itemInventory.GetItemsAt(slot).ToList())
{
if (HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true))
{
takenItemCount++;
if (takenItemCount >= maxStackSize) { break; }
}
else
{
break;
}
}
}
}
foreach (var item in droppedStack)
{
if (item == TargetItem) { continue; }
if (HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true))
{
takenItemCount++;
if (takenItemCount >= maxStackSize) { break; }
}
else
{
break;
}
}
}
@@ -411,7 +441,7 @@ namespace Barotrauma
if (!CheckItem(item)) { continue; }
if (item.Container != null)
{
if (item.Container.HasTag("donttakeitems")) { continue; }
if (item.Container.HasTag(Tags.DontTakeItems)) { continue; }
if (ignoredItems.Contains(item.Container)) { continue; }
if (ignoredContainerIdentifiers != null)
{
@@ -12,6 +12,7 @@ namespace Barotrauma
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public override bool AllowMultipleInstances => true;
public override bool AllowWhileHandcuffed => false;
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
@@ -40,55 +41,48 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands)
if (subObjectivesCreated) { return; }
foreach (Identifier tag in gearTags)
{
Abandon = true;
return;
}
if (!subObjectivesCreated)
{
foreach (Identifier tag in gearTags)
{
if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; }
int count = gearTags.Count(t => t == tag);
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
AllowVariants = AllowVariants,
Wear = Wear,
TakeWholeStack = TakeWholeStack,
AllowStealing = AllowStealing,
ignoredIdentifiersOrTags = ignoredTags,
CheckPathForEachItem = CheckPathForEachItem,
RequireNonEmpty = RequireNonEmpty,
ItemCount = count,
SpeakIfFails = RequireAllItems
},
onCompleted: () =>
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
achievedItems.Add(item);
}
},
onAbandon: () =>
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
}
subObjectivesCreated = true;
if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; }
int count = gearTags.Count(t => t == tag);
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
AllowVariants = AllowVariants,
Wear = Wear,
TakeWholeStack = TakeWholeStack,
AllowStealing = AllowStealing,
ignoredIdentifiersOrTags = ignoredTags,
CheckPathForEachItem = CheckPathForEachItem,
RequireNonEmpty = RequireNonEmpty,
ItemCount = count,
SpeakIfFails = RequireAllItems
},
onCompleted: () =>
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
achievedItems.Add(item);
}
},
onAbandon: () =>
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
}
subObjectivesCreated = true;
}
public override void Reset()
@@ -223,23 +223,30 @@ namespace Barotrauma
Hull targetHull = GetTargetHull();
if (!IsFollowOrder)
{
// Abandon if going through unsafe paths or targeting unsafe hulls.
bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull);
if (!objectiveManager.CurrentObjective.IgnoreUnsafeHulls)
{
if (HumanAIController.UnsafeHulls.Contains(targetHull))
// Wait orders check this so that the bot temporarily leaves the unsafe hull.
// Non-orders (that are not set to ignore the unsafe hulls) abandon. In practice this means e.g. repair and clean up item subobjectives (of the looping parent objective).
// Other orders are only abandoned if the hull is unreachable, because the path is invalid or not found at all.
if (IsWaitOrder || !objectiveManager.HasOrders())
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(targetHull);
}
else if (PathSteering?.CurrentPath != null)
{
foreach (WayPoint wp in PathSteering.CurrentPath.Nodes)
if (HumanAIController.UnsafeHulls.Contains(targetHull))
{
if (wp.CurrentHull == null) { continue; }
if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull))
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(targetHull);
}
else if (PathSteering?.CurrentPath != null)
{
foreach (WayPoint wp in PathSteering.CurrentPath.Nodes)
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull);
if (wp.CurrentHull == null) { continue; }
if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull))
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull);
}
}
}
}
@@ -803,7 +810,7 @@ namespace Barotrauma
private void StopMovement()
{
SteeringManager.Reset();
SteeringManager?.Reset();
if (Target != null)
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
@@ -382,7 +382,9 @@ namespace Barotrauma
{
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != currentHull || !item.HasTag("chair")) { continue; }
if (item.CurrentHull != currentHull || !item.HasTag(Tags.ChairItem)) { continue; }
//not possible in vanilla game, but a mod might have holdable/attachable chairs
if (item.ParentInventory != null || item.body is { Enabled: true }) { continue; }
var controller = item.GetComponent<Controller>();
if (controller == null || controller.User != null) { continue; }
item.TryInteract(character, forceSelectKey: true);
@@ -17,6 +17,8 @@ namespace Barotrauma
set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
}
public override bool AllowWhileHandcuffed => false;
private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; }
private Item Container { get; }
private ItemContainer ItemContainer { get; }
@@ -161,8 +163,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition))
@@ -299,7 +300,7 @@ namespace Barotrauma
if (rootInventoryOwner is Character owner && owner != character) { return false; }
if (rootInventoryOwner is Item parentItem)
{
if (parentItem.HasTag("donttakeitems")) { return false; }
if (parentItem.HasTag(Tags.DontTakeItems)) { return false; }
}
if (!item.HasAccess(character)) { return false; }
if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; }
@@ -44,6 +44,8 @@ namespace Barotrauma
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowSubObjectiveSorting => true;
public override bool AllowWhileHandcuffed => false;
public virtual bool InverseTargetEvaluation => false;
protected virtual bool ResetWhenClearingIgnoreList => true;
protected virtual bool ForceOrderPriority => true;
@@ -117,51 +119,44 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
HandleNonAllowed();
return Priority;
}
if (character.LockHands)
// 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 = currentSubObjective.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
{
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
if (InverseTargetEvaluation)
if (objectiveManager.IsOrder(this))
{
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 = currentSubObjective.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;
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue;
}
else
{
if (objectiveManager.IsOrder(this))
float max = AIObjectiveManager.LowestOrderPriority - 1;
if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character))
{
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue;
}
else
{
float max = AIObjectiveManager.LowestOrderPriority - 1;
if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character))
{
// Allow higher prio
max = AIObjectiveManager.EmergencyObjectivePriority;
}
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
// Allow higher prio
max = AIObjectiveManager.EmergencyObjectivePriority;
}
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
}
return Priority;
@@ -530,7 +530,7 @@ namespace Barotrauma
case "cleanupitems":
if (order.TargetEntity is Item targetItem)
{
if (targetItem.HasTag("allowcleanup") && targetItem.ParentInventory == null && targetItem.OwnInventory != null)
if (targetItem.HasTag(Tags.AllowCleanup) && targetItem.ParentInventory == null && targetItem.OwnInventory != null)
{
// Target all items inside the container
newObjective = new AIObjectiveCleanupItems(character, this, targetItem.OwnInventory.AllItems, priorityModifier);
@@ -14,6 +14,7 @@ namespace Barotrauma
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
private readonly ItemComponent component, controller;
@@ -47,10 +48,9 @@ namespace Barotrauma
protected override float GetPriority()
{
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed || character.LockHands)
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
HandleNonAllowed();
return Priority;
}
if (!isOrder && component.Item.ConditionPercentage <= 0)
@@ -13,6 +13,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool KeepDivingGearOnAlsoWhenInactive => true;
public override bool PrioritizeIfSubObjectivesActive => true;
public override bool AllowWhileHandcuffed => false;
private AIObjectiveGetItem getSingleItemObjective;
private AIObjectiveGetItems getAllItemsObjective;
@@ -60,8 +61,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
Priority = objectiveManager.GetOrderPriority(this);
@@ -75,11 +75,6 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (!subObjectivesCreated)
{
if (FindAllItems && targetItem == null)
@@ -12,6 +12,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "pump water".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowWhileHandcuffed => false;
private List<Pump> pumpList;
@@ -54,7 +55,7 @@ namespace Barotrauma
var pump = item.GetComponent<Pump>();
if (pump == null || pump.Item.Submarine == null || pump.Item.CurrentHull == null) { continue; }
if (pump.Item.Submarine.TeamID != character.TeamID) { continue; }
if (pump.Item.HasTag("ballast")) { continue; }
if (pump.Item.HasTag(Tags.Ballast)) { continue; }
pumpList.Add(pump);
}
}
@@ -10,8 +10,9 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "repair item".ToIdentifier();
public override bool AllowInAnySub => true;
public override bool AllowInFriendlySubs => true;
public override bool KeepDivingGearOn => Item?.CurrentHull == null;
public override bool AllowWhileHandcuffed => false;
public Item Item { get; private set; }
@@ -36,10 +37,13 @@ namespace Barotrauma
protected override float GetPriority()
{
if (!IsAllowed || Item.IgnoreByAI(character))
if (!IsAllowed) { HandleNonAllowed(); }
if (Item.IgnoreByAI(character))
{
Priority = 0;
Abandon = true;
}
if (Abandon)
{
if (IsRepairing())
{
Item.Repairables.ForEach(r => r.StopRepairing(character));
@@ -136,32 +140,35 @@ namespace Barotrauma
}
if (repairTool != null)
{
if (repairTool.Item.OwnInventory == null)
if (repairTool.requiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems))
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
RelatedItem item = null;
Item fuel = null;
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
{
item = requiredItem;
fuel = repairTool.Item.OwnInventory.AllItems.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, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
if (repairTool.Item.OwnInventory == null)
{
RemoveExisting = true
},
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"{repairTool}\" has no proper inventory.");
#endif
Abandon = true;
return;
}
RelatedItem item = null;
Item fuel = null;
foreach (RelatedItem requiredItem in requiredItems)
{
item = requiredItem;
fuel = repairTool.Item.OwnInventory.AllItems.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, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
RemoveExisting = true
},
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
}
}
}
if (character.CanInteractWith(Item, out _, checkLinked: false))
@@ -170,10 +177,8 @@ namespace Barotrauma
if (waitTimer < WaitTimeBeforeRepair) { return; }
HumanAIController.FaceTarget(Item);
if (repairTool != null)
{
OperateRepairTool(deltaTime);
}
bool repairThroughRepairInterface = false;
foreach (Repairable repairable in Item.Repairables)
{
if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
@@ -185,10 +190,11 @@ namespace Barotrauma
{
if (character.SelectedItem != Item)
{
if (Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true) ||
Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true))
if (Item.TryInteract(character, forceUseKey: true) ||
Item.TryInteract(character, forceSelectKey: true))
{
character.SelectedItem = Item;
repairThroughRepairInterface = true;
}
else
{
@@ -209,8 +215,17 @@ namespace Barotrauma
{
repairable.StartRepairing(character, Repairable.FixActions.Repair);
}
else
{
repairThroughRepairInterface = true;
}
break;
}
if (!repairThroughRepairInterface && repairTool != null && !Abandon)
{
OperateRepairTool(deltaTime);
}
}
else
{
@@ -222,7 +237,8 @@ namespace Barotrauma
{
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
{
TargetName = Item.Name
TargetName = Item.Name,
SpeakCannotReachCondition = () => isPriority
};
if (repairTool != null)
{
@@ -19,7 +19,7 @@ namespace Barotrauma
public Item PrioritizedItem { get; private set; }
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
public override bool AllowInFriendlySubs => true;
public readonly static float RequiredSuccessFactor = 0.4f;
@@ -16,12 +16,13 @@ namespace Barotrauma
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
const float TreatmentDelay = 0.5f;
const float CloseEnoughToTreat = 100.0f;
private readonly Character targetCharacter;
public readonly Character Target;
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem replaceOxygenObjective;
@@ -44,7 +45,7 @@ namespace Barotrauma
Abandon = true;
return;
}
this.targetCharacter = targetCharacter;
Target = targetCharacter;
}
protected override void OnAbandon()
@@ -61,55 +62,55 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands || targetCharacter == null || targetCharacter.Removed || targetCharacter.IsDead)
if (Target == null || Target.Removed || Target.IsDead)
{
Abandon = true;
return;
}
var otherRescuer = targetCharacter.SelectedBy;
var otherRescuer = Target.SelectedBy;
if (otherRescuer != null && otherRescuer != character)
{
// Someone else is rescuing/holding the target.
Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical");
return;
}
if (targetCharacter != character)
if (Target != character)
{
if (targetCharacter.IsIncapacitated)
if (Target.IsIncapacitated)
{
// Check if the character needs more oxygen
if (!ignoreOxygen && character.SelectedCharacter == targetCharacter || character.CanInteractWith(targetCharacter))
if (!ignoreOxygen && character.SelectedCharacter == Target || character.CanInteractWith(Target))
{
// Replace empty oxygen and welding fuel.
if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out IEnumerable<Item> suits, requireEquipped: true))
if (HumanAIController.HasItem(Target, Tags.HeavyDivingGear, out IEnumerable<Item> suits, requireEquipped: true))
{
Item suit = suits.FirstOrDefault();
if (suit != null)
{
AIController.UnequipEmptyItems(character, suit);
AIController.UnequipContainedItems(character, suit, it => it.HasTag("weldingfuel"));
AIController.UnequipContainedItems(character, suit, it => it.HasTag(Tags.WeldingFuel));
}
}
else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
else if (HumanAIController.HasItem(Target, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
{
Item mask = masks.FirstOrDefault();
if (mask != null)
{
AIController.UnequipEmptyItems(character, mask);
AIController.UnequipContainedItems(character, mask, it => it.HasTag("weldingfuel"));
AIController.UnequipContainedItems(character, mask, it => it.HasTag(Tags.WeldingFuel));
}
}
bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0;
bool ShouldRemoveDivingSuit() => Target.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && Target.CurrentHull?.LethalPressure <= 0;
if (ShouldRemoveDivingSuit())
{
suits.ForEach(suit => suit.Drop(character));
}
else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0)))
else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(Tags.OxygenSource) && it.ConditionPercentage > 0)))
{
// The target has a suit equipped with an empty oxygen tank.
// Can't remove the suit, because the target needs it.
// If we happen to have an extra oxygen tank in the inventory, let's swap it.
Item spareOxygenTank = FindOxygenTank(targetCharacter) ?? FindOxygenTank(character);
Item spareOxygenTank = FindOxygenTank(Target) ?? FindOxygenTank(character);
if (spareOxygenTank != null)
{
Item suit = suits.FirstOrDefault();
@@ -133,36 +134,36 @@ namespace Barotrauma
Item FindOxygenTank(Character c) =>
c.Inventory.FindItem(i =>
i.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) &&
i.HasTag(Tags.OxygenSource) &&
i.ConditionPercentage > 1 &&
i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag("diving")) == null,
i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag(Tags.DivingGear)) == null,
recursive: true);
}
}
if (character.Submarine != null && targetCharacter.CurrentHull != null)
if (character.Submarine != null && Target.CurrentHull != null)
{
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
if (HumanAIController.GetHullSafety(Target.CurrentHull, Target) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
// Incapacitated target is not in a safe place -> Move to a safe place first
if (character.SelectedCharacter != targetCharacter)
if (character.SelectedCharacter != Target)
{
if (HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
if (HumanAIController.VisibleHulls.Contains(Target.CurrentHull) && Target.CurrentHull.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
null, 1.0f, $"foundunconscioustarget{targetCharacter.Name}".ToIdentifier(), 60.0f);
("[targetname]", Target.Name, FormatCapitals.No),
("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
null, 1.0f, $"foundunconscioustarget{Target.Name}".ToIdentifier(), 60.0f);
}
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
if (!character.CanInteractWith(Target))
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
TargetName = targetCharacter.DisplayName
TargetName = Target.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
@@ -173,7 +174,7 @@ namespace Barotrauma
}
else
{
character.SelectCharacter(targetCharacter);
character.SelectCharacter(Target);
}
}
else
@@ -213,16 +214,16 @@ namespace Barotrauma
if (subObjectives.Any()) { return; }
if (targetCharacter != character && !character.CanInteractWith(targetCharacter))
if (Target != character && !character.CanInteractWith(Target))
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
// Go to the target and select it
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
TargetName = targetCharacter.DisplayName
TargetName = Target.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
@@ -234,14 +235,14 @@ namespace Barotrauma
else
{
// We can start applying treatment
if (character != targetCharacter && character.SelectedCharacter != targetCharacter)
if (character != Target && character.SelectedCharacter != Target)
{
if (targetCharacter.CurrentHull?.DisplayName != null)
if (Target.CurrentHull?.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
null, 1.0f, $"foundwoundedtarget{targetCharacter.Name}".ToIdentifier(), 60.0f);
("[targetname]", Target.Name, FormatCapitals.No),
("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
null, 1.0f, $"foundwoundedtarget{Target.Name}".ToIdentifier(), 60.0f);
}
}
GiveTreatment(deltaTime);
@@ -253,7 +254,7 @@ namespace Barotrauma
private readonly Dictionary<Identifier, float> currentTreatmentSuitabilities = new Dictionary<Identifier, float>();
private void GiveTreatment(float deltaTime)
{
if (targetCharacter == null)
if (Target == null)
{
string errorMsg = $"{character.Name}: Attempted to update a Rescue objective with no target!";
DebugConsole.ThrowError(errorMsg);
@@ -263,10 +264,10 @@ namespace Barotrauma
SteeringManager.Reset();
if (!targetCharacter.IsPlayer)
if (!Target.IsPlayer)
{
// If the target is a bot, don't let it move
targetCharacter.AIController?.SteeringManager?.Reset();
Target.AIController?.SteeringManager?.Reset();
}
if (treatmentTimer > 0.0f)
{
@@ -275,28 +276,28 @@ namespace Barotrauma
}
treatmentTimer = TreatmentDelay;
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
float cprSuitability = Target.Oxygen < 0.0f ? -Target.Oxygen * 100.0f : 0.0f;
//find which treatments are the most suitable to treat the character's current condition
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f);
Target.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f);
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
foreach (Affliction affliction in GetSortedAfflictions(Target))
{
if (affliction == null) { throw new Exception("Affliction was null"); }
if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); }
float bestSuitability = 0.0f;
Item bestItem = null;
foreach (KeyValuePair<Identifier, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
foreach (KeyValuePair<Identifier, float> treatmentSuitability in affliction.Prefab.TreatmentSuitabilities)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) &&
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
//allow taking items from the target's inventory too if the target is unconscious
if (matchingItem == null && targetCharacter.IsIncapacitated)
if (matchingItem == null && Target.IsIncapacitated)
{
matchingItem ??= targetCharacter.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true);
matchingItem ??= Target.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true);
}
if (matchingItem != null)
{
@@ -307,7 +308,7 @@ namespace Barotrauma
}
if (bestItem != null)
{
if (targetCharacter != character) { character.SelectCharacter(targetCharacter); }
if (Target != character) { character.SelectCharacter(Target); }
ApplyTreatment(affliction, bestItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
@@ -370,12 +371,12 @@ namespace Barotrauma
("[treatment1]", itemListStr),
("[treatment2]", itemNameList.Last()));
}
if (targetCharacter != character && character.IsOnPlayerTeam)
if (Target != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
("[targetname]", Target.Name, FormatCapitals.No),
("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value,
null, 2.0f, $"listrequiredtreatments{targetCharacter.Name}".ToIdentifier(), 60.0f);
null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f);
}
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
@@ -397,18 +398,18 @@ namespace Barotrauma
}
}
}
else if (!targetCharacter.IsUnconscious)
else if (!Target.IsUnconscious)
{
Abandon = true;
//no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
SpeakCannotTreat();
return;
}
if (character != targetCharacter)
if (character != Target)
{
if (cprSuitability > 0.0f)
{
character.SelectCharacter(targetCharacter);
character.SelectCharacter(Target);
character.AnimController.Anim = AnimController.Animation.CPR;
performedCpr = true;
}
@@ -421,40 +422,40 @@ namespace Barotrauma
private void SpeakCannotTreat()
{
LocalizedString msg = character == targetCharacter ?
LocalizedString msg = character == Target ?
TextManager.Get("dialogcannottreatself") :
TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No);
TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", Target.DisplayName, FormatCapitals.No);
character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
}
private void ApplyTreatment(Affliction affliction, Item item)
{
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
item.ApplyTreatment(character, Target, Target.CharacterHealth.GetAfflictionLimb(affliction));
}
protected override bool CheckObjectiveSpecific()
{
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target);
if (isCompleted && Target != character && character.IsOnPlayerTeam)
{
string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed";
string message = TextManager.GetWithVariable(textTag, "[targetname]", targetCharacter.Name)?.Value;
character.Speak(message, delay: 1.0f, identifier: $"targethealed{targetCharacter.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value;
character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
}
return isCompleted;
}
protected override float GetPriority()
{
if (!IsAllowed || targetCharacter == null)
if (Target == null) { Abandon = true; }
if (!IsAllowed) { HandleNonAllowed(); }
if (Abandon)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (character.CurrentHull != null)
{
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
if (Character.CharacterList.Any(c => c.CurrentHull == Target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
{
// Don't go into rooms that have enemies
Priority = 0;
@@ -462,18 +463,18 @@ namespace Barotrauma
return Priority;
}
}
float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y);
float horizontalDistance = Math.Abs(character.WorldPosition.X - Target.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - Target.WorldPosition.Y);
if (character.Submarine?.Info is { IsRuin: false })
{
verticalDistance *= 2;
}
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
if (character.CurrentHull != null && targetCharacter.CurrentHull == character.CurrentHull)
if (character.CurrentHull != null && Target.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(Target) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
return Priority;
@@ -34,24 +34,29 @@ namespace Barotrauma
protected override bool Filter(Character target)
{
if (!IsValidTarget(target, character, requireTreatableAfflictions: false)) { return false; }
if (GetTreatableAfflictions(target).Any())
if (!IsValidTarget(target, character, out bool ignoredasMinorWounds))
{
return true;
}
else
{
//the target might be at a low enough health to be considered a valid target,
//but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them
// -> make the bot speak to make it clear the bot intentionally ignores very minor injuries
if (!charactersWithMinorInjuries.Contains(character))
if (ignoredasMinorWounds)
{
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
null, 1.0f, $"notreatableafflictions{target.Name}".ToIdentifier(), 10.0f);
charactersWithMinorInjuries.Add(character);
//the target might be at a low enough health to be considered a valid target,
//but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them
// -> make the bot speak to make it clear the bot intentionally ignores very minor injuries
if (character.IsOnPlayerTeam && target != character && !charactersWithMinorInjuries.Contains(target))
{
// But only speak about targets when we are not already actively treating, in which case we should be speaking about the current target.
if (objectiveManager.GetFirstActiveObjective<AIObjectiveRescue>() == null)
{
charactersWithMinorInjuries.Add(target);
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
delay: 1.0f,
identifier: $"notreatableafflictions{target.Name}".ToIdentifier(),
minDurationBetweenSimilar: 10.0f);
}
}
}
return false;
}
}
return true;
}
protected override IEnumerable<Character> GetList() => Character.CharacterList;
@@ -103,12 +108,13 @@ namespace Barotrauma
return Math.Clamp(vitality, 0, 100);
}
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold = false)
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff) { continue; }
if (!affliction.Prefab.HasTreatments) { continue; }
if (!ignoreTreatmentThreshold)
{
//other afflictions of the same type increase the "treatability"
@@ -116,7 +122,6 @@ namespace Barotrauma
float totalAfflictionStrength = character.CharacterHealth.GetTotalAdjustedAfflictionStrength(affliction);
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
}
if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
}
@@ -128,39 +133,50 @@ namespace Barotrauma
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
public static bool IsValidTarget(Character target, Character character, bool requireTreatableAfflictions = true)
public static bool IsValidTarget(Character target, Character character, out bool ignoredAsMinorWounds)
{
ignoredAsMinorWounds = false;
if (target == null || target.IsDead || target.Removed) { return false; }
if (target.IsInstigator) { return false; }
if (target.IsPet) { return false; }
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
bool isBelowTreatmentThreshold;
float vitalityFactor;
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target))
{
return false;
}
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
{
if (!character.IsMedic && target != character)
{
// Don't allow to treat others autonomously, unless we are a medic
return false;
}
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
return false;
}
}
if (requireTreatableAfflictions && GetTreatableAfflictions(target).None())
{
return false;
}
if (!IsValidTargetForAI(target, humanAI)) { return false; }
vitalityFactor = GetVitalityFactor(target);
isBelowTreatmentThreshold = vitalityFactor < GetVitalityThreshold(humanAI.ObjectiveManager, character, target);
}
else
{
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
vitalityFactor = GetVitalityFactor(target);
isBelowTreatmentThreshold = vitalityFactor < vitalityThreshold;
}
bool hasTreatableAfflictions = GetTreatableAfflictions(target, ignoreTreatmentThreshold: false).Any();
bool isValidTarget = isBelowTreatmentThreshold && hasTreatableAfflictions;
if (!isValidTarget)
{
ignoredAsMinorWounds = hasTreatableAfflictions || vitalityFactor < 100;
}
return isValidTarget;
}
private static bool IsValidTargetForAI(Character target, HumanAIController humanAI)
{
Character character = humanAI.Character;
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
{
if (!character.IsMedic && target != character)
{
// Don't allow to treat others autonomously, unless we are a medic
return false;
}
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
return false;
}
}
if (character.Submarine != null)
{
@@ -189,11 +205,5 @@ namespace Barotrauma
}
return character.GetDamageDoneByAttacker(target) <= 0;
}
public override void Reset()
{
base.Reset();
charactersWithMinorInjuries.Clear();
}
}
}
@@ -20,7 +20,7 @@ namespace Barotrauma
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
if (ReturnTarget == null)
{
DebugConsole.ThrowError("Error with a Return objective: no suitable return target found");
DebugConsole.AddSafeError("Error with a Return objective: no suitable return target found");
Abandon = true;
}
@@ -47,8 +47,7 @@ namespace Barotrauma
}
else
{
// TODO: Consider if this needs to be addressed
Priority = 0;
Priority = AIObjectiveManager.LowestOrderPriority - 1;
}
return Priority;
}
@@ -91,7 +90,7 @@ namespace Barotrauma
targetHull = d.Item.CurrentHull;
break;
}
if (targetHull != null && !targetHull.IsTaggedAirlock())
if (targetHull != null && !targetHull.IsAirlock)
{
// Target the closest airlock
float closestDist = 0;
@@ -99,7 +98,7 @@ namespace Barotrauma
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine != targetHull.Submarine) { continue; }
if (!hull.IsTaggedAirlock()) { continue; }
if (!hull.IsAirlock) { continue; }
float dist = Vector2.DistanceSquared(targetHull.Position, hull.Position);
if (airlock == null || closestDist <= 0 || dist < closestDist)
{
@@ -146,7 +145,7 @@ namespace Barotrauma
bool targetIsAirlock = false;
foreach (var hull in ReturnTarget.GetHulls(false))
{
bool hullIsAirlock = hull.IsTaggedAirlock();
bool hullIsAirlock = hull.IsAirlock;
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
{
float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);