1 using Microsoft.Xna.Framework;
3 using System.Collections.Generic;
27 private readonly List<Character> monsters =
new List<Character>();
37 private readonly
float scatter;
42 private readonly
float offset;
47 private readonly
float delayBetweenSpawns;
52 private float resetTime;
53 private float resetTimer;
55 private Vector2? spawnPos;
57 private bool disallowed;
67 private readonly
string spawnPointTag;
69 private bool spawnPending, spawnReady;
77 private readonly
float? overridePlayDeadProbability;
79 public IReadOnlyList<Character>
Monsters => monsters;
85 get {
return spawnPos ?? Vector2.Zero; }
92 return $
"MonsterEvent ({SpeciesName}, {SpawnPosType})";
96 return $
"MonsterEvent ({SpeciesName} x{MinAmount}-{MaxAmount}, {SpawnPosType})";
100 return $
"MonsterEvent ({SpeciesName} x{MaxAmount}, {SpawnPosType})";
109 if (characterPrefab !=
null)
120 throw new Exception(
"speciesname is null!");
143 if (playDeadProbability >= 0)
145 overridePlayDeadProbability = playDeadProbability;
151 Identifier tryKey = monsterNames.Find(s =>
SpeciesName == s);
163 private static Submarine GetReferenceSub()
173 DebugConsole.ThrowError($
"Failed to find config file for species \"{SpeciesName}\".",
188 if (GameSettings.CurrentConfig.VerboseLogging)
190 DebugConsole.NewMessage($
"PvP setting: disabling monster event ({SpeciesName})", Color.Yellow);
197 if (parentSet !=
null && resetTime == 0)
202 if (GameSettings.CurrentConfig.VerboseLogging)
204 DebugConsole.NewMessage(
"Initialized MonsterEvent (" +
SpeciesName +
")", Color.White);
211 for (
int i = 0; i < amount; i++)
214 Character createdCharacter =
Character.
Create(
SpeciesName, Vector2.Zero, seed, characterInfo:
null, isRemotePlayer:
false, hasAi:
true, createNetworkEvent:
true, throwErrorIfNotFound:
false);
215 if (createdCharacter ==
null)
217 DebugConsole.AddWarning($
"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown
"}\".",
222 if (overridePlayDeadProbability.HasValue)
235 monsters.Add(createdCharacter);
242 $
"Finished: {IsFinished.ColorizeObject()}\n" +
243 $
"Amount: {MinAmount.ColorizeObject()} - {MaxAmount.ColorizeObject()}\n" +
244 $
"Spawn pending: {SpawnPending.ColorizeObject()}\n" +
245 $
"Spawn position: {SpawnPos.ColorizeObject()}";
252 foreach (var position
in availablePositions)
256 removals.Add(position);
259 if (position.Submarine !=
null)
261 if (position.Submarine.WreckAI !=
null && position.Submarine.WreckAI.IsAlive)
263 removals.Add(position);
270 if (position.PositionType != Level.PositionType.MainPath &&
271 position.PositionType != Level.PositionType.SidePath)
275 if (Level.Loaded.IsPositionInsideWall(position.Position.ToVector2()))
277 removals.Add(position);
279 if (position.Position.Y < Level.Loaded.GetBottomPosition(position.Position.X).Y)
281 removals.Add(position);
284 removals.ForEach(r => availablePositions.Remove(r));
285 return availablePositions;
289 private void FindSpawnPosition(
bool affectSubImmediately)
291 if (disallowed) {
return; }
293 spawnPos = Vector2.Zero;
294 var availablePositions = GetAvailableSpawnPositions();
295 chosenPosition =
new Level.
InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid:
false);
296 bool isRuinOrWreckOrCave =
301 if (affectSubImmediately && !isRuinOrWreckOrCave && !
SpawnPosType.HasFlag(Level.PositionType.Abyss))
303 if (availablePositions.None())
313 refSub =
Submarine.MainSubs.GetRandom(Rand.RandSync.Unsynced);
315 float closestDist =
float.PositiveInfinity;
317 foreach (var position
in availablePositions)
319 Vector2 pos = position.Position.ToVector2();
320 float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition);
321 foreach (Submarine sub
in Submarine.Loaded)
325 !sub.IsRespawnShuttle)
330 float minDistToSub = GetMinDistanceToSub(sub);
331 if (dist < minDistToSub * minDistToSub) {
continue; }
333 if (closestDist ==
float.PositiveInfinity)
336 chosenPosition = position;
341 if (chosenPosition.
Position.X < refSub.WorldPosition.X)
343 if (dist < closestDist || pos.X > refSub.WorldPosition.X)
346 chosenPosition = position;
350 else if (chosenPosition.
Position.X > refSub.WorldPosition.X)
352 if (dist < closestDist && pos.X > refSub.WorldPosition.X)
355 chosenPosition = position;
362 if (closestDist > 15000.0f * 15000.0f)
364 foreach (var position
in availablePositions)
366 float dist = Vector2.DistanceSquared(position.Position.ToVector2(), refSub.WorldPosition);
367 if (dist < closestDist)
370 chosenPosition = position;
377 if (!isRuinOrWreckOrCave)
379 float minDistance = 20000;
380 for (
int i = 0; i <
Submarine.MainSubs.Length; i++)
382 if (
Submarine.MainSubs[i] ==
null) {
continue; }
383 availablePositions.
RemoveAll(p => Vector2.DistanceSquared(
Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
386 if (availablePositions.None())
393 chosenPosition = availablePositions.GetRandomUnsynced();
395 if (chosenPosition.IsValid)
397 spawnPos = chosenPosition.
Position.ToVector2();
398 if (chosenPosition.
Submarine !=
null || chosenPosition.Ruin !=
null)
400 var spawnPoint = WayPoint.GetRandom(
SpawnType.Enemy, sub: chosenPosition.
Submarine ?? chosenPosition.Ruin?.
Submarine, useSyncedRand:
false, spawnPointTag: spawnPointTag);
401 if (spawnPoint !=
null)
403 System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.
Submarine ?? chosenPosition.Ruin?.
Submarine));
404 spawnPos = spawnPoint.WorldPosition;
414 else if (chosenPosition.
PositionType == Level.PositionType.MainPath || chosenPosition.
PositionType == Level.PositionType.SidePath)
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 &&
422 wp.Tunnel?.Type == tunnelType &&
423 wp.WorldPosition.X > spawnPos.Value.X);
425 if (waypoints.None())
427 DebugConsole.AddWarning($
"Failed to find a spawn position offset from {spawnPos.Value}.",
432 float offsetSqr = offset * offset;
434 var targetWaypoint = waypoints.OrderBy(wp =>
435 Math.Abs(Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value) - offsetSqr)).FirstOrDefault();
436 if (targetWaypoint !=
null)
438 spawnPos = targetWaypoint.WorldPosition;
443 if (
Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(spawnPos.Value)))
455 private float GetMinDistanceToSub(Submarine submarine)
457 float minDist = Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height),
Sonar.
DefaultSonarRange * 0.9f);
465 public override void Update(
float deltaTime)
467 if (disallowed) {
return; }
471 resetTimer -= deltaTime;
488 if (spawnPos ==
null)
503 FindSpawnPosition(affectSubImmediately:
true);
511 System.Diagnostics.Debug.Assert(spawnPos.HasValue);
512 if (spawnPos ==
null)
523 float minDist = GetMinDistanceToSub(submarine);
524 if (Vector2.DistanceSquared(submarine.
WorldPosition, spawnPos.Value) < minDist * minDist)
532 if (spawnDistance <= 0)
536 spawnDistance = 8000;
540 spawnDistance = 5000;
544 spawnDistance = 3000;
547 if (spawnDistance > 0)
549 bool someoneNearby =
false;
553 float distanceSquared = Vector2.DistanceSquared(submarine.
WorldPosition, spawnPos.Value);
554 if (distanceSquared < MathUtils.Pow2(spawnDistance))
556 someoneNearby =
true;
561 if (CheckLineOfSight(from, to, chosenPosition.
Submarine))
577 float distanceSquared = Vector2.DistanceSquared(c.
WorldPosition, spawnPos.Value);
578 if (distanceSquared < MathUtils.Pow2(spawnDistance))
580 someoneNearby =
true;
585 if (CheckLineOfSight(from, to, chosenPosition.
Submarine))
599 if (!someoneNearby) {
return; }
601 static bool CheckLineOfSight(Vector2 from, Vector2 to,
Submarine targetSub)
604 foreach (var b
in bodies)
615 if (b.UserData is
Item item && item.GetComponent<
Door>() is
Door door)
617 if (!door.IsBroken && !door.IsOpen)
629 bool anyInAbyss =
false;
639 if (!anyInAbyss) {
return; }
642 spawnPending =
false;
644 float scatterAmount = scatter;
650 scatterAmount = Math.Min(scatter, sidePaths.Min(t => t.MinWidth) / 2);
654 scatterAmount = scatter;
665 CoroutineManager.Invoke(() =>
670 if (monster.
Removed) { return; }
674 Vector2 pos = spawnPos.Value;
675 if (scatterAmount > 0)
682 pos = spawnPos.Value + Rand.Vector(Rand.Range(0.0f, scatterAmount));
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)) ||
694 if (Level.Loaded.Caves.None(c => c.Area.Contains(pos)))
708 pos = spawnPos.Value;
718 if (eventManager !=
null && monster.
Params.
AI !=
null)
720 if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath))
722 eventManager.CumulativeMonsterStrengthMain += monster.Params.AI.CombatStrength;
723 eventManager.AddTimeStamp(this);
727 eventManager.CumulativeMonsterStrengthRuins += monster.Params.AI.CombatStrength;
731 eventManager.CumulativeMonsterStrengthWrecks += monster.Params.AI.CombatStrength;
735 eventManager.CumulativeMonsterStrengthCaves += monster.Params.AI.CombatStrength;
739 if (monster == monsters.Last())
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);
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);
754 }, delayBetweenSpawns * i);
765 else if (monsters.All(m => m.IsDead))
769 resetTimer = resetTime;
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)
readonly CharacterParams Params
CharacterHealth CharacterHealth
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 EvaluatePlayDeadProbability(float? probability=null)
static readonly List< Character > CharacterList
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,...
static Character? Controlled
readonly AnimController AnimController
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
Func< Level.InterestingPosition, bool > SpawnPosFilter
readonly EventPrefab prefab
static ISpatialEntity GetRefEntity()
Get the entity that should be used in determining how far the player has progressed in the level....
readonly ContentXElement ConfigElement
Event sets are sets of random events that occur within a level (most commonly, monster spawns and scr...
readonly float ResetTime
If set, the event set can trigger again after this amount of seconds has passed since it last trigger...
static GameSession?? GameSession
static NetworkMember NetworkMember
bool IsCurrentLocationRadiated()
readonly EventManager EventManager
const float DefaultSonarRange
bool IsPositionInsideWall(Vector2 worldPosition)
List< InterestingPosition > PositionsOfInterest
override string ToString()
override void Update(float deltaTime)
MonsterEvent(EventPrefab prefab, int seed)
readonly Identifier SpeciesName
The name of the species to spawn
IReadOnlyList< Character > Monsters
readonly int MaxAmount
Maximum amount of monsters to spawn. You can also use "Amount" if you want to spawn a fixed number of...
readonly float SpawnDistance
The monsters are spawned at least this distance away from the players and submarines.
readonly int MinAmount
Minimum amount of monsters to spawn. You can also use "Amount" if you want to spawn a fixed number of...
readonly int MaxAmountPerLevel
Maximum number of the specific type of monster in the entire level. Can be used to prevent the event ...
override Vector2?? DebugDrawPos
override string GetDebugInfo()
readonly Level.PositionType SpawnPosType
Where should the monster spawn?
override void InitEventSpecific(EventSet parentSet)
override IEnumerable< ContentFile > GetFilesToPreload()
readonly ContentFile ContentFile
ContentPackage? ContentPackage
readonly Identifier Identifier
void SetPosition(Vector2 simPosition, bool lerp=false, bool ignorePlatforms=true, bool forceMainLimbToCollider=false, bool moveLatchers=true)
override Vector2? WorldPosition
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 List< Submarine > Loaded
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)