Merge branch 'master' of https://github.com/Regalis11/Barotrauma.git into Regalis11-master
This commit is contained in:
@@ -93,13 +93,6 @@ namespace Barotrauma
|
||||
_abandon = value;
|
||||
if (_abandon)
|
||||
{
|
||||
#if DEBUG
|
||||
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && !objectiveManager.IsCurrentOrder<AIObjectiveReturn>())
|
||||
{
|
||||
// TODO: dismiss
|
||||
throw new Exception("Order abandoned!");
|
||||
}
|
||||
#endif
|
||||
OnAbandon();
|
||||
}
|
||||
}
|
||||
@@ -247,7 +240,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected bool IsAllowed
|
||||
public bool IsAllowed
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -271,7 +264,7 @@ namespace Barotrauma
|
||||
if (!IsAllowed)
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = !isOrder;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
if (isOrder)
|
||||
@@ -290,9 +283,9 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public float CalculatePriority()
|
||||
{
|
||||
ForceWalk = false;
|
||||
Priority = GetPriority();
|
||||
ForceHighestPriority = false;
|
||||
ForceWalk = false;
|
||||
return Priority;
|
||||
}
|
||||
|
||||
@@ -508,5 +501,34 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static bool CanEquip(Character character, Item item)
|
||||
{
|
||||
bool canEquip = item != null;
|
||||
if (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any))
|
||||
{
|
||||
canEquip = false;
|
||||
var inv = character.Inventory;
|
||||
foreach (var allowedSlot in item.AllowedSlots)
|
||||
{
|
||||
foreach (var slotType in inv.SlotTypes)
|
||||
{
|
||||
if (!allowedSlot.HasFlag(slotType)) { continue; }
|
||||
for (int i = 0; i < inv.Capacity; i++)
|
||||
{
|
||||
canEquip = true;
|
||||
if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null)
|
||||
{
|
||||
canEquip = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return canEquip;
|
||||
}
|
||||
|
||||
protected bool CanEquip(Item item) => CanEquip(character, item);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-29
@@ -68,15 +68,12 @@ namespace Barotrauma
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveCleanupItems, Item>(character, target);
|
||||
|
||||
private static bool IsItemInsideValidSubmarine(Item item, Character character)
|
||||
public static bool IsItemInsideValidSubmarine(Item item, Character character)
|
||||
{
|
||||
if (item.CurrentHull == null) { return false; }
|
||||
if (item.Submarine == null) { return false; }
|
||||
if (item.Submarine.TeamID != character.TeamID) { return false; }
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
|
||||
}
|
||||
if (character.Submarine != null && !character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -94,7 +91,7 @@ namespace Barotrauma
|
||||
if (item == null) { return false; }
|
||||
if (item.IgnoreByAI(character)) { return false; }
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.SpawnedInOutpost) { return false; }
|
||||
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
if (item.Container == null)
|
||||
@@ -129,29 +126,7 @@ namespace Barotrauma
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool canEquip = true;
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any))
|
||||
{
|
||||
canEquip = false;
|
||||
var inv = character.Inventory;
|
||||
foreach (var allowedSlot in item.AllowedSlots)
|
||||
{
|
||||
foreach (var slotType in inv.SlotTypes)
|
||||
{
|
||||
if (!allowedSlot.HasFlag(slotType)) { continue; }
|
||||
for (int i = 0; i < inv.Capacity; i++)
|
||||
{
|
||||
canEquip = true;
|
||||
if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null)
|
||||
{
|
||||
canEquip = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return canEquip;
|
||||
return CanEquip(character, item);
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
|
||||
+76
-44
@@ -111,7 +111,7 @@ namespace Barotrauma
|
||||
public CombatMode Mode { get; private set; }
|
||||
|
||||
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
|
||||
private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f);
|
||||
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator;
|
||||
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
|
||||
|
||||
private float AimSpeed => HumanAIController.AimSpeed;
|
||||
@@ -158,7 +158,7 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
}
|
||||
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, HumanAIController.GetDamageDoneByAttacker(Enemy) / 100.0f);
|
||||
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f);
|
||||
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
|
||||
return Priority;
|
||||
}
|
||||
@@ -177,11 +177,12 @@ namespace Barotrauma
|
||||
ignoredWeapons.Clear();
|
||||
ignoreWeaponTimer = ignoredWeaponsClearTime;
|
||||
}
|
||||
if (findSafety != null)
|
||||
bool isCurrentObjective = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
|
||||
if (findSafety != null && isCurrentObjective)
|
||||
{
|
||||
findSafety.Priority = 0;
|
||||
}
|
||||
if (!AllowCoolDown && !character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
|
||||
if (!AllowCoolDown && !character.IsOnPlayerTeam && !isCurrentObjective)
|
||||
{
|
||||
distanceTimer -= deltaTime;
|
||||
if (distanceTimer < 0)
|
||||
@@ -204,13 +205,18 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (IsEnemyDisabled)
|
||||
{
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
if (AllowCoolDown)
|
||||
{
|
||||
coolDownTimer -= deltaTime;
|
||||
}
|
||||
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
{
|
||||
if (Mode != CombatMode.Retreat && TryArm() && !IsEnemyDisabled)
|
||||
if (Mode != CombatMode.Retreat && TryArm())
|
||||
{
|
||||
OperateWeapon(deltaTime);
|
||||
}
|
||||
@@ -283,10 +289,12 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
AskHelp();
|
||||
Retreat(deltaTime);
|
||||
}
|
||||
break;
|
||||
case CombatMode.Retreat:
|
||||
AskHelp();
|
||||
Retreat(deltaTime);
|
||||
break;
|
||||
default:
|
||||
@@ -294,10 +302,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsLoaded(ItemComponent weapon, bool checkContainedItems = true) =>
|
||||
weapon.HasRequiredContainedItems(character, addMessage: false) &&
|
||||
(!checkContainedItems || weapon.Item.OwnInventory == null || weapon.Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
|
||||
|
||||
private bool TryArm()
|
||||
{
|
||||
if (character.LockHands || Enemy == null)
|
||||
@@ -325,7 +329,7 @@ namespace Barotrauma
|
||||
Weapon = null;
|
||||
continue;
|
||||
}
|
||||
if (IsLoaded(WeaponComponent, checkContainedItems: true))
|
||||
if (WeaponComponent.IsLoaded(character))
|
||||
{
|
||||
// All good, the weapon is loaded
|
||||
break;
|
||||
@@ -367,6 +371,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (WeaponComponent == null)
|
||||
{
|
||||
SpeakNoWeapons();
|
||||
Mode = CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
@@ -380,6 +385,7 @@ namespace Barotrauma
|
||||
constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false)
|
||||
{
|
||||
AllowStealing = HumanAIController.IsMentallyUnstable,
|
||||
EvaluateCombatPriority = false, // Use a custom formula instead
|
||||
GetItemPriority = i =>
|
||||
{
|
||||
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
|
||||
@@ -412,6 +418,7 @@ namespace Barotrauma
|
||||
onCompleted: () => RemoveSubObjective(ref seekWeaponObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
SpeakNoWeapons();
|
||||
RemoveSubObjective(ref seekWeaponObjective);
|
||||
Mode = CombatMode.Retreat;
|
||||
});
|
||||
@@ -433,7 +440,7 @@ namespace Barotrauma
|
||||
// Not in the inventory anymore or cannot find the weapon component
|
||||
return false;
|
||||
}
|
||||
if (!IsLoaded(WeaponComponent))
|
||||
if (!WeaponComponent.IsLoaded(character))
|
||||
{
|
||||
// Try reloading (and seek ammo)
|
||||
if (!Reload(seekAmmo))
|
||||
@@ -475,7 +482,7 @@ namespace Barotrauma
|
||||
foreach (var weapon in weaponList)
|
||||
{
|
||||
float priority = weapon.CombatPriority;
|
||||
if (!IsLoaded(weapon))
|
||||
if (!weapon.IsLoaded(character))
|
||||
{
|
||||
if (weapon is RangedWeapon && enemyIsClose)
|
||||
{
|
||||
@@ -564,31 +571,6 @@ namespace Barotrauma
|
||||
}
|
||||
return weaponComponent.Item;
|
||||
|
||||
static Attack GetAttackDefinition(ItemComponent weapon)
|
||||
{
|
||||
Attack attack = null;
|
||||
if (weapon is MeleeWeapon meleeWeapon)
|
||||
{
|
||||
attack = meleeWeapon.Attack;
|
||||
}
|
||||
else if (weapon is RangedWeapon rangedWeapon)
|
||||
{
|
||||
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
|
||||
}
|
||||
return attack;
|
||||
}
|
||||
|
||||
static float GetLethalDamage(ItemComponent weapon)
|
||||
{
|
||||
float lethalDmg = 0;
|
||||
Attack attack = GetAttackDefinition(weapon);
|
||||
if (attack != null)
|
||||
{
|
||||
lethalDmg = attack.GetTotalDamage();
|
||||
}
|
||||
return lethalDmg;
|
||||
}
|
||||
|
||||
float ApproximateStunDamage(ItemComponent weapon, Attack attack)
|
||||
{
|
||||
// Try to reduce the priority using the actual damage values and status effects.
|
||||
@@ -628,6 +610,31 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static float GetLethalDamage(ItemComponent weapon)
|
||||
{
|
||||
float lethalDmg = 0;
|
||||
Attack attack = GetAttackDefinition(weapon);
|
||||
if (attack != null)
|
||||
{
|
||||
lethalDmg = attack.GetTotalDamage();
|
||||
}
|
||||
return lethalDmg;
|
||||
}
|
||||
|
||||
private static Attack GetAttackDefinition(ItemComponent weapon)
|
||||
{
|
||||
Attack attack = null;
|
||||
if (weapon is MeleeWeapon meleeWeapon)
|
||||
{
|
||||
attack = meleeWeapon.Attack;
|
||||
}
|
||||
else if (weapon is RangedWeapon rangedWeapon)
|
||||
{
|
||||
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
|
||||
}
|
||||
return attack;
|
||||
}
|
||||
|
||||
private HashSet<ItemComponent> FindWeaponsFromInventory()
|
||||
{
|
||||
weapons.Clear();
|
||||
@@ -661,6 +668,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
if (Weapon.AllowedSlots.Contains(InvSlotType.Bag))
|
||||
{
|
||||
if (character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Bag }))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
Weapon.Drop(character);
|
||||
}
|
||||
}
|
||||
@@ -673,22 +687,25 @@ namespace Barotrauma
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!character.HasEquippedItem(Weapon))
|
||||
if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType))
|
||||
{
|
||||
Weapon.TryInteract(character, forceSelectKey: true);
|
||||
var slots = Weapon.AllowedSlots.Where(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
|
||||
var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
|
||||
if (character.Inventory.TryPutItem(Weapon, character, slots))
|
||||
{
|
||||
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
SpeakNoWeapons();
|
||||
Weapon = null;
|
||||
Mode = CombatMode.Retreat;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
|
||||
}
|
||||
|
||||
private float findHullTimer;
|
||||
@@ -788,7 +805,6 @@ namespace Barotrauma
|
||||
{
|
||||
UsePathingOutside = false,
|
||||
IgnoreIfTargetDead = true,
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = Enemy.DisplayName,
|
||||
AlwaysUseEuclideanDistance = false
|
||||
},
|
||||
@@ -812,7 +828,7 @@ namespace Barotrauma
|
||||
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs");
|
||||
if (prefab != null)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInOutpost = true);
|
||||
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true);
|
||||
}
|
||||
}
|
||||
RemoveFollowTarget();
|
||||
@@ -896,8 +912,9 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref seekAmmunitionObjective,
|
||||
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
|
||||
{
|
||||
targetItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
|
||||
checkInventory = false
|
||||
ItemCount = Weapon.GetComponent<ItemContainer>().Capacity * Weapon.GetComponent<ItemContainer>().MaxStackSize,
|
||||
checkInventory = false,
|
||||
MoveWholeStack = true
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective),
|
||||
onAbandon: () =>
|
||||
@@ -1103,7 +1120,7 @@ namespace Barotrauma
|
||||
if (WeaponComponent is RangedWeapon rangedWeapon)
|
||||
{
|
||||
// If the weapon is just equipped, we can't shoot just yet.
|
||||
if (rangedWeapon.ReloadTimer <= 0)
|
||||
if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
|
||||
{
|
||||
reloadTime = rangedWeapon.Reload;
|
||||
}
|
||||
@@ -1159,6 +1176,21 @@ namespace Barotrauma
|
||||
retreatTarget = null;
|
||||
}
|
||||
|
||||
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons", delay: 0, minDuration: 30);
|
||||
private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0, 1), minDuration: 20);
|
||||
|
||||
private void Speak(string textIdentifier, float delay, float minDuration)
|
||||
{
|
||||
if (character.IsOnPlayerTeam && !character.IsInFriendlySub)
|
||||
{
|
||||
string msg = TextManager.Get(textIdentifier, true);
|
||||
if (msg != null)
|
||||
{
|
||||
character.Speak(msg, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//private float CalculateEnemyStrength()
|
||||
//{
|
||||
// float enemyStrength = 0;
|
||||
|
||||
+23
-9
@@ -11,12 +11,11 @@ namespace Barotrauma
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
|
||||
public int targetItemCount = 1;
|
||||
public string[] ignoredContainerIdentifiers;
|
||||
public bool checkInventory = true;
|
||||
|
||||
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
|
||||
private bool spawnItemIfNotFound = false;
|
||||
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs and in some cases also enemy NPCs, like pirates)
|
||||
private readonly bool spawnItemIfNotFound;
|
||||
|
||||
//can either be a tag or an identifier
|
||||
public readonly string[] itemIdentifiers;
|
||||
@@ -35,9 +34,24 @@ namespace Barotrauma
|
||||
public bool Equip { get; set; }
|
||||
public bool RemoveEmpty { get; set; } = true;
|
||||
public bool RemoveExisting { get; set; }
|
||||
/// <summary>
|
||||
/// Only remove existing items when the contain target can't be put in the inventory
|
||||
/// </summary>
|
||||
public bool RemoveExistingWhenNecessary { get; set; }
|
||||
public Func<Item, bool> RemoveExistingPredicate { get; set; }
|
||||
public int? RemoveMax { get; set; }
|
||||
|
||||
public bool MoveWholeStack { get; set; }
|
||||
|
||||
private int _itemCount = 1;
|
||||
public int ItemCount
|
||||
{
|
||||
get { return _itemCount; }
|
||||
set
|
||||
{
|
||||
_itemCount = Math.Max(value, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
@@ -83,7 +97,7 @@ namespace Barotrauma
|
||||
containedItemCount++;
|
||||
}
|
||||
}
|
||||
return containedItemCount >= targetItemCount;
|
||||
return containedItemCount >= ItemCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +120,9 @@ namespace Barotrauma
|
||||
}
|
||||
if (character.CanInteractWith(container.Item, checkLinked: false))
|
||||
{
|
||||
if (RemoveExisting)
|
||||
if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(item)))
|
||||
{
|
||||
HumanAIController.UnequipContainedItems(container.Item);
|
||||
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax);
|
||||
}
|
||||
else if (RemoveEmpty)
|
||||
{
|
||||
@@ -127,7 +141,6 @@ namespace Barotrauma
|
||||
container.Inventory.TryPutItem(item, null);
|
||||
}
|
||||
}
|
||||
|
||||
IsCompleted = true;
|
||||
}
|
||||
}
|
||||
@@ -144,7 +157,6 @@ namespace Barotrauma
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = container.Item.Name,
|
||||
AbortCondition = obj =>
|
||||
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
|
||||
@@ -174,7 +186,9 @@ namespace Barotrauma
|
||||
AllowToFindDivingGear = AllowToFindDivingGear,
|
||||
AllowDangerousPressure = AllowDangerousPressure,
|
||||
TargetCondition = ConditionLevel,
|
||||
ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)
|
||||
ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem),
|
||||
ItemCount = ItemCount,
|
||||
TakeWholeStack = MoveWholeStack
|
||||
}, onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
|
||||
+15
-1
@@ -36,6 +36,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool DropIfFails { get; set; } = true;
|
||||
|
||||
public bool RemoveExistingWhenNecessary { get; set; }
|
||||
public Func<Item, bool> RemoveExistingPredicate { get; set; }
|
||||
public int? RemoveExistingMax { get; set; }
|
||||
public string AbandonGetItemDialogueIdentifier { get; set; }
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
@@ -86,6 +91,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (itemToDecontain.Container != sourceContainer.Item)
|
||||
{
|
||||
itemToDecontain.Drop(character);
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
@@ -98,7 +104,12 @@ namespace Barotrauma
|
||||
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
|
||||
{
|
||||
TryAddSubObjective(ref getItemObjective,
|
||||
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) { TakeWholeStack = this.TakeWholeStack },
|
||||
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip)
|
||||
{
|
||||
CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier,
|
||||
SpeakIfFails = AbandonGetItemDialogueIdentifier != null,
|
||||
TakeWholeStack = this.TakeWholeStack
|
||||
},
|
||||
onAbandon: () => Abandon = true);
|
||||
return;
|
||||
}
|
||||
@@ -110,6 +121,9 @@ namespace Barotrauma
|
||||
MoveWholeStack = TakeWholeStack,
|
||||
Equip = Equip,
|
||||
RemoveEmpty = false,
|
||||
RemoveExistingWhenNecessary = RemoveExistingWhenNecessary,
|
||||
RemoveExistingPredicate = RemoveExistingPredicate,
|
||||
RemoveMax = RemoveExistingMax,
|
||||
GetItemPriority = GetItemPriority,
|
||||
ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null
|
||||
},
|
||||
|
||||
+8
-2
@@ -31,7 +31,7 @@ namespace Barotrauma
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Character target)
|
||||
{
|
||||
AIObjectiveCombat.CombatMode combatMode = target.IsEscorted && character.TeamID == CharacterTeamType.Team1 ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive;
|
||||
AIObjectiveCombat.CombatMode combatMode = ShouldArrest(target, character) ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive;
|
||||
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
@@ -41,7 +41,7 @@ namespace Barotrauma
|
||||
combatObjective.holdFireCondition = () =>
|
||||
{
|
||||
//hold fire while the enemy is in the airlock (except if they've attacked us)
|
||||
if (HumanAIController.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
|
||||
if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
|
||||
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t.Equals("airlock", System.StringComparison.OrdinalIgnoreCase));
|
||||
};
|
||||
character.Speak(TextManager.Get("dialogenteroutpostwarning"), null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning", 30.0f);
|
||||
@@ -65,7 +65,13 @@ namespace Barotrauma
|
||||
if (HumanAIController.IsFriendly(character, target)) { return false; }
|
||||
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
|
||||
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
|
||||
if (target.IsArrested) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ShouldArrest(Character target, Character character)
|
||||
{
|
||||
return target != null && target.IsEscorted && character.TeamID == CharacterTeamType.Team1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -19,10 +19,15 @@ namespace Barotrauma
|
||||
private AIObjectiveContainItem getOxygen;
|
||||
private Item targetItem;
|
||||
|
||||
public static float MIN_OXYGEN = 10;
|
||||
public static string HEAVY_DIVING_GEAR = "deepdiving";
|
||||
public static string LIGHT_DIVING_GEAR = "lightdiving";
|
||||
public static string OXYGEN_SOURCE = "oxygensource";
|
||||
public const float MIN_OXYGEN = 10;
|
||||
|
||||
public const string HEAVY_DIVING_GEAR = "deepdiving";
|
||||
public const string LIGHT_DIVING_GEAR = "lightdiving";
|
||||
/// <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 const string DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors";
|
||||
public const string OXYGEN_SOURCE = "oxygensource";
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
|
||||
|
||||
|
||||
+14
-5
@@ -46,10 +46,19 @@ namespace Barotrauma
|
||||
}
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() ||
|
||||
objectiveManager.IsCurrentOrder<AIObjectiveReturn>() ||
|
||||
objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat))
|
||||
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
|
||||
if (!character.NeedsAir)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
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))
|
||||
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -253,7 +262,7 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (Character enemy in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy)) { continue; }
|
||||
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; }
|
||||
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
|
||||
{
|
||||
Vector2 dir = character.Position - enemy.Position;
|
||||
|
||||
+170
-62
@@ -1,6 +1,7 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -11,6 +12,7 @@ namespace Barotrauma
|
||||
public override string Identifier { get; set; } = "get item";
|
||||
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool AllowMultipleInstances => true;
|
||||
|
||||
public HashSet<Item> ignoredItems = new HashSet<Item>();
|
||||
|
||||
@@ -19,7 +21,7 @@ namespace Barotrauma
|
||||
public float TargetCondition { get; set; } = 1;
|
||||
public bool AllowDangerousPressure { get; set; }
|
||||
|
||||
private readonly string[] identifiersOrTags;
|
||||
public readonly ImmutableArray<string> IdentifiersOrTags;
|
||||
|
||||
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
|
||||
private bool spawnItemIfNotFound = false;
|
||||
@@ -31,6 +33,7 @@ namespace Barotrauma
|
||||
public Item TargetItem => targetItem;
|
||||
private int currSearchIndex;
|
||||
public string[] ignoredContainerIdentifiers;
|
||||
public string[] ignoredIdentifiersOrTags;
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private float currItemPriority;
|
||||
private readonly bool checkInventory;
|
||||
@@ -51,6 +54,21 @@ namespace Barotrauma
|
||||
public bool AllowVariants { get; set; }
|
||||
public bool Equip { get; set; }
|
||||
public bool Wear { get; set; }
|
||||
public bool RequireLoaded { get; set; }
|
||||
public bool EvaluateCombatPriority { get; set; }
|
||||
public bool CheckPathForEachItem { get; set; }
|
||||
public bool SpeakIfFails { get; set; }
|
||||
public string CannotFindDialogueIdentifierOverride { get; set; }
|
||||
|
||||
private int _itemCount = 1;
|
||||
public int ItemCount
|
||||
{
|
||||
get { return _itemCount; }
|
||||
set
|
||||
{
|
||||
_itemCount = Math.Max(value, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public InvSlotType? EquipSlotType { get; set; }
|
||||
|
||||
@@ -67,23 +85,46 @@ namespace Barotrauma
|
||||
public AIObjectiveGetItem(Character character, string identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
|
||||
: this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { }
|
||||
|
||||
public AIObjectiveGetItem(Character character, string[] identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
|
||||
public AIObjectiveGetItem(Character character, IEnumerable<string> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
currSearchIndex = -1;
|
||||
Equip = equip;
|
||||
this.identifiersOrTags = identifiersOrTags;
|
||||
this.spawnItemIfNotFound = spawnItemIfNotFound;
|
||||
for (int i = 0; i < identifiersOrTags.Length; i++)
|
||||
{
|
||||
identifiersOrTags[i] = identifiersOrTags[i].ToLowerInvariant();
|
||||
}
|
||||
this.checkInventory = checkInventory;
|
||||
IdentifiersOrTags = ParseGearTags(identifiersOrTags).ToImmutableArray();
|
||||
ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToArray();
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ParseGearTags(IEnumerable<string> identifiersOrTags)
|
||||
{
|
||||
var tags = new List<string>();
|
||||
foreach (string tag in identifiersOrTags)
|
||||
{
|
||||
if (!tag.Contains('!'))
|
||||
{
|
||||
tags.Add(tag.ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ParseIgnoredTags(IEnumerable<string> identifiersOrTags)
|
||||
{
|
||||
var ignoredTags = new List<string>();
|
||||
foreach (string tag in identifiersOrTags)
|
||||
{
|
||||
if (tag.Contains('!'))
|
||||
{
|
||||
ignoredTags.Add(tag.Remove("!").ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
return ignoredTags;
|
||||
}
|
||||
|
||||
private bool CheckInventory()
|
||||
{
|
||||
if (identifiersOrTags == null) { return false; }
|
||||
if (IdentifiersOrTags == null) { return false; }
|
||||
var item = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
|
||||
if (item != null)
|
||||
{
|
||||
@@ -93,6 +134,19 @@ namespace Barotrauma
|
||||
return item != null;
|
||||
}
|
||||
|
||||
private bool CountItems()
|
||||
{
|
||||
int itemCount = 0;
|
||||
foreach (Item it in character.Inventory.AllItems)
|
||||
{
|
||||
if (CheckItem(it))
|
||||
{
|
||||
itemCount++;
|
||||
}
|
||||
}
|
||||
return itemCount >= ItemCount;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (character.LockHands)
|
||||
@@ -105,7 +159,7 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (identifiersOrTags != null && !isDoneSeeking)
|
||||
if (IdentifiersOrTags != null && !isDoneSeeking)
|
||||
{
|
||||
if (checkInventory)
|
||||
{
|
||||
@@ -122,7 +176,7 @@ namespace Barotrauma
|
||||
if (dangerousPressure)
|
||||
{
|
||||
#if DEBUG
|
||||
string itemName = targetItem != null ? targetItem.Name : identifiersOrTags.FirstOrDefault();
|
||||
string itemName = targetItem != null ? targetItem.Name : IdentifiersOrTags.FirstOrDefault();
|
||||
DebugConsole.NewMessage($"{character.Name}: Seeking item ({itemName}) aborted, because the pressure is dangerous.", Color.Yellow);
|
||||
#endif
|
||||
Abandon = true;
|
||||
@@ -215,10 +269,28 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
IsCompleted = true;
|
||||
if (IdentifiersOrTags == null)
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsCompleted = CountItems();
|
||||
if (!IsCompleted)
|
||||
{
|
||||
ResetInternal();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Equip)
|
||||
{
|
||||
// Try equipping and wearing the item
|
||||
Wear = true;
|
||||
Equip = true;
|
||||
return;
|
||||
}
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
|
||||
#endif
|
||||
@@ -243,6 +315,10 @@ namespace Barotrauma
|
||||
{
|
||||
// Try again
|
||||
ignoredItems.Add(targetItem);
|
||||
if (targetItem != moveToTarget && moveToTarget is Item item)
|
||||
{
|
||||
ignoredItems.Add(item);
|
||||
}
|
||||
ResetInternal();
|
||||
}
|
||||
else
|
||||
@@ -256,7 +332,7 @@ namespace Barotrauma
|
||||
|
||||
private void FindTargetItem()
|
||||
{
|
||||
if (identifiersOrTags == null)
|
||||
if (IdentifiersOrTags == null)
|
||||
{
|
||||
if (targetItem == null)
|
||||
{
|
||||
@@ -269,7 +345,15 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
|
||||
bool checkPath = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.isFollowOrderObjective);
|
||||
if (!CheckPathForEachItem)
|
||||
{
|
||||
// While following the player, let's ensure that there's a valid path to the target before accepting it.
|
||||
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
|
||||
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
|
||||
// Only allow one path find call per frame.
|
||||
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective);
|
||||
}
|
||||
bool checkPath = CheckPathForEachItem;
|
||||
bool hasCalledPathFinder = false;
|
||||
int itemsPerFrame = (int)priority;
|
||||
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
|
||||
@@ -280,14 +364,20 @@ namespace Barotrauma
|
||||
if (itemSub == null) { continue; }
|
||||
Submarine mySub = character.Submarine;
|
||||
if (mySub == null) { continue; }
|
||||
if (!checkInventory)
|
||||
{
|
||||
// Ignore items in the inventory when defined not to check it.
|
||||
if (item.IsOwnedBy(character)) { continue; }
|
||||
}
|
||||
if (!AllowStealing)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; }
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInCurrentOutpost) { continue; }
|
||||
}
|
||||
if (!CheckItem(item)) { continue; }
|
||||
if (item.Container != null)
|
||||
{
|
||||
if (item.Container.HasTag("donttakeitems")) { continue; }
|
||||
if (ignoredItems.Contains(item.Container)) { continue; }
|
||||
if (ignoredContainerIdentifiers != null)
|
||||
{
|
||||
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
|
||||
@@ -315,17 +405,51 @@ namespace Barotrauma
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
|
||||
yDist = yDist > 100 ? yDist * 5 : 0;
|
||||
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
|
||||
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist));
|
||||
float minDistFactor = EvaluateCombatPriority ? 0.1f : 0;
|
||||
float distanceFactor = MathHelper.Lerp(1, minDistFactor, MathUtils.InverseLerp(100, 10000, dist));
|
||||
itemPriority *= distanceFactor;
|
||||
itemPriority *= item.Condition / item.MaxCondition;
|
||||
if (EvaluateCombatPriority)
|
||||
{
|
||||
var mw = item.GetComponent<MeleeWeapon>();
|
||||
var rw = item.GetComponent<RangedWeapon>();
|
||||
float combatFactor = 0;
|
||||
if (mw != null)
|
||||
{
|
||||
if (mw.CombatPriority > 0)
|
||||
{
|
||||
combatFactor = mw.CombatPriority / 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The combat factor of items with zero combat priority is not allowed to be greater than 0.1f
|
||||
combatFactor = Math.Min(AIObjectiveCombat.GetLethalDamage(mw) / 1000, 0.1f);
|
||||
}
|
||||
}
|
||||
else if (rw != null)
|
||||
{
|
||||
if (rw.CombatPriority > 0)
|
||||
{
|
||||
combatFactor = rw.CombatPriority / 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
combatFactor = Math.Min(AIObjectiveCombat.GetLethalDamage(rw) / 1000, 0.1f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
combatFactor = Math.Min(item.Components.Sum(ic => AIObjectiveCombat.GetLethalDamage(ic)) / 1000, 0.1f);
|
||||
}
|
||||
itemPriority *= combatFactor;
|
||||
}
|
||||
else
|
||||
{
|
||||
itemPriority *= item.Condition / item.MaxCondition;
|
||||
}
|
||||
// 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, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
if (path.Unreachable) { continue; }
|
||||
@@ -341,10 +465,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (spawnItemIfNotFound)
|
||||
{
|
||||
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
|
||||
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 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);
|
||||
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;
|
||||
}
|
||||
@@ -355,7 +479,7 @@ namespace Barotrauma
|
||||
targetItem = spawnedItem;
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
|
||||
{
|
||||
spawnedItem.SpawnedInOutpost = true;
|
||||
spawnedItem.SpawnedInCurrentOutpost = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -363,9 +487,8 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow);
|
||||
#endif
|
||||
SpeakCannotFind();
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
@@ -375,7 +498,16 @@ namespace Barotrauma
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (targetItem != null)
|
||||
if (targetItem == null)
|
||||
{
|
||||
// Not yet ready
|
||||
return false;
|
||||
}
|
||||
if (IdentifiersOrTags != null && ItemCount > 1)
|
||||
{
|
||||
return CountItems();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Equip && EquipSlotType.HasValue)
|
||||
{
|
||||
@@ -386,23 +518,6 @@ namespace Barotrauma
|
||||
return character.HasItem(targetItem, Equip);
|
||||
}
|
||||
}
|
||||
else if (identifiersOrTags != null)
|
||||
{
|
||||
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
|
||||
if (matchingItem != null)
|
||||
{
|
||||
if (Equip && EquipSlotType.HasValue)
|
||||
{
|
||||
return character.HasEquippedItem(matchingItem, EquipSlotType.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return !Equip || character.HasEquippedItem(matchingItem);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool CheckItem(Item item)
|
||||
@@ -410,9 +525,11 @@ namespace Barotrauma
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
|
||||
if (ignoredItems.Contains(item)) { return false; };
|
||||
if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.prefab.Identifier == id || item.HasTag(id))) { return false; }
|
||||
if (item.Condition < TargetCondition) { return false; }
|
||||
if (ItemFilter != null && !ItemFilter(item)) { return false; }
|
||||
return identifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id));
|
||||
if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; }
|
||||
return IdentifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id));
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
@@ -437,37 +554,28 @@ namespace Barotrauma
|
||||
protected override void OnAbandon()
|
||||
{
|
||||
base.OnAbandon();
|
||||
if (moveToTarget == null) { return; }
|
||||
if (moveToTarget != null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
|
||||
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
|
||||
#endif
|
||||
}
|
||||
if (SpeakIfFails)
|
||||
{
|
||||
SpeakCannotFind();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
#nullable enable
|
||||
using Barotrauma.Extensions;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveGetItems : AIObjective
|
||||
{
|
||||
public override string Identifier { get; set; } = "get items";
|
||||
public override string DebugTag => $"{Identifier}";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowMultipleInstances => true;
|
||||
|
||||
public bool AllowStealing { get; set; }
|
||||
public bool TakeWholeStack { get; set; }
|
||||
public bool AllowVariants { get; set; }
|
||||
public bool Equip { get; set; }
|
||||
public bool Wear { get; set; }
|
||||
public bool CheckInventory { get; set; }
|
||||
public bool EvaluateCombatPriority { get; set; }
|
||||
public bool CheckPathForEachItem { get; set; }
|
||||
public bool RequireLoaded { get; set; }
|
||||
public bool RequireAllItems { get; set; }
|
||||
|
||||
private readonly ImmutableArray<string> gearTags;
|
||||
private readonly string[] ignoredTags;
|
||||
private bool subObjectivesCreated;
|
||||
|
||||
public readonly HashSet<Item> achievedItems = new HashSet<Item>();
|
||||
|
||||
public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
gearTags = AIObjectiveGetItem.ParseGearTags(identifiersOrTags).ToImmutableArray();
|
||||
ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToArray();
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => subObjectivesCreated && subObjectives.None();
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (character.LockHands)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (!subObjectivesCreated)
|
||||
{
|
||||
foreach (string 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,
|
||||
RequireLoaded = RequireLoaded,
|
||||
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()
|
||||
{
|
||||
base.Reset();
|
||||
subObjectivesCreated = false;
|
||||
achievedItems.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
-71
@@ -22,15 +22,23 @@ namespace Barotrauma
|
||||
public Func<bool> requiredCondition;
|
||||
public Func<PathNode, bool> endNodeFilter;
|
||||
|
||||
public Func<float> priorityGetter;
|
||||
public Func<float> PriorityGetter;
|
||||
|
||||
public bool IsFollowOrderObjective;
|
||||
public bool Mimic;
|
||||
|
||||
public bool isFollowOrderObjective;
|
||||
public bool mimic;
|
||||
public bool SpeakIfFails { get; set; } = true;
|
||||
public bool DebugLogWhenFails { get; set; } = true;
|
||||
public bool UsePathingOutside { get; set; } = true;
|
||||
|
||||
public float extraDistanceWhileSwimming;
|
||||
public float extraDistanceOutsideSub;
|
||||
public float ExtraDistanceWhileSwimming;
|
||||
public float ExtraDistanceOutsideSub;
|
||||
private float _closeEnoughMultiplier = 1;
|
||||
public float CloseEnoughMultiplier
|
||||
{
|
||||
get { return _closeEnoughMultiplier; }
|
||||
set { _closeEnoughMultiplier = Math.Max(value, 1); }
|
||||
}
|
||||
private float _closeEnough = 50;
|
||||
private readonly float minDistance = 50;
|
||||
private readonly float seekGapsInterval = 1;
|
||||
@@ -44,14 +52,15 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
float dist = _closeEnough;
|
||||
float dist = _closeEnough * CloseEnoughMultiplier;
|
||||
float extraMultiplier = Math.Clamp(CloseEnoughMultiplier * 0.6f, 1, 3);
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
dist += extraDistanceWhileSwimming;
|
||||
dist += ExtraDistanceWhileSwimming * extraMultiplier;
|
||||
}
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
dist += extraDistanceOutsideSub;
|
||||
dist += ExtraDistanceOutsideSub * extraMultiplier;
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
@@ -61,6 +70,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Currently we never check the visibility (to the end node), which is actually unintentional.
|
||||
// I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better.
|
||||
// However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true.
|
||||
public bool CheckVisibility { get; set; }
|
||||
public bool IgnoreIfTargetDead { get; set; }
|
||||
public bool AllowGoingOutside { get; set; }
|
||||
@@ -77,7 +89,7 @@ namespace Barotrauma
|
||||
public override bool AllowOutsideSubmarine => AllowGoingOutside;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
public string DialogueIdentifier { get; set; }
|
||||
public string DialogueIdentifier { get; set; } = "dialogcannotreachtarget";
|
||||
public string TargetName { get; set; }
|
||||
|
||||
public ISpatialEntity Target { get; private set; }
|
||||
@@ -105,9 +117,9 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (priorityGetter != null)
|
||||
if (PriorityGetter != null)
|
||||
{
|
||||
Priority = priorityGetter();
|
||||
Priority = PriorityGetter();
|
||||
}
|
||||
else if (OverridePriority.HasValue)
|
||||
{
|
||||
@@ -149,7 +161,10 @@ namespace Barotrauma
|
||||
private void SpeakCannotReach()
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
|
||||
if (DebugLogWhenFails)
|
||||
{
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
|
||||
}
|
||||
#endif
|
||||
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails)
|
||||
{
|
||||
@@ -176,11 +191,6 @@ namespace Barotrauma
|
||||
character.AIController.SteeringManager.Reset();
|
||||
return;
|
||||
}
|
||||
if (cannotFollow)
|
||||
{
|
||||
// Wait
|
||||
character.AIController.SteeringManager.Reset();
|
||||
}
|
||||
if (!character.IsClimbing)
|
||||
{
|
||||
character.SelectedConstruction = null;
|
||||
@@ -198,7 +208,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
Hull targetHull = GetTargetHull();
|
||||
if (!isFollowOrderObjective)
|
||||
if (!IsFollowOrderObjective)
|
||||
{
|
||||
// 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 = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
|
||||
@@ -211,26 +221,33 @@ namespace Barotrauma
|
||||
}
|
||||
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
|
||||
bool isInside = character.CurrentHull != null;
|
||||
bool targetIsOutside = (Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
|
||||
if (isInside && targetIsOutside && !AllowGoingOutside)
|
||||
bool hasOutdoorNodes = insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes;
|
||||
if (isInside && hasOutdoorNodes && !AllowGoingOutside)
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
else if (HumanAIController.IsCurrentPathNullOrUnreachable)
|
||||
else if (HumanAIController.SteeringManager == PathSteering)
|
||||
{
|
||||
waitUntilPathUnreachable -= deltaTime;
|
||||
SteeringManager.Reset();
|
||||
if (waitUntilPathUnreachable < 0)
|
||||
if (HumanAIController.IsCurrentPathNullOrUnreachable)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
if (waitUntilPathUnreachable < 0)
|
||||
{
|
||||
waitUntilPathUnreachable = pathWaitingTime;
|
||||
if (repeat)
|
||||
{
|
||||
SpeakCannotReach();
|
||||
}
|
||||
else
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false))
|
||||
{
|
||||
waitUntilPathUnreachable = pathWaitingTime;
|
||||
if (repeat)
|
||||
{
|
||||
SpeakCannotReach();
|
||||
}
|
||||
else
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Abandon)
|
||||
@@ -238,16 +255,16 @@ namespace Barotrauma
|
||||
if (getDivingGearIfNeeded && !character.LockHands)
|
||||
{
|
||||
Character followTarget = Target as Character;
|
||||
bool needsDivingSuit = !isInside || targetIsOutside;
|
||||
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
|
||||
if (mimic)
|
||||
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
|
||||
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)) && character.NeedsAir;
|
||||
if (Mimic)
|
||||
{
|
||||
if (HumanAIController.HasDivingSuit(followTarget))
|
||||
if (HumanAIController.HasDivingSuit(followTarget) && character.NeedsAir)
|
||||
{
|
||||
needsDivingGear = true;
|
||||
needsDivingSuit = true;
|
||||
}
|
||||
else if (HumanAIController.HasDivingMask(followTarget))
|
||||
else if (HumanAIController.HasDivingMask(followTarget) && character.NeedsAir)
|
||||
{
|
||||
needsDivingGear = true;
|
||||
}
|
||||
@@ -268,7 +285,7 @@ namespace Barotrauma
|
||||
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
|
||||
{
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
|
||||
onAbandon: () => Abort(),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () =>
|
||||
{
|
||||
cannotFollow = false;
|
||||
@@ -278,7 +295,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
|
||||
onAbandon: () => Abort(),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () =>
|
||||
{
|
||||
cannotFollow = false;
|
||||
@@ -311,7 +328,7 @@ namespace Barotrauma
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
if (character.CurrentHull == null ||
|
||||
isFollowOrderObjective &&
|
||||
IsFollowOrderObjective &&
|
||||
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
|
||||
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
|
||||
{
|
||||
@@ -354,7 +371,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (TargetGap != null)
|
||||
{
|
||||
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, isFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
|
||||
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
|
||||
return;
|
||||
@@ -373,7 +390,7 @@ namespace Barotrauma
|
||||
Item scooter = null;
|
||||
float closeEnough = 250;
|
||||
float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition);
|
||||
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic ||
|
||||
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!Mimic ||
|
||||
(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))
|
||||
{
|
||||
@@ -382,24 +399,32 @@ namespace Barotrauma
|
||||
}
|
||||
else if (shouldUseScooter)
|
||||
{
|
||||
bool hasBattery = false;
|
||||
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
|
||||
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
|
||||
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
|
||||
bool handsFull =
|
||||
(leftHandItem != null && character.Inventory.CheckIfAnySlotAvailable(leftHandItem, inWrongSlot: false) == -1) ||
|
||||
(rightHandItem != null && character.Inventory.CheckIfAnySlotAvailable(rightHandItem, inWrongSlot: false) == -1);
|
||||
if (!handsFull)
|
||||
{
|
||||
// Non-equipped scooter with a battery
|
||||
scooter = nonEquippedScooters.FirstOrDefault();
|
||||
hasBattery = true;
|
||||
}
|
||||
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter without a battery
|
||||
scooter = _nonEquippedScooters.FirstOrDefault();
|
||||
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
|
||||
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
|
||||
}
|
||||
if (scooter != null && hasBattery)
|
||||
{
|
||||
// Equip only if we have a battery available
|
||||
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
|
||||
bool hasBattery = false;
|
||||
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter with a battery
|
||||
scooter = nonEquippedScooters.FirstOrDefault();
|
||||
hasBattery = true;
|
||||
}
|
||||
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
|
||||
{
|
||||
// Non-equipped scooter without a battery
|
||||
scooter = _nonEquippedScooters.FirstOrDefault();
|
||||
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
|
||||
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
|
||||
}
|
||||
if (scooter != null && hasBattery)
|
||||
{
|
||||
// Equip only if we have a battery available
|
||||
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool isScooterEquipped = scooter != null && character.HasEquippedItem(scooter);
|
||||
@@ -588,7 +613,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (gap.Open < 1) { continue; }
|
||||
if (gap.Submarine == null) { continue; }
|
||||
if (!isFollowOrderObjective)
|
||||
if (!IsFollowOrderObjective)
|
||||
{
|
||||
if (gap.FlowTargetHull == null) { continue; }
|
||||
if (gap.Submarine != Target.Submarine) { continue; }
|
||||
@@ -669,18 +694,6 @@ namespace Barotrauma
|
||||
return IsCompleted;
|
||||
}
|
||||
|
||||
private void Abort()
|
||||
{
|
||||
if (!objectiveManager.IsOrder(this))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
cannotFollow = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAbandon()
|
||||
{
|
||||
StopMovement();
|
||||
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveLoadItem : AIObjective
|
||||
{
|
||||
public override string Identifier { get; set; } = "load item";
|
||||
public override bool IsLoop
|
||||
{
|
||||
get => true;
|
||||
set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
|
||||
}
|
||||
|
||||
private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; }
|
||||
private Item Container { get; }
|
||||
private ItemContainer ItemContainer { get; }
|
||||
private ImmutableArray<string> TargetContainerTags { get; }
|
||||
|
||||
private int itemIndex = 0;
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
|
||||
private Item targetItem;
|
||||
private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable";
|
||||
|
||||
public AIObjectiveLoadItem(Item container, ImmutableArray<string> targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, string option, Character character, AIObjectiveManager objectiveManager, float priorityModifier)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
Container = container;
|
||||
ItemContainer = container?.GetComponent<ItemContainer>();
|
||||
if (ItemContainer?.Inventory == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
TargetContainerTags = targetTags;
|
||||
TargetItemCondition = targetCondition;
|
||||
if (!string.IsNullOrEmpty(option))
|
||||
{
|
||||
string optionSpecificDialogueIdentifier = $"{abandonGetItemDialogueIdentifier}.{option}";
|
||||
if (TextManager.ContainsTag(optionSpecificDialogueIdentifier))
|
||||
{
|
||||
abandonGetItemDialogueIdentifier = optionSpecificDialogueIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition))
|
||||
{
|
||||
// Reduce priority to 0 if the this isn't a valid container right now
|
||||
Priority = 0;
|
||||
}
|
||||
else if (targetItem == null)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
float dist = 0.0f;
|
||||
if (character.CurrentHull != targetItem.CurrentHull)
|
||||
{
|
||||
AddDistance(character.WorldPosition, targetItem.WorldPosition);
|
||||
}
|
||||
if (targetItem.CurrentHull != Container.CurrentHull)
|
||||
{
|
||||
AddDistance(targetItem.WorldPosition, Container.WorldPosition);
|
||||
}
|
||||
void AddDistance(Vector2 startPos, Vector2 targetPos)
|
||||
{
|
||||
float yDist = Math.Abs(startPos.Y - targetPos.Y);
|
||||
// If we're on the same level with the target, we'll disregard the vertical distance
|
||||
if (yDist > 100) { dist += yDist * 5; }
|
||||
dist += Math.Abs(character.WorldPosition.X - targetPos.X);
|
||||
}
|
||||
float distanceFactor = dist > 0.0f ? MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)) : 0.9f;
|
||||
bool hasContainable = character.HasItem(targetItem);
|
||||
float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100;
|
||||
float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2);
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
|
||||
if (decontainObjective != null && targetItem.Container != Container)
|
||||
{
|
||||
if (!IsValidContainable(targetItem))
|
||||
{
|
||||
// Target is not valid anymore, abandon the objective
|
||||
decontainObjective.Abandon = true;
|
||||
}
|
||||
else if (!ItemContainer.Inventory.CanBePut(targetItem) && ItemContainer.Inventory.AllItems.None(i => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition)))
|
||||
{
|
||||
// The container is full and there's no item that should be removed, abandon the objective
|
||||
decontainObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
if (ItemContainer.Inventory.IsFull())
|
||||
{
|
||||
// Prioritize containers that still have empty space by lowering the priority of objectives with a full target container
|
||||
Priority /= 4;
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
if (targetItem == null)
|
||||
{
|
||||
if (character.FindItem(ref itemIndex, out Item item, identifiers: ItemContainer.ContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetConditionBasedPriority))
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
// No possible containables found, abandon the objective
|
||||
Abandon = true;
|
||||
}
|
||||
targetItem = item;
|
||||
}
|
||||
// Prefer items closer to full condition when target condition is Empty, and vice versa
|
||||
float GetConditionBasedPriority(Item item)
|
||||
{
|
||||
try
|
||||
{
|
||||
return TargetItemCondition switch
|
||||
{
|
||||
AIObjectiveLoadItems.ItemCondition.Full => MathUtils.InverseLerp(100.0f, 0.0f, item.ConditionPercentage),
|
||||
AIObjectiveLoadItems.ItemCondition.Empty => MathUtils.InverseLerp(0.0f, 100.0f, item.ConditionPercentage),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ShowError($"Unexpected target condition \"{TargetItemCondition}\" in local function GetConditionBasedProperty");
|
||||
#endif
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (targetItem != null)
|
||||
{
|
||||
if(decontainObjective == null && !IsValidContainable(targetItem))
|
||||
{
|
||||
IgnoreTargetItem();
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
TryAddSubObjective(ref decontainObjective,
|
||||
constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
|
||||
{
|
||||
AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier,
|
||||
Equip = true,
|
||||
RemoveExistingWhenNecessary = true,
|
||||
RemoveExistingPredicate = (i) => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
|
||||
RemoveExistingMax = 1
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
IsCompleted = true;
|
||||
RemoveSubObjective(ref decontainObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
// Try again
|
||||
IgnoreTargetItem();
|
||||
Reset();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidContainable(Item item)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (item.Removed) { return false; }
|
||||
if (ignoredItems.Contains(item)) { return false; }
|
||||
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
|
||||
var rootInventoryOwner = item.GetRootInventoryOwner();
|
||||
if (rootInventoryOwner is Character owner && owner != character) { return false; }
|
||||
if (rootInventoryOwner is Item parentItem)
|
||||
{
|
||||
if (parentItem.HasTag("donttakeitems")) { return false; }
|
||||
if (!(parentItem.GetComponent<ItemContainer>()?.HasAccess(character) ?? true)) { return false; }
|
||||
}
|
||||
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
|
||||
if (!character.HasItem(item) && !CanEquip(item)) { return false; }
|
||||
if (!ItemContainer.HasAccess(character)) { return false; }
|
||||
if (!ItemContainer.CanBeContained(item)) { return false; }
|
||||
if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; }
|
||||
if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full)
|
||||
{
|
||||
// Ignore items that have had their condition increase recently
|
||||
if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full && item.ConditionIncreasedRecently) { return false; }
|
||||
// Ignore items inside their (condition-restricted) primary containers
|
||||
if (item.ParentInventory is ItemInventory itemInventory && item.IsContainerPreferred(itemInventory.Container, out bool _, out bool isSecondary, requireConditionRestriction: true) && !isSecondary) { return false; }
|
||||
}
|
||||
// Ignore items inside another valid container
|
||||
if (AIObjectiveLoadItems.IsValidTarget(item.Container, character, TargetContainerTags)) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
// Don't reset the target item when resetting the objective because it affects priority calculations
|
||||
decontainObjective = null;
|
||||
itemIndex = 0;
|
||||
}
|
||||
|
||||
private void IgnoreTargetItem()
|
||||
{
|
||||
if(targetItem == null) { return; }
|
||||
ignoredItems.Add(targetItem);
|
||||
targetItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveLoadItems : AIObjectiveLoop<Item>
|
||||
{
|
||||
public override string Identifier { get; set; } = "load items";
|
||||
protected override float IgnoreListClearInterval => 20.0f;
|
||||
protected override bool ResetWhenClearingIgnoreList => false;
|
||||
|
||||
private ImmutableArray<string> TargetContainerTags { get; }
|
||||
private List<Item> TargetContainers { get; } = new List<Item>();
|
||||
private ItemCondition TargetCondition { get; }
|
||||
|
||||
public enum ItemCondition
|
||||
{
|
||||
Empty,
|
||||
Full
|
||||
}
|
||||
|
||||
public AIObjectiveLoadItems(Character character, AIObjectiveManager objectiveManager, string option, ImmutableArray<string> containerTags, Item targetContainer = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier, option)
|
||||
{
|
||||
if ((containerTags == null || containerTags.None()) && targetContainer == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetContainerTags = containerTags.ToImmutableArray();
|
||||
}
|
||||
if (targetContainer != null)
|
||||
{
|
||||
TargetContainers.Add(targetContainer);
|
||||
}
|
||||
TargetCondition = option == "turretammo" ? ItemCondition.Empty : ItemCondition.Full;
|
||||
}
|
||||
|
||||
protected override bool Filter(Item target)
|
||||
{
|
||||
if (!IsValidTarget(target, character, TargetContainerTags, TargetCondition)) { return false; }
|
||||
if (target.CurrentHull == null || target.CurrentHull.FireSources.Count > 0) { return false; }
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsValidTarget(Item item, Character character, ImmutableArray<string>? targetContainerTags = null, ItemCondition? targetCondition = null)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (item.Removed) { return false; }
|
||||
if (targetContainerTags.HasValue && !Order.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; }
|
||||
if (!(item.GetComponent<ItemContainer>() is ItemContainer container)) { return false; }
|
||||
if (container.Inventory == null) { return false; }
|
||||
if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; }
|
||||
if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; }
|
||||
if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; }
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
|
||||
if (!container.HasAccess(character)) { return false; }
|
||||
// Ignore items that require power but don't have it
|
||||
if (item.GetComponent<Powered>() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ItemMatchesTargetCondition(Item item, ItemCondition targetCondition)
|
||||
{
|
||||
if(item == null) { return false; }
|
||||
try
|
||||
{
|
||||
return targetCondition switch
|
||||
{
|
||||
ItemCondition.Empty => item.Condition <= 0.1f,
|
||||
ItemCondition.Full => item.IsFullCondition,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ShowError($"Unexpected target condition \"{targetCondition}\" in AIObjectiveLoadItems.ItemMatchesTargetCondition");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<Item> GetList() => TargetContainers.Any() ? TargetContainers : Item.ItemList;
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Item target)
|
||||
=> new AIObjectiveLoadItem(target, TargetContainerTags, TargetCondition, Option, character, objectiveManager, PriorityModifier);
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveLoadItems, Item>(character, target);
|
||||
|
||||
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() || so.Priority <= 0))
|
||||
{
|
||||
ForceWalk = true;
|
||||
}
|
||||
return prio;
|
||||
}
|
||||
return AIObjectiveManager.RunPriority - 0.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -45,6 +45,7 @@ namespace Barotrauma
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool AllowSubObjectiveSorting => true;
|
||||
public virtual bool InverseTargetEvaluation => false;
|
||||
protected virtual bool ResetWhenClearingIgnoreList => true;
|
||||
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
|
||||
|
||||
@@ -55,7 +56,15 @@ namespace Barotrauma
|
||||
{
|
||||
if (ignoreListTimer > IgnoreListClearInterval)
|
||||
{
|
||||
Reset();
|
||||
if (ResetWhenClearingIgnoreList)
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
ignoreList.Clear();
|
||||
ignoreListTimer = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -113,7 +122,7 @@ namespace Barotrauma
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
if (character.LockHands || character.Submarine == null)
|
||||
if (character.LockHands)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -131,7 +140,7 @@ namespace Barotrauma
|
||||
// If the priority is higher than the target value, let's just use it.
|
||||
// The priority calculation is more precise, but it takes into account things like distances,
|
||||
// so it's better not to use it if it's lower than the rougher targetValue.
|
||||
targetValue = Priority;
|
||||
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)
|
||||
|
||||
+57
-63
@@ -137,7 +137,7 @@ namespace Barotrauma
|
||||
Item item = null;
|
||||
if (orderPrefab.MustSetTarget)
|
||||
{
|
||||
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandom();
|
||||
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character, orderOption: autonomousObjective.option)?.GetRandom();
|
||||
}
|
||||
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
|
||||
if (order == null) { continue; }
|
||||
@@ -288,6 +288,7 @@ namespace Barotrauma
|
||||
float highestPriority = 0;
|
||||
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (CurrentOrders.Count <= i) { break; }
|
||||
var orderObjective = CurrentOrders[i].Objective;
|
||||
if (orderObjective == null) { continue; }
|
||||
orderObjective.CalculatePriority();
|
||||
@@ -332,7 +333,6 @@ namespace Barotrauma
|
||||
SortObjectives();
|
||||
}
|
||||
|
||||
private CoroutineHandle speakRoutine;
|
||||
public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak)
|
||||
{
|
||||
if (character.IsDead)
|
||||
@@ -364,6 +364,7 @@ namespace Barotrauma
|
||||
// Make sure the order priorities reflect those set by the player
|
||||
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (CurrentOrders.Count <= i) { break; }
|
||||
var currentOrder = CurrentOrders[i];
|
||||
if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option))
|
||||
{
|
||||
@@ -388,6 +389,7 @@ namespace Barotrauma
|
||||
var newCurrentOrder = CreateObjective(order, option, orderGiver);
|
||||
if (newCurrentOrder != null)
|
||||
{
|
||||
newCurrentOrder.Abandoned += () => DismissSelf(order, option);
|
||||
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
|
||||
}
|
||||
if (!HasOrders())
|
||||
@@ -395,53 +397,12 @@ namespace Barotrauma
|
||||
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
|
||||
CreateAutonomousObjectives();
|
||||
}
|
||||
else
|
||||
else if (newCurrentOrder != null)
|
||||
{
|
||||
// This should be redundant, because all the objectives are reset when they are selected as active.
|
||||
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 (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);
|
||||
string msg = newCurrentOrder.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative");
|
||||
character.Speak(msg, delay: 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,13 +417,14 @@ namespace Barotrauma
|
||||
if (orderGiver == null) { return null; }
|
||||
newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
|
||||
{
|
||||
CloseEnough = Rand.Range(90, 100) + Rand.Range(50, 70) * Math.Min(HumanAIController.CountCrew(c => c.ObjectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.Target == orderGiver, onlyBots: true), 4),
|
||||
extraDistanceOutsideSub = 100,
|
||||
extraDistanceWhileSwimming = 100,
|
||||
CloseEnough = Rand.Range(80, 100),
|
||||
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == orderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4),
|
||||
ExtraDistanceOutsideSub = 100,
|
||||
ExtraDistanceWhileSwimming = 100,
|
||||
AllowGoingOutside = true,
|
||||
IgnoreIfTargetDead = true,
|
||||
isFollowOrderObjective = true,
|
||||
mimic = true,
|
||||
IsFollowOrderObjective = true,
|
||||
Mimic = character.IsOnPlayerTeam,
|
||||
DialogueIdentifier = "dialogcannotreachplace"
|
||||
};
|
||||
break;
|
||||
@@ -474,7 +436,6 @@ namespace Barotrauma
|
||||
break;
|
||||
case "return":
|
||||
newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier);
|
||||
newObjective.Abandoned += () => DismissSelf(order, option);
|
||||
newObjective.Completed += () => DismissSelf(order, option);
|
||||
break;
|
||||
case "fixleaks":
|
||||
@@ -500,12 +461,10 @@ namespace Barotrauma
|
||||
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
|
||||
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
|
||||
{
|
||||
IsLoop = true,
|
||||
IsLoop = false,
|
||||
Override = orderGiver != null && orderGiver.IsCommanding
|
||||
};
|
||||
// ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs,
|
||||
// if we want that the bot just switches the pump on/off and continues doing something else.
|
||||
// If we want that the bot does the objective and then forgets about it, I think we could do the same plus dismiss when the bot is done.
|
||||
newObjective.Completed += () => DismissSelf(order, option);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -575,6 +534,39 @@ namespace Barotrauma
|
||||
case "escapehandcuffs":
|
||||
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
|
||||
break;
|
||||
case "prepareforexpedition":
|
||||
newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems)
|
||||
{
|
||||
KeepActiveWhenReady = true,
|
||||
CheckInventory = true,
|
||||
Equip = false,
|
||||
FindAllItems = true
|
||||
};
|
||||
break;
|
||||
case "findweapon":
|
||||
AIObjectivePrepare prepareObjective;
|
||||
if (order.TargetEntity is Item tItem)
|
||||
{
|
||||
prepareObjective = new AIObjectivePrepare(character, this, targetItem: tItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems)
|
||||
{
|
||||
KeepActiveWhenReady = false,
|
||||
CheckInventory = false,
|
||||
EvaluateCombatPriority = true,
|
||||
FindAllItems = false
|
||||
};
|
||||
}
|
||||
prepareObjective.KeepActiveWhenReady = false;
|
||||
prepareObjective.Equip = true;
|
||||
newObjective = prepareObjective;
|
||||
newObjective.Completed += () => DismissSelf(order, option);
|
||||
break;
|
||||
case "loaditems":
|
||||
newObjective = new AIObjectiveLoadItems(character, this, option, order.GetTargetItems(option), order.TargetEntity as Item, priorityModifier);
|
||||
break;
|
||||
default:
|
||||
if (order.TargetItemComponent == null) { return null; }
|
||||
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
|
||||
@@ -606,17 +598,20 @@ namespace Barotrauma
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
Order dismissOrder = Order.GetPrefab("dismissed");
|
||||
var orderOption = Order.GetDismissOrderOption(currentOrder);
|
||||
int priority = currentOrder.ManualPriority;
|
||||
#if CLIENT
|
||||
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
|
||||
{
|
||||
GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character);
|
||||
GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder, orderOption, priority, character);
|
||||
}
|
||||
#else
|
||||
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, currentOrder.Order?.TargetSpatialEntity, character, character));
|
||||
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, orderOption, priority, currentOrder.Order.TargetSpatialEntity, character, character));
|
||||
SetOrder(dismissOrder, orderOption, priority, character, speak: false);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
private bool IsAllowedToWait()
|
||||
{
|
||||
if (!character.IsOnPlayerTeam) { return false; }
|
||||
@@ -665,10 +660,9 @@ namespace Barotrauma
|
||||
return ForcedOrder != null || CurrentOrders.Any();
|
||||
}
|
||||
|
||||
public bool HasOrder<T>() where T : AIObjective
|
||||
{
|
||||
return ForcedOrder is T || CurrentOrders.Any(o => o.Objective is T);
|
||||
}
|
||||
public bool HasOrder<T>(Func<T, bool> predicate = null) where T : AIObjective =>
|
||||
ForcedOrder is T forcedOrder && (predicate == null || predicate(forcedOrder)) ||
|
||||
CurrentOrders.Any(o => o.Objective is T order && (predicate == null || predicate(order)));
|
||||
|
||||
public float GetOrderPriority(AIObjective objective)
|
||||
{
|
||||
|
||||
+1
-2
@@ -202,7 +202,7 @@ namespace Barotrauma
|
||||
HumanAIController.FaceTarget(target.Item);
|
||||
if (character.SelectedConstruction != target.Item)
|
||||
{
|
||||
target.Item.TryInteract(character, false, true);
|
||||
target.Item.TryInteract(character, forceSelectKey: true);
|
||||
}
|
||||
if (component.AIOperate(deltaTime, character, this))
|
||||
{
|
||||
@@ -213,7 +213,6 @@ namespace Barotrauma
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = target.Item.Name,
|
||||
endNodeFilter = node => node.Waypoint.Ladders == null
|
||||
},
|
||||
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectivePrepare : AIObjective
|
||||
{
|
||||
public override string Identifier { get; set; } = "prepare";
|
||||
public override string DebugTag => $"{Identifier}";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool PrioritizeIfSubObjectivesActive => true;
|
||||
|
||||
private AIObjectiveGetItem getSingleItemObjective;
|
||||
private AIObjectiveGetItems getAllItemsObjective;
|
||||
private AIObjectiveGetItems getMultipleItemsObjective;
|
||||
private bool subObjectivesCreated;
|
||||
private readonly Item targetItem;
|
||||
private readonly ImmutableArray<string> requiredItems;
|
||||
private readonly ImmutableArray<string> optionalItems;
|
||||
private readonly HashSet<Item> items = new HashSet<Item>();
|
||||
public bool KeepActiveWhenReady { get; set; }
|
||||
public bool CheckInventory { get; set; }
|
||||
public bool FindAllItems { get; set; }
|
||||
public bool Equip { get; set; }
|
||||
public bool EvaluateCombatPriority { get; set; }
|
||||
|
||||
private AIObjective GetSubObjective()
|
||||
{
|
||||
if (getSingleItemObjective != null) { return getSingleItemObjective; }
|
||||
if (getAllItemsObjective == null || getAllItemsObjective.IsCompleted)
|
||||
{
|
||||
return getMultipleItemsObjective;
|
||||
}
|
||||
return getAllItemsObjective;
|
||||
}
|
||||
public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, Item targetItem, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.targetItem = targetItem;
|
||||
}
|
||||
|
||||
public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable<string> optionalItems, IEnumerable<string> requiredItems = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.optionalItems = optionalItems.ToImmutableArray();
|
||||
if (requiredItems != null)
|
||||
{
|
||||
this.requiredItems = requiredItems.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
Priority = objectiveManager.GetOrderPriority(this);
|
||||
var subObjective = GetSubObjective();
|
||||
if (subObjective != null && subObjective.IsCompleted)
|
||||
{
|
||||
Priority = 0;
|
||||
items.RemoveWhere(i => i == null || i.Removed || !i.IsOwnedBy(character));
|
||||
if (items.None())
|
||||
{
|
||||
Abandon = true;
|
||||
|
||||
}
|
||||
else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character))))
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (character.LockHands)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (!subObjectivesCreated)
|
||||
{
|
||||
if (FindAllItems && targetItem == null)
|
||||
{
|
||||
getMultipleItemsObjective = CreateObjectives(optionalItems, requireAll: false);
|
||||
if (requiredItems != null && requiredItems.Any())
|
||||
{
|
||||
getAllItemsObjective = CreateObjectives(requiredItems, requireAll: true);
|
||||
}
|
||||
AIObjectiveGetItems CreateObjectives(IEnumerable<string> itemTags, bool requireAll)
|
||||
{
|
||||
AIObjectiveGetItems objectiveReference = null;
|
||||
if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags)
|
||||
{
|
||||
CheckInventory = CheckInventory,
|
||||
Equip = Equip,
|
||||
EvaluateCombatPriority = EvaluateCombatPriority,
|
||||
RequireLoaded = true,
|
||||
RequireAllItems = requireAll
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (KeepActiveWhenReady)
|
||||
{
|
||||
if (objectiveReference != null)
|
||||
{
|
||||
foreach (var item in objectiveReference.achievedItems)
|
||||
{
|
||||
if (item?.IsOwnedBy(character) != null)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
},
|
||||
onAbandon: () => Abandon = true))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
return objectiveReference;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Func<AIObjectiveGetItem> getItemConstructor;
|
||||
if (targetItem != null)
|
||||
{
|
||||
getItemConstructor = () => new AIObjectiveGetItem(character, targetItem, objectiveManager, equip: Equip)
|
||||
{
|
||||
SpeakIfFails = true
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
IEnumerable<string> allItems = optionalItems;
|
||||
if (requiredItems != null && requiredItems.Any())
|
||||
{
|
||||
allItems = requiredItems;
|
||||
}
|
||||
getItemConstructor = () => new AIObjectiveGetItem(character, allItems, objectiveManager, equip: Equip, checkInventory: CheckInventory)
|
||||
{
|
||||
EvaluateCombatPriority = EvaluateCombatPriority,
|
||||
SpeakIfFails = true,
|
||||
RequireLoaded = true
|
||||
};
|
||||
}
|
||||
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (KeepActiveWhenReady)
|
||||
{
|
||||
if (getSingleItemObjective != null)
|
||||
{
|
||||
var item = getSingleItemObjective?.TargetItem;
|
||||
if (item?.IsOwnedBy(character) != null)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
},
|
||||
onAbandon: () => Abandon = true))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
subObjectivesCreated = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
items.Clear();
|
||||
subObjectivesCreated = false;
|
||||
getMultipleItemsObjective = null;
|
||||
getSingleItemObjective = null;
|
||||
getAllItemsObjective = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-5
@@ -20,6 +20,9 @@ namespace Barotrauma
|
||||
private float previousCondition = -1;
|
||||
private RepairTool repairTool;
|
||||
|
||||
private const float WaitTimeBeforeRepair = 0.5f;
|
||||
private float waitTimer;
|
||||
|
||||
private bool IsRepairing() => IsRepairing(character, Item);
|
||||
private readonly bool isPriority;
|
||||
|
||||
@@ -107,7 +110,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (RelatedItem requiredItem in kvp.Value)
|
||||
{
|
||||
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true)
|
||||
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, equip: true)
|
||||
{
|
||||
AllowVariants = requiredItem.AllowVariants
|
||||
};
|
||||
@@ -160,6 +163,9 @@ namespace Barotrauma
|
||||
}
|
||||
if (!character.IsClimbing && character.CanInteractWith(Item, out _, checkLinked: false))
|
||||
{
|
||||
waitTimer += deltaTime;
|
||||
if (waitTimer < WaitTimeBeforeRepair) { return; }
|
||||
|
||||
HumanAIController.FaceTarget(Item);
|
||||
if (repairTool != null)
|
||||
{
|
||||
@@ -176,8 +182,12 @@ namespace Barotrauma
|
||||
{
|
||||
if (character.SelectedConstruction != Item)
|
||||
{
|
||||
if (!Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) &&
|
||||
!Item.TryInteract(character, ignoreRequiredItems: true, forceActionKey: true))
|
||||
if (Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) ||
|
||||
Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true))
|
||||
{
|
||||
character.SelectedConstruction = Item;
|
||||
}
|
||||
else
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
@@ -209,6 +219,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
waitTimer = 0.0f;
|
||||
RemoveSubObjective(ref refuelObjective);
|
||||
// If cannot reach the item, approach it.
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
@@ -219,8 +230,7 @@ namespace Barotrauma
|
||||
{
|
||||
// Don't stop in ladders, because we can't interact with other items while holding the ladders.
|
||||
endNodeFilter = node => node.Waypoint.Ladders == null,
|
||||
// Allow repairing hatches and airlock doors.
|
||||
AllowGoingOutside = HumanAIController.ObjectiveManager.IsCurrentOrder<AIObjectiveRepairItems>() && Item.GetComponent<Door>() != null
|
||||
TargetName = Item.Name
|
||||
};
|
||||
if (repairTool != null)
|
||||
{
|
||||
|
||||
+1
-2
@@ -91,8 +91,7 @@ namespace Barotrauma
|
||||
|
||||
public static bool NearlyFullCondition(Item item)
|
||||
{
|
||||
float condition = item.ConditionPercentage;
|
||||
return item.Repairables.All(r => condition >= r.RepairThreshold);
|
||||
return item.Repairables.All(r => !r.IsBelowRepairThreshold);
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
|
||||
+135
-83
@@ -36,9 +36,9 @@ namespace Barotrauma
|
||||
{
|
||||
if (targetCharacter == null)
|
||||
{
|
||||
string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace.CleanupStackTrace();
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
string errorMsg = $"Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace.CleanupStackTrace();
|
||||
DebugConsole.ThrowError(character.Name + ": " + errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
@@ -59,7 +59,7 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
if (character.LockHands || targetCharacter == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
@@ -137,66 +137,69 @@ namespace Barotrauma
|
||||
recursive: true);
|
||||
}
|
||||
}
|
||||
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
// Incapacitated target is not in a safe place -> Move to a safe place first
|
||||
if (character.SelectedCharacter != targetCharacter)
|
||||
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
|
||||
// Incapacitated target is not in a safe place -> Move to a safe place first
|
||||
if (character.SelectedCharacter != targetCharacter)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
|
||||
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
|
||||
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
|
||||
}
|
||||
// Go to the target and select it
|
||||
if (!character.CanInteractWith(targetCharacter))
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
|
||||
if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient",
|
||||
TargetName = targetCharacter.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
|
||||
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
|
||||
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
|
||||
}
|
||||
// Go to the target and select it
|
||||
if (!character.CanInteractWith(targetCharacter))
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
Abandon = true;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
character.SelectCharacter(targetCharacter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Drag the character into safety
|
||||
if (safeHull == null)
|
||||
{
|
||||
if (findHullTimer > 0)
|
||||
{
|
||||
findHullTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
|
||||
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
|
||||
}
|
||||
}
|
||||
if (safeHull != null && character.CurrentHull != safeHull)
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient",
|
||||
TargetName = targetCharacter.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
safeHull = character.CurrentHull;
|
||||
Abandon = true;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
character.SelectCharacter(targetCharacter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Drag the character into safety
|
||||
if (safeHull == null)
|
||||
{
|
||||
if (findHullTimer > 0)
|
||||
{
|
||||
findHullTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
|
||||
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
|
||||
}
|
||||
}
|
||||
if (safeHull != null && character.CurrentHull != safeHull)
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
safeHull = character.CurrentHull;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,7 +231,7 @@ namespace Barotrauma
|
||||
// We can start applying treatment
|
||||
if (character != targetCharacter && character.SelectedCharacter != targetCharacter)
|
||||
{
|
||||
if (targetCharacter.CurrentHull.DisplayName != null)
|
||||
if (targetCharacter.CurrentHull?.DisplayName != null)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" },
|
||||
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
|
||||
@@ -252,7 +255,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
SteeringManager?.Reset();
|
||||
SteeringManager.Reset();
|
||||
|
||||
if (!targetCharacter.IsPlayer)
|
||||
{
|
||||
@@ -269,26 +272,36 @@ namespace Barotrauma
|
||||
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
|
||||
|
||||
//find which treatments are the most suitable to treat the character's current condition
|
||||
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false);
|
||||
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false, predictFutureDuration: 10.0f);
|
||||
|
||||
//check if we already have a suitable treatment for any of the afflictions
|
||||
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
|
||||
{
|
||||
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<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
|
||||
{
|
||||
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f)
|
||||
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) &&
|
||||
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
|
||||
{
|
||||
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
|
||||
if (matchingItem == null) { continue; }
|
||||
if (targetCharacter != character) { character.SelectCharacter(targetCharacter); }
|
||||
ApplyTreatment(affliction, matchingItem);
|
||||
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
|
||||
treatmentTimer = TreatmentDelay * 4;
|
||||
return;
|
||||
if (matchingItem != null)
|
||||
{
|
||||
bestItem = matchingItem;
|
||||
bestSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestItem != null)
|
||||
{
|
||||
if (targetCharacter != character) { character.SelectCharacter(targetCharacter); }
|
||||
ApplyTreatment(affliction, bestItem);
|
||||
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
|
||||
treatmentTimer = TreatmentDelay * 4;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Find treatments outside of own inventory only if inside the own sub.
|
||||
if (character.Submarine != null && character.Submarine.TeamID == character.TeamID)
|
||||
@@ -319,9 +332,32 @@ namespace Barotrauma
|
||||
{
|
||||
itemListStr = itemNameList[0];
|
||||
}
|
||||
else if (itemNameList.Count == 2)
|
||||
{
|
||||
//[treatment1] or [treatment2]
|
||||
itemListStr = TextManager.GetWithVariables(
|
||||
"DialogRequiredTreatmentOptionsLast",
|
||||
new string[] { "[treatment1]", "[treatment2]" },
|
||||
new string[] { itemNameList[0], itemNameList[1] });
|
||||
}
|
||||
else
|
||||
{
|
||||
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
|
||||
//[treatment1], [treatment2], [treatment3] ... or [treatmentx]
|
||||
itemListStr = TextManager.GetWithVariables(
|
||||
"DialogRequiredTreatmentOptionsFirst",
|
||||
new string[] { "[treatment1]", "[treatment2]" },
|
||||
new string[] { itemNameList[0], itemNameList[1] });
|
||||
for (int i = 2; i < itemNameList.Count - 1; i++)
|
||||
{
|
||||
itemListStr = TextManager.GetWithVariables(
|
||||
"DialogRequiredTreatmentOptionsFirst",
|
||||
new string[] { "[treatment1]", "[treatment2]" },
|
||||
new string[] { itemListStr, itemNameList[i] });
|
||||
}
|
||||
itemListStr = TextManager.GetWithVariables(
|
||||
"DialogRequiredTreatmentOptionsLast",
|
||||
new string[] { "[treatment1]", "[treatment2]" },
|
||||
new string[] { itemListStr, itemNameList.Last() });
|
||||
}
|
||||
if (targetCharacter != character && character.IsOnPlayerTeam)
|
||||
{
|
||||
@@ -395,21 +431,7 @@ namespace Barotrauma
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
// Don't go into rooms that have enemies
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
bool isCompleted =
|
||||
AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) ||
|
||||
targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Prefab.IsBuff || a.Strength <= a.Prefab.TreatmentThreshold);
|
||||
|
||||
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
|
||||
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
|
||||
@@ -426,16 +448,36 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
if (!objectiveManager.HasOrder<AIObjectiveRescueAll>())
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
}
|
||||
else if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
|
||||
{
|
||||
// Don't go into rooms that have enemies
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
if (targetCharacter == null)
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
|
||||
float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f;
|
||||
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
|
||||
float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X);
|
||||
float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.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 (targetCharacter.CurrentHull == character.CurrentHull)
|
||||
{
|
||||
distanceFactor = 1;
|
||||
@@ -449,6 +491,16 @@ namespace Barotrauma
|
||||
|
||||
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
|
||||
|
||||
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
|
||||
{
|
||||
foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions())
|
||||
{
|
||||
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
|
||||
if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; }
|
||||
yield return affliction;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
|
||||
+23
-10
@@ -64,8 +64,15 @@ namespace Barotrauma
|
||||
|
||||
public static float GetVitalityFactor(Character character)
|
||||
{
|
||||
float vitality = character.HealthPercentage - (character.Bleeding * 2) - character.Bloodloss + Math.Min(character.Oxygen, 0);
|
||||
float vitality = 100;
|
||||
vitality -= character.Bleeding * 2;
|
||||
vitality += Math.Min(character.Oxygen, 0);
|
||||
vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis");
|
||||
foreach (Affliction affliction in AIObjectiveRescue.GetTreatableAfflictions(character))
|
||||
{
|
||||
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
|
||||
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
|
||||
}
|
||||
return Math.Clamp(vitality, 0, 100);
|
||||
}
|
||||
|
||||
@@ -79,11 +86,11 @@ namespace Barotrauma
|
||||
{
|
||||
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; }
|
||||
if (character.AIController is HumanAIController humanAI)
|
||||
{
|
||||
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target) ||
|
||||
target.CharacterHealth.GetAllAfflictions().All(a => a.Prefab.IsBuff || a.Strength <= a.Prefab.TreatmentThreshold))
|
||||
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -105,10 +112,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
|
||||
}
|
||||
if (target.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.
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; }
|
||||
if (target != character &&!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
|
||||
if (target.Submarine != character.Submarine) { return false; }
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
// Don't allow going into another sub, unless it's connected and of the same team and type.
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; }
|
||||
}
|
||||
if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
|
||||
{
|
||||
// Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters
|
||||
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
|
||||
@@ -119,9 +129,12 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Don't go into rooms that have enemies
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; }
|
||||
return true;
|
||||
if (target.CurrentHull != null)
|
||||
{
|
||||
// Don't go into rooms that have enemies
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; }
|
||||
}
|
||||
return character.GetDamageDoneByAttacker(target) <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user