using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Items.Components; namespace Barotrauma { partial class HumanAIController : AIController { public static bool debugai; public static bool DisableCrewAI; private readonly AIObjectiveManager objectiveManager; private float sortTimer; private float crouchRaycastTimer; private float reactTimer; private float unreachableClearTimer; private bool shouldCrouch; const float reactionTime = 0.3f; const float crouchRaycastInterval = 1; const float sortObjectiveInterval = 1; const float clearUnreachableInterval = 30; private float flipTimer; private const float FlipInterval = 0.5f; public static float HULL_SAFETY_THRESHOLD = 50; private static readonly float characterWaitOnSwitch = 5; public readonly HashSet UnreachableHulls = new HashSet(); public readonly HashSet UnsafeHulls = new HashSet(); public readonly List IgnoredItems = new List(); private float respondToAttackTimer; private const float RespondToAttackInterval = 1.0f; private bool wasConscious; private bool freezeAI; private readonly float maxSteeringBuffer = 5000; private readonly float minSteeringBuffer = 500; private readonly float steeringBufferIncreaseSpeed = 100; private float steeringBuffer; private readonly float obstacleRaycastInterval = 1; private float obstacleRaycastTimer; /// /// List of previous attacks done to this character /// private readonly Dictionary previousAttackResults = new Dictionary(); private readonly SteeringManager outsideSteering, insideSteering; public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController; public override AIObjectiveManager ObjectiveManager { get { return objectiveManager; } } public Order CurrentOrder { get; private set; } public string CurrentOrderOption { get; private set; } public float CurrentHullSafety { get; private set; } = 100; private readonly Dictionary damageDoneByAttacker = new Dictionary(); private readonly HashSet attackers = new HashSet(); private readonly Dictionary knownHulls = new Dictionary(); private class HullSafety { public float safety; public float timer; public bool IsStale => timer <= 0; public HullSafety(float safety) { Reset(safety); } public void Reset(float safety) { this.safety = safety; // How long before the hull safety is considered stale timer = 0.5f; } /// /// Returns true when the safety is stale /// public bool Update(float deltaTime) { timer = Math.Max(timer - deltaTime, 0); return IsStale; } } public HumanAIController(Character c) : base(c) { if (!c.IsHuman) { throw new Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!"); } insideSteering = new IndoorsSteeringManager(this, true, false); outsideSteering = new SteeringManager(this); objectiveManager = new AIObjectiveManager(c); reactTimer = GetReactionTime(); sortTimer = Rand.Range(0f, sortObjectiveInterval); InitProjSpecific(); } partial void InitProjSpecific(); public override void Update(float deltaTime) { if (DisableCrewAI || Character.Removed) { return; } //slowly forget about damage done by attackers foreach (Character enemy in attackers) { float cumulativeDamage = damageDoneByAttacker[enemy]; if (cumulativeDamage > 0) { float reduction = deltaTime; if (cumulativeDamage < 2) { // If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage) reduction *= 0.5f; } damageDoneByAttacker[enemy] -= reduction; } } bool isIncapacitated = Character.IsIncapacitated; if (freezeAI && !isIncapacitated) { freezeAI = false; } if (isIncapacitated) { return; } wasConscious = true; respondToAttackTimer -= deltaTime; if (respondToAttackTimer <= 0.0f) { foreach (var previousAttackResult in previousAttackResults) { RespondToAttack(previousAttackResult.Key, previousAttackResult.Value); } previousAttackResults.Clear(); respondToAttackTimer = RespondToAttackInterval; } base.Update(deltaTime); foreach (var values in knownHulls) { HullSafety hullSafety = values.Value; hullSafety.Update(deltaTime); } if (unreachableClearTimer > 0) { unreachableClearTimer -= deltaTime; } else { unreachableClearTimer = clearUnreachableInterval; UnreachableHulls.Clear(); IgnoredItems.Clear(); } 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); bool hasValidPath = steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable; if (Character.Submarine == null && hasValidPath) { obstacleRaycastTimer -= deltaTime; if (obstacleRaycastTimer <= 0) { obstacleRaycastTimer = obstacleRaycastInterval; // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) { if (connectedSub == Submarine.MainSub) { continue; } Vector2 rayStart = SimPosition - connectedSub.SimPosition; Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) { PathSteering.CurrentPath.Unreachable = true; break; } } } } if (Character.Submarine != null || hasValidPath && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer)) { if (steeringManager != insideSteering) { insideSteering.Reset(); } steeringManager = insideSteering; steeringBuffer += steeringBufferIncreaseSpeed * deltaTime; } else { if (steeringManager != outsideSteering) { outsideSteering.Reset(); } steeringManager = outsideSteering; steeringBuffer = minSteeringBuffer; } steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer); AnimController.Crouching = shouldCrouch; CheckCrouching(deltaTime); Character.ClearInputs(); if (sortTimer > 0.0f) { sortTimer -= deltaTime; } else { objectiveManager.SortObjectives(); sortTimer = sortObjectiveInterval; } objectiveManager.UpdateObjectives(deltaTime); if (reactTimer > 0.0f) { reactTimer -= deltaTime; if (findItemState != FindItemState.None) { // Update every frame only when seeking items UnequipUnnecessaryItems(); } } else { if (Character.CurrentHull != null) { if (Character.TeamID == Character.TeamType.FriendlyNPC) { // Outpost npcs don't inform each other about threads, like crew members do. VisibleHulls.ForEach(h => RefreshHullSafety(h)); } else { VisibleHulls.ForEach(h => PropagateHullSafety(Character, h)); } } if (Character.SpeechImpediment < 100.0f) { if (Character.Submarine != null && Character.Submarine.TeamID == Character.TeamID && !Character.Submarine.Info.IsWreck) { ReportProblems(); } UpdateSpeaking(); } UnequipUnnecessaryItems(); reactTimer = GetReactionTime(); } if (objectiveManager.CurrentObjective == null) { return; } objectiveManager.DoCurrentObjective(deltaTime); bool run = objectiveManager.CurrentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority; if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null) { if (Character.CurrentHull == null) { run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300; } else { float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y; if (Math.Abs(yDiff) > 100) { run = true; } else { float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X; run = Math.Abs(xDiff) > 500; } } } steeringManager.Update(Character.AnimController.GetCurrentSpeed(run && Character.CanRun)); bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); if (steeringManager == insideSteering) { var currPath = PathSteering.CurrentPath; if (currPath != null && currPath.CurrentNode != null) { if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y) { // Don't allow to jump from too high. float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2; float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y); ignorePlatforms = height < allowedJumpHeight; } } if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent) { Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); } } Character.AnimController.IgnorePlatforms = ignorePlatforms; Vector2 targetMovement = AnimController.TargetMovement; if (!Character.AnimController.InWater) { targetMovement = new Vector2(Character.AnimController.TargetMovement.X, MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f)); } Character.AnimController.TargetMovement = Character.ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run)); flipTimer -= deltaTime; if (flipTimer <= 0.0f) { Direction newDir = Character.AnimController.TargetDir; if (Character.IsKeyDown(InputType.Aim)) { var cursorDiffX = Character.CursorPosition.X - Character.Position.X; if (cursorDiffX > 10.0f) { newDir = Direction.Right; } else if (cursorDiffX < -10.0f) { newDir = Direction.Left; } if (Character.SelectedConstruction != null) { Character.SelectedConstruction.SecondaryUse(deltaTime, Character); } } else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater) { newDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left; } if (newDir != Character.AnimController.TargetDir) { Character.AnimController.TargetDir = newDir; flipTimer = FlipInterval; } } } private void UnequipUnnecessaryItems() { if (Character.LockHands) { return; } if (ObjectiveManager.CurrentObjective == null) { return; } if (Character.CurrentHull == null) { return; } bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold; bool isCarrying = ObjectiveManager.HasActiveObjective() || ObjectiveManager.HasActiveObjective(); bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) { bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; Hull targetHull = gotoObjective.GetTargetHull(); return gotoObjective.Target != null && targetHull == null || NeedsDivingGear(targetHull, out _) || insideSteering && (PathSteering.CurrentPath.HasOutdoorsNodes || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _))); } if (isCarrying) { if (findItemState != FindItemState.OtherItem) { if (ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective)) { gotoObjective.Abandon = true; } } if (!oxygenLow) { return; } } // Diving gear if (oxygenLow || findItemState != FindItemState.OtherItem) { if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow) { bool shouldKeepTheGearOn = Character.AnimController.HeadInWater || Character.Submarine == null || Character.Submarine.TeamID != Character.TeamID || ObjectiveManager.IsCurrentObjective() || ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character // wait order || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); if (oxygenLow && Character.CurrentHull.Oxygen > 0) { shouldKeepTheGearOn = false; } else if (Character.CurrentHull.Oxygen < CharacterHealth.LowOxygenThreshold) { shouldKeepTheGearOn = true; } bool removeDivingSuit = !shouldKeepTheGearOn; bool takeMaskOff = !shouldKeepTheGearOn; if (!shouldKeepTheGearOn && !oxygenLow) { if (ObjectiveManager.IsCurrentObjective()) { removeDivingSuit = true; takeMaskOff = true; } else { bool removeSuit = false; bool removeMask = false; foreach (var objective in ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(includingSelf: true)) { if (objective is AIObjectiveGoTo gotoObjective) { if (NeedsDivingGearOnPath(gotoObjective)) { removeDivingSuit = false; takeMaskOff = false; break; } else if (gotoObjective.mimic) { if (!removeSuit) { removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character); if (removeDivingSuit) { removeSuit = true; } } if (!removeMask) { takeMaskOff = !HasDivingMask(gotoObjective.Target as Character); if (takeMaskOff) { removeMask = true; } } } } } } } if (removeDivingSuit) { var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR); if (divingSuit != null) { if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { divingSuit.Drop(Character); HandleRelocation(divingSuit); } else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit) { findItemState = FindItemState.DivingSuit; if (FindSuitableContainer(divingSuit, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; if (targetContainer != null) { var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent()) { DropIfFails = false }; decontainObjective.Abandoned += () => { ReequipUnequipped(); IgnoredItems.Add(targetContainer); }; decontainObjective.Completed += () => ReequipUnequipped(); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } else { divingSuit.Drop(Character); HandleRelocation(divingSuit); } } } } } if (takeMaskOff) { if (Character.HasEquippedItem(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR)) { var mask = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR); if (mask != null) { if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) { if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { mask.Drop(Character); HandleRelocation(mask); } 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) { var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent()); decontainObjective.Abandoned += () => { ReequipUnequipped(); IgnoredItems.Add(targetContainer); }; decontainObjective.Completed += () => ReequipUnequipped(); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } else { mask.Drop(Character); HandleRelocation(mask); } } } } else { ReequipUnequipped(); } } } } } } // Other items if (isCarrying) { return; } if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; } foreach (var item in Character.Inventory.Items) { if (item == null) { continue; } if (Character.HasEquippedItem(item) && (Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) || Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) || Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand))) { if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any })) { if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem) { findItemState = FindItemState.OtherItem; if (FindSuitableContainer(item, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; if (targetContainer != null) { var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent()); decontainObjective.Abandoned += () => { ReequipUnequipped(); IgnoredItems.Add(targetContainer); }; ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } else { item.Drop(Character); HandleRelocation(item); } } } } } } } private readonly HashSet itemsToRelocate = new HashSet(); private void HandleRelocation(Item item) { if (item.Submarine?.TeamID == Character.TeamType.FriendlyNPC) { if (itemsToRelocate.Contains(item)) { return; } itemsToRelocate.Add(item); if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort)) { myPort.OnUnDocked += Relocate; } var campaign = GameMain.GameSession.Campaign; if (campaign != null) { // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. campaign.BeforeLevelLoading += Relocate; } } void Relocate() { if (item == null || item.Removed) { return; } if (!itemsToRelocate.Contains(item)) { return; } var mainSub = Submarine.MainSub; if (item.ParentInventory != null) { if (item.ParentInventory.Owner is Character c) { if (c.TeamID == Character.TeamType.Team1 || c.TeamID == Character.TeamType.Team2) { // Taken by a player/bot (if npc or monster would take the item, we'd probably still want it to spawn back to the main sub. return; } } else if (item.ParentInventory.Owner.Submarine == mainSub) { // Placed inside an inventory that's already in the main sub. return; } } // Laying on ground inside the main sub. if (item.Submarine == mainSub) { return; } WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub); if (wp != null) { item.Submarine = mainSub; item.SetTransform(wp.SimPosition, 0.0f); } itemsToRelocate.Remove(item); } } public void ReequipUnequipped() { foreach (var item in unequippedItems) { if (item != null && !item.Removed && Character.HasItem(item)) { TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false); } } unequippedItems.Clear(); } private enum FindItemState { None, DivingSuit, DivingMask, OtherItem } private FindItemState findItemState; private int itemIndex; public bool FindSuitableContainer(Item containableItem, out Item suitableContainer) => FindSuitableContainer(Character, containableItem, IgnoredItems, ref itemIndex, out suitableContainer); public static bool FindSuitableContainer(Character character, Item containableItem, List ignoredItems, ref int itemIndex, out Item suitableContainer) { suitableContainer = null; if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, customPriorityFunction: i => { if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; } var container = i.GetComponent(); if (container == null) { return 0; } if (container.Inventory.IsFull()) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { if (isRestrictionsDefined) { return 4; } else { if (containableItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) { return isPreferencesDefined ? isSecondary ? 2 : 3 : 1; } else { return isPreferencesDefined ? 0 : 1; } } } else { return 0; } })) { suitableContainer = targetContainer; return true; } return false; } protected void ReportProblems() { Order newOrder = null; Hull targetHull = null; if (Character.CurrentHull != null) { bool isFighting = ObjectiveManager.HasActiveObjective(); bool isFleeing = ObjectiveManager.HasActiveObjective(); foreach (var hull in VisibleHulls) { foreach (Character target in Character.CharacterList) { if (target.CurrentHull != hull || !target.Enabled) { continue; } if (AIObjectiveFightIntruders.IsValidTarget(target, Character)) { if (AddTargets(Character, target) && newOrder == null) { var orderPrefab = Order.GetPrefab("reportintruders"); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } } } if (AIObjectiveExtinguishFires.IsValidTarget(hull, Character)) { if (AddTargets(Character, hull) && newOrder == null) { var orderPrefab = Order.GetPrefab("reportfire"); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } } if (!isFighting) { foreach (var gap in hull.ConnectedGaps) { if (AIObjectiveFixLeaks.IsValidTarget(gap, Character)) { if (AddTargets(Character, gap) && newOrder == null && !gap.IsRoomToRoom) { var orderPrefab = Order.GetPrefab("reportbreach"); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } } } if (!isFleeing) { foreach (Character target in Character.CharacterList) { if (target.CurrentHull != hull) { continue; } if (AIObjectiveRescueAll.IsValidTarget(target, Character)) { if (AddTargets(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective()) { var orderPrefab = Order.GetPrefab("requestfirstaid"); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } } } foreach (Item item in Item.ItemList) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, Character)) { if (item.Repairables.All(r => item.ConditionPercentage > r.RepairIconThreshold)) { continue; } if (AddTargets(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective()) { var orderPrefab = Order.GetPrefab("reportbrokendevices"); newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character); targetHull = hull; } } } } } } } if (newOrder != null) { if (Character.TeamID == Character.TeamType.FriendlyNPC) { Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default, identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"), minDurationBetweenSimilar: 60.0f); } else if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) { Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order); #if SERVER GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", targetHull, null, Character)); #endif } } } public static void ReportProblem(Character reporter, Order order) { if (reporter == null || order == null) { return; } var visibleHulls = new List(reporter.GetVisibleHulls()); foreach (var hull in visibleHulls) { PropagateHullSafety(reporter, hull); RefreshTargets(reporter, order, hull); } } private void UpdateSpeaking() { if (Character.Oxygen < 20.0f) { Character.Speak(TextManager.Get("DialogLowOxygen"), null, Rand.Range(0.5f, 5.0f), "lowoxygen", 30.0f); } if (Character.Bleeding > 2.0f) { Character.Speak(TextManager.Get("DialogBleeding"), null, Rand.Range(0.5f, 5.0f), "bleeding", 30.0f); } if (Character.PressureTimer > 50.0f && Character.CurrentHull != null) { Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, Rand.Range(0.5f, 5.0f), "pressure", 30.0f); } } public override void OnAttacked(Character attacker, AttackResult attackResult) { // The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs if (wasConscious && (Character.IsIncapacitated || Character.Stun > 0.0f)) { RespondToAttack(attacker, attackResult); wasConscious = false; return; } if (Character.IsDead) { return; } if (attacker == null || Character.IsPlayer) { // The player characters need to "respond" to the attack always, because the update loop doesn't run for them. // Otherwise other NPCs totally ignore when player characters are attacked. RespondToAttack(attacker, attackResult); return; } if (previousAttackResults.ContainsKey(attacker)) { foreach (Affliction newAffliction in attackResult.Afflictions) { var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source); if (matchingAffliction == null) { previousAttackResults[attacker].Afflictions.Add(newAffliction); } else { matchingAffliction.Strength += newAffliction.Strength; } } previousAttackResults[attacker] = new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb); } else { previousAttackResults.Add(attacker, attackResult); } } private void RespondToAttack(Character attacker, AttackResult attackResult) { // excluding poisons etc float realDamage = attackResult.Damage; // including poisons etc float totalDamage = realDamage; foreach (Affliction affliction in attackResult.Afflictions) { totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; } if (totalDamage <= 0.01f) { return; } if (Character.IsBot) { if (attacker != null) { if (!damageDoneByAttacker.ContainsKey(attacker)) { damageDoneByAttacker[attacker] = 0.0f; } damageDoneByAttacker[attacker] += totalDamage; attackers.Add(attacker); } if (!freezeAI && !Character.IsDead && Character.IsIncapacitated) { // Removes the combat objective and resets all objectives. objectiveManager.CreateAutonomousObjectives(); objectiveManager.SortObjectives(); freezeAI = true; } } if (attacker == null || attacker.IsDead || attacker.Removed) { // Don't react on the damage if there's no attacker. // We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying. // But fires and enemies should already be handled by the FindSafetyObjective. return; // Ignore damage from falling etc that we shouldn't react to. //if (Character.LastDamageSource == null) { return; } //AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced)); } else if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID)) { // Don't react on damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player return; } else if (IsFriendly(attacker)) { if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character) { // Don't attack characters that damage you while doing cpr, because let's assume that they are helping you. // Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate). return; } float cumulativeDamage = GetDamageDoneByAttacker(attacker); if (!Character.IsSecurity && attacker.IsBot && Character.CombatAction == null) { if (cumulativeDamage > 1) { // Don't retaliate on damage done by human ai, because we know it's accidental AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } } else { (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult); // Inform other NPCs if (cumulativeDamage > 1) { foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed || otherCharacter.Info?.Job == null || otherCharacter.TeamID != Character.TeamType.FriendlyNPC || !(otherCharacter.AIController is HumanAIController otherHumanAI) || otherCharacter.IsInstigator) { continue; } if (!otherHumanAI.IsFriendly(Character)) { continue; } bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); if (otherCharacter.IsSecurity) { // Alert all the security officers magically float delay = isWitnessing ? GetReactionTime() * 2 : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced); otherHumanAI.AddCombatObjective(DetermineCombatMode(otherCharacter, cumulativeDamage), attacker, delay); } else if (isWitnessing) { var mode = Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat; // Other witnesses retreat to safety otherHumanAI.AddCombatObjective(mode, attacker, GetReactionTime()); } } } if (Character.IsBot) { if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; } if (Character.IsSecurity) { if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10) { Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f); } else { Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f); } } else if (!Character.IsInstigator && cumulativeDamage > 1) { Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f); } if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID) { // If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay. AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0); } else { bool allowOffensive = HasItem(attacker, "handlocker", out _, requireEquipped: true); if (attackResult.Afflictions.Any(a => a is AfflictionHusk)) { cumulativeDamage = 100; } // Don't react on minor (accidental) dmg done by characters that are in the same team if (cumulativeDamage < 10) { if (!Character.IsSecurity && cumulativeDamage > 1) { AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } } else { AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 20, allowOffensive: allowOffensive), attacker, GetReactionTime() * 2); } } } } } else if (Character.IsBot) { // Non-friendly AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker); } AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, float dmgThreshold = 10, bool allowOffensive = true) { if (!IsFriendly(attacker)) { return c.IsSecurity ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive; } else { // If there are any enemies around, just ignore the friendly fire if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull))) { return AIObjectiveCombat.CombatMode.None; } if (Character.IsInstigator && attacker.IsPlayer) { // The guards don't react when the player attacks instigators. return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); } else if (attacker.TeamID == Character.TeamType.FriendlyNPC) { if (c.IsSecurity) { return Character.CombatAction != null ? Character.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.None; } else { return Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.None; } } else { if (cumulativeDamage > dmgThreshold) { if (c.IsSecurity) { return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest; } else { return c == Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat; } } else { return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat; } } } } } private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func abortCondition = null, Action onAbort = null, bool allowHoldFire = false) { if (mode == AIObjectiveCombat.CombatMode.None) { return; } if (Character.IsDead || Character.IsIncapacitated) { return; } if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective) { // Don't replace offensive mode with something else if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; } if (combatObjective.Mode != mode || combatObjective.Enemy != attacker || (combatObjective.Enemy == null && attacker == null)) { // Replace the old objective with the new. ObjectiveManager.Objectives.Remove(combatObjective); ObjectiveManager.AddObjective(CreateCombatObjective()); } } else { if (delay > 0) { ObjectiveManager.AddObjective(CreateCombatObjective(), delay); } else { ObjectiveManager.AddObjective(CreateCombatObjective()); } } AIObjectiveCombat CreateCombatObjective() { var objective = new AIObjectiveCombat(Character, attacker, mode, objectiveManager) { HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman" || Character.CurrentHull == null && ObjectiveManager.IsCurrentOrder(), abortCondition = abortCondition, allowHoldFire = allowHoldFire, }; if (onAbort != null) { objective.Abandoned += onAbort; } return objective; } } public void SetOrder(Order order, string option, Character orderGiver, bool speak = true) { CurrentOrderOption = option; CurrentOrder = order; objectiveManager.SetOrder(order, option, orderGiver); if (ObjectiveManager.CurrentOrder != null && speak && Character.SpeechImpediment < 100.0f) { if (ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None()) { Character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets"); } else if (ObjectiveManager.CurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None()) { Character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries"); } else if (ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None()) { Character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire"); } else if (ObjectiveManager.CurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None()) { Character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks"); } else if (ObjectiveManager.CurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None()) { Character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies"); } else if (ObjectiveManager.CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None()) { Character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets"); } else if (ObjectiveManager.CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None()) { Character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps"); } else { Character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f); } } } public override void SelectTarget(AITarget target) { SelectedAiTarget = target; } public override void Reset() { base.Reset(); objectiveManager.SortObjectives(); sortTimer = sortObjectiveInterval; float waitDuration = characterWaitOnSwitch; if (ObjectiveManager.IsCurrentObjective()) { waitDuration *= 2; } ObjectiveManager.WaitTimer = waitDuration; } private void CheckCrouching(float deltaTime) { crouchRaycastTimer -= deltaTime; if (crouchRaycastTimer > 0.0f) { return; } crouchRaycastTimer = crouchRaycastInterval; //start the raycast in front of the character in the direction it's heading to Vector2 startPos = Character.SimPosition; startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f); //do a raycast upwards to find any walls float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f; shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null; } public bool AllowCampaignInteraction() { if (Character == null || Character.Removed || Character.IsIncapacitated) { return false; } switch (ObjectiveManager.CurrentObjective) { case AIObjectiveCombat _: case AIObjectiveFindSafety _: case AIObjectiveExtinguishFires _: case AIObjectiveFightIntruders _: case AIObjectiveFixLeaks _: return false; } return true; } private readonly HashSet unequippedItems = new HashSet(); public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false) { var pickable = item.GetComponent(); if (item.ParentInventory is ItemInventory itemInventory) { if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; } } if (equip) { int targetSlot = -1; //check if all the slots required by the item are free foreach (InvSlotType slots in pickable.AllowedSlots) { if (slots.HasFlag(InvSlotType.Any)) { continue; } for (int i = 0; i < targetInventory.Items.Length; i++) { if (targetInventory is CharacterInventory characterInventory) { //slot not needed by the item, continue if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; } } targetSlot = i; //slot free, continue var otherItem = targetInventory.Items[i]; if (otherItem == null) { continue; } //try to move the existing item to LimbSlot.Any and continue if successful if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot)) { if (storeUnequipped && targetInventory.Owner == Character) { unequippedItems.Add(otherItem); } continue; } if (dropOtherIfCannotMove) { //if everything else fails, simply drop the existing item otherItem.Drop(Character); } } } return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character); } else { return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot); } } public static bool NeedsDivingGear(Hull hull, out bool needsSuit) { needsSuit = false; if (hull == null || hull.WaterPercentage > 90 || hull.LethalPressure > 0 || hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f)) { needsSuit = true; return true; } if (hull.WaterPercentage > 60 || hull.Oxygen < CharacterHealth.LowOxygenThreshold) { return true; } return false; } public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage); /// /// Check whether the character has a diving suit in usable condition plus some oxygen. /// public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. /// public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true); private static List matchingItems = new List(); public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false) { matchingItems.Clear(); items = matchingItems; if (character == null) { return false; } if (character.Inventory == null) { return false; } matchingItems = character.Inventory.FindAllItems(i => i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier), recursive: true, matchingItems); items = matchingItems; return matchingItems.Any(i => i != null && i.ConditionPercentage >= conditionPercentage && (!requireEquipped || character.HasEquippedItem(i)) && (containedTag == null || (i.OwnInventory?.Items != null && i.OwnInventory.Items.Any(it => it != null && it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)))); } public static void ItemTaken(Item item, Character character) { if (item == null || character == null || item.GetComponent() != null) { return; } Character thief = character; bool someoneSpoke = false; if (item.SpawnedInOutpost && thief.TeamID != Character.TeamType.FriendlyNPC && !item.HasTag("handlocker")) { foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsDead || otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) || !otherHumanAI.VisibleHulls.Contains(thief.CurrentHull)) { continue; } //if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; } if (!otherCharacter.CanSeeCharacter(thief)) { 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 (character.Submarine != null) { var connectedHulls = character.Submarine.GetHulls(alsoFromConnectedSubs: true); if (item.HasTag("fireextinguisher") && connectedHulls.Any(h => h.FireSources.Any())) { continue; } if (item.HasTag("diving") && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; } } if (!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f) { if (!item.StolenDuringRound && 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.Value -= reputationLoss; } item.StolenDuringRound = true; otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f); someoneSpoke = true; } // React if we are security 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))) { if (TriggerSecurity(security.AIController as HumanAIController)) { // Only alert one guard at a time break; } } } } } else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost, true) is { } foundItem) { ItemTaken(foundItem, character); } bool TriggerSecurity(HumanAIController humanAI) { if (humanAI == null) { return false; } if (!humanAI.Character.IsSecurity) { return false; } if (humanAI.ObjectiveManager.IsCurrentObjective()) { return false; } humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(), abortCondition: () => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null, onAbort: () => { if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective()) { humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false) { BasePriority = 10 }); } }, allowHoldFire: true); return true; } } // 0.225 - 0.375 private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f); /// /// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threads. /// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders. /// There's now a cached value per each hull, which should prevent too frequent calculations. /// public static void PropagateHullSafety(Character character, Hull hull) { DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull)); } private void RefreshHullSafety(Hull hull) { if (GetHullSafety(hull, Character, VisibleHulls) > HULL_SAFETY_THRESHOLD) { UnsafeHulls.Remove(hull); } else { UnsafeHulls.Add(hull); } } public static void RefreshTargets(Character character, Order order, Hull hull) { switch (order.Identifier) { case "reportfire": AddTargets(character, hull); break; case "reportbreach": foreach (var gap in hull.ConnectedGaps) { if (AIObjectiveFixLeaks.IsValidTarget(gap, character)) { AddTargets(character, gap); } } break; case "reportbrokendevices": foreach (var item in Item.ItemList) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, character)) { if (item.Repairables.All(r => item.ConditionPercentage >= r.RepairThreshold)) { continue; } AddTargets(character, item); } } break; case "reportintruders": foreach (var enemy in Character.CharacterList) { if (enemy.CurrentHull != hull) { continue; } if (AIObjectiveFightIntruders.IsValidTarget(enemy, character)) { AddTargets(character, enemy); } } break; case "requestfirstaid": foreach (var c in Character.CharacterList) { if (c.CurrentHull != hull) { continue; } if (AIObjectiveRescueAll.IsValidTarget(c, character)) { AddTargets(character, c); } } break; default: #if DEBUG DebugConsole.ThrowError(order.Identifier + " not implemented!"); #endif break; } } private static bool AddTargets(Character caller, T2 target) where T1 : AIObjectiveLoop { bool targetAdded = false; DoForEachCrewMember(caller, humanAI => { var objective = humanAI.ObjectiveManager.GetObjective(); if (objective != null) { if (!targetAdded && objective.AddTarget(target)) { targetAdded = true; } } }); return targetAdded; } public static void RemoveTargets(Character caller, T2 target) where T1 : AIObjectiveLoop { DoForEachCrewMember(caller, humanAI => humanAI.ObjectiveManager.GetObjective()?.ReportedTargets.Remove(target)); } public float GetDamageDoneByAttacker(Character attacker) { if (!damageDoneByAttacker.TryGetValue(attacker, out float dmg)) { dmg = 0; } return dmg; } private void StoreHullSafety(Hull hull, HullSafety safety) { if (knownHulls.ContainsKey(hull)) { // Update existing. Shouldn't currently happen, but things might change. knownHulls[hull] = safety; } else { // Add new knownHulls.Add(hull, safety); } } private float CalculateHullSafety(Hull hull, Character character, IEnumerable visibleHulls = null) { bool isCurrentHull = character == Character && character.CurrentHull == hull; if (hull == null) { if (isCurrentHull) { CurrentHullSafety = 0; } return CurrentHullSafety; } if (isCurrentHull && visibleHulls == null) { // Use the cached visible hulls visibleHulls = VisibleHulls; } bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); bool ignoreWater = HasDivingSuit(character); bool ignoreOxygen = ignoreWater || HasDivingMask(character); bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders); float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) { CurrentHullSafety = safety; } return safety; } private static float CalculateHullSafety(Hull hull, IEnumerable visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) { if (hull == null) { return 0; } if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; } // TODO: take the visiblehulls into account? float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp(0.25f, 1, hull.OxygenPercentage / 100); float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, 0.25f, hull.WaterPercentage / 100); if (!character.NeedsAir) { oxygenFactor = 1; waterFactor = 1; } float fireFactor = 1; if (!ignoreFire) { float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X; // Even the smallest fire reduces the safety by 50% float fire = visibleHulls == null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h)); fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1)); } float enemyFactor = 1; if (!ignoreEnemies) { bool isValidTarget(Character e) => IsActive(e) && !IsFriendly(character, e); int enemyCount = visibleHulls == null ? Character.CharacterList.Count(e => isValidTarget(e) && e.CurrentHull == hull) : Character.CharacterList.Count(e => isValidTarget(e) && visibleHulls.Contains(e.CurrentHull)); // The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages) enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1)); } float dangerousItemsFactor = 1f; foreach (Item item in Item.ItemList) { if (item.CurrentHull != hull) { continue; } if (item.Prefab != null && item.Prefab.IsDangerous) { dangerousItemsFactor = 0; } } float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor; return MathHelper.Clamp(safety * 100, 0, 100); } public float GetHullSafety(Hull hull, Character character, IEnumerable visibleHulls = null) { if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety)) { hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls)); StoreHullSafety(hull, hullSafety); } else if (hullSafety.IsStale) { hullSafety.Reset(CalculateHullSafety(hull, character, visibleHulls)); } return hullSafety.safety; } public static float GetHullSafety(Hull hull, IEnumerable visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) { HullSafety hullSafety; if (character.AIController is HumanAIController controller) { if (!controller.knownHulls.TryGetValue(hull, out hullSafety)) { hullSafety = new HullSafety(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies)); controller.StoreHullSafety(hull, hullSafety); } else if (hullSafety.IsStale) { hullSafety.Reset(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies)); } } else { #if DEBUG DebugConsole.ThrowError("Cannot store the hull safety, because was unable to cast the AIController as HumanAIController. This should never happen!"); #endif return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); } return hullSafety.safety; } public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { bool sameTeam = me.TeamID == other.TeamID; bool friendlyTeam = IsOnFriendlyTeam(GameMain.GameSession?.GameMode, me, other); bool teamGood = sameTeam || friendlyTeam && !onlySameTeam; if (!teamGood) { return false; } bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); if (!speciesGood) { return false; } if (me.TeamID == Character.TeamType.FriendlyNPC && other.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) { var reputation = campaign.Map?.CurrentLocation?.Reputation; if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) { return false; } } return true; } private static bool IsOnFriendlyTeam(GameMode mode, Character me, Character other) { // Only enemies are in the Team "None" bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None; // When playing a combat mission, we need to be on the same team to be friendlies if (friendlyTeam && mode is MissionMode mm && mm.Mission is CombatMission) { friendlyTeam = me.TeamID == other.TeamID; } return friendlyTeam; } public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; public static bool IsTrueForAllCrewMembers(Character character, Func predicate) { if (character == null) { return false; } foreach (var c in Character.CharacterList) { if (FilterCrewMember(character, c)) { if (!predicate(c.AIController as HumanAIController)) { return false; } } } return true; } public static bool IsTrueForAnyCrewMember(Character character, Func predicate) { if (character == null) { return false; } foreach (var c in Character.CharacterList) { if (FilterCrewMember(character, c)) { if (predicate(c.AIController as HumanAIController)) { return true; } } } return false; } public static int CountCrew(Character character, Func predicate = null, bool onlyActive = true, bool onlyBots = false) { if (character == null) { return 0; } int count = 0; foreach (var other in Character.CharacterList) { if (onlyActive && !IsActive(other)) { continue; } if (onlyBots && other.IsPlayer) { continue; } if (FilterCrewMember(character, other)) { if (predicate == null || predicate(other.AIController as HumanAIController)) { count++; } } } return count; } public static void DoForEachCrewMember(Character character, Action action) { if (character == null) { return; } foreach (var c in Character.CharacterList) { if (FilterCrewMember(character, c)) { action(c.AIController as HumanAIController); } } } private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self); public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter) { operatingCharacter = null; if (target?.Item == null) { return false; } foreach (var c in Character.CharacterList) { if (character == null) { continue; } if (c == character) { continue; } if (c.IsDead || c.IsIncapacitated) { continue; } if (c.SelectedConstruction != target.Item) { continue; } if (!IsFriendly(character, c, onlySameTeam: true)) { continue; } operatingCharacter = c; // If the other character is player, don't try to operate if (c.IsPlayer) { return true; } if (c.AIController is HumanAIController controllingHumanAi) { Item otherTarget = controllingHumanAi.objectiveManager.GetActiveObjective()?.Component.Item ?? c.SelectedConstruction; if (otherTarget != target.Item) { continue; } // If the other character is ordered to operate the item, let him do it if (controllingHumanAi.ObjectiveManager.IsCurrentOrder()) { return true; } else { if (character == null) { return true; } else if (target is Steering) { // Steering is hard-coded -> cannot use the required skills collection defined in the xml return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"); } else { return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c); } } } else { // Shouldn't go here, unless we allow non-humans to operate items return false; } } return false; } #region Wrappers public bool IsFriendly(Character other) => IsFriendly(Character, other); public void DoForEachCrewMember(Action action) => DoForEachCrewMember(Character, action); public bool IsTrueForAnyCrewMember(Func predicate) => IsTrueForAnyCrewMember(Character, predicate); public bool IsTrueForAllCrewMembers(Func predicate) => IsTrueForAllCrewMembers(Character, predicate); public int CountCrew(Func predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots); public bool IsItemOperatedByAnother(ItemComponent target, out Character operatingCharacter) => IsItemOperatedByAnother(Character, target, out operatingCharacter); #endregion } }