Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Xml.Linq;
8 
9 namespace Barotrauma
10 {
11  partial class EscortMission : Mission
12  {
13  private readonly ContentXElement characterConfig;
14  private readonly ContentXElement itemConfig;
15 
16  private readonly List<Character> characters = new List<Character>();
17  private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
18  private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
19 
20  private readonly int baseEscortedCharacters;
21  private readonly float scalingEscortedCharacters;
22  private readonly float terroristChance;
23 
24  private int calculatedReward;
25  private Submarine missionSub;
26 
27  private Character vipCharacter;
28 
29  private readonly List<Character> terroristCharacters = new List<Character>();
30  private bool terroristsShouldAct = false;
31  private float terroristDistanceSquared;
32  private const string TerroristTeamChangeIdentifier = "terrorist";
33  private readonly string terroristAnnounceDialogTag = string.Empty;
34 
35  public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
36  : base(prefab, locations, sub)
37  {
38  missionSub = sub;
39  characterConfig = prefab.ConfigElement.GetChildElement("Characters");
40  baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1);
41  scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
42  terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
43  itemConfig = prefab.ConfigElement.GetChildElement("TerroristItems");
44  terroristAnnounceDialogTag = prefab.ConfigElement.GetAttributeString("terroristannouncedialogtag", string.Empty);
45  CalculateReward();
46  }
47 
48  private void CalculateReward()
49  {
50  if (missionSub == null)
51  {
52  calculatedReward = Prefab.Reward;
53  return;
54  }
55 
56  // Disabled for now, because they make balancing the missions a pain.
57  int multiplier = 1;//CalculateScalingEscortedCharacterCount();
58  calculatedReward = Prefab.Reward * multiplier;
59 
60  string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(missionSub))}‖end‖";
61  if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
62  }
63 
64  public override int GetBaseReward(Submarine sub)
65  {
66  if (sub != missionSub)
67  {
68  missionSub = sub;
69  CalculateReward();
70  }
71  return calculatedReward;
72  }
73 
74  int CalculateScalingEscortedCharacterCount(bool inMission = false)
75  {
76  if (missionSub == null || missionSub.Info == null) // UI logic failing to get the correct value is not important, but the mission logic must succeed
77  {
78  if (inMission)
79  {
80  DebugConsole.ThrowError("MainSub was null when trying to retrieve submarine size for determining escorted character count!",
81  contentPackage: Prefab.ContentPackage);
82  }
83  return 1;
84  }
85  return (int)Math.Round(baseEscortedCharacters + scalingEscortedCharacters * (missionSub.Info.RecommendedCrewSizeMin + missionSub.Info.RecommendedCrewSizeMax) / 2);
86  }
87 
88  private void InitEscort()
89  {
90  characters.Clear();
91  characterItems.Clear();
92 
93  WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub);
94  Rand.RandSync randSync = Rand.RandSync.ServerAndClient;
95 
96  if (terroristChance > 0f)
97  {
98  // in terrorist missions, reroll characters each retry to avoid confusion as to who the terrorists are
99  randSync = Rand.RandSync.Unsynced;
100  }
101 
102  List<HumanPrefab> humanPrefabsToSpawn = new List<HumanPrefab>();
103  foreach (ContentXElement characterElement in characterConfig.Elements())
104  {
105  int count = CalculateScalingEscortedCharacterCount(inMission: true);
106  var humanPrefab = GetHumanPrefabFromElement(characterElement);
107  for (int i = 0; i < count; i++)
108  {
109  humanPrefabsToSpawn.Add(humanPrefab);
110  }
111  foreach (var element in characterElement.Elements())
112  {
113  if (element.NameAsIdentifier() == "statuseffect")
114  {
115  var newEffect = StatusEffect.Load(element, parentDebugName: Prefab.Name.Value);
116  if (newEffect == null) { continue; }
117  if (!characterStatusEffects.ContainsKey(humanPrefab))
118  {
119  characterStatusEffects[humanPrefab] = new List<StatusEffect> { newEffect };
120  }
121  else
122  {
123  characterStatusEffects[humanPrefab].Add(newEffect);
124  }
125  }
126  }
127  }
128 
129  //if any of the escortees have a job defined, try to use a spawnpoint designated for that job
130  foreach (var humanPrefab in humanPrefabsToSpawn)
131  {
132  if (humanPrefab == null || humanPrefab.Job.IsEmpty || humanPrefab.Job == "any") { continue; }
133  var jobPrefab = humanPrefab.GetJobPrefab(randSync);
134  if (jobPrefab != null)
135  {
136  var jobSpecificSpawnPos = WayPoint.GetRandom(SpawnType.Human, jobPrefab, Submarine.MainSub);
137  if (jobSpecificSpawnPos != null)
138  {
139  explicitStayInHullPos = jobSpecificSpawnPos;
140  break;
141  }
142  }
143  }
144  foreach (var humanPrefab in humanPrefabsToSpawn)
145  {
146  Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync);
147  if (spawnedCharacter.AIController is HumanAIController humanAI)
148  {
149  humanAI.InitMentalStateManager();
150  }
151  if (characterStatusEffects.TryGetValue(humanPrefab, out var statusEffectList))
152  {
153  foreach (var statusEffect in statusEffectList)
154  {
155  statusEffect.Apply(statusEffect.type, 1.0f, spawnedCharacter, spawnedCharacter);
156  }
157  }
158  }
159 
160 
161  if (terroristChance > 0f)
162  {
163  int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count);
164  terroristCount = Math.Clamp(terroristCount, 1, characters.Count);
165 
166  terroristCharacters.Clear();
167  characters.GetRange(0, terroristCount).ForEach(c => terroristCharacters.Add(c));
168  terroristCharacters.ForEach(c => c.IsHostileEscortee = true);
169  terroristDistanceSquared = Vector2.DistanceSquared(Level.Loaded.StartPosition, Level.Loaded.EndPosition) * Rand.Range(0.35f, 0.65f);
170 #if DEBUG
171  DebugConsole.AddWarning("Terrorists will trigger at range " + Math.Sqrt(terroristDistanceSquared));
172  foreach (Character character in terroristCharacters)
173  {
174  DebugConsole.AddWarning(character.Name + " is a terrorist.");
175  }
176 #endif
177  }
178  }
179 
180  private void InitCharacters()
181  {
182  int scalingCharacterCount = CalculateScalingEscortedCharacterCount(inMission: true);
183 
184  if (scalingCharacterCount * characterConfig.Elements().Count() != characters.Count)
185  {
186  DebugConsole.AddWarning("Character count did not match expected character count in InitCharacters of EscortMission",
188  return;
189  }
190  int i = 0;
191 
192  foreach (ContentXElement element in characterConfig.Elements())
193  {
194  string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty);
195  string colorIdentifier = element.GetAttributeString("color", string.Empty);
196  for (int k = 0; k < scalingCharacterCount; k++)
197  {
198  // for each element defined, we need to initialize that type of character equal to the scaling escorted character count
199  characters[k + i].IsEscorted = true;
200  if (escortIdentifier != string.Empty)
201  {
202  if (escortIdentifier == "vip")
203  {
204  vipCharacter = characters[k + i];
205  }
206  }
207  characters[k + i].UniqueNameColor = element.GetAttributeColor("color", Color.LightGreen);
208  }
209  i++;
210  }
211  }
212 
213  protected override void StartMissionSpecific(Level level)
214  {
215  if (characters.Count > 0)
216  {
217 #if DEBUG
218  throw new Exception($"characters.Count > 0 ({characters.Count})");
219 #else
220  DebugConsole.AddWarning("Character list was not empty at the start of a escort mission. The mission instance may not have been ended correctly on previous rounds.");
221  characters.Clear();
222 #endif
223  }
224 
225  if (characterConfig == null)
226  {
227  DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)",
228  contentPackage: Prefab.ContentPackage);
229  return;
230  }
231 
232  // to ensure single missions run without issues, default to mainsub
233  if (missionSub == null)
234  {
235  missionSub = Submarine.MainSub;
236  CalculateReward();
237  }
238 
239  if (!IsClient)
240  {
241  InitEscort();
242  InitCharacters();
243  }
244  }
245 
246  void TryToTriggerTerrorists()
247  {
248  if (terroristsShouldAct)
249  {
250  // decoupled from range check to prevent from weirdness if players handcuff a terrorist and move backwards
251  foreach (Character character in terroristCharacters)
252  {
253  character.IsHostileEscortee = true;
254  if (character.HasTeamChange(TerroristTeamChangeIdentifier))
255  {
256  // already triggered
257  continue;
258  }
259 
260  if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands)
261  {
262  character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true));
263  if (!string.IsNullOrEmpty(terroristAnnounceDialogTag))
264  {
265  character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f));
266  }
267  ContentXElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty);
268  if (randomElement != null)
269  {
270  HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true);
271  }
272  }
273  }
274  }
275  else if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) < terroristDistanceSquared)
276  {
277  foreach (Character character in terroristCharacters)
278  {
279  if (character.AIController is HumanAIController humanAI)
280  {
281  humanAI.ObjectiveManager.AddObjective(new AIObjectiveEscapeHandcuffs(character, humanAI.ObjectiveManager, shouldSwitchTeams: false, beginInstantly: true));
282  }
283  }
284  terroristsShouldAct = true;
285  }
286  }
287 
288  bool NonTerroristsStillAlive(IEnumerable<Character> characterList)
289  {
290  return characterList.All(c => terroristCharacters.Contains(c) || IsAlive(c));
291  }
292 
293  protected override void UpdateMissionSpecific(float deltaTime)
294  {
295  if (!IsClient)
296  {
297  int newState = State;
298  TryToTriggerTerrorists();
299  switch (State)
300  {
301  case 0: // base
302  if (!NonTerroristsStillAlive(characters))
303  {
304  newState = 1;
305  }
306  if (terroristCharacters.Any() && terroristCharacters.All(c => !IsAlive(c)))
307  {
308  newState = 2;
309  }
310  break;
311  case 1: // failure
312  break;
313  case 2: // terrorists killed
314  if (!NonTerroristsStillAlive(characters))
315  {
316  newState = 1;
317  }
318  break;
319  }
320  State = newState;
321  }
322  }
323 
324  private static bool Survived(Character character)
325  {
326  return IsAlive(character) && character.CurrentHull?.Submarine != null &&
327  (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine));
328  }
329 
330  private static bool IsAlive(Character character)
331  {
332  return character != null && !character.Removed && !character.IsDead;
333  }
334 
335  protected override bool DetermineCompleted()
336  {
338  {
339  bool friendliesSurvived = characters.Except(terroristCharacters).All(c => Survived(c));
340  bool vipDied = false;
341 
342  // this logic is currently irrelevant, as the mission is failed regardless of who dies
343  if (vipCharacter != null)
344  {
345  vipDied = !Survived(vipCharacter);
346  }
347 
348  if (friendliesSurvived && !vipDied)
349  {
350  return true;
351  }
352  }
353  return false;
354  }
355 
356  protected override void EndMissionSpecific(bool completed)
357  {
358  if (!IsClient)
359  {
360  foreach (Character character in characters)
361  {
362  if (character.Inventory == null) { continue; }
363  foreach (Item item in character.Inventory.AllItemsMod)
364  {
365  //item didn't spawn with the characters -> drop it
366  if (!characterItems.Any(c => c.Value.Contains(item)))
367  {
368  item.Drop(character);
369  }
370  }
371  }
372 
373  // characters that survived will take their items with them, in case players tried to be crafty and steal them
374  // this needs to run here in case players abort the mission by going back home
375  foreach (var characterItem in characterItems)
376  {
377  if (Survived(characterItem.Key) || !completed)
378  {
379  foreach (Item item in characterItem.Value)
380  {
381  if (!item.Removed)
382  {
383  item.Remove();
384  }
385  }
386  }
387  }
388  }
389 
390  characters.Clear();
391  characterItems.Clear();
392  failed = !completed;
393  }
394  }
395 }
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
bool IsHostileEscortee
Set true only, if the character is turned hostile from an escort mission (See EscortMission).
bool TryAddNewTeamChange(string identifier, ActiveTeamChange newTeamChange)
string? GetAttributeString(string key, string? def)
float GetAttributeFloat(string key, float def)
ContentXElement? GetChildElement(string name)
int GetAttributeInt(string key, int def)
Submarine Submarine
Definition: Entity.cs:53
EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
override int GetBaseReward(Submarine sub)
Calculates the base reward, can be overridden for different mission types
IEnumerable< Item > AllItemsMod
All items contained in the inventory. Allows modifying the contents of the inventory while being enum...
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
LocalizedString Replace(Identifier find, LocalizedString replace, StringComparison stringComparison=StringComparison.Ordinal)
static Character CreateHuman(HumanPrefab humanPrefab, List< Character > characters, Dictionary< Character, List< Item >> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn=null, Rand.RandSync humanPrefabRandSync=Rand.RandSync.ServerAndClient)
ContentPackage? ContentPackage
Definition: Prefab.cs:37