using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace Barotrauma { class AIObjectiveRescue : AIObjective { public override string DebugTag => "rescue"; public override bool ForceRun => true; const float TreatmentDelay = 0.5f; private readonly Character targetCharacter; private AIObjectiveGoTo goToObjective; private float treatmentTimer; public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { this.targetCharacter = targetCharacter; } public override bool IsDuplicate(AIObjective otherObjective) { AIObjectiveRescue rescueObjective = otherObjective as AIObjectiveRescue; return rescueObjective != null && rescueObjective.targetCharacter == targetCharacter; } protected override void Act(float deltaTime) { // Unconcious target is not in a safe place -> Move to a safe place first if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) { if (character.SelectedCharacter != targetCharacter) { character.Speak(TextManager.Get("DialogFoundUnconsciousTarget") .Replace("[targetname]", targetCharacter.Name).Replace("[roomname]", character.CurrentHull.DisplayName), null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); // Go to the target and select it if (!character.CanInteractWith(targetCharacter)) { if (goToObjective != null && goToObjective.Target != targetCharacter) { goToObjective = null; } TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)); } else { character.SelectCharacter(targetCharacter); } } else { // Drag the character into safety if (goToObjective != null && goToObjective.Target == targetCharacter) { goToObjective = null; } var findSafety = objectiveManager.GetObjective(); if (findSafety == null) { // Ensure that we have the find safety objective (should always be the case) objectiveManager.AddObjective(new AIObjectiveFindSafety(character, objectiveManager)); } TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(findSafety.FindBestHull(new Hull[] { character.CurrentHull }), character, objectiveManager)); } return; } if (subObjectives.Any()) { return; } if (!character.CanInteractWith(targetCharacter)) { // Go to the target and select it TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)); } else { // We can start applying treatment if (character.SelectedCharacter != targetCharacter) { character.Speak(TextManager.Get("DialogFoundWoundedTarget") .Replace("[targetname]", targetCharacter.Name).Replace("[roomname]", character.CurrentHull.DisplayName), null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f); character.SelectCharacter(targetCharacter); } GiveTreatment(deltaTime); } } // TODO: consider optimizing a bit private void GiveTreatment(float deltaTime) { if (treatmentTimer > 0.0f) { treatmentTimer -= deltaTime; } treatmentTimer = TreatmentDelay; var allAfflictions = targetCharacter.CharacterHealth.GetAllAfflictions() .Where(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth) > 0) .ToList(); allAfflictions.Sort((a1, a2) => { return Math.Sign(a2.GetVitalityDecrease(targetCharacter.CharacterHealth) - a1.GetVitalityDecrease(targetCharacter.CharacterHealth)); }); //check if we already have a suitable treatment for any of the afflictions foreach (Affliction affliction in allAfflictions) { foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) { if (treatmentSuitability.Value > 0.0f) { Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key); if (matchingItem == null) { continue; } ApplyTreatment(affliction, matchingItem); return; } } } //didn't have any suitable treatments available, try to find some medical items HashSet suitableItemIdentifiers = new HashSet(); foreach (Affliction affliction in allAfflictions) { foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) { if (treatmentSuitability.Value > 0.0f) { suitableItemIdentifiers.Add(treatmentSuitability.Key); } } } if (suitableItemIdentifiers.Count > 0) { List itemNameList = new List(); foreach (string itemIdentifier in suitableItemIdentifiers) { if (MapEntityPrefab.Find(null, itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab) { itemNameList.Add(itemPrefab.Name); } //only list the first 4 items if (itemNameList.Count >= 4) { break; } } if (itemNameList.Count > 0) { string itemListStr = ""; if (itemNameList.Count == 1) { itemListStr = itemNameList[0]; } else { itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last()); } character.Speak(TextManager.Get("DialogListRequiredTreatments") .Replace("[targetname]", targetCharacter.Name) .Replace("[treatmentlist]", itemListStr), null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f); } character.DeselectCharacter(); AddSubObjective(new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true)); } character.AnimController.Anim = AnimController.Animation.CPR; } private void ApplyTreatment(Affliction affliction, Item item) { var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction); bool remove = false; foreach (ItemComponent ic in item.Components) { if (!ic.HasRequiredContainedItems(addMessage: false)) { continue; } #if CLIENT ic.PlaySound(ActionType.OnUse, character.WorldPosition, character); #endif ic.WasUsed = true; ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb); if (ic.DeleteOnUse) { remove = true; } } if (remove) { Entity.Spawner?.AddToRemoveQueue(item); } } public override bool IsCompleted() { bool isCompleted = targetCharacter.Vitality / targetCharacter.MaxVitality > AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager); if (isCompleted) { character.Speak(TextManager.Get("DialogTargetHealed").Replace("[targetname]", targetCharacter.Name), null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f); } return isCompleted || targetCharacter.Removed || targetCharacter.IsDead; } public override float GetPriority() { if (targetCharacter == null) { return 0; } if (targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) { abandon = true; return 0; } // Don't go into rooms that have enemies if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(c))) { abandon = true; return 0; } // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f; float distanceFactor = MathHelper.Lerp(1, 0.5f, MathUtils.InverseLerp(0, 10000, dist)); float vitalityFactor = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter); float devotion = Math.Min(Priority, 10) / 100; return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + vitalityFactor * distanceFactor, 0, 1)); } } }