using Barotrauma.Extensions; using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Linq; namespace Barotrauma { class AIObjectiveFixLeak : AIObjective { public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; protected override bool AllowInFriendlySubs => true; protected override bool AllowInAnySub => true; protected override bool AllowWhileHandcuffed => false; public Gap Leak { get; private set; } private AIObjectiveGetItem getWeldingTool; private AIObjectiveContainItem refuelObjective; private AIObjectiveGoTo gotoObjective; private AIObjectiveOperateItem operateObjective; public readonly bool isPriority; public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false) : base (character, objectiveManager, priorityModifier) { Leak = leak; this.isPriority = isPriority; } protected override bool CheckObjectiveSpecific() => Leak.Open <= 0 || Leak.Removed; protected override float GetPriority() { if (!IsAllowed) { HandleDisallowed(); return Priority; } float coopMultiplier = 1; foreach (var c in Character.CharacterList) { if (!HumanAIController.IsActive(c)) { continue; } if (c.TeamID != character.TeamID) { continue; } if (c == character) { continue; } if (c.IsPlayer) { continue; } if (c.AIController is HumanAIController otherAI ) { if (otherAI.ObjectiveManager.GetFirstActiveObjective() is AIObjectiveFixLeak fixLeak) { if (fixLeak.Leak == Leak) { // Ignore leaks that others are already targeting Priority = 0; return Priority; } if (fixLeak.Leak.FlowTargetHull == Leak.FlowTargetHull) { // Reduce the priority of leaks that others should be targeting coopMultiplier = 0.1f; break; } } } } float reduction = isPriority ? 1 : 2; float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction; if (operateObjective != null && objectiveManager.GetFirstActiveObjective() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this) { // Prioritize leaks that we are already fixing Priority = maxPriority; } else { float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull)) { // Double the distance when the leak can be accessed from the current hull. distanceFactor *= 2; } float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; float devotion = CumulatedDevotion / 100; Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier * coopMultiplier), 0, 1)); } return Priority; } protected override void Act(float deltaTime) { var weldingTool = character.Inventory.FindItemByTag("weldingequipment".ToIdentifier(), true); var repairTool = weldingTool?.GetComponent(); if (weldingTool == null) { TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment".ToIdentifier(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), onAbandon: () => { if (character.IsOnPlayerTeam && objectiveManager.IsCurrentOrder()) { character.Speak(TextManager.Get("dialogcannotfindweldingequipment").Value, null, 0.0f, "dialogcannotfindweldingequipment".ToIdentifier(), 10.0f); } Abandon = true; }, onCompleted: () => RemoveSubObjective(ref getWeldingTool)); return; } else { if (repairTool == null) { #if DEBUG DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no RepairTool component but is tagged as a welding tool"); #endif Abandon = true; return; } if (weldingTool.OwnInventory == null && repairTool.RequiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained)) { #if DEBUG DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no proper inventory"); #endif Abandon = true; return; } if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag(Tags.WeldingFuel) && i.Condition > 0.0f)) { TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, Tags.WeldingFuel, weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { RemoveExisting = true }, onAbandon: () => { Abandon = true; ReportWeldingFuelTankCount(); }, onCompleted: () => { RemoveSubObjective(ref refuelObjective); ReportWeldingFuelTankCount(); }); void ReportWeldingFuelTankCount() { if (character.Submarine != Submarine.MainSub) { return; } int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.WeldingFuel) && i.Condition > 1); if (remainingOxygenTanks == 0) { character.Speak(TextManager.Get("DialogOutOfWeldingFuel").Value, null, 0.0f, "outofweldingfuel".ToIdentifier(), 30.0f); } else if (remainingOxygenTanks < 4) { character.Speak(TextManager.Get("DialogLowOnWeldingFuel").Value, null, 0.0f, "lowonweldingfuel".ToIdentifier(), 30.0f); } } return; } } if (subObjectives.Any()) { return; } Vector2 toLeak = Leak.WorldPosition - character.AnimController.AimSourceWorldPos; // TODO: use the collider size/reach? if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150) { HumanAIController.AnimController.Crouching = true; } float reach = CalculateReach(repairTool, character); bool canOperate = toLeak.LengthSquared() < reach * reach; if (canOperate) { TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak) { // Use an empty filter to override the default EndNodeFilter = n => true }, onAbandon: () => Abandon = true, onCompleted: () => { if (CheckObjectiveSpecific()) { IsCompleted = true; } else { // Failed to operate. Probably too far. Abandon = true; } }); } else { TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager) { UseDistanceRelativeToAimSourcePos = true, CloseEnough = reach, DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty, TargetName = Leak.FlowTargetHull?.DisplayName, requiredCondition = () => Leak.Submarine == character.Submarine && Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))), endNodeFilter = IsSuitableEndNode, // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) // Only report about contextual targets. SpeakCannotReachCondition = () => isPriority && !CheckObjectiveSpecific() }, onAbandon: () => { if (CheckObjectiveSpecific()) { IsCompleted = true; } else if ((Leak.WorldPosition - character.AnimController.AimSourceWorldPos).LengthSquared() > MathUtils.Pow(reach * 2, 2)) { // Too far Abandon = true; } else { // We are close, try again. RemoveSubObjective(ref gotoObjective); } }, onCompleted: () => RemoveSubObjective(ref gotoObjective)); bool IsSuitableEndNode(PathNode n) { if (n.Waypoint.CurrentHull is null) { return false; } if (n.Waypoint.CurrentHull.ConnectedGaps.Contains(Leak)) { return true; } // Accept also nodes located in the linked hulls (multi-hull rooms) return Leak.linkedTo.Any(e => e is Hull h && h.linkedTo.Contains(n.Waypoint.CurrentHull)); } } } public override void Reset() { base.Reset(); getWeldingTool = null; refuelObjective = null; gotoObjective = null; operateObjective = null; } public static float CalculateReach(RepairTool repairTool, Character character) { float armLength = ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength); // This is an approximation, because we don't know the exact reach until the pose is taken. // And even then the actual range depends on the direction we are aiming to. // Found out that without any multiplier the value (209) is often too short. return repairTool.Range + armLength * 2; } } }