Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2023-12-14 11:56:39 -03:00
376 changed files with 7775 additions and 2879 deletions
@@ -91,6 +91,11 @@ namespace Barotrauma
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f;
/// <summary>
/// Returns hulls that are visible to the character, including the current hull.
/// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls.
/// </summary>
public IEnumerable<Hull> VisibleHulls
{
get
@@ -353,7 +358,7 @@ namespace Barotrauma
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
{
var inventory = parentItem.OwnInventory;
if (inventory == null) { return; }
if (inventory == null || !inventory.Container.DrawInventory) { return; }
int removed = 0;
if (predicate == null || inventory.AllItems.Any(predicate))
{
@@ -262,7 +262,8 @@ namespace Barotrauma
if (aiElements.Count == 0)
{
DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found.");
DebugConsole.ThrowError("Error in file \"" + c.Params.File.Path + "\" - no AI element found.",
contentPackage: c.Prefab?.ContentPackage);
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, false, false);
return;
@@ -311,7 +312,7 @@ namespace Barotrauma
}
ReevaluateAttacks();
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors);
insideSteering = new IndoorsSteeringManager(this, AIParams.CanOpenDoors, canAttackDoors);
steeringManager = outsideSteering;
State = AIState.Idle;
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
@@ -321,6 +322,10 @@ namespace Barotrauma
}
private CharacterParams.AIParams _aiParams;
/// <summary>
/// Shorthand for <see cref="Character.Params.AI"/> with null checking.
/// </summary>
/// <returns><see cref="Character.Params.AI"/> or an empty params. Does not return nulls.</returns>
public CharacterParams.AIParams AIParams
{
get
@@ -330,7 +335,8 @@ namespace Barotrauma
_aiParams = Character.Params.AI;
if (_aiParams == null)
{
DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.");
DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.",
contentPackage: Character.Prefab.ContentPackage);
Enabled = false;
_aiParams = new CharacterParams.AIParams(null, Character.Params);
}
@@ -563,7 +569,7 @@ namespace Barotrauma
}
}
if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors)
if (Character.Params.UsePathFinding && AIParams.UsePathFindingToGetInside && AIParams.CanOpenDoors)
{
// Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk).
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
@@ -2503,7 +2509,8 @@ namespace Barotrauma
Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head);
if (mouthLimb == null)
{
DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)");
DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)",
contentPackage: Character.Prefab.ContentPackage);
State = AIState.Idle;
return;
}
@@ -2540,7 +2547,11 @@ namespace Barotrauma
item.body.LinearVelocity -= velocity * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.LastEatenTime = (float)Timing.TotalTimeUnpaused;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
item.AddDamage(Character,
item.WorldPosition,
new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed),
impulseDirection: Vector2.Zero,
deltaTime);
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
if (item.Condition <= 0.0f)
{
@@ -3090,7 +3101,10 @@ namespace Barotrauma
break;
}
valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist);
valueModifier *=
targetMemory.Priority /
//sqrt = the further the target is, the less the distance matters
MathF.Sqrt(dist);
if (valueModifier > targetValue)
{
@@ -685,8 +685,8 @@ namespace Barotrauma
}
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear);
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors))
var divingSuit = Character.Inventory.FindEquippedItemByTag(Tags.HeavyDivingGear);
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(Character))
{
if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
@@ -727,54 +727,51 @@ namespace Barotrauma
}
}
if (takeMaskOff)
{
if (Character.HasEquippedItem(Tags.LightDivingGear))
{
var mask = Character.Inventory.FindEquippedItemByTag(Tags.LightDivingGear);
if (mask != null)
{
var mask = Character.Inventory.FindItemByTag(Tags.LightDivingGear);
if (mask != null)
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
}
}
else
{
ReequipUnequipped();
}
}
}
else
{
ReequipUnequipped();
}
}
}
}
}
@@ -784,13 +781,11 @@ namespace Barotrauma
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
for (int i = 0; i < 2; i++)
foreach (Item item in Character.HeldItems)
{
var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand;
Item item = Character.Inventory.GetItemInLimbSlot(hand);
if (item == null) { continue; }
if (item == null || !item.IsInteractable(Character)) { continue; }
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID )
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID)
{
if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Bag })) { continue; }
findItemState = FindItemState.OtherItem;
@@ -1389,7 +1384,10 @@ namespace Barotrauma
// Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
continue;
}
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
bool isWitnessing =
otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) ||
otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
if (!isWitnessing)
{
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
@@ -1754,11 +1752,11 @@ namespace Barotrauma
if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
otherCharacter.AIController is not HumanAIController otherHumanAI ||
!otherHumanAI.VisibleHulls.Contains(character.CurrentHull))
Vector2.DistanceSquared(otherCharacter.WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f)
{
continue;
}
if (!otherCharacter.CanSeeTarget(character)) { continue; }
if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
@@ -1796,7 +1794,7 @@ namespace Barotrauma
if (!TriggerSecurity(otherHumanAI, combatMode))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
{
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
{
@@ -1840,13 +1838,13 @@ namespace Barotrauma
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f ||
otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) ||
!otherHumanAI.VisibleHulls.Contains(thief.CurrentHull))
otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI ||
Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f)
{
continue;
}
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
if (!otherCharacter.CanSeeTarget(thief)) { continue; }
if (!otherCharacter.CanSeeTarget(thief, seeThroughWindows: true)) { continue; }
// Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding
// -> allow them to use the emergency items
if (thief.Submarine != null)
@@ -1857,16 +1855,11 @@ namespace Barotrauma
}
if (!someoneSpoke)
{
if (!item.StolenDuringRound &&
Level.Loaded?.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
if (!item.StolenDuringRound)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss);
ApplyStealingReputationLoss(item);
item.StolenDuringRound = true;
}
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
someoneSpoke = true;
#if CLIENT
@@ -1877,7 +1870,7 @@ namespace Barotrauma
if (!TriggerSecurity(otherHumanAI))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition)))
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition)))
{
if (TriggerSecurity(security.AIController as HumanAIController))
{
@@ -1898,6 +1891,10 @@ namespace Barotrauma
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
if (humanAI.ObjectiveManager.GetObjective<AIObjectiveFindThieves>() is { } findThieves)
{
findThieves.InspectEveryone();
}
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
onAbort: () =>
@@ -1915,6 +1912,18 @@ namespace Barotrauma
}
}
public static void ApplyStealingReputationLoss(Item item)
{
if (Level.Loaded?.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss);
}
}
// 0.225 - 0.375
private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
@@ -197,17 +197,6 @@ namespace Barotrauma
}
}
/// <summary>
/// This method allows multiple subobjectives of same type. Use with caution.
/// </summary>
public void AddSubObjectiveInQueue(AIObjective objective)
{
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
}
public void RemoveSubObjective<T>(ref T objective) where T : AIObjective
{
if (objective != null)
@@ -0,0 +1,160 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveCheckStolenItems : AIObjective
{
public override Identifier Identifier { get; set; } = "check stolen items".ToIdentifier();
public override bool AllowOutsideSubmarine => false;
public override bool AllowInAnySub => false;
public float FindStolenItemsProbability = 1.0f;
enum State
{
GotoTarget,
Inspect,
Warn,
Done
}
private float inspectDelay;
private float warnDelay;
private State currentState;
public readonly Character TargetCharacter;
private AIObjectiveGoTo? goToObjective;
private readonly List<Item> stolenItems = new List<Item>();
public AIObjectiveCheckStolenItems(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) :
base(character, objectiveManager, priorityModifier)
{
TargetCharacter = targetCharacter;
inspectDelay = 5.0f;
warnDelay = 5.0f;
}
public override bool IsLoop
{
get => false;
set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
}
protected override bool CheckObjectiveSpecific() => false;
protected override float GetPriority()
{
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
{
Priority = objectiveManager.GetOrderPriority(this);
}
else
{
Priority = AIObjectiveManager.LowestOrderPriority - 1;
}
return Priority;
}
public void ForceComplete()
{
IsCompleted = true;
}
protected override void Act(float deltaTime)
{
switch (currentState)
{
case State.GotoTarget:
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
return new AIObjectiveGoTo(TargetCharacter, character, objectiveManager, repeat: false)
{
SpeakIfFails = false
};
},
onCompleted: () =>
{
RemoveSubObjective(ref goToObjective);
currentState = State.Inspect;
stolenItems.Clear();
TargetCharacter.Inventory.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true, stolenItems);
character.Speak(TextManager.Get("dialogcheckstolenitems").Value);
},
onAbandon: () =>
{
Abandon = true;
});
break;
case State.Inspect:
Inspect(deltaTime);
break;
case State.Warn:
Warn(deltaTime);
break;
}
}
private void Inspect(float deltaTime)
{
if (inspectDelay > 0.0f)
{
character.SelectCharacter(TargetCharacter);
inspectDelay -= deltaTime;
return;
}
if (stolenItems.Any() &&
Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) < FindStolenItemsProbability)
{
character.Speak(TextManager.Get("dialogcheckstolenitems.warn").Value);
currentState = State.Warn;
}
else
{
character.Speak(TextManager.Get("dialogcheckstolenitems.nostolenitems").Value);
currentState = State.Done;
IsCompleted = true;
}
character.DeselectCharacter();
}
private void Warn(float deltaTime)
{
if (warnDelay > 0.0f)
{
warnDelay -= deltaTime;
return;
}
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == TargetCharacter);
if (stolenItemsOnCharacter.Any())
{
character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value);
HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, TargetCharacter);
foreach (var stolenItem in stolenItemsOnCharacter)
{
HumanAIController.ApplyStealingReputationLoss(stolenItem);
}
}
else
{
character.Speak(TextManager.Get("dialogcheckstolenitems.comply").Value);
}
foreach (var item in stolenItems)
{
HumanAIController.ObjectiveManager.AddObjective(new AIObjectiveGetItem(character, item, objectiveManager, equip: false)
{
BasePriority = 10
});
}
currentState = State.Done;
IsCompleted = true;
}
}
}
@@ -1070,7 +1070,8 @@ namespace Barotrauma
{
// Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
Item ammunition = character.Inventory.FindItem(i =>
i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive: true);
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
@@ -1089,6 +1090,9 @@ namespace Barotrauma
}
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{
// Inventory not drawn = it's not interactable
// If the weapon is empty and the inventory is inaccessible, it can't be reloaded
if (!Weapon.OwnInventory.Container.DrawInventory) { return false; }
SeekAmmunition(ammunitionIdentifiers);
}
return false;
@@ -14,7 +14,7 @@ namespace Barotrauma
private int escapeProgress;
private bool isBeingWatched;
private bool shouldSwitchTeams;
private readonly bool shouldSwitchTeams;
const string EscapeTeamChangeIdentifier = "escape";
@@ -88,10 +88,12 @@ namespace Barotrauma
escapeProgress += Rand.Range(2, 5);
if (escapeProgress > 15)
{
Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem);
if (handcuffs != null)
foreach (var it in character.HeldItems)
{
handcuffs.Drop(character);
if (it.HasTag(Tags.HandLockerItem) && it.IsInteractable(character))
{
it.Drop(character);
}
}
}
escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f);
@@ -0,0 +1,152 @@
#nullable enable
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveFindThieves : AIObjectiveLoop<Character>
{
public override Identifier Identifier { get; set; } = "find thieves".ToIdentifier();
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
protected override float TargetUpdateTimeMultiplier => 1.0f;
const float DefaultInspectDistance = 200.0f;
/// <summary>
/// How close the NPC must be to the target to the inspect them? You can use high values to make the NPC
/// systematically go through targets no matter where they are, and low values to check targets they happen to come across.
/// </summary>
public float InspectDistance = DefaultInspectDistance;
private float? overrideInspectProbability;
/// <summary>
/// Chance of inspecting a valid target. The NPC won't try to inspect that target again for <see cref="inspectionInterval"/>
/// regardless if the target is inspected or not.
/// </summary>
public float InspectProbability
{
get
{
if (overrideInspectProbability.HasValue)
{
return overrideInspectProbability.Value;
}
if (GameMain.GameSession?.Campaign is { } campaign)
{
if (campaign.Map?.CurrentLocation?.Reputation is { } reputation)
{
return MathHelper.Lerp(
campaign.Settings.MaxStolenItemInspectionProbability,
campaign.Settings.MinStolenItemInspectionProbability,
reputation.NormalizedValue);
}
}
return 0.2f;
}
}
/// <summary>
/// When did the character last inspect whether some other character has stolen items on them?
/// </summary>
private static readonly Dictionary<Character, double> lastInspectionTimes = new Dictionary<Character, double>();
private readonly float inspectionInterval = 120.0f;
public AIObjectiveFindThieves(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target)
{
if (!IsValidTarget(target, character)) { return false; }
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > InspectDistance * InspectDistance) { return false; }
if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime))
{
if (Timing.TotalTime < lastInspectionTime + inspectionInterval)
{
return false;
}
}
return true;
}
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
{
return subObjectives.Any() ? 50 : 0;
}
public void InspectEveryone()
{
lastInspectionTimes.Clear();
overrideInspectProbability = 1.0f;
InspectDistance = DefaultInspectDistance * 2;
}
protected override AIObjective ObjectiveConstructor(Character target)
{
var checkStolenItemsObjective = new AIObjectiveCheckStolenItems(character, target, objectiveManager);
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= InspectProbability)
{
checkStolenItemsObjective.ForceComplete();
lastInspectionTimes[target] = Timing.TotalTime;
}
return checkStolenItemsObjective;
}
private float checkVisibleStolenItemsTimer;
private const float CheckVisibleStolenItemsInterval = 5.0f;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (checkVisibleStolenItemsTimer > 0.0f)
{
checkVisibleStolenItemsTimer -= deltaTime;
return;
}
foreach (var target in Character.CharacterList)
{
if (!IsValidTarget(target, character)) { continue; }
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
if (target.Inventory.AllItems.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing && target.HasEquippedItem(it)) &&
character.CanSeeTarget(target, seeThroughWindows: true))
{
AIObjectiveCheckStolenItems? existingObjective =
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.TargetCharacter == target);
if (existingObjective == null)
{
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
lastInspectionTimes[target] = Timing.TotalTime;
}
}
}
checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval;
}
private bool IsValidTarget(Character target, Character character)
{
if (target == null || target.Removed) { return false; }
if (target.IsIncapacitated) { return false; }
if (target == character) { return false; }
if (target.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
if (target.CurrentHull == null) { return false; }
if (target.Submarine != character.Submarine) { return false; }
//only player's crew can steal, ignore other teams
if (!target.IsOnPlayerTeam) { return false; }
if (target.IsArrested) { return false; }
return true;
}
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
{
lastInspectionTimes[target] = Timing.TotalTime;
}
}
}
@@ -120,7 +120,7 @@ namespace Barotrauma
// The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path.
timerMargin += 0.5f;
timerMargin = Math.Min(timerMargin, newTargetIntervalMin);
newTargetTimer = Math.Min(newTargetTimer, timerMargin);
newTargetTimer = Math.Max(newTargetTimer, timerMargin);
}
private void SetTargetTimerHigh()
@@ -178,7 +178,7 @@ namespace Barotrauma
if (!objectiveManager.IsOrder(this))
{
// Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention.
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater;
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater || this is AIObjectiveFindThieves;
if (!ignore && !ReportedTargets.Contains(target)) { continue; }
}
if (!Filter(target)) { continue; }
@@ -151,6 +151,7 @@ namespace Barotrauma
prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t));
}
AddObjective(newIdleObjective);
int objectiveCount = Objectives.Count;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
@@ -558,6 +559,9 @@ namespace Barotrauma
case "escapehandcuffs":
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
break;
case "findthieves":
newObjective = new AIObjectiveFindThieves(character, this, priorityModifier: priorityModifier);
break;
case "prepareforexpedition":
newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems)
{
@@ -441,7 +441,8 @@ namespace Barotrauma
}
catch (NotImplementedException e)
{
DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}");
DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}",
contentPackage: ContentPackage);
return null;
}
}
@@ -663,6 +664,13 @@ namespace Barotrauma
WallSectionIndex = wallSectionIndex ?? other.WallSectionIndex;
UseController = useController ?? other.UseController;
#if DEBUG
if (UseController && ConnectedController == null)
{
DebugConsole.ThrowError($"AI: Created an Order {Identifier} that's set to use a Controller, but a Controller was not specified.\n{Environment.StackTrace.CleanupStackTrace()}");
}
#endif
}
public Order WithOption(Identifier option)
@@ -712,7 +720,12 @@ namespace Barotrauma
public Order WithItemComponent(Item item, ItemComponent component = null)
{
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item));
Controller controller = null;
if (UseController)
{
controller = item?.FindController(tags: ControllerTags);
}
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item), connectedController: controller);
}
public Order WithWallSection(Structure wall, int? sectionIndex)