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