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(Tags.MedicalSkill) < otherRescuer.GetSkillLevel(Tags.MedicalSkill);
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 
395  var itemsToFind = currentTreatmentSuitabilities
396  //items that have a positive effect and that the bot doesn't yet have
397  .Where(kvp => kvp.Value > 0.0f && character.Inventory.AllItems.None(it => it.Prefab.Identifier == kvp.Key))
398  .Select(kvp => kvp.Key);
399 
400  RemoveSubObjective(ref getItemObjective);
401  TryAddSubObjective(ref getItemObjective,
402  constructor: () => new AIObjectiveGetItem(character, itemsToFind, objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
403  {
404  GetItemPriority = it => currentTreatmentSuitabilities.GetValueOrDefault(it.Prefab.Identifier)
405  },
406  onCompleted: () => RemoveSubObjective(ref getItemObjective),
407  onAbandon: () =>
408  {
409  Abandon = true;
411  {
412  SpeakCannotTreat();
413  }
414  });
415  }
416  else if (cprSuitability <= 0)
417  {
418  Abandon = true;
419  SpeakCannotTreat();
420  }
421  }
422  }
423  else if (!Target.IsUnconscious)
424  {
425  Abandon = true;
426  //no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
427  SpeakCannotTreat();
428  return;
429  }
430  if (character != Target)
431  {
432  if (cprSuitability > 0.0f)
433  {
435  character.AnimController.Anim = AnimController.Animation.CPR;
436  performedCpr = true;
437  }
438  else
439  {
441  }
442  }
443  }
444 
445  public static Item FindMedicalItem(Inventory inventory, Identifier itemIdentifier)
446  {
447  return FindMedicalItem(inventory, it => it.Prefab.Identifier == itemIdentifier);
448  }
449 
450  public static Item FindMedicalItem(Inventory inventory, Func<Item, bool> predicate)
451  {
452  if (inventory == null) { return null; }
453  //prefer items not in a container
454  Item match = inventory.FindItem(predicate, recursive: false);
455  if (match != null) { return match; }
456 
457  //start from the inventories with most slots
458  //= 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
459  foreach (var potentialContainer in inventory.AllItems.OrderByDescending(it => it.OwnInventory?.Capacity ?? -1))
460  {
461  match = potentialContainer.OwnInventory?.FindItem(predicate, recursive: true);
462  if (match != null) { return match; }
463  }
464  return null;
465  }
466 
467  private void SpeakCannotTreat()
468  {
469  LocalizedString msg = character == Target ?
470  TextManager.Get("dialogcannottreatself") :
471  TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", Target.DisplayName, FormatCapitals.No);
472  character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
473  }
474 
475  private void ApplyTreatment(Affliction affliction, Item item)
476  {
477  item.ApplyTreatment(character, Target, Target.CharacterHealth.GetAfflictionLimb(affliction));
478  }
479 
480  protected override bool CheckObjectiveState()
481  {
484  {
485  string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed";
486  string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value;
487  character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
488  }
489  return IsCompleted;
490  }
491 
492  protected override float GetPriority()
493  {
494  if (Target == null) { Abandon = true; }
495  if (!IsAllowed) { HandleDisallowed(); }
496  if (Abandon)
497  {
498  return Priority;
499  }
500  if (character.CurrentHull != null)
501  {
503  {
504  // Don't go into rooms that have enemies
505  Priority = 0;
506  Abandon = true;
507  return Priority;
508  }
509  }
510  float horizontalDistance = Math.Abs(character.WorldPosition.X - Target.WorldPosition.X);
511  float verticalDistance = Math.Abs(character.WorldPosition.Y - Target.WorldPosition.Y);
512  if (character.Submarine?.Info is { IsRuin: false })
513  {
514  verticalDistance *= 2;
515  }
516  float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
518  {
519  distanceFactor = 1;
520  }
521  float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(Target) / 100;
522  float devotion = CumulatedDevotion / 100;
523  Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
524  return Priority;
525  }
526 
527  public static IEnumerable<Affliction> GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs);
528 
529  public override void Reset()
530  {
531  base.Reset();
532  goToObjective = null;
533  getItemObjective = null;
534  replaceOxygenObjective = null;
535  safeHull = null;
536  ignoreOxygen = false;
538  }
539 
540  public override void OnDeselected()
541  {
542  base.OnDeselected();
544  }
545  }
546 }
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
override bool CheckObjectiveState()
Should return whether the objective is completed or not.
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)
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 ...
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)
float GetSkillLevel(Identifier skillIdentifier)
Get the character's current skill level, taking into account any temporary boosts from wearables and ...
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)
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)