Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs
3 using FarseerPhysics;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Xml.Linq;
9 
10 namespace Barotrauma
11 {
12  partial class PirateMission : Mission
13  {
14  private readonly ContentXElement submarineTypeConfig;
15  private readonly ContentXElement characterConfig;
16  private readonly ContentXElement characterTypeConfig;
17  private readonly float addedMissionDifficultyPerPlayer;
18 
19  private float missionDifficulty;
20  private int alternateReward;
21 
22  private Identifier factionIdentifier;
23 
24  private Submarine enemySub;
25  private readonly List<Character> characters = new List<Character>();
26  private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
27 
28  // Update the last sighting periodically so that the players can find the pirate sub even if they have lost the track of it.
29  private readonly float pirateSightingUpdateFrequency = 30;
30  private float pirateSightingUpdateTimer;
31  private Vector2? lastSighting;
32 
33  private LevelData levelData;
34 
35  public override int TeamCount => 2;
36 
37  private bool outsideOfSonarRange;
38 
39  private readonly List<Vector2> patrolPositions = new List<Vector2>();
40 
41  public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
42  {
43  get
44  {
45  if (!outsideOfSonarRange || state > 1)
46  {
47  yield break;
48 
49  }
50  else if (state == 0)
51  {
52  foreach (Vector2 patrolPos in patrolPositions)
53  {
54  yield return (Prefab.SonarLabel, patrolPos);
55  }
56  }
57  else if (state == 1)
58  {
59  if (lastSighting.HasValue)
60  {
61  yield return (Prefab.SonarLabel, lastSighting.Value);
62  }
63  else
64  {
65  yield break;
66  }
67  }
68  }
69  }
70 
71  public override int GetBaseReward(Submarine sub)
72  {
73  return alternateReward;
74  }
75 
76  private SubmarineInfo submarineInfo;
77 
79  {
80  get
81  {
82  return submarineInfo;
83  }
84  }
85 
86  // these values could also be defined within the mission XML
87  private const float RandomnessModifier = 25;
88  private const float ShipRandomnessModifier = 15;
89 
90  private const float MaxDifficulty = 100;
91 
92  public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
93  {
94  submarineTypeConfig = prefab.ConfigElement.GetChildElement("SubmarineTypes");
95  characterConfig = prefab.ConfigElement.GetChildElement("Characters");
96  characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes");
97  addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
98 
99  //make sure all referenced character types are defined
100  foreach (XElement characterElement in characterConfig.Elements())
101  {
102  var characterId = characterElement.GetAttributeString("typeidentifier", string.Empty);
103  var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId);
104  if (characterTypeElement == null)
105  {
106  DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\".",
107  contentPackage: Prefab.ContentPackage);
108  }
109  }
110  //make sure all defined character types can be found from human prefabs
111  foreach (XElement characterTypeElement in characterTypeConfig.Elements())
112  {
113  foreach (XElement characterElement in characterTypeElement.Elements())
114  {
115  Identifier characterIdentifier = characterElement.GetAttributeIdentifier("identifier", Identifier.Empty);
116  Identifier characterFrom = characterElement.GetAttributeIdentifier("from", Identifier.Empty);
117  HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
118  if (humanPrefab == null)
119  {
120  DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".",
121  contentPackage: Prefab.ContentPackage);
122  }
123  }
124  }
125 
126  // for campaign missions, set level at construction
127  LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
128  if (levelData != null)
129  {
130  SetLevel(levelData);
131  }
132  }
133 
134  public override void SetLevel(LevelData level)
135  {
136  if (levelData != null)
137  {
138  //level already set
139  return;
140  }
141  submarineInfo = null;
142 
143  levelData = level;
144  missionDifficulty = level?.Difficulty ?? 0;
145 
146  XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
147  alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
148  factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", Identifier.Empty);
149 
150  string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖";
151  if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
152 
153  ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage);
154  if (submarinePath.IsNullOrEmpty())
155  {
156  DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!",
157  contentPackage: Prefab.ContentPackage);
158  return;
159  }
160 
161  BaseSubFile contentFile =
162  GetSubFile<EnemySubmarineFile>(submarinePath) ??
163  GetSubFile<SubmarineFile>(submarinePath);
164  BaseSubFile GetSubFile<T>(ContentPath path) where T : BaseSubFile
165  {
166  return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<T>()).FirstOrDefault(f => f.Path == submarinePath);
167  }
168 
169  if (contentFile == null)
170  {
171  DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!",
172  contentPackage: Prefab.ContentPackage);
173  return;
174  }
175 
176  submarineInfo = new SubmarineInfo(contentFile.Path.Value);
177  }
178 
179  private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
180  {
181  return Math.Abs(levelDifficulty - preferredDifficulty + MathHelper.Lerp(-randomnessModifier, randomnessModifier, (float)rand.NextDouble()));
182  }
183  private static int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand)
184  {
185  return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount);
186  }
187 
188  private XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier)
189  {
190  Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
191  // look for the element that is closest to our difficulty, with some randomness
192  XElement bestElement = null;
193  float bestValue = float.MaxValue;
194  foreach (XElement element in parentElement.Elements())
195  {
196  float applicabilityValue = GetDifficultyModifiedValue(element.GetAttributeFloat(0f, "preferreddifficulty"), levelDifficulty, randomnessModifier, rand);
197  if (applicabilityValue < bestValue)
198  {
199  bestElement = element;
200  bestValue = applicabilityValue;
201  }
202  }
203  return bestElement;
204  }
205 
206  private void CreateMissionPositions(out Vector2 preferredSpawnPos)
207  {
208  Vector2 patrolPos = Level.Loaded.EndPosition;
209  Point subSize = enemySub.GetDockedBorders().Size;
210 
211  preferredSpawnPos = Level.Loaded.EndPosition;
212 
213  if (Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out var potentialSpawnPos))
214  {
215  preferredSpawnPos = potentialSpawnPos.Position.ToVector2();
216  }
217  else
218  {
219  DebugConsole.ThrowError("Could not spawn pirate submarine in an interesting location! " + this,
220  contentPackage: Prefab.ContentPackage);
221  }
222  if (Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out var potentialPatrolPos, preferredSpawnPos, minDistFromPoint: 10000f))
223  {
224  patrolPos = potentialPatrolPos.Position.ToVector2();
225  }
226  else
227  {
228  DebugConsole.ThrowError("Could not give pirate submarine an interesting location to patrol to! " + this,
229  contentPackage: Prefab.ContentPackage);
230  }
231 
232  patrolPos = enemySub.FindSpawnPos(patrolPos, subSize);
233 
234  patrolPositions.Add(patrolPos);
235  patrolPositions.Add(preferredSpawnPos);
236 
237  if (!IsClient)
238  {
239  PathFinder pathFinder = new PathFinder(WayPoint.WayPointList, false);
240  var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos));
241  if (!path.Unreachable)
242  {
243  var validNodes = path.Nodes.FindAll(n => !Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(n.WorldPosition))));
244  if (validNodes.Any())
245  {
246  preferredSpawnPos = validNodes.GetRandomUnsynced().WorldPosition; // spawn the sub in a random point in the path if possible
247  }
248  }
249 
250  int graceDistance = 500; // the sub still spawns awkwardly close to walls, so this helps. could also be given as a parameter instead
251  preferredSpawnPos = enemySub.FindSpawnPos(preferredSpawnPos, new Point(subSize.X + graceDistance, subSize.Y + graceDistance));
252  }
253  }
254 
255  private void InitPirateShip()
256  {
257  enemySub.NeutralizeBallast();
258  if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag(Tags.Reactor) && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
259  {
260  reactor.PowerUpImmediately();
261  }
262  enemySub.EnableMaintainPosition();
263  enemySub.TeamID = CharacterTeamType.None;
264  //make the enemy sub withstand atleast the same depth as the player sub
265  enemySub.SetCrushDepth(Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth));
266  if (Level.Loaded != null)
267  {
268  //...and the depth of the patrol positions + 1000 m
269  foreach (var patrolPos in patrolPositions)
270  {
271  enemySub.SetCrushDepth(Math.Max(enemySub.RealWorldCrushDepth, Level.Loaded.GetRealWorldDepth(patrolPos.Y) + 1000));
272  }
273  }
274  enemySub.ImmuneToBallastFlora = true;
275  enemySub.EnableFactionSpecificEntities(factionIdentifier);
276  }
277 
278  private void InitPirates()
279  {
280  characters.Clear();
281  characterItems.Clear();
282 
283  if (characterConfig == null)
284  {
285  DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)",
286  contentPackage: Prefab.ContentPackage);
287  return;
288  }
289 
290  int playerCount = 1;
291 
292 #if SERVER
293  playerCount = GameMain.Server.ConnectedClients.Where(c => !c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating).Count();
294 #endif
295 
296  float enemyCreationDifficulty = missionDifficulty + playerCount * addedMissionDifficultyPerPlayer;
297 
298  Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
299 
300  bool commanderAssigned = false;
301  foreach (ContentXElement element in characterConfig.Elements())
302  {
303  // it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional
304  // if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns
305  int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty, rand);
306  var characterId = element.GetAttributeString("typeidentifier", string.Empty);
307  for (int i = 0; i < amountCreated; i++)
308  {
309  XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId).FirstOrDefault();
310 
311  if (characterType == null)
312  {
313  DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".",
314  contentPackage: element.ContentPackage);
315  return;
316  }
317 
318  XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
319 
320  var humanPrefab = GetHumanPrefabFromElement(variantElement);
321  if (humanPrefab == null) { continue; }
322 
323  Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, enemySub, CharacterTeamType.None, null);
324  if (!commanderAssigned)
325  {
326  bool isCommander = variantElement.GetAttributeBool("iscommander", false);
327  if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController)
328  {
329  humanAIController.InitShipCommandManager();
330  foreach (var patrolPos in patrolPositions)
331  {
332  humanAIController.ShipCommandManager.patrolPositions.Add(patrolPos);
333  }
334  commanderAssigned = true;
335  }
336  }
337 
338  foreach (Item item in spawnedCharacter.Inventory.AllItems)
339  {
340  if (item?.GetComponent<IdCard>() != null)
341  {
342  item.AddTag("id_pirate");
343  }
344  }
345  }
346  }
347  }
348 
349  protected override void StartMissionSpecific(Level level)
350  {
351  if (characters.Count > 0)
352  {
353 #if DEBUG
354  throw new Exception($"characters.Count > 0 ({characters.Count})");
355 #else
356  DebugConsole.AddWarning("Character list was not empty at the start of a pirate mission. The mission instance may not have been ended correctly on previous rounds.");
357  characters.Clear();
358 #endif
359  }
360 
361  if (patrolPositions.Count > 0)
362  {
363 #if DEBUG
364  throw new Exception($"patrolPositions.Count > 0 ({patrolPositions.Count})");
365 #else
366  DebugConsole.AddWarning("Patrol point list was not empty at the start of a pirate mission. The mission instance may not have been ended correctly on previous rounds.");
367  patrolPositions.Clear();
368 #endif
369  }
370 
371  enemySub = Submarine.MainSubs[1];
372 
373  if (enemySub == null)
374  {
375  DebugConsole.ThrowError(submarineInfo == null ?
376  $"Error in PirateMission: enemy sub was not created (submarineInfo == null)." :
377  $"Error in PirateMission: enemy sub was not created.",
378  contentPackage: Prefab.ContentPackage);
379  return;
380  }
381 
382  CreateMissionPositions(out Vector2 spawnPos); // patrol positions are not explicitly replicated, instead they are acquired the same way the server acquires them
383 #if DEBUG
384  if (IsClient)
385  {
386  DebugConsole.NewMessage("The patrol positions set by client were: ");
387  }
388  else
389  {
390  DebugConsole.NewMessage("The patrol positions set by server were: ");
391  }
392  foreach (var patrolPos in patrolPositions)
393  {
394  DebugConsole.NewMessage("Patrol pos: " + patrolPos);
395  }
396 #endif
397  enemySub.SetPosition(spawnPos);
398  if (!IsClient)
399  {
400  InitPirateShip();
401  }
402 
403  // flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections in the submarine
404  // creating the pirates has to be done after the sub has been flipped, or it seems to break the AI pathing
405  enemySub.FlipX();
406  enemySub.ShowSonarMarker = false;
407 
408  if (!IsClient)
409  {
410  InitPirates();
411  }
412  }
413 
414  protected override void UpdateMissionSpecific(float deltaTime)
415  {
416  if (state >= 2 || enemySub == null) { return; }
417 
418  float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange);
419  outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange;
420 
421  if (CheckWinState())
422  {
423  State = 2;
424  }
425  else
426  {
427  switch (State)
428  {
429  case 0:
430  for (int i = patrolPositions.Count - 1; i >= 0; i--)
431  {
432  if (Vector2.DistanceSquared(patrolPositions[i], Submarine.MainSub.WorldPosition) < sqrSonarRange)
433  {
434  patrolPositions.RemoveAt(i);
435  }
436  }
437  if (!outsideOfSonarRange || patrolPositions.None())
438  {
439  State = 1;
440  }
441  break;
442  case 1:
443  if (outsideOfSonarRange)
444  {
445  if (lastSighting.HasValue && Vector2.DistanceSquared(lastSighting.Value, Submarine.MainSub.WorldPosition) < sqrSonarRange)
446  {
447  lastSighting = null;
448  }
449  pirateSightingUpdateTimer -= deltaTime;
450  if (pirateSightingUpdateTimer < 0)
451  {
452  pirateSightingUpdateTimer = pirateSightingUpdateFrequency;
453  lastSighting = enemySub.WorldPosition;
454  }
455  }
456  else
457  {
458  lastSighting = enemySub.WorldPosition;
459  pirateSightingUpdateTimer = 0;
460  }
461  break;
462  }
463  }
464  }
465 
466  private bool CheckWinState() => !IsClient && characters.All(m => DeadOrCaptured(m));
467 
468  private static bool DeadOrCaptured(Character character)
469  {
470  return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated;
471  }
472 
473  protected override bool DetermineCompleted()
474  {
475  return state == 2;
476  }
477 
478  protected override void EndMissionSpecific(bool completed)
479  {
480  characters.Clear();
481  characterItems.Clear();
482  failed = !completed;
483  submarineInfo = null;
484  }
485  }
486 }
readonly ContentPath Path
Definition: ContentFile.cs:137
string???????????? Value
Definition: ContentPath.cs:27
float GetAttributeFloat(string key, float def)
ContentXElement? GetChildElement(string name)
Submarine Submarine
Definition: Entity.cs:53
Defines a point in the event that GoTo actions can jump to.
Definition: Label.cs:7
LevelData(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome)
Definition: LevelData.cs:108
readonly string Seed
Definition: LevelData.cs:21
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)
override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
override int GetBaseReward(Submarine sub)
Calculates the base reward, can be overridden for different mission types
PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub)
ContentPackage? ContentPackage
Definition: Prefab.cs:37
List< Item > GetItems(bool alsoFromConnectedSubs)
void SetCrushDepth(float realWorldCrushDepth)
Normally crush depth is determined by the crush depths of the walls and upgrades applied on them....
void EnableFactionSpecificEntities(Identifier factionIdentifier)
void FlipX(List< Submarine > parents=null)
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
void SetPosition(Vector2 position, List< Submarine > checkd=null, bool forceUndockFromStaticSubmarines=true)
Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize=null, float subDockingPortOffset=0.0f, int verticalMoveDir=0)
Attempt to find a spawn position close to the specified position where the sub doesn't collide with w...