Client LuaCsForBarotrauma
AIObjectiveRescue.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
8 
9 namespace Barotrauma
10 {
12  {
13  public override Identifier Identifier { get; set; } = "rescue".ToIdentifier();
14  public override bool ForceRun => true;
15  public override bool KeepDivingGearOn => true;
16  protected override bool AllowOutsideSubmarine => true;
17  protected override bool AllowInAnySub => true;
18  protected override bool AllowWhileHandcuffed => false;
19 
20  const float TreatmentDelay = 0.5f;
21 
22  const float CloseEnoughToTreat = 100.0f;
23 
24  public readonly Character Target;
25 
26  private AIObjectiveGoTo goToObjective;
27  private AIObjectiveContainItem replaceOxygenObjective;
28  private AIObjectiveGetItem getItemObjective;
29  private float treatmentTimer;
30  private Hull safeHull;
31  private float findHullTimer;
32  private bool ignoreOxygen;
33  private readonly float findHullInterval = 1.0f;
34  private bool performedCpr;
35 
36  public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
37  : base(character, objectiveManager, priorityModifier)
38  {
39  if (targetCharacter == null)
40  {
41  string errorMsg = $"Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace.CleanupStackTrace();
42  DebugConsole.ThrowError(character.Name + ": " + errorMsg);
43  GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
44  Abandon = true;
45  return;
46  }
47  Target = targetCharacter;
48  }
49 
50  protected override void OnAbandon()
51  {
53  base.OnAbandon();
54  }
55 
56  protected override void OnCompleted()
57  {
59  base.OnCompleted();
60  }
61 
62  protected override void Act(float deltaTime)
63  {
64  if (Target == null || Target.Removed || Target.IsDead)
65  {
66  Abandon = true;
67  return;
68  }
69  var otherRescuer = Target.SelectedBy;
70  if (otherRescuer != null && otherRescuer != character)
71  {
72  // Someone else is rescuing/holding the target.
73  Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical");
74  return;
75  }
76  if (Target != character)
77  {
79  {
80  // Check if the character needs more oxygen
82  {
83  // Replace empty oxygen and welding fuel.
84  if (HumanAIController.HasItem(Target, Tags.HeavyDivingGear, out IEnumerable<Item> suits, requireEquipped: true))
85  {
86  Item suit = suits.FirstOrDefault();
87  if (suit != null)
88  {
90  AIController.UnequipContainedItems(character, suit, it => it.HasTag(Tags.WeldingFuel));
91  }
92  }
93  else if (HumanAIController.HasItem(Target, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
94  {
95  Item mask = masks.FirstOrDefault();
96  if (mask != null)
97  {
99  AIController.UnequipContainedItems(character, mask, it => it.HasTag(Tags.WeldingFuel));
100  }
101  }
103  if (ShouldRemoveDivingSuit())
104  {
105  suits.ForEach(suit => suit.Drop(character));
106  }
107  else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(Tags.OxygenSource) && it.ConditionPercentage > 0)))
108  {
109  // The target has a suit equipped with an empty oxygen tank.
110  // Can't remove the suit, because the target needs it.
111  // If we happen to have an extra oxygen tank in the inventory, let's swap it.
112  Item spareOxygenTank = FindOxygenTank(Target) ?? FindOxygenTank(character);
113  if (spareOxygenTank != null)
114  {
115  Item suit = suits.FirstOrDefault();
116  if (suit != null)
117  {
118  // Insert the new oxygen tank
119  TryAddSubObjective(ref replaceOxygenObjective, () => new AIObjectiveContainItem(character, spareOxygenTank, suit.GetComponent<ItemContainer>(), objectiveManager),
120  onCompleted: () => RemoveSubObjective(ref replaceOxygenObjective),
121  onAbandon: () =>
122  {
123  RemoveSubObjective(ref replaceOxygenObjective);
124  ignoreOxygen = true;
125  if (ShouldRemoveDivingSuit())
126  {
127  suits.ForEach(suit => suit.Drop(character));
128  }
129  });
130  return;
131  }
132  }
133 
134  Item FindOxygenTank(Character c) =>
135  c.Inventory.FindItem(i =>
136  i.HasTag(Tags.OxygenSource) &&
137  i.ConditionPercentage > 1 &&
138  i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag(Tags.DivingGear)) == null,
139  recursive: true);
140  }
141  }
142  if (character.Submarine != null && Target.CurrentHull != null)
143  {
145  {
146  // Incapacitated target is not in a safe place -> Move to a safe place first
148  {
150  {
151  character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget",
152  ("[targetname]", Target.Name, FormatCapitals.No),
153  ("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
154  null, 1.0f, $"foundunconscioustarget{Target.Name}".ToIdentifier(), 60.0f);
155  }
156  // Go to the target and select it
158  {
159  RemoveSubObjective(ref replaceOxygenObjective);
160  RemoveSubObjective(ref goToObjective);
161  TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
162  {
163  CloseEnough = CloseEnoughToTreat,
164  DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
165  TargetName = Target.DisplayName
166  },
167  onCompleted: () => RemoveSubObjective(ref goToObjective),
168  onAbandon: () =>
169  {
170  RemoveSubObjective(ref goToObjective);
171  Abandon = true;
172  });
173  }
174  else
175  {
177  }
178  }
179  else
180  {
181  // Drag the character into safety
182  if (safeHull == null)
183  {
184  if (findHullTimer > 0)
185  {
186  findHullTimer -= deltaTime;
187  }
188  else
189  {
190  HullSearchStatus hullSearchStatus = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls);
191  if (hullSearchStatus != HullSearchStatus.Finished) { return; }
192  safeHull = potentialSafeHull;
193  findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
194  }
195  }
196  if (safeHull != null && character.CurrentHull != safeHull)
197  {
198  RemoveSubObjective(ref replaceOxygenObjective);
199  RemoveSubObjective(ref goToObjective);
200  TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
201  onCompleted: () => RemoveSubObjective(ref goToObjective),
202  onAbandon: () =>
203  {
204  RemoveSubObjective(ref goToObjective);
205  safeHull = character.CurrentHull;
206  });
207  }
208  }
209  }
210  }
211  }
212  }
213 
214  if (subObjectives.Any()) { return; }
215 
217  {
218  RemoveSubObjective(ref replaceOxygenObjective);
219  RemoveSubObjective(ref goToObjective);
220  // Go to the target and select it
221  TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
222  {
223  CloseEnough = CloseEnoughToTreat,
224  DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
225  TargetName = Target.DisplayName
226  },
227  onCompleted: () => RemoveSubObjective(ref goToObjective),
228  onAbandon: () =>
229  {
230  RemoveSubObjective(ref goToObjective);
231  Abandon = true;
232  });
233  }
234  else
235  {
236  // We can start applying treatment
238  {
239  if (Target.CurrentHull?.DisplayName != null)
240  {
241  character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget",
242  ("[targetname]", Target.Name, FormatCapitals.No),
243  ("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
244  null, 1.0f, $"foundwoundedtarget{Target.Name}".ToIdentifier(), 60.0f);
245  }
246  }
247  GiveTreatment(deltaTime);
248  }
249  }
250 
251  private readonly List<Identifier> suitableItemIdentifiers = new List<Identifier>();
252  private readonly List<LocalizedString> itemNameList = new List<LocalizedString>();
253  private readonly Dictionary<Identifier, float> currentTreatmentSuitabilities = new Dictionary<Identifier, float>();
254  private void GiveTreatment(float deltaTime)
255  {
256  if (Target == null)
257  {
258  string errorMsg = $"{character.Name}: Attempted to update a Rescue objective with no target!";
259  DebugConsole.ThrowError(errorMsg);
260  Abandon = true;
261  return;
262  }
263 
265 
266  if (!Target.IsPlayer)
267  {
268  // If the target is a bot, don't let it move
270  }
271  if (treatmentTimer > 0.0f)
272  {
273  treatmentTimer -= deltaTime;
274  return;
275  }
276  treatmentTimer = TreatmentDelay;
277 
278  float cprSuitability = Target.Oxygen < 0.0f ? -Target.Oxygen * 100.0f : 0.0f;
279 
280  float bestSuitability = 0.0f;
281  Item bestItem = null;
282  Affliction afflictionToTreat = null;
283  foreach (Affliction affliction in GetSortedAfflictions(Target))
284  {
285  //find which treatments are the most suitable to treat the character's current condition
287  currentTreatmentSuitabilities,
288  limb: Target.CharacterHealth.GetAfflictionLimb(affliction),
289  user: character,
290  predictFutureDuration: 10.0f);
291 
292  foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
293  {
294  float thisSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key];
295  if (thisSuitability <= 0) { continue; }
296 
297  Item matchingItem = FindMedicalItem(character.Inventory, treatmentSuitability.Key);
298  //allow taking items from the target's inventory too if the target is unconscious
299  if (matchingItem == null && Target.IsIncapacitated)
300  {
301  matchingItem = FindMedicalItem(Target.Inventory, treatmentSuitability.Key);
302  }
303  if (matchingItem == null) { continue; }
304 
305  //also check how suitable the treatment is for the specific affliction we're now checking
306  //we don't want to e.g. give fentanyl for oxygen low just because the character has burns on other limbs
307  //that would also be healed by it!
308  float suitabilityForThisAffliction = affliction.Prefab.GetTreatmentSuitability(matchingItem);
309  float totalSuitability = thisSuitability * suitabilityForThisAffliction;
310  if (matchingItem != null && totalSuitability > bestSuitability)
311  {
312  bestItem = matchingItem;
313  afflictionToTreat = affliction;
314  bestSuitability = totalSuitability;
315  }
316  }
317  }
318 
319  if (bestItem != null && bestSuitability > cprSuitability)
320  {
322  ApplyTreatment(afflictionToTreat, bestItem);
323  //wait a bit longer after applying a treatment to wait for potential side-effects to manifest
324  treatmentTimer = TreatmentDelay * 4;
325  return;
326  }
327 
328  // Find treatments outside of own inventory only if inside the own sub.
330  {
331  //get "overall" suitability for no specific limb at this point
333  currentTreatmentSuitabilities, user: character, predictFutureDuration: 10.0f);
334  //didn't have any suitable treatments available, try to find some medical items
335  if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
336  {
337  itemNameList.Clear();
338  suitableItemIdentifiers.Clear();
339  foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities.OrderByDescending(s => s.Value))
340  {
341  if (treatmentSuitability.Value <= cprSuitability) { continue; }
342  if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab))
343  {
344  if (Item.ItemList.None(it => it.Prefab.Identifier == treatmentSuitability.Key)) { continue; }
345  suitableItemIdentifiers.Add(itemPrefab.Identifier);
346  //only list the first 4 items
347  if (itemNameList.Count < 4)
348  {
349  itemNameList.Add(itemPrefab.Name);
350  }
351  }
352  }
353  if (itemNameList.Any())
354  {
355  LocalizedString itemListStr = "";
356  if (itemNameList.Count == 1)
357  {
358  itemListStr = itemNameList[0];
359  }
360  else if (itemNameList.Count == 2)
361  {
362  //[treatment1] or [treatment2]
363  itemListStr = TextManager.GetWithVariables(
364  "DialogRequiredTreatmentOptionsLast",
365  ("[treatment1]", itemNameList[0]),
366  ("[treatment2]", itemNameList[1]));
367  }
368  else
369  {
370  //[treatment1], [treatment2], [treatment3] ... or [treatmentx]
371  itemListStr = TextManager.GetWithVariables(
372  "DialogRequiredTreatmentOptionsFirst",
373  ("[treatment1]", itemNameList[0]),
374  ("[treatment2]", itemNameList[1]));
375  for (int i = 2; i < itemNameList.Count - 1; i++)
376  {
377  itemListStr = TextManager.GetWithVariables(
378  "DialogRequiredTreatmentOptionsFirst",
379  ("[treatment1]", itemListStr),
380  ("[treatment2]", itemNameList[i]));
381  }
382  itemListStr = TextManager.GetWithVariables(
383  "DialogRequiredTreatmentOptionsLast",
384  ("[treatment1]", itemListStr),
385  ("[treatment2]", itemNameList.Last()));
386  }
388  {
389  character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments",
390  ("[targetname]", Target.Name, FormatCapitals.No),
391  ("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value,
392  null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f);
393  }
394  RemoveSubObjective(ref getItemObjective);
395  TryAddSubObjective(ref getItemObjective,
396  constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
397  onCompleted: () => RemoveSubObjective(ref getItemObjective),
398  onAbandon: () =>
399  {
400  Abandon = true;
402  {
403  SpeakCannotTreat();
404  }
405  });
406  }
407  else if (cprSuitability <= 0)
408  {
409  Abandon = true;
410  SpeakCannotTreat();
411  }
412  }
413  }
414  else if (!Target.IsUnconscious)
415  {
416  Abandon = true;
417  //no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
418  SpeakCannotTreat();
419  return;
420  }
421  if (character != Target)
422  {
423  if (cprSuitability > 0.0f)
424  {
426  character.AnimController.Anim = AnimController.Animation.CPR;
427  performedCpr = true;
428  }
429  else
430  {
432  }
433  }
434  }
435 
436  public static Item FindMedicalItem(Inventory inventory, Identifier itemIdentifier)
437  {
438  return FindMedicalItem(inventory, it => it.Prefab.Identifier == itemIdentifier);
439  }
440 
441  public static Item FindMedicalItem(Inventory inventory, Func<Item, bool> predicate)
442  {
443  if (inventory == null) { return null; }
444  //prefer items not in a container
445  Item match = inventory.FindItem(predicate, recursive: false);
446  if (match != null) { return match; }
447 
448  //start from the inventories with most slots
449  //= prefer taking items from things like toolbelts or doctor's uniforms, as opposed to e.g. autoinjectors which tend to have one or two slots
450  foreach (var potentialContainer in inventory.AllItems.OrderByDescending(it => it.OwnInventory?.Capacity ?? -1))
451  {
452  match = potentialContainer.OwnInventory?.FindItem(predicate, recursive: true);
453  if (match != null) { return match; }
454  }
455  return null;
456  }
457 
458  private void SpeakCannotTreat()
459  {
460  LocalizedString msg = character == Target ?
461  TextManager.Get("dialogcannottreatself") :
462  TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", Target.DisplayName, FormatCapitals.No);
463  character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
464  }
465 
466  private void ApplyTreatment(Affliction affliction, Item item)
467  {
468  item.ApplyTreatment(character, Target, Target.CharacterHealth.GetAfflictionLimb(affliction));
469  }
470 
471  protected override bool CheckObjectiveSpecific()
472  {
474  if (isCompleted && Target != character && character.IsOnPlayerTeam)
475  {
476  string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed";
477  string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value;
478  character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
479  }
480  return isCompleted;
481  }
482 
483  protected override float GetPriority()
484  {
485  if (Target == null) { Abandon = true; }
486  if (!IsAllowed) { HandleDisallowed(); }
487  if (Abandon)
488  {
489  return Priority;
490  }
491  if (character.CurrentHull != null)
492  {
494  {
495  // Don't go into rooms that have enemies
496  Priority = 0;
497  Abandon = true;
498  return Priority;
499  }
500  }
501  float horizontalDistance = Math.Abs(character.WorldPosition.X - Target.WorldPosition.X);
502  float verticalDistance = Math.Abs(character.WorldPosition.Y - Target.WorldPosition.Y);
503  if (character.Submarine?.Info is { IsRuin: false })
504  {
505  verticalDistance *= 2;
506  }
507  float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
509  {
510  distanceFactor = 1;
511  }
512  float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(Target) / 100;
513  float devotion = CumulatedDevotion / 100;
514  Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
515  return Priority;
516  }
517 
518  public static IEnumerable<Affliction> GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs);
519 
520  public override void Reset()
521  {
522  base.Reset();
523  goToObjective = null;
524  getItemObjective = null;
525  replaceOxygenObjective = null;
526  safeHull = null;
527  ignoreOxygen = false;
529  }
530 
531  public override void OnDeselected()
532  {
533  base.OnDeselected();
535  }
536  }
537 }
void UnequipContainedItems(Item parentItem, Func< Item, bool > predicate=null, bool avoidDroppingInSea=true, int? unequipMax=null)
void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea=true)
IEnumerable< Hull > VisibleHulls
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
float Priority
Final priority value after all calculations.
const float EmergencyObjectivePriority
Priority of objectives such as finding safety, rescuing someone in a critical state or defending agai...
static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
static float GetVitalityFactor(Character character)
override void Act(float deltaTime)
override Identifier Identifier
static IEnumerable< Affliction > GetSortedAfflictions(Character character, bool excludeBuffs=true)
static Item FindMedicalItem(Inventory inventory, Func< Item, bool > predicate)
static Item FindMedicalItem(Inventory inventory, Identifier itemIdentifier)
AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier=1)
override bool CheckObjectiveSpecific()
Should return whether the objective is completed or not.
static IEnumerable< Affliction > SortAfflictionsBySeverity(IEnumerable< Affliction > afflictions, bool excludeBuffs=true)
Automatically filters out buffs.
void GetSuitableTreatments(Dictionary< Identifier, float > treatmentSuitability, Character user, Limb limb=null, bool ignoreHiddenAfflictions=false, float predictFutureDuration=0.0f)
Get the identifiers of the items that can be used to treat the character. Takes into account all the ...
float GetSkillLevel(string skillIdentifier)
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
bool CanInteractWith(Character c, float maxDist=200.0f, bool checkVisibility=true, bool skipDistanceCheck=false)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable< Item > items, Identifier containedTag=default, float conditionPercentage=0, bool requireEquipped=false, bool recursive=true, Func< Item, bool > predicate=null)
Note: uses a single list for matching items. The item is reused each time when the method is called....
static bool IsActive(Character c)
float GetHullSafety(Hull hull, Character character, IEnumerable< Hull > visibleHulls=null)
static bool IsFriendly(Character me, Character other, bool onlySameTeam=false)
Item FindItem(Func< Item, bool > predicate, bool recursive)
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)