Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Events/Missions/NestMission.cs
2 using FarseerPhysics;
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using Voronoi2;
8 
9 namespace Barotrauma
10 {
11  partial class NestMission : Mission
12  {
13  private readonly ContentXElement itemConfig;
14  private readonly List<Item> items = new List<Item>();
15  private readonly Dictionary<Item, StatusEffect> statusEffectOnApproach = new Dictionary<Item, StatusEffect>();
16 
17  //string = filename, point = min,max
18  private readonly HashSet<Tuple<CharacterPrefab, Point>> monsterPrefabs = new HashSet<Tuple<CharacterPrefab, Point>>();
19 
20  private float itemSpawnRadius = 800.0f;
21  private readonly float approachItemsRadius = 1000.0f;
22  private readonly float nestObjectRadius = 1000.0f;
23  private readonly float monsterSpawnRadius = 3000.0f;
24  private readonly int nestObjectAmount = 10;
25 
26  private readonly bool requireDelivery;
27 
28  private readonly Level.PositionType spawnPositionType;
29 
30  private Vector2 nestPosition;
31 
32  private Level.Cave selectedCave;
33 
34 
35  public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
36  {
37  get
38  {
39  if (State > 0)
40  {
41  yield break;
42  }
43  else
44  {
45  yield return (Prefab.SonarLabel, nestPosition);
46  }
47  }
48  }
49 
50  public NestMission(MissionPrefab prefab, Location[] locations, Submarine sub)
51  : base(prefab, locations, sub)
52  {
53  itemConfig = prefab.ConfigElement.GetChildElement("Items");
54 
55  itemSpawnRadius = prefab.ConfigElement.GetAttributeFloat("itemspawnradius", 800.0f);
56  approachItemsRadius = prefab.ConfigElement.GetAttributeFloat("approachitemsradius", itemSpawnRadius * 2.0f);
57  monsterSpawnRadius = prefab.ConfigElement.GetAttributeFloat("monsterspawnradius", approachItemsRadius * 2.0f);
58 
59  nestObjectRadius = prefab.ConfigElement.GetAttributeFloat("nestobjectradius", itemSpawnRadius * 2.0f);
60  nestObjectAmount = prefab.ConfigElement.GetAttributeInt("nestobjectamount", 10);
61 
62  requireDelivery = prefab.ConfigElement.GetAttributeBool("requiredelivery", false);
63 
64  string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
65  if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) ||
66  !Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType))
67  {
68  spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin;
69  }
70 
71  foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
72  {
73  Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty);
74  int defaultCount = monsterElement.GetAttributeInt("count", -1);
75  if (defaultCount < 0)
76  {
77  defaultCount = monsterElement.GetAttributeInt("amount", 1);
78  }
79  int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255);
80  int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255);
81  var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
82  if (characterPrefab != null)
83  {
84  monsterPrefabs.Add(new Tuple<CharacterPrefab, Point>(characterPrefab, new Point(min, max)));
85  }
86  else
87  {
88  DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".",
89  contentPackage: Prefab.ContentPackage);
90  }
91  }
92 
93  }
94 
95  protected override void StartMissionSpecific(Level level)
96  {
97  if (items.Any())
98  {
99 #if DEBUG
100  throw new Exception($"items.Count > 0 ({items.Count})");
101 #else
102  DebugConsole.AddWarning("Item list was not empty at the start of a nest mission. The mission instance may not have been ended correctly on previous rounds.");
103  items.Clear();
104 #endif
105  }
106 
107  if (!IsClient)
108  {
109  //ruin/cave/wreck items are allowed to spawn close to the sub
110  float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Cave || spawnPositionType == Level.PositionType.Wreck ?
111  0.0f : Level.Loaded.Size.X * 0.3f;
112 
113  nestPosition = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f);
114  List<GraphEdge> spawnEdges = new List<GraphEdge>();
115  if (spawnPositionType == Level.PositionType.Cave)
116  {
117  Level.Cave closestCave = null;
118  float closestCaveDist = float.PositiveInfinity;
119  foreach (var cave in Level.Loaded.Caves)
120  {
121  float dist = Vector2.DistanceSquared(nestPosition, cave.Area.Center.ToVector2());
122  if (dist < closestCaveDist)
123  {
124  closestCave = cave;
125  closestCaveDist = dist;
126  }
127  }
128  if (closestCave != null)
129  {
130  selectedCave = closestCave;
131  selectedCave.MissionsToDisplayOnSonar.Add(this);
132  SpawnNestObjects(level, closestCave);
133  }
134  var nearbyCells = Level.Loaded.GetCells(nestPosition, searchDepth: 3);
135  if (nearbyCells.Any())
136  {
137  List<GraphEdge> validEdges = new List<GraphEdge>();
138  foreach (var edge in nearbyCells.SelectMany(c => c.Edges))
139  {
140  if (!edge.NextToCave || !edge.IsSolid) { continue; }
141  if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(edge.Center + edge.GetNormal(edge.Cell1 ?? edge.Cell2) * 100.0f))) { continue; }
142  validEdges.Add(edge);
143  }
144 
145  if (validEdges.Any())
146  {
147  spawnEdges.AddRange(validEdges.Where(e => MathUtils.LineSegmentToPointDistanceSquared(e.Point1.ToPoint(), e.Point2.ToPoint(), nestPosition.ToPoint()) < itemSpawnRadius * itemSpawnRadius).Distinct());
148  }
149  //no valid edges found close enough to the nest position, find the closest one
150  if (!spawnEdges.Any())
151  {
152  GraphEdge closestEdge = null;
153  float closestDistSqr = float.PositiveInfinity;
154  foreach (var edge in nearbyCells.SelectMany(c => c.Edges))
155  {
156  if (!edge.NextToCave || !edge.IsSolid) { continue; }
157  float dist = Vector2.DistanceSquared(edge.Center, nestPosition);
158  if (dist < closestDistSqr)
159  {
160  closestEdge = edge;
161  closestDistSqr = dist;
162  }
163  }
164  if (closestEdge != null)
165  {
166  spawnEdges.Add(closestEdge);
167  itemSpawnRadius = Math.Max(itemSpawnRadius, (float)Math.Sqrt(closestDistSqr) * 1.5f);
168  }
169  }
170  }
171  }
172 
173  foreach (var subElement in itemConfig.Elements())
174  {
175  var itemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
176  if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is not ItemPrefab itemPrefab)
177  {
178  DebugConsole.ThrowError("Couldn't spawn item for nest mission: item prefab \"" + itemIdentifier + "\" not found",
179  contentPackage: Prefab.ContentPackage);
180  continue;
181  }
182 
183  Vector2 spawnPos = nestPosition;
184  float rotation = 0.0f;
185  if (spawnEdges.Any())
186  {
187  const float MinDistanceFromOtherItems = 30.0f;
188  const int MaxTries = 10;
189  for (int i = 0; i < MaxTries; i++)
190  {
191  var edge = spawnEdges.GetRandom(Rand.RandSync.ServerAndClient);
192  spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.ServerAndClient));
193  Vector2 normal = Vector2.UnitY;
194  if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid)
195  {
196  normal = edge.GetNormal(edge.Cell1);
197  }
198  else if (edge.Cell2 != null && edge.Cell2.CellType == CellType.Solid)
199  {
200  normal = edge.GetNormal(edge.Cell2);
201  }
202  spawnPos += normal * 10.0f;
203  rotation = MathUtils.VectorToAngle(normal) - MathHelper.PiOver2;
204 
205  if (items.All(it => Vector2.DistanceSquared(it.WorldPosition, spawnPos) > MinDistanceFromOtherItems)) { break; }
206  }
207  }
208 
209  var item = new Item(itemPrefab, spawnPos, null);
210  item.body.FarseerBody.BodyType = BodyType.Kinematic;
211  item.body.SetTransformIgnoreContacts(item.body.SimPosition, rotation);
212  item.FindHull();
213  item.AddTag("nestmission");
214  item.AddTag(Prefab.Identifier);
215  items.Add(item);
216 
217  var statusEffectElement =
218  subElement.GetChildElement("StatusEffectOnApproach")
219  ?? subElement.GetChildElement("statuseffectonapproach");
220  if (statusEffectElement != null)
221  {
222  statusEffectOnApproach.Add(item, StatusEffect.Load(statusEffectElement, Prefab.Identifier.Value));
223  }
224  }
225  }
226  }
227 
228  private void SpawnNestObjects(Level level, Level.Cave cave)
229  {
230  level.LevelObjectManager.PlaceNestObjects(level, cave, nestPosition, nestObjectRadius, nestObjectAmount);
231  }
232 
233  protected override void UpdateMissionSpecific(float deltaTime)
234  {
235  if (IsClient)
236  {
237  foreach (Item item in items)
238  {
239  if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
240  }
241  return;
242  }
243  switch (State)
244  {
245  case 0:
246  foreach (Item item in items)
247  {
248  if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
249  if (statusEffectOnApproach.ContainsKey(item))
250  {
251  foreach (Character character in Character.CharacterList)
252  {
253  if (character.IsPlayer && Vector2.DistanceSquared(nestPosition, character.WorldPosition) < approachItemsRadius * approachItemsRadius)
254  {
255  statusEffectOnApproach[item].Apply(statusEffectOnApproach[item].type, 1.0f, item, item);
256  statusEffectOnApproach.Remove(item);
257  break;
258  }
259  }
260  }
261  }
262  if (monsterPrefabs.Any())
263  {
264  foreach (Character character in Character.CharacterList)
265  {
266  if (character.IsPlayer && Vector2.DistanceSquared(nestPosition, character.WorldPosition) < monsterSpawnRadius * monsterSpawnRadius)
267  {
268  foreach (var monster in monsterPrefabs)
269  {
270  int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1);
271  for (int i = 0; i < amount; i++)
272  {
273  Vector2 offsetPosition;
274  int tries = 0;
275  do
276  {
277  offsetPosition = nestPosition + Rand.Vector(100.0f);
278  tries++;
279  if (tries > 10)
280  {
281  offsetPosition = nestPosition;
282  break;
283  }
284  } while (Level.Loaded.IsPositionInsideWall(offsetPosition));
285  Character.Create(monster.Item1.Identifier, offsetPosition, ToolBox.RandomSeed(8), createNetworkEvent: true);
286  }
287  }
288  if (Level.Loaded.IsPositionInsideWall(nestPosition))
289  {
290  DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition}).",
292  }
293  monsterPrefabs.Clear();
294  break;
295  }
296  }
297  }
298 
299  //continue when all items are in the sub or destroyed
300  if (AllItemsDestroyedOrRetrieved())
301  {
302  State = 1;
303  }
304 
305  break;
306  case 1:
307  if (!Submarine.MainSub.AtEitherExit) { return; }
308  State = 2;
309  break;
310  }
311  }
312 
313  private bool AllItemsDestroyedOrRetrieved()
314  {
315  if (requireDelivery)
316  {
317  foreach (Item item in items)
318  {
319  Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine;
320  if (parentSub?.Info?.Type == SubmarineType.Player) { continue; }
321  return false;
322  }
323  }
324  else
325  {
326  foreach (Item item in items)
327  {
328  if (item.Removed || item.Condition <= 0.0f) { continue; }
329  if (Vector2.Distance(item.WorldPosition, nestPosition) > Math.Max(itemSpawnRadius * 2, 3000.0f)) { continue; }
330  Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine;
331  if (parentSub?.Info?.Type == SubmarineType.Player) { continue; }
332  return false;
333  }
334  }
335  return true;
336  }
337 
338  protected override bool DetermineCompleted()
339  {
340  return AllItemsDestroyedOrRetrieved();
341  }
342 
343  protected override void EndMissionSpecific(bool completed)
344  {
345  foreach (Item item in items)
346  {
347  if (item != null && !item.Removed)
348  {
349  item.Remove();
350  }
351  }
352  items.Clear();
353  failed = !completed && state > 0;
354  }
355  }
356 }
static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, RagdollParams ragdoll=null, bool spawnInitialItems=true)
Create a new character
static CharacterPrefab FindBySpeciesName(Identifier speciesName)
string? GetAttributeString(string key, string? def)
float GetAttributeFloat(string key, float def)
ContentXElement? GetChildElement(string name)
bool GetAttributeBool(string key, bool def)
int GetAttributeInt(string key, int def)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
Defines a point in the event that GoTo actions can jump to.
Definition: Label.cs:7
Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall=10.0f, Func< InterestingPosition, bool > filter=null)
List< VoronoiCell > GetCells(Vector2 worldPos, int searchDepth=2)
void PlaceNestObjects(Level level, Level.Cave cave, Vector2 nestPosition, float nestRadius, int objectAmount)
static MapEntityPrefab FindByIdentifier(Identifier identifier)
NestMission(MissionPrefab prefab, Location[] locations, Submarine sub)
override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
static StatusEffect Load(ContentXElement element, string parentDebugName)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
Vector2 GetNormal(VoronoiCell cell)
Returns the normal of the edge that points outwards from the specified cell