2 using Microsoft.Xna.Framework;
4 using System.Collections.Generic;
46 [
Serialize(
"",
IsPropertySaveable.Yes, description:
"The spawned entity will be assigned this tag. The tag can be used to refer to the entity by other actions of the event.")]
64 [
Serialize(
false,
IsPropertySaveable.Yes, description:
"Should we spawn the entity even when no spawn points with matching tags were found?")]
67 private readonly HashSet<Identifier> targetModuleTags =
new HashSet<Identifier>();
69 [
Serialize(
true,
IsPropertySaveable.Yes, description:
"If false, we won't spawn another character if one with the same identifier has already been spawned.")]
75 [
Serialize(
true,
IsPropertySaveable.Yes, description:
"Should the item be spawned even if the target inventory is full (just spawning it at the position of the target)? Only valid if spawning an item in an inventory.")]
84 get =>
string.Join(
",", targetModuleTags);
87 targetModuleTags.Clear();
88 if (!
string.IsNullOrWhiteSpace(value))
90 string[] splitTags = value.Split(
',');
91 foreach (var s
in splitTags)
93 targetModuleTags.Add(s.ToIdentifier());
99 [
Serialize(
false,
IsPropertySaveable.Yes, description:
"Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")]
102 [
Serialize(
true,
IsPropertySaveable.Yes, description:
"If disabled, the action will choose a spawn position away from players' views if one is available.")]
105 [
Serialize(
false,
IsPropertySaveable.Yes, description:
"Should the event continue even if the entity failed to spawn for whatever reason?")]
108 private bool spawned;
109 private Entity spawnedEntity;
111 private readonly
bool ignoreSpawnPointType;
115 ignoreSpawnPointType = element.
GetAttribute(
"spawnpointtype") ==
null;
117 TeamID = element.GetAttributeEnum(
"teamtag", element.GetAttributeEnum(
"team",
TeamID));
120 DebugConsole.ThrowError(
121 $
"Error in even \"{(parentEvent.Prefab?.Identifier.ToString() ?? "unknown
")}\". " +
122 $
"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?",
142 spawnedEntity =
null;
145 public override void Update(
float deltaTime)
147 if (spawned) {
return; }
155 TryFindHumanPrefab(startLocation.Faction) ??
156 TryFindHumanPrefab(startLocation.SecondaryFaction);
160 if (faction ==
null) {
return null; }
167 NPCIdentifier.Replace(
"[faction]".ToIdentifier(),
"coalition".ToIdentifier()),
173 if (humanPrefab !=
null)
182 if (spawnPos !=
null)
184 for (
int i = 0; i <
Amount; i++)
188 if (newCharacter == null) { return; }
189 newCharacter.HumanPrefab = humanPrefab;
194 newCharacter.SetOriginalTeamAndChangeTeam(
TeamID, processImmediately:
true);
195 newCharacter.EnableDespawn =
false;
196 humanPrefab.
GiveItems(newCharacter, newCharacter.Submarine, spawnPos as
WayPoint);
199 foreach (
Item item
in newCharacter.Inventory.FindAllItems(recursive:
true))
201 item.SpawnedInCurrentOutpost = true;
202 item.AllowStealing = false;
206 if (!
TargetTag.IsEmpty && newCharacter !=
null)
208 ParentEvent.AddTarget(TargetTag, newCharacter);
210 spawnedEntity = newCharacter;
213 outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier);
214 foreach (Identifier tag in humanPrefab.GetTags())
216 outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag);
220 newCharacter.LoadTalents();
228 else if (!SpeciesName.IsEmpty)
230 if (!AllowDuplicates && Character.CharacterList.Any(c => c.SpeciesName == SpeciesName))
235 ISpatialEntity spawnPos = GetSpawnPos();
236 if (spawnPos !=
null)
238 for (
int i = 0; i < Amount; i++)
240 Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter =>
242 if (!TargetTag.IsEmpty && newCharacter != null)
244 ParentEvent.AddTarget(TargetTag, newCharacter);
246 spawnedEntity = newCharacter;
251 else if (!ItemIdentifier.IsEmpty || !ItemTag.IsEmpty)
253 ItemPrefab itemPrefab =
null;
254 if (!ItemIdentifier.IsEmpty)
256 itemPrefab = MapEntityPrefab.FindByIdentifier(ItemIdentifier) as ItemPrefab;
257 if (itemPrefab ==
null)
259 DebugConsole.ThrowError($
"Error in SpawnAction (item prefab \"{ItemIdentifier}\" not found)",
260 contentPackage: ParentEvent.Prefab.ContentPackage);
263 else if (!ItemTag.IsEmpty)
265 itemPrefab = ItemPrefab.Prefabs.Where(ip => ip.Tags.Contains(ItemTag)).GetRandom(Rand.RandSync.Unsynced);
268 Inventory spawnInventory =
null;
269 if (!TargetInventory.IsEmpty)
271 var targets = ParentEvent.GetTargets(TargetInventory);
274 var target = targets.First(t => t is Item || t is Character);
275 if (target is Character character)
277 spawnInventory = character.Inventory;
279 else if (target is Item item)
281 spawnInventory = item.OwnInventory;
285 if (spawnInventory ==
null)
287 DebugConsole.ThrowError($
"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.",
288 contentPackage: ParentEvent.Prefab.ContentPackage);
292 if (spawnInventory ==
null)
294 ISpatialEntity spawnPos = GetSpawnPos();
295 if (spawnPos !=
null)
297 for (
int i = 0; i < Amount; i++)
299 Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned);
305 for (
int i = 0; i < Amount; i++)
307 Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, spawnIfInventoryFull: SpawnIfInventoryFull, onSpawned: onSpawned);
311 void onSpawned(Item newItem)
315 if (!TargetTag.IsEmpty)
317 ParentEvent.AddTarget(TargetTag, newItem);
321 newItem.AddTag(
"ignorebyai");
324 spawnedEntity = newItem;
335 pos += Rand.Vector(offset);
338 float margin = 50.0f;
348 if (!SpawnPointTag.IsEmpty)
350 IEnumerable<Item> potentialItems =
Item.
ItemList.Where(it => IsValidSubmarineType(SpawnLocation, it.Submarine));
351 if (!AllowInPlayerView)
353 potentialItems = GetEntitiesNotInPlayerView(potentialItems);
355 var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced();
356 if (item !=
null) {
return item; }
358 var potentialTargets = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine));
359 if (!AllowInPlayerView)
361 potentialTargets = GetEntitiesNotInPlayerView(potentialTargets);
363 var target = potentialTargets.GetRandomUnsynced();
364 if (target !=
null) {
return target; }
368 if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; }
370 return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag, allowInPlayerView: AllowInPlayerView);
373 private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine)
375 return spawnLocation
switch
377 SpawnLocationType.Any =>
true,
378 SpawnLocationType.MainSub => submarine ==
Submarine.MainSub,
379 SpawnLocationType.NearMainSub or SpawnLocationType.MainPath or SpawnLocationType.Cave or SpawnLocationType.AbyssCave => submarine ==
null,
380 SpawnLocationType.Outpost => submarine is { Info.IsOutpost:
true },
381 SpawnLocationType.Wreck => submarine is { Info.IsWreck:
true },
382 SpawnLocationType.Ruin => submarine is { Info.IsRuin:
true },
383 SpawnLocationType.BeaconStation => submarine?.Info?.BeaconStationInfo !=
null,
384 _ =>
throw new NotImplementedException(),
391 private static IEnumerable<T> GetEntitiesNotInPlayerView<T>(IEnumerable<T> entities) where T : ISpatialEntity
393 if (entities.Any(e => !IsInPlayerView(e)))
395 return entities.Where(e => !IsInPlayerView(e));
400 private static bool IsInPlayerView(ISpatialEntity entity)
402 foreach (var character
in Character.CharacterList)
404 if (!character.IsPlayer || character.IsDead) {
continue; }
405 if (character.CanSeeTarget(entity)) {
return true; }
410 public static WayPoint GetSpawnPos(
SpawnLocationType spawnLocation,
SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags =
null, IEnumerable<Identifier> spawnpointTags =
null,
bool asFarAsPossibleFromAirlock =
false,
bool requireTaggedSpawnPoint =
false,
bool allowInPlayerView =
true)
413 List<WayPoint> potentialSpawnPoints =
WayPoint.
WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull !=
null || !requireHull));
414 potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor ==
null && wp.Ladders ==
null && wp.IsTraversable);
415 if (moduleFlags !=
null && moduleFlags.Any())
417 var spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull is
Hull h && h.
OutpostModuleTags.Any(moduleFlags.Contains));
418 if (spawnPoints.Any())
420 potentialSpawnPoints = spawnPoints.ToList();
423 if (spawnpointTags !=
null && spawnpointTags.Any())
425 var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor ==
null && wp.IsTraversable));
426 if (requireTaggedSpawnPoint || spawnPoints.Any())
428 potentialSpawnPoints = spawnPoints.ToList();
431 if (potentialSpawnPoints.None())
433 if (requireTaggedSpawnPoint && spawnpointTags !=
null && spawnpointTags.Any())
435 DebugConsole.NewMessage($
"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation} (tag: {string.Join(",
", spawnpointTags)}), skipping.", color: Color.White);
439 DebugConsole.ThrowError($
"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})");
444 IEnumerable<WayPoint> validSpawnPoints;
445 if (spawnPointType.HasValue)
447 validSpawnPoints = potentialSpawnPoints.FindAll(wp => spawnPointType.Value.HasFlag(wp.SpawnType));
451 validSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.SpawnType !=
SpawnType.Path);
452 if (!validSpawnPoints.Any()) { validSpawnPoints = potentialSpawnPoints; }
456 var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains(
"airlock".ToIdentifier()) ??
false);
457 if (airlockSpawnPoints.Count() < validSpawnPoints.Count())
459 validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints);
462 if (validSpawnPoints.None())
464 DebugConsole.ThrowError($
"Could not find a spawn point of the correct type for a SpawnAction (spawn location: {spawnLocation}, type: {spawnPointType}, module flags: {((moduleFlags == null || !moduleFlags.Any()) ? "none
" : string.Join(",
", moduleFlags))})");
465 return potentialSpawnPoints.GetRandomUnsynced();
468 switch (spawnLocation)
472 validSpawnPoints = validSpawnPoints.Where(p =>
473 Submarine.
Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition)));
476 validSpawnPoints = validSpawnPoints.Where(p =>
478 p.Cave ==
null && p.Ruin ==
null);
482 validSpawnPoints = validSpawnPoints.Where(p => p.WorldPosition.Y >
Level.
Loaded.
AbyssStart && p.Cave !=
null);
485 validSpawnPoints = validSpawnPoints.Where(p => p.WorldPosition.Y <
Level.
Loaded.
AbyssStart && p.Cave !=
null);
490 if (validSpawnPoints.Any(wp => wp.SpawnType !=
SpawnType.Path))
492 validSpawnPoints = validSpawnPoints.Where(wp => wp.SpawnType !=
SpawnType.Path);
496 if (spawnpointTags ==
null || spawnpointTags.None())
498 var spawnPoints = validSpawnPoints.Where(wp => !wp.Tags.Any());
499 if (spawnPoints.Any())
501 validSpawnPoints = spawnPoints.ToList();
505 if (!allowInPlayerView)
507 validSpawnPoints = GetEntitiesNotInPlayerView(validSpawnPoints);
512 WayPoint closestPoint = validSpawnPoints.First();
513 float closestDist =
float.PositiveInfinity;
514 foreach (
WayPoint wp
in validSpawnPoints)
517 if (dist < closestDist)
525 else if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any())
527 WayPoint furthestPoint = validSpawnPoints.First();
528 float furthestDist = 0.0f;
529 foreach (
WayPoint waypoint
in validSpawnPoints)
531 float dist = Vector2.DistanceSquared(waypoint.
WorldPosition, airlockSpawnPoints.First().WorldPosition);
532 if (dist > furthestDist)
535 furthestPoint = waypoint;
538 return furthestPoint;
542 return validSpawnPoints.GetRandomUnsynced();
548 return $
"{ToolBox.GetDebugSymbol(spawned)} {nameof(SpawnAction)} -> (Spawned entity: {spawnedEntity.ColorizeObject()})";
static readonly List< Character > CharacterList
static readonly Identifier HumanSpeciesName
XAttribute? GetAttribute(string name)
static EntitySpawner Spawner
virtual Vector2 WorldPosition
void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, Action< Character > onSpawn=null)
readonly ScriptedEvent ParentEvent
static NetworkMember NetworkMember
IEnumerable< Identifier > OutpostModuleTags
Inherited flags from outpost generation.
static Hull FindHull(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any)
CharacterInfo CreateCharacterInfo(Rand.RandSync randSync=Rand.RandSync.Unsynced)
Creates a character info from the human prefab. If there are custom character infos defined,...
void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn=null)
bool GiveItems(Character character, Submarine submarine, WayPoint spawnPoint, Rand.RandSync randSync=Rand.RandSync.Unsynced, bool createNetworkEvents=true)
static readonly List< Item > ItemList
ContentPackage? ContentPackage
Spawns an entity (e.g. item, NPC, monster).
static Vector2 OffsetSpawnPos(Vector2 pos, float offset)
bool RequireSpawnPointTag
override bool IsFinished(ref string goTo)
Has the action finished.
override string ToDebugString()
Rich test to display in debugdraw
bool SpawnIfInventoryFull
Identifier ItemIdentifier
Identifier NPCSetIdentifier
bool ContinueIfFailedToSpawn
SpawnAction(ScriptedEvent parentEvent, ContentXElement element)
static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable< Identifier > moduleFlags=null, IEnumerable< Identifier > spawnpointTags=null, bool asFarAsPossibleFromAirlock=false, bool requireTaggedSpawnPoint=false, bool allowInPlayerView=true)
override void Update(float deltaTime)
Identifier TargetInventory
SpawnLocationType SpawnLocation
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 List< Submarine > Loaded
static List< WayPoint > WayPointList
@ Character
Characters only