#nullable enable using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; using Barotrauma.Items.Components; namespace Barotrauma { class AIObjectiveFindThieves : AIObjectiveLoop { 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; /// /// How long the round must have ran before NPCs can start doing inspections /// (prevents "unfair" inspections you have no chance to react to if you happen to spawn right next to a security NPC with stolen items on you) /// private const float DelayOnRoundStart = 30.0f; private const float DefaultInspectDistance = 200.0f; /// /// Used when something is stolen and when the guards decide to inspect everyone. /// private const float ExtendedInspectDistance = 400.0f; /// /// Used when the target is tagged as a criminal (= suspective). /// private const float CriminalInspectDistance = 500.0f; private const float CriminalInspectProbability = 1.0f; /// /// 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. /// private float inspectDistance = DefaultInspectDistance; private float? overrideInspectProbability; /// /// Chance of inspecting a valid target. The NPC won't try to inspect that target again for /// regardless if the target is inspected or not. /// 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.PatdownProbabilityMax, campaign.Settings.PatdownProbabilityMin, reputation.NormalizedValue); } } return 0.2f; } } /// /// When did the character last inspect whether some other character has stolen items on them? /// private static readonly Dictionary lastInspectionTimes = new Dictionary(); private const float NormalInspectionInterval = 120.0f; private const float CriminalInspectionInterval = 30.0f; public AIObjectiveFindThieves(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } protected override bool IsValidTarget(Character target) { if (GameMain.GameSession is not { RoundDuration: > DelayOnRoundStart }) { return false; } if (!IsValidTarget(target, character)) { return false; } float inspectDist = target.IsCriminal ? CriminalInspectDistance : inspectDistance; if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > inspectDist * inspectDist) { return false; } if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime)) { float inspectionInterval = target.IsCriminal ? CriminalInspectionInterval : NormalInspectionInterval; if (Timing.TotalTime < lastInspectionTime + inspectionInterval) { return false; } } return true; } protected override IEnumerable GetList() => Character.CharacterList; protected override float GetTargetPriority() { if (character.IsClimbing) { // Don't inspect while climbing, because cannot grab while holding the ladders. // Can lead to abandoning the objective when we need to climb the ladders to get to the target, but I think that's acceptable. return 0; } return subObjectives.Any() ? 50 : 0; } public void InspectEveryone() { lastInspectionTimes.Clear(); overrideInspectProbability = 1.0f; inspectDistance = ExtendedInspectDistance; } protected override AIObjective ObjectiveConstructor(Character target) { var checkStolenItemsObjective = new AIObjectiveCheckStolenItems(character, target, objectiveManager); float probabity = target.IsCriminal ? CriminalInspectProbability : InspectProbability; if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= probabity) { 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 || character.IsClimbing) { checkVisibleStolenItemsTimer -= deltaTime; return; } if (character.SelectedSecondaryItem?.GetComponent() != null) { // Might be e.g. sitting on a chair. character.SelectedSecondaryItem = null; } 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.Illegitimate && target.HasEquippedItem(it)) && character.CanSeeTarget(target, seeThroughWindows: true)) { AIObjectiveCheckStolenItems? existingObjective = objectiveManager.GetActiveObjectives().FirstOrDefault(o => o.Target == 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.IsHandcuffed) { return false; } // Ignore targets that are climbing, because might need to use ladders to get to them. if (target.IsClimbing) { return false; } if (HumanAIController.IsTrueForAnyBotInTheCrew(bot => bot != HumanAIController && ((bot.ObjectiveManager.GetActiveObjective() is AIObjectiveCheckStolenItems checkObj && checkObj.Target == target) || (bot.ObjectiveManager.GetActiveObjective() is AIObjectiveCombat combatObj && combatObj.Enemy == target)))) { // Already being inspected by someone or fighting with someone in our team. return false; } return true; } protected override void OnObjectiveCompleted(AIObjective objective, Character target) { lastInspectionTimes[target] = Timing.TotalTime; } public override void OnDeselected() { base.OnDeselected(); character.DeselectCharacter(); } } }