using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Linq; using Barotrauma.Extensions; namespace Barotrauma { class AIObjectiveRepairItem : AIObjective { public override Identifier Identifier { get; set; } = "repair item".ToIdentifier(); protected override bool AllowInFriendlySubs => true; public override bool KeepDivingGearOn => Item?.CurrentHull == null; protected override bool AllowWhileHandcuffed => false; public Item Item { get; private set; } private AIObjectiveGoTo goToObjective; private AIObjectiveContainItem refuelObjective; private RepairTool repairTool; private const float WaitTimeBeforeRepair = 0.5f; private float waitTimer; private bool IsRepairing() => IsRepairing(character, Item); private readonly bool isPriority; public static bool IsRepairing(Character character, Item item) => character.SelectedItem == item && item.Repairables.Any(r => r.CurrentFixer == character); public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false) : base(character, objectiveManager, priorityModifier) { Item = item; this.isPriority = isPriority; } protected override float GetPriority() { if (!IsAllowed) { HandleDisallowed(); } if (Item.IgnoreByAI(character)) { Abandon = true; } if (Abandon) { if (IsRepairing()) { Item.Repairables.ForEach(r => r.StopRepairing(character)); } return Priority; } if (HumanAIController.IsItemRepairedByAnother(Item, out _)) { Priority = 0; IsCompleted = true; } else if (Item.IsClaimedByBallastFlora) { Priority = 0; } else { float distanceFactor = 1; if (!isPriority && Item.CurrentHull != character.CurrentHull) { distanceFactor = GetDistanceFactor(Item.WorldPosition, factorAtMaxDistance: 0.25f, verticalDistanceMultiplier: 5, maxDistance: 4000); } float requiredSuccessFactor = objectiveManager.HasOrder() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor; float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100; bool isSelected = IsRepairing(); float selectedBonus = isSelected ? 100 - MaxDevotion : 0; float devotion = (CumulatedDevotion + selectedBonus) / 100; float reduction = isPriority ? 1 : isSelected ? 2 : 3; float max = AIObjectiveManager.LowestOrderPriority - reduction; float highestWeight = -1; foreach (Identifier tag in Item.Prefab.Tags) { if (JobPrefab.ItemRepairPriorities.TryGetValue(tag, out float weight) && weight > highestWeight) { highestWeight = weight; } } if (highestWeight == -1) { // Predefined weight not found. highestWeight = 1; } Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * highestWeight * PriorityModifier), 0, 1)); } return Priority; } protected override bool CheckObjectiveSpecific() { IsCompleted = Item.IsFullCondition; if (character.IsOnPlayerTeam && IsCompleted && IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "itemrepaired".ToIdentifier(), 10.0f); } return IsCompleted; } protected override void Act(float deltaTime) { // Only continue when the get item sub objectives have been completed. if (subObjectives.Any()) { return; } foreach (Repairable repairable in Item.Repairables) { if (!repairable.HasRequiredItems(character, false)) { //make sure we have all the items required to fix the target item foreach (var kvp in repairable.RequiredItems) { foreach (RelatedItem requiredItem in kvp.Value) { var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, equip: true) { AllowVariants = requiredItem.AllowVariants }; if (objectiveManager.IsCurrentOrder()) { if (character.IsOnPlayerTeam) { getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair").Value, null, 0.0f, "dialogcannotfindrequireditemtorepair".ToIdentifier(), 10.0f); } } subObjectives.Add(getItemObjective); } } return; } } if (repairTool == null) { FindRepairTool(); } if (repairTool != null) { if (repairTool.RequiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems)) { if (repairTool.Item.OwnInventory == null) { #if DEBUG DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"{repairTool}\" has no proper inventory."); #endif Abandon = true; return; } RelatedItem item = null; Item fuel = null; foreach (RelatedItem requiredItem in requiredItems) { item = requiredItem; fuel = repairTool.Item.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it)); if (fuel != null) { break; } } if (fuel == null) { RemoveSubObjective(ref goToObjective); TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { RemoveExisting = true }, onCompleted: () => RemoveSubObjective(ref refuelObjective), onAbandon: () => Abandon = true); return; } } } if (character.CanInteractWith(Item, out _, checkLinked: false)) { waitTimer += deltaTime; if (waitTimer < WaitTimeBeforeRepair) { return; } HumanAIController.FaceTarget(Item); bool repairThroughRepairInterface = false; foreach (Repairable repairable in Item.Repairables) { if (repairable.CurrentFixer != null && repairable.CurrentFixer != character) { // Someone else is repairing the target. Abandon the objective if the other is better at this than us. Abandon = repairable.CurrentFixer.IsPlayer || repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer); } if (!Abandon) { if (character.SelectedItem != Item) { if (Item.TryInteract(character, forceUseKey: true) || Item.TryInteract(character, forceSelectKey: true)) { character.SelectedItem = Item; repairThroughRepairInterface = true; } else { Abandon = true; } } CheckPreviousCondition(deltaTime); } if (Abandon) { if (character.IsOnPlayerTeam && IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "cannotrepair".ToIdentifier(), 10.0f); } repairable.StopRepairing(character); } else if (repairable.CurrentFixer != character) { repairable.StartRepairing(character, Repairable.FixActions.Repair); } else { repairThroughRepairInterface = true; } break; } if (!repairThroughRepairInterface && repairTool != null && !Abandon) { OperateRepairTool(deltaTime); } } else { waitTimer = 0.0f; RemoveSubObjective(ref refuelObjective); // If cannot reach the item, approach it. TryAddSubObjective(ref goToObjective, constructor: () => { var objective = new AIObjectiveGoTo(Item, character, objectiveManager) { TargetName = Item.Name, SpeakCannotReachCondition = () => isPriority }; if (repairTool != null) { objective.CloseEnough = AIObjectiveFixLeak.CalculateReach(repairTool, character); } return objective; }, onAbandon: () => { Abandon = true; if (character.IsOnPlayerTeam && IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "cannotrepair".ToIdentifier(), 10.0f); } }); } } private const float conditionCheckDelay = 1; private float conditionCheckTimer; private float previousCondition; private void CheckPreviousCondition(float deltaTime) { if (Item == null || Item.Removed) { return; } conditionCheckTimer -= deltaTime; if (conditionCheckTimer > 0) { return; } conditionCheckTimer = conditionCheckDelay; if (previousCondition > -1 && Item.Condition < previousCondition) { // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. Abandon = true; } else { // If the previous condition is not yet stored or if it's valid (greater or equal to current condition), save the condition for the next check here. previousCondition = Item.Condition; } } private void FindRepairTool() { foreach (Repairable repairable in Item.Repairables) { foreach (var kvp in repairable.RequiredItems) { foreach (RelatedItem requiredItem in kvp.Value) { foreach (var item in character.Inventory.AllItems) { if (requiredItem.MatchesItem(item)) { repairTool = item.GetComponent(); } } } } } } private void OperateRepairTool(float deltaTime) { character.CursorPosition = Item.WorldPosition; if (character.Submarine != null) { character.CursorPosition -= character.Submarine.Position; } if (repairTool.Item.RequireAimToUse) { character.SetInput(InputType.Aim, false, true); } Vector2 fromToolToTarget = Item.Position - repairTool.Item.Position; if (fromToolToTarget.LengthSquared() < MathUtils.Pow(repairTool.Range / 2, 2)) { // Too close -> steer away character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - Item.SimPosition) / 2); } else { character.AIController.SteeringManager.Reset(); } if (VectorExtensions.Angle(VectorExtensions.Forward(repairTool.Item.body.TransformedRotation), fromToolToTarget) < MathHelper.PiOver4) { repairTool.Use(deltaTime, character); } } public override void Reset() { base.Reset(); goToObjective = null; refuelObjective = null; repairTool = null; } } }