Client LuaCsForBarotrauma
AIObjectiveFindDivingGear.cs
3 using System.Collections.Generic;
4 using System.Linq;
5 
6 namespace Barotrauma
7 {
9  {
10  public override Identifier Identifier { get; set; } = "find diving gear".ToIdentifier();
11  public override string DebugTag => $"{Identifier} ({gearTag})";
12  public override bool ForceRun => true;
13  public override bool KeepDivingGearOn => true;
14  public override bool AbandonWhenCannotCompleteSubObjectives => false;
15  protected override bool AllowWhileHandcuffed => false;
16 
17  private readonly Identifier gearTag;
18 
19  private AIObjectiveGetItem getDivingGear;
20  private AIObjectiveContainItem getOxygen;
21  private Item targetItem;
22  private int? oxygenSourceSlotIndex;
23 
24  public const float MIN_OXYGEN = 10;
25 
26  protected override bool CheckObjectiveState() =>
27  targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
28 
29  public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
30  {
31  gearTag = needsDivingSuit ? Tags.HeavyDivingGear : Tags.LightDivingGear;
32  }
33 
34  protected override void Act(float deltaTime)
35  {
36  TrySetTargetItem(character.Inventory.FindItem(it => it.HasTag(gearTag) && IsSuitablePressureProtection(it, gearTag, character), true));
37  if (targetItem == null && gearTag == Tags.LightDivingGear)
38  {
39  TrySetTargetItem(character.Inventory.FindItem(
40  it => it.HasTag(Tags.HeavyDivingGear) && IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true));
41  }
42 
43  bool findDivingGear = targetItem == null ||
44  (!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && targetItem.ContainedItems.Any(IsSuitableContainedOxygenSource));
45 
46  if (findDivingGear)
47  {
48  bool mustFindMorePressureProtection = !objectiveManager.FailedToFindDivingGearForDepth &&
49  character.Inventory.FindItem(it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null;
50 
51  if (gearTag == Tags.LightDivingGear)
52  {
53  if (character.GetEquippedItem(Tags.HeavyDivingGear, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes) is Item divingSuit && divingSuit.ContainedItems.None(IsSuitableContainedOxygenSource))
54  {
55  // A special case: we are already wearing a suit without enough oxygen, but seeking for a mask, because a suit is not really needed.
56  // This would result into wearing boh the mask and the suit (because the suit shouldn't be unequipped in this situation), which is a bit weird and also suboptimal, because the mask uses the oxygen 2x faster.
57  // So, let's target the diving suit and try to find oxygen instead.
58  targetItem = divingSuit;
59  findDivingGear = false;
60  }
61  }
62  if (findDivingGear)
63  {
64  TryAddSubObjective(ref getDivingGear, () =>
65  {
66  if (targetItem == null && character.IsOnPlayerTeam)
67  {
68  character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
69  }
70  var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
71  {
73  AllowToFindDivingGear = false,
74  AllowDangerousPressure = true,
75  EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
76  Wear = true
77  };
78  if (gearTag == Tags.HeavyDivingGear)
79  {
80  if (mustFindMorePressureProtection)
81  {
82  //if we're looking for a suit specifically because the current suit isn't enough,
83  //let's ignore unsuitable suits altogether...
84  getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character);
85  }
86  else
87  {
88  //...Otherwise it's fine to give a very small priority
89  //to inadequate suits (a suit not adequate for the depth is better than no suit)
90  getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f;
91  }
92  getItemObjective.GetItemPriority = it =>
93  {
94  if (IsSuitablePressureProtection(it, gearTag, character))
95  {
96  return 1000.0f;
97  }
98  else
99  {
100  //if we're looking for a suit specifically because the current suit isn't enough,
101  //let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority
102  //to inadequate suits (a suit not adequate for the depth is better than no suit)
103  return mustFindMorePressureProtection ? 0.0f : 1.0f;
104  }
105  };
106  }
107  return getItemObjective;
108  },
109  onAbandon: () =>
110  {
111  if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; }
112  Abandon = true;
113  },
114  onCompleted: () =>
115  {
116  RemoveSubObjective(ref getDivingGear);
117  if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
118  {
119  foreach (Item mask in masks)
120  {
121  if (mask != targetItem)
122  {
124  }
125  }
126  }
127  });
128  }
129  }
130  if (!findDivingGear)
131  {
132  float min = GetMinOxygen(character);
133  if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(IsSuitableContainedOxygenSource))
134  {
135  TryAddSubObjective(ref getOxygen, () =>
136  {
137  if (character.IsOnPlayerTeam)
138  {
139  if (HumanAIController.HasItem(character, Tags.OxygenSource, out _, conditionPercentage: min))
140  {
141  character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f);
142  if (character.Inventory.FindAllItems(i => i.HasTag(Tags.OxygenSource) && i.Condition > min, recursive: true).Count == 1)
143  {
144  character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f);
145  }
146  }
147  else
148  {
149  character.Speak(TextManager.Get("DialogGetOxygenTank").Value, null, 0, "getoxygentank".ToIdentifier(), 30.0f);
150  }
151  }
152  var container = targetItem.GetComponent<ItemContainer>();
153  var objective = new AIObjectiveContainItem(character, Tags.OxygenSource, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
154  {
155  AllowToFindDivingGear = false,
156  AllowDangerousPressure = true,
157  ConditionLevel = MIN_OXYGEN,
158  RemoveExistingWhenNecessary = true,
159  TargetSlot = oxygenSourceSlotIndex
160  };
161  if (container.HasSubContainers)
162  {
163  objective.TargetSlot = container.FindSuitableSubContainerIndex(Tags.OxygenSource);
164  }
165  // Only remove the oxygen source being replaced
166  objective.RemoveExistingPredicate = i => objective.IsInTargetSlot(i);
167  return objective;
168  },
169  onAbandon: () =>
170  {
171  getOxygen = null;
172  int remainingTanks = ReportOxygenTankCount();
173  // Try to seek any oxygen sources, even if they have minimal amount of oxygen.
174  TryAddSubObjective(ref getOxygen, () =>
175  {
176  return new AIObjectiveContainItem(character, Tags.OxygenSource, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
177  {
178  AllowToFindDivingGear = false,
179  AllowDangerousPressure = true,
180  RemoveExisting = true
181  };
182  },
183  onAbandon: () =>
184  {
185  Abandon = true;
186  if (remainingTanks > 0 && !HumanAIController.HasItem(character, Tags.OxygenSource, out _, conditionPercentage: 0.01f))
187  {
188  character.Speak(TextManager.Get("dialogcantfindtoxygen").Value, null, 0, "cantfindoxygen".ToIdentifier(), 30.0f);
189  }
190  },
191  onCompleted: () => RemoveSubObjective(ref getOxygen));
192  },
193  onCompleted: () =>
194  {
195  RemoveSubObjective(ref getOxygen);
196  ReportOxygenTankCount();
197  });
198 
199  int ReportOxygenTankCount()
200  {
201  if (character.Submarine != Submarine.MainSub) { return 1; }
202  int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1);
203  if (remainingOxygenTanks == 0)
204  {
205  character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f);
206  }
207  else if (remainingOxygenTanks < 10)
208  {
209  character.Speak(TextManager.Get("DialogLowOnOxygenTanks").Value, null, 0.0f, "lowonoxygentanks".ToIdentifier(), 30.0f);
210  }
211  return remainingOxygenTanks;
212  }
213  }
214  }
215  }
216 
217  public static bool IsSuitablePressureProtection(Item item, Identifier tag, Character character)
218  {
219  if (tag == Tags.HeavyDivingGear)
220  {
221  float realWorldDepth = Level.Loaded?.GetRealWorldDepth(character.WorldPosition.Y) ?? 0.0f;
222  if (item.GetComponent<Wearable>() is not { } wearable || wearable.PressureProtection < realWorldDepth + Steering.PressureWarningThreshold)
223  {
224  return false;
225  }
226  }
227  return true;
228  }
229 
230 
231  private bool IsSuitableContainedOxygenSource(Item item)
232  {
233  return
234  item != null &&
235  item.HasTag(Tags.OxygenSource) &&
236  item.Condition > 0 &&
237  (oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value));
238  }
239 
240  private void TrySetTargetItem(Item item)
241  {
242  if (targetItem == item) { return; }
243  targetItem = item;
244  oxygenSourceSlotIndex = targetItem?.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(Tags.OxygenSource);
245  }
246 
247  public override void Reset()
248  {
249  base.Reset();
250  getDivingGear = null;
251  getOxygen = null;
252  targetItem = null;
253  oxygenSourceSlotIndex = null;
254  }
255 
256  public static float GetMinOxygen(Character character)
257  {
258  // Seek oxygen that has at least 10% condition left, if we are inside a friendly sub.
259  // The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
260  // When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
261  float min = 0.01f;
262  float minOxygen = character.IsInFriendlySub ? MIN_OXYGEN : min;
263  if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag(Tags.OxygenSource) && i.ConditionPercentage >= minOxygen))
264  {
265  // There's a valid oxygen tank in the inventory -> no need to swap the tank too early.
266  minOxygen = min;
267  }
268  return minOxygen;
269  }
270  }
271 }
static float GetMinOxygen(Character character)
AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier=1)
static bool IsSuitablePressureProtection(Item item, Identifier tag, Character character)
override bool CheckObjectiveState()
Should return whether the objective is completed or not.
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
Item GetEquippedItem(Identifier tagOrIdentifier=default, InvSlotType? slotType=null)
override bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
virtual Vector2 WorldPosition
Definition: Entity.cs:49
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....
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....
int? FindSuitableSubContainerIndex(Identifier itemTagOrIdentifier)
Returns the index of the first slot whose restrictions match the specified tag or identifier
const float PressureWarningThreshold
How many units before crush depth the pressure warning is shown
readonly float PressureProtection
How deep down does the item protect from pressure? Determined by status effects.
float GetRealWorldDepth(float worldPositionY)
Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav termina...