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 public IReadOnlyList<Character>
Monsters => monsters;
83 get {
return spawnPos ?? Vector2.Zero; }
90 return $
"MonsterEvent ({SpeciesName}, {SpawnPosType})";
94 return $
"MonsterEvent ({SpeciesName} x{MinAmount}-{MaxAmount}, {SpawnPosType})";
98 return $
"MonsterEvent ({SpeciesName} x{MaxAmount}, {SpawnPosType})";
107 if (characterPrefab !=
null)
118 throw new Exception(
"speciesname is null!");
144 Identifier tryKey = monsterNames.Find(s =>
SpeciesName == s);
156 private static Submarine GetReferenceSub()
166 DebugConsole.ThrowError($
"Failed to find config file for species \"{SpeciesName}\".",
178 if (parentSet !=
null && resetTime == 0)
183 if (GameSettings.CurrentConfig.VerboseLogging)
185 DebugConsole.NewMessage(
"Initialized MonsterEvent (" +
SpeciesName +
")", Color.White);
192 for (
int i = 0; i < amount; i++)
195 Character createdCharacter =
Character.
Create(
SpeciesName, Vector2.Zero, seed, characterInfo:
null, isRemotePlayer:
false, hasAi:
true, createNetworkEvent:
true, throwErrorIfNotFound:
false);
196 if (createdCharacter ==
null)
198 DebugConsole.AddWarning($
"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown
"}\".",
212 monsters.Add(createdCharacter);
219 $
"Finished: {IsFinished.ColorizeObject()}\n" +
220 $
"Amount: {MinAmount.ColorizeObject()} - {MaxAmount.ColorizeObject()}\n" +
221 $
"Spawn pending: {SpawnPending.ColorizeObject()}\n" +
222 $
"Spawn position: {SpawnPos.ColorizeObject()}";
229 foreach (var position
in availablePositions)
233 removals.Add(position);
236 if (position.Submarine !=
null)
238 if (position.Submarine.WreckAI !=
null && position.Submarine.WreckAI.IsAlive)
240 removals.Add(position);
247 if (position.PositionType != Level.PositionType.MainPath &&
248 position.PositionType != Level.PositionType.SidePath)
252 if (Level.Loaded.IsPositionInsideWall(position.Position.ToVector2()))
254 removals.Add(position);
256 if (position.Position.Y < Level.Loaded.GetBottomPosition(position.Position.X).Y)
258 removals.Add(position);
261 removals.ForEach(r => availablePositions.Remove(r));
262 return availablePositions;
266 private void FindSpawnPosition(
bool affectSubImmediately)
268 if (disallowed) {
return; }
270 spawnPos = Vector2.Zero;
271 var availablePositions = GetAvailableSpawnPositions();
272 chosenPosition =
new Level.
InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid:
false);
273 bool isRuinOrWreckOrCave =
278 if (affectSubImmediately && !isRuinOrWreckOrCave && !
SpawnPosType.HasFlag(Level.PositionType.Abyss))
280 if (availablePositions.None())
290 refSub =
Submarine.MainSubs.GetRandom(Rand.RandSync.Unsynced);
292 float closestDist =
float.PositiveInfinity;
294 foreach (var position
in availablePositions)
296 Vector2 pos = position.Position.ToVector2();
297 float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition);
298 foreach (Submarine sub
in Submarine.Loaded)
302 sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
307 float minDistToSub = GetMinDistanceToSub(sub);
308 if (dist < minDistToSub * minDistToSub) {
continue; }
310 if (closestDist ==
float.PositiveInfinity)
313 chosenPosition = position;
318 if (chosenPosition.
Position.X < refSub.WorldPosition.X)
320 if (dist < closestDist || pos.X > refSub.WorldPosition.X)
323 chosenPosition = position;
327 else if (chosenPosition.
Position.X > refSub.WorldPosition.X)
329 if (dist < closestDist && pos.X > refSub.WorldPosition.X)
332 chosenPosition = position;
339 if (closestDist > 15000.0f * 15000.0f)
341 foreach (var position
in availablePositions)
343 float dist = Vector2.DistanceSquared(position.Position.ToVector2(), refSub.WorldPosition);
344 if (dist < closestDist)
347 chosenPosition = position;
354 if (!isRuinOrWreckOrCave)
356 float minDistance = 20000;
357 for (
int i = 0; i <
Submarine.MainSubs.Length; i++)
359 if (
Submarine.MainSubs[i] ==
null) {
continue; }
360 availablePositions.
RemoveAll(p => Vector2.DistanceSquared(
Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
363 if (availablePositions.None())
370 chosenPosition = availablePositions.GetRandomUnsynced();
372 if (chosenPosition.IsValid)
374 spawnPos = chosenPosition.
Position.ToVector2();
375 if (chosenPosition.
Submarine !=
null || chosenPosition.Ruin !=
null)
377 var spawnPoint = WayPoint.GetRandom(
SpawnType.Enemy, sub: chosenPosition.
Submarine ?? chosenPosition.Ruin?.
Submarine, useSyncedRand:
false, spawnPointTag: spawnPointTag);
378 if (spawnPoint !=
null)
380 System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.
Submarine ?? chosenPosition.Ruin?.
Submarine));
381 spawnPos = spawnPoint.WorldPosition;
391 else if (chosenPosition.
PositionType == Level.PositionType.MainPath || chosenPosition.
PositionType == Level.PositionType.SidePath)
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 &&
399 wp.Tunnel?.Type == tunnelType &&
400 wp.WorldPosition.X > spawnPos.Value.X);
402 if (waypoints.None())
404 DebugConsole.AddWarning($
"Failed to find a spawn position offset from {spawnPos.Value}.",
409 float offsetSqr = offset * offset;
411 var targetWaypoint = waypoints.OrderBy(wp =>
412 Math.Abs(Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value) - offsetSqr)).FirstOrDefault();
413 if (targetWaypoint !=
null)
415 spawnPos = targetWaypoint.WorldPosition;
420 if (
Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(spawnPos.Value)))
432 private float GetMinDistanceToSub(Submarine submarine)
434 float minDist = Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height),
Sonar.
DefaultSonarRange * 0.9f);
442 public override void Update(
float deltaTime)
444 if (disallowed) {
return; }
448 resetTimer -= deltaTime;
465 if (spawnPos ==
null)
480 FindSpawnPosition(affectSubImmediately:
true);
488 System.Diagnostics.Debug.Assert(spawnPos.HasValue);
489 if (spawnPos ==
null)
500 float minDist = GetMinDistanceToSub(submarine);
501 if (Vector2.DistanceSquared(submarine.
WorldPosition, spawnPos.Value) < minDist * minDist)
509 if (spawnDistance <= 0)
513 spawnDistance = 8000;
517 spawnDistance = 5000;
521 spawnDistance = 3000;
524 if (spawnDistance > 0)
526 bool someoneNearby =
false;
530 float distanceSquared = Vector2.DistanceSquared(submarine.
WorldPosition, spawnPos.Value);
531 if (distanceSquared < MathUtils.Pow2(spawnDistance))
533 someoneNearby =
true;
538 if (CheckLineOfSight(from, to, chosenPosition.
Submarine))
554 float distanceSquared = Vector2.DistanceSquared(c.
WorldPosition, spawnPos.Value);
555 if (distanceSquared < MathUtils.Pow2(spawnDistance))
557 someoneNearby =
true;
562 if (CheckLineOfSight(from, to, chosenPosition.
Submarine))
576 if (!someoneNearby) {
return; }
578 static bool CheckLineOfSight(Vector2 from, Vector2 to,
Submarine targetSub)
581 foreach (var b
in bodies)
592 if (b.UserData is
Item item && item.GetComponent<
Door>() is
Door door)
594 if (!door.IsBroken && !door.IsOpen)
606 bool anyInAbyss =
false;
616 if (!anyInAbyss) {
return; }
619 spawnPending =
false;
621 float scatterAmount = scatter;
627 scatterAmount = Math.Min(scatter, sidePaths.Min(t => t.MinWidth) / 2);
631 scatterAmount = scatter;
642 CoroutineManager.Invoke(() =>
647 if (monster.
Removed) { return; }
651 Vector2 pos = spawnPos.Value;
652 if (scatterAmount > 0)
659 pos = spawnPos.Value + Rand.Vector(Rand.Range(0.0f, scatterAmount));
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)) ||
671 if (Level.Loaded.Caves.None(c => c.Area.Contains(pos)))
685 pos = spawnPos.Value;
695 if (eventManager !=
null && monster.
Params.
AI !=
null)
697 if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath))
699 eventManager.CumulativeMonsterStrengthMain += monster.Params.AI.CombatStrength;
700 eventManager.AddTimeStamp(this);
704 eventManager.CumulativeMonsterStrengthRuins += monster.Params.AI.CombatStrength;
708 eventManager.CumulativeMonsterStrengthWrecks += monster.Params.AI.CombatStrength;
712 eventManager.CumulativeMonsterStrengthCaves += monster.Params.AI.CombatStrength;
716 if (monster == monsters.Last())
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);
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);
731 }, delayBetweenSpawns * i);
742 else if (monsters.All(m => m.IsDead))
746 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)
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
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 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)