Client LuaCsForBarotrauma
MonsterEvent.cs
1 using Microsoft.Xna.Framework;
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
7 using FarseerPhysics;
8 
9 namespace Barotrauma
10 {
12  {
16  public readonly Identifier SpeciesName;
17 
21  public readonly int MinAmount;
25  public readonly int MaxAmount;
26 
27  private readonly List<Character> monsters = new List<Character>();
28 
32  public readonly float SpawnDistance;
33 
37  private readonly float scatter;
38 
42  private readonly float offset;
43 
47  private readonly float delayBetweenSpawns;
48 
52  private float resetTime;
53  private float resetTimer;
54 
55  private Vector2? spawnPos;
56 
57  private bool disallowed;
58 
62  public readonly Level.PositionType SpawnPosType;
63 
67  private readonly string spawnPointTag;
68 
69  private bool spawnPending, spawnReady;
70 
75  public readonly int MaxAmountPerLevel;
76 
77  private readonly float? overridePlayDeadProbability;
78 
79  public IReadOnlyList<Character> Monsters => monsters;
80  public Vector2? SpawnPos => spawnPos;
81  public bool SpawnPending => spawnPending;
82 
83  public override Vector2 DebugDrawPos
84  {
85  get { return spawnPos ?? Vector2.Zero; }
86  }
87 
88  public override string ToString()
89  {
90  if (MaxAmount <= 1)
91  {
92  return $"MonsterEvent ({SpeciesName}, {SpawnPosType})";
93  }
94  else if (MinAmount < MaxAmount)
95  {
96  return $"MonsterEvent ({SpeciesName} x{MinAmount}-{MaxAmount}, {SpawnPosType})";
97  }
98  else
99  {
100  return $"MonsterEvent ({SpeciesName} x{MaxAmount}, {SpawnPosType})";
101  }
102  }
103 
104  public MonsterEvent(EventPrefab prefab, int seed)
105  : base(prefab, seed)
106  {
107  string speciesFile = prefab.ConfigElement.GetAttributeString("characterfile", "");
108  CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesFile);
109  if (characterPrefab != null)
110  {
111  SpeciesName = characterPrefab.Identifier;
112  }
113  else
114  {
115  SpeciesName = speciesFile.ToIdentifier();
116  }
117 
118  if (SpeciesName.IsEmpty)
119  {
120  throw new Exception("speciesname is null!");
121  }
122 
123  int defaultAmount = prefab.ConfigElement.GetAttributeInt("amount", 1);
124  MinAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount);
125  MaxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), MinAmount);
126 
127  MaxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue);
128 
129  SpawnPosType = prefab.ConfigElement.GetAttributeEnum("spawntype", Level.PositionType.MainPath);
130  //backwards compatibility
131  if (prefab.ConfigElement.GetAttributeBool("spawndeep", false))
132  {
134  }
135 
136  spawnPointTag = prefab.ConfigElement.GetAttributeString("spawnpointtag", string.Empty);
137  SpawnDistance = prefab.ConfigElement.GetAttributeFloat("spawndistance", 0);
138  offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
139  scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
140  delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f);
141  resetTime = prefab.ConfigElement.GetAttributeFloat("resettime", 0);
142  float playDeadProbability = prefab.ConfigElement.GetAttributeFloat("playdeadprobability", -1f);
143  if (playDeadProbability >= 0)
144  {
145  overridePlayDeadProbability = playDeadProbability;
146  }
147 
148  if (GameMain.NetworkMember != null)
149  {
150  List<Identifier> monsterNames = GameMain.NetworkMember.ServerSettings.MonsterEnabled.Keys.ToList();
151  Identifier tryKey = monsterNames.Find(s => SpeciesName == s);
152 
153  if (!tryKey.IsEmpty)
154  {
155  if (!GameMain.NetworkMember.ServerSettings.MonsterEnabled[tryKey])
156  {
157  disallowed = true; //spawn was disallowed by host
158  }
159  }
160  }
161  }
162 
163  private static Submarine GetReferenceSub()
164  {
166  }
167 
168  public override IEnumerable<ContentFile> GetFilesToPreload()
169  {
171  if (file == null)
172  {
173  DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\".",
174  contentPackage: Prefab.ContentPackage);
175  yield break;
176  }
177  else
178  {
179  yield return file;
180  }
181  }
182 
183  protected override void InitEventSpecific(EventSet parentSet)
184  {
185  // apply pvp stun resistance (reduce stun amount via resist multiplier)
186  if (GameMain.NetworkMember is { } networkMember && GameMain.GameSession?.GameMode is PvPMode && !networkMember.ServerSettings.PvPSpawnMonsters)
187  {
188  if (GameSettings.CurrentConfig.VerboseLogging)
189  {
190  DebugConsole.NewMessage($"PvP setting: disabling monster event ({SpeciesName})", Color.Yellow);
191  }
192 
193  disallowed = true;
194  return;
195  }
196 
197  if (parentSet != null && resetTime == 0)
198  {
199  // Use the parent reset time only if there's no reset time defined for the event.
200  resetTime = parentSet.ResetTime;
201  }
202  if (GameSettings.CurrentConfig.VerboseLogging)
203  {
204  DebugConsole.NewMessage("Initialized MonsterEvent (" + SpeciesName + ")", Color.White);
205  }
206 
207  monsters.Clear();
208 
209  //+1 because Range returns an integer less than the max value
210  int amount = Rand.Range(MinAmount, MaxAmount + 1);
211  for (int i = 0; i < amount; i++)
212  {
213  string seed = i.ToString() + Level.Loaded.Seed;
214  Character createdCharacter = Character.Create(SpeciesName, Vector2.Zero, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true, throwErrorIfNotFound: false);
215  if (createdCharacter == null)
216  {
217  DebugConsole.AddWarning($"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\".",
219  disallowed = true;
220  continue;
221  }
222  if (overridePlayDeadProbability.HasValue)
223  {
224  createdCharacter.EvaluatePlayDeadProbability(overridePlayDeadProbability);
225  }
227  {
229  Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength);
230  createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction);
231  // TODO test multiplayer
232  createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false);
233  }
234  createdCharacter.DisabledByEvent = true;
235  monsters.Add(createdCharacter);
236  }
237  }
238 
239  public override string GetDebugInfo()
240  {
241  return
242  $"Finished: {IsFinished.ColorizeObject()}\n" +
243  $"Amount: {MinAmount.ColorizeObject()} - {MaxAmount.ColorizeObject()}\n" +
244  $"Spawn pending: {SpawnPending.ColorizeObject()}\n" +
245  $"Spawn position: {SpawnPos.ColorizeObject()}";
246  }
247 
248  private List<Level.InterestingPosition> GetAvailableSpawnPositions()
249  {
250  var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => SpawnPosType.HasFlag(p.PositionType));
251  var removals = new List<Level.InterestingPosition>();
252  foreach (var position in availablePositions)
253  {
254  if (SpawnPosFilter != null && !SpawnPosFilter(position))
255  {
256  removals.Add(position);
257  continue;
258  }
259  if (position.Submarine != null)
260  {
261  if (position.Submarine.WreckAI != null && position.Submarine.WreckAI.IsAlive)
262  {
263  removals.Add(position);
264  }
265  else
266  {
267  continue;
268  }
269  }
270  if (position.PositionType != Level.PositionType.MainPath &&
271  position.PositionType != Level.PositionType.SidePath)
272  {
273  continue;
274  }
275  if (Level.Loaded.IsPositionInsideWall(position.Position.ToVector2()))
276  {
277  removals.Add(position);
278  }
279  if (position.Position.Y < Level.Loaded.GetBottomPosition(position.Position.X).Y)
280  {
281  removals.Add(position);
282  }
283  }
284  removals.ForEach(r => availablePositions.Remove(r));
285  return availablePositions;
286  }
287 
288  private Level.InterestingPosition chosenPosition;
289  private void FindSpawnPosition(bool affectSubImmediately)
290  {
291  if (disallowed) { return; }
292 
293  spawnPos = Vector2.Zero;
294  var availablePositions = GetAvailableSpawnPositions();
295  chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false);
296  bool isRuinOrWreckOrCave =
297  SpawnPosType.HasFlag(Level.PositionType.Ruin) ||
298  SpawnPosType.HasFlag(Level.PositionType.Wreck) ||
299  SpawnPosType.HasFlag(Level.PositionType.Cave) ||
300  SpawnPosType.HasFlag(Level.PositionType.AbyssCave);
301  if (affectSubImmediately && !isRuinOrWreckOrCave && !SpawnPosType.HasFlag(Level.PositionType.Abyss))
302  {
303  if (availablePositions.None())
304  {
305  //no suitable position found, disable the event
306  spawnPos = null;
307  disallowed = true;
308  return;
309  }
310  Submarine refSub = GetReferenceSub();
311  if (Submarine.MainSubs.Length == 2 && Submarine.MainSubs[1] != null)
312  {
313  refSub = Submarine.MainSubs.GetRandom(Rand.RandSync.Unsynced);
314  }
315  float closestDist = float.PositiveInfinity;
316  //find the closest spawnposition that isn't too close to any of the subs
317  foreach (var position in availablePositions)
318  {
319  Vector2 pos = position.Position.ToVector2();
320  float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition);
321  foreach (Submarine sub in Submarine.Loaded)
322  {
323  if (sub.Info.Type != SubmarineType.Player &&
324  sub.Info.Type != SubmarineType.EnemySubmarine &&
325  !sub.IsRespawnShuttle)
326  {
327  continue;
328  }
329 
330  float minDistToSub = GetMinDistanceToSub(sub);
331  if (dist < minDistToSub * minDistToSub) { continue; }
332 
333  if (closestDist == float.PositiveInfinity)
334  {
335  closestDist = dist;
336  chosenPosition = position;
337  continue;
338  }
339 
340  //chosen position behind the sub -> override with anything that's closer or to the right
341  if (chosenPosition.Position.X < refSub.WorldPosition.X)
342  {
343  if (dist < closestDist || pos.X > refSub.WorldPosition.X)
344  {
345  closestDist = dist;
346  chosenPosition = position;
347  }
348  }
349  //chosen position ahead of the sub -> only override with a position that's also ahead
350  else if (chosenPosition.Position.X > refSub.WorldPosition.X)
351  {
352  if (dist < closestDist && pos.X > refSub.WorldPosition.X)
353  {
354  closestDist = dist;
355  chosenPosition = position;
356  }
357  }
358  }
359  }
360  //only found a spawnpos that's very far from the sub, pick one that's closer
361  //and wait for the sub to move further before spawning
362  if (closestDist > 15000.0f * 15000.0f)
363  {
364  foreach (var position in availablePositions)
365  {
366  float dist = Vector2.DistanceSquared(position.Position.ToVector2(), refSub.WorldPosition);
367  if (dist < closestDist)
368  {
369  closestDist = dist;
370  chosenPosition = position;
371  }
372  }
373  }
374  }
375  else
376  {
377  if (!isRuinOrWreckOrCave)
378  {
379  float minDistance = 20000;
380  for (int i = 0; i < Submarine.MainSubs.Length; i++)
381  {
382  if (Submarine.MainSubs[i] == null) { continue; }
383  availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
384  }
385  }
386  if (availablePositions.None())
387  {
388  //no suitable position found, disable the event
389  spawnPos = null;
390  disallowed = true;
391  return;
392  }
393  chosenPosition = availablePositions.GetRandomUnsynced();
394  }
395  if (chosenPosition.IsValid)
396  {
397  spawnPos = chosenPosition.Position.ToVector2();
398  if (chosenPosition.Submarine != null || chosenPosition.Ruin != null)
399  {
400  var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag);
401  if (spawnPoint != null)
402  {
403  System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine));
404  spawnPos = spawnPoint.WorldPosition;
405  }
406  else
407  {
408  //no suitable position found, disable the event
409  spawnPos = null;
410  disallowed = true;
411  return;
412  }
413  }
414  else if (chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath)
415  {
416  if (offset > 0)
417  {
418  var tunnelType = chosenPosition.PositionType == Level.PositionType.MainPath ? Level.TunnelType.MainPath : Level.TunnelType.SidePath;
419  var waypoints = WayPoint.WayPointList.FindAll(wp =>
420  wp.Submarine == null &&
421  wp.Ruin == null &&
422  wp.Tunnel?.Type == tunnelType &&
423  wp.WorldPosition.X > spawnPos.Value.X);
424 
425  if (waypoints.None())
426  {
427  DebugConsole.AddWarning($"Failed to find a spawn position offset from {spawnPos.Value}.",
429  }
430  else
431  {
432  float offsetSqr = offset * offset;
433  //find the waypoint whose distance from the spawnPos is closest to the desired offset
434  var targetWaypoint = waypoints.OrderBy(wp =>
435  Math.Abs(Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value) - offsetSqr)).FirstOrDefault();
436  if (targetWaypoint != null)
437  {
438  spawnPos = targetWaypoint.WorldPosition;
439  }
440  }
441  }
442  // Ensure that the position is not inside a submarine (in practice wrecks).
443  if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(spawnPos.Value)))
444  {
445  //no suitable position found, disable the event
446  spawnPos = null;
447  disallowed = true;
448  return;
449  }
450  }
451  spawnPending = true;
452  }
453  }
454 
455  private float GetMinDistanceToSub(Submarine submarine)
456  {
457  float minDist = Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f);
458  if (SpawnPosType.HasFlag(Level.PositionType.Abyss))
459  {
460  minDist *= 2;
461  }
462  return minDist;
463  }
464 
465  public override void Update(float deltaTime)
466  {
467  if (disallowed) { return; }
468 
469  if (resetTimer > 0)
470  {
471  resetTimer -= deltaTime;
472  if (resetTimer <= 0)
473  {
474  if (ParentSet?.ResetTime > 0)
475  {
476  // If parent has reset time defined, the set is recreated. Otherwise we'll just reset this event.
477  Finish();
478  }
479  else
480  {
481  spawnReady = false;
482  spawnPos = null;
483  }
484  }
485  return;
486  }
487 
488  if (spawnPos == null)
489  {
490  if (MaxAmountPerLevel < int.MaxValue)
491  {
492  if (Character.CharacterList.Count(c => c.SpeciesName == SpeciesName) >= MaxAmountPerLevel)
493  {
494  // If the event is set to reset, let's just wait until the old corpse is removed (after being disabled).
495  if (resetTime == 0)
496  {
497  disallowed = true;
498  }
499  return;
500  }
501  }
502 
503  FindSpawnPosition(affectSubImmediately: true);
504  //the event gets marked as disallowed if a spawn point is not found
505  if (isFinished || disallowed) { return; }
506  spawnPending = true;
507  }
508 
509  if (spawnPending)
510  {
511  System.Diagnostics.Debug.Assert(spawnPos.HasValue);
512  if (spawnPos == null)
513  {
514  disallowed = true;
515  return;
516  }
517  //wait until there are no submarines at the spawnpos
518  if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath) || SpawnPosType.HasFlag(Level.PositionType.Abyss))
519  {
520  foreach (Submarine submarine in Submarine.Loaded)
521  {
522  if (submarine.Info.Type != SubmarineType.Player) { continue; }
523  float minDist = GetMinDistanceToSub(submarine);
524  if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist)
525  {
526  // Too close to a player sub.
527  return;
528  }
529  }
530  }
531  float spawnDistance = SpawnDistance;
532  if (spawnDistance <= 0)
533  {
534  if (SpawnPosType.HasFlag(Level.PositionType.Cave))
535  {
536  spawnDistance = 8000;
537  }
538  else if (SpawnPosType.HasFlag(Level.PositionType.Ruin))
539  {
540  spawnDistance = 5000;
541  }
542  else if (SpawnPosType.HasFlag(Level.PositionType.Wreck) || SpawnPosType.HasFlag(Level.PositionType.BeaconStation))
543  {
544  spawnDistance = 3000;
545  }
546  }
547  if (spawnDistance > 0)
548  {
549  bool someoneNearby = false;
550  foreach (Submarine submarine in Submarine.Loaded)
551  {
552  if (submarine.Info.Type != SubmarineType.Player) { continue; }
553  float distanceSquared = Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value);
554  if (distanceSquared < MathUtils.Pow2(spawnDistance))
555  {
556  someoneNearby = true;
557  if (chosenPosition.Submarine != null)
558  {
559  Vector2 from = Submarine.GetRelativeSimPositionFromWorldPosition(spawnPos.Value, chosenPosition.Submarine, chosenPosition.Submarine);
560  Vector2 to = Submarine.GetRelativeSimPositionFromWorldPosition(submarine.WorldPosition, chosenPosition.Submarine, submarine);
561  if (CheckLineOfSight(from, to, chosenPosition.Submarine))
562  {
563  // Line of sight to a player sub -> don't spawn yet.
564  return;
565  }
566  }
567  else
568  {
569  break;
570  }
571  }
572  }
573  foreach (Character c in Character.CharacterList)
574  {
575  if (c == Character.Controlled || c.IsRemotePlayer)
576  {
577  float distanceSquared = Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value);
578  if (distanceSquared < MathUtils.Pow2(spawnDistance))
579  {
580  someoneNearby = true;
581  if (chosenPosition.Submarine != null)
582  {
583  Vector2 from = Submarine.GetRelativeSimPositionFromWorldPosition(spawnPos.Value, chosenPosition.Submarine, chosenPosition.Submarine);
585  if (CheckLineOfSight(from, to, chosenPosition.Submarine))
586  {
587  // Line of sight to a player character -> don't spawn. Disable the event to prevent monsters "magically" spawning here.
588  disallowed = true;
589  return;
590  }
591  }
592  else
593  {
594  break;
595  }
596  }
597  }
598  }
599  if (!someoneNearby) { return; }
600 
601  static bool CheckLineOfSight(Vector2 from, Vector2 to, Submarine targetSub)
602  {
603  var bodies = Submarine.PickBodies(from, to, ignoredBodies: null, Physics.CollisionWall);
604  foreach (var b in bodies)
605  {
606  if (b.UserData is ISpatialEntity spatialEntity && spatialEntity.Submarine != targetSub)
607  {
608  // Different sub -> ignore
609  continue;
610  }
611  if (b.UserData is Structure s && !s.IsPlatform && s.CastShadow)
612  {
613  return false;
614  }
615  if (b.UserData is Item item && item.GetComponent<Door>() is Door door)
616  {
617  if (!door.IsBroken && !door.IsOpen)
618  {
619  return false;
620  }
621  }
622  }
623  return true;
624  }
625  }
626 
627  if (SpawnPosType.HasFlag(Level.PositionType.Abyss) || SpawnPosType.HasFlag(Level.PositionType.AbyssCave))
628  {
629  bool anyInAbyss = false;
630  foreach (Submarine submarine in Submarine.Loaded)
631  {
632  if (submarine.Info.Type != SubmarineType.Player || submarine.IsRespawnShuttle) { continue; }
633  if (submarine.WorldPosition.Y < 0)
634  {
635  anyInAbyss = true;
636  break;
637  }
638  }
639  if (!anyInAbyss) { return; }
640  }
641 
642  spawnPending = false;
643 
644  float scatterAmount = scatter;
645  if (SpawnPosType.HasFlag(Level.PositionType.SidePath))
646  {
647  var sidePaths = Level.Loaded.Tunnels.Where(t => t.Type == Level.TunnelType.SidePath);
648  if (sidePaths.Any())
649  {
650  scatterAmount = Math.Min(scatter, sidePaths.Min(t => t.MinWidth) / 2);
651  }
652  else
653  {
654  scatterAmount = scatter;
655  }
656  }
657  else if (SpawnPosType.IsIndoorsArea())
658  {
659  scatterAmount = 0;
660  }
661 
662  int i = 0;
663  foreach (Character monster in monsters)
664  {
665  CoroutineManager.Invoke(() =>
666  {
667  //round ended before the coroutine finished
668  if (GameMain.GameSession == null || Level.Loaded == null) { return; }
669 
670  if (monster.Removed) { return; }
671 
672  System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events.");
673 
674  Vector2 pos = spawnPos.Value;
675  if (scatterAmount > 0)
676  {
677  //try finding an offset position that's not inside a wall
678  int tries = 10;
679  do
680  {
681  tries--;
682  pos = spawnPos.Value + Rand.Vector(Rand.Range(0.0f, scatterAmount));
683 
684  bool isValidPos = true;
685  if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos)) ||
686  Level.Loaded.Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).ContainsWorld(pos)) ||
688  {
689  isValidPos = false;
690  }
691  else if (SpawnPosType.HasFlag(Level.PositionType.Cave) || SpawnPosType.HasFlag(Level.PositionType.AbyssCave))
692  {
693  //trying to spawn in a cave, but the position is not inside a cave -> not valid
694  if (Level.Loaded.Caves.None(c => c.Area.Contains(pos)))
695  {
696  isValidPos = false;
697  }
698  }
699 
700  if (isValidPos)
701  {
702  //not inside anything, all good!
703  break;
704  }
705  // This was the last try and couldn't find an offset position, let's use the exact spawn position.
706  if (tries == 0)
707  {
708  pos = spawnPos.Value;
709  }
710  } while (tries > 0);
711  }
712 
713  monster.Enabled = true;
714  monster.DisabledByEvent = false;
715  monster.AnimController.SetPosition(FarseerPhysics.ConvertUnits.ToSimUnits(pos));
716 
717  var eventManager = GameMain.GameSession.EventManager;
718  if (eventManager != null && monster.Params.AI != null)
719  {
720  if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath))
721  {
722  eventManager.CumulativeMonsterStrengthMain += monster.Params.AI.CombatStrength;
723  eventManager.AddTimeStamp(this);
724  }
725  else if (SpawnPosType.HasFlag(Level.PositionType.Ruin))
726  {
727  eventManager.CumulativeMonsterStrengthRuins += monster.Params.AI.CombatStrength;
728  }
729  else if (SpawnPosType.HasFlag(Level.PositionType.Wreck))
730  {
731  eventManager.CumulativeMonsterStrengthWrecks += monster.Params.AI.CombatStrength;
732  }
733  else if (SpawnPosType.HasFlag(Level.PositionType.Cave))
734  {
735  eventManager.CumulativeMonsterStrengthCaves += monster.Params.AI.CombatStrength;
736  }
737  }
738 
739  if (monster == monsters.Last())
740  {
741  spawnReady = true;
742  //this will do nothing if the monsters have no swarm behavior defined,
743  //otherwise it'll make the spawned characters act as a swarm
744  SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
745  DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI?.CombatStrength ?? 0))}.", Color.LightBlue, debugOnly: true);
746  }
747 
748  if (GameMain.GameSession != null)
749  {
750  GameAnalyticsManager.AddDesignEvent(
751  $"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier.Value ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"}:{SpawnPosType}:{SpeciesName}",
752  value: GameMain.GameSession.RoundDuration);
753  }
754  }, delayBetweenSpawns * i);
755  i++;
756  }
757  }
758 
759  if (spawnReady)
760  {
761  if (monsters.None())
762  {
763  Finish();
764  }
765  else if (monsters.All(m => m.IsDead))
766  {
767  if (resetTime > 0)
768  {
769  resetTimer = resetTime;
770  }
771  else
772  {
773  Finish();
774  }
775  }
776  }
777  }
778  }
779 }
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
readonly float MaxStrength
The maximum strength this affliction can have.
static AfflictionPrefab RadiationSickness
void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking=true, bool ignoreUnkillability=false, bool recalculateVitality=true)
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
void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage=false, bool log=true)
bool DisabledByEvent
MonsterEvents disable monsters (which includes removing them from the character list,...
bool IsRemotePlayer
Is the character controlled by another human player (should always be false in single player)
AIParams AI
Parameters for EnemyAIController. Not used by HumanAIController.
static CharacterPrefab FindByFilePath(string filePath)
static CharacterPrefab FindBySpeciesName(Identifier speciesName)
string? GetAttributeString(string key, string? def)
float GetAttributeFloat(string key, float def)
bool GetAttributeBool(string key, bool def)
int GetAttributeInt(string key, int def)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
static void RemoveAll()
Definition: Entity.cs:212
Submarine Submarine
Definition: Entity.cs:53
virtual Vector2 Position
Definition: Entity.cs:47
Func< Level.InterestingPosition, bool > SpawnPosFilter
Definition: Event.cs:22
EventSet ParentSet
Definition: Event.cs:18
EventPrefab Prefab
Definition: Event.cs:16
virtual void Finish()
Definition: Event.cs:73
readonly EventPrefab prefab
Definition: Event.cs:14
bool isFinished
Definition: Event.cs:10
static ISpatialEntity GetRefEntity()
Get the entity that should be used in determining how far the player has progressed in the level....
readonly ContentXElement ConfigElement
Definition: EventPrefab.cs:13
Event sets are sets of random events that occur within a level (most commonly, monster spawns and scr...
Definition: EventSet.cs:31
readonly float ResetTime
If set, the event set can trigger again after this amount of seconds has passed since it last trigger...
Definition: EventSet.cs:232
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
override string ToString()
Definition: MonsterEvent.cs:88
override void Update(float deltaTime)
MonsterEvent(EventPrefab prefab, int seed)
readonly Identifier SpeciesName
The name of the species to spawn
Definition: MonsterEvent.cs:16
IReadOnlyList< Character > Monsters
Definition: MonsterEvent.cs:79
readonly int MaxAmount
Maximum amount of monsters to spawn. You can also use "Amount" if you want to spawn a fixed number of...
Definition: MonsterEvent.cs:25
readonly float SpawnDistance
The monsters are spawned at least this distance away from the players and submarines.
Definition: MonsterEvent.cs:32
readonly int MinAmount
Minimum amount of monsters to spawn. You can also use "Amount" if you want to spawn a fixed number of...
Definition: MonsterEvent.cs:21
readonly int MaxAmountPerLevel
Maximum number of the specific type of monster in the entire level. Can be used to prevent the event ...
Definition: MonsterEvent.cs:75
override Vector2?? DebugDrawPos
Definition: MonsterEvent.cs:84
override string GetDebugInfo()
readonly Level.PositionType SpawnPosType
Where should the monster spawn?
Definition: MonsterEvent.cs:62
override void InitEventSpecific(EventSet parentSet)
override IEnumerable< ContentFile > GetFilesToPreload()
readonly ContentFile ContentFile
Definition: Prefab.cs:35
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
void SetPosition(Vector2 simPosition, bool lerp=false, bool ignorePlatforms=true, bool forceMainLimbToCollider=false, bool moveLatchers=true)
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
static Vector2 GetRelativeSimPositionFromWorldPosition(Vector2 targetWorldPos, Submarine fromSub, Submarine toSub)
static IEnumerable< Body > PickBodies(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
Returns a list of physics bodies the ray intersects with, sorted according to distance (the closest b...
InterestingPosition(Point position, PositionType positionType, Submarine submarine=null, bool isValid=true)