7 using Microsoft.Xna.Framework;
11 internal partial class EntitySpawnerComponent : ItemComponent, IDrawableComponent
19 [
Editable, Serialize(
"",
IsPropertySaveable.Yes,
"Identifier of the item to spawn, does nothing if SpeciesName is set. Separate by comma to have multiple items spawn at random.")]
20 public string? ItemIdentifier {
get;
set; }
22 [
Editable, Serialize(
"",
IsPropertySaveable.Yes,
"Species name of the creature to spawn, takes priority if ItemIdentifier is set. Separate by comma to have multiple creatures spawn at random.")]
23 public string? SpeciesName {
get;
set; }
26 public bool OnlySpawnWhenCrewInRange {
get;
set; }
29 public AreaShape CrewAreaShape {
get;
set; }
31 [
Editable(MaxValueFloat =
int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(
"500,500",
IsPropertySaveable.Yes,
"Size of the rectangle where crew members need to stay. Does nothing if CrewAreaShape is set to Circle")]
32 public Vector2 CrewAreaBounds {
get;
set; }
34 [
Editable(MaxValueFloat =
int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f,
IsPropertySaveable.Yes,
"Radius of the circle to spawn stuff in. Does nothing if CrewAreaShape is set to Rectangle")]
35 public float CrewAreaRadius {
get;
set; }
37 [
Editable(MaxValueFloat =
int.MaxValue, MinValueFloat =
int.MinValue, ValueStep = 10f), Serialize(
"0,0",
IsPropertySaveable.Yes,
"Offset of the crew area from the center of the item")]
38 public Vector2 CrewAreaOffset {
get;
set; }
41 public AreaShape SpawnAreaShape {
get;
set; }
43 [
Editable(MaxValueFloat =
int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(
"500,500",
IsPropertySaveable.Yes,
"Size of the rectangle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Circle")]
44 public Vector2 SpawnAreaBounds {
get;
set; }
46 [
Editable(MaxValueFloat =
int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f,
IsPropertySaveable.Yes,
"Radius of the circle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Rectangle")]
47 public float SpawnAreaRadius {
get;
set; }
49 [
Editable(MaxValueFloat =
int.MaxValue, MinValueFloat =
int.MinValue, ValueStep = 10f), Serialize(
"0,0",
IsPropertySaveable.Yes,
"Offset of the spawn area from the center of the item")]
50 public Vector2 SpawnAreaOffset {
get;
set; }
52 [
Editable(MaxValueFloat =
int.MaxValue, MinValueFloat =
int.MinValue, ValueStep = 1f), Serialize(
"10,40",
IsPropertySaveable.Yes,
"Time range between spawn attempts in seconds. Set both to a negative value to disable automatic spawning.")]
53 public Vector2 SpawnTimerRange {
get;
set; }
55 [
Editable(MaxValueFloat =
int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize(
"1,3",
IsPropertySaveable.Yes,
"Minumum and maximum amount of items or creatures to spawn in one attempt")]
56 public Vector2 SpawnAmountRange {
get;
set; }
58 [
Editable(MinValueInt = 0, MaxValueInt =
int.MaxValue), Serialize(8,
IsPropertySaveable.Yes,
"Total maximum amount of items or creatures that can be spawned. 0 = unrestricted.")]
59 public int MaximumAmount {
get;
set; }
61 [
Editable(MinValueInt = 0, MaxValueInt =
int.MaxValue), Serialize(8,
IsPropertySaveable.Yes,
"Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned. 0 = unrestricted.")]
62 public int MaximumAmountInArea {
get;
set; }
64 [
Editable(MaxValueFloat =
int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f,
IsPropertySaveable.Yes,
"Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")]
65 public float MaximumAmountRangePadding {
get;
set; }
68 public bool CanSpawn {
get;
set; } =
true;
71 public bool PreloadCharacter {
get;
set; }
73 private float spawnTimer;
74 private float? spawnTimerGoal;
76 private int spawnedAmount = 0;
80 private bool preloadInitiated;
82 public EntitySpawnerComponent(
Item item, ContentXElement element) : base(item, element)
87 public override void OnItemLoaded()
89 if (!
string.IsNullOrWhiteSpace(ItemIdentifier))
91 string[] allItems = ItemIdentifier.Split(
',');
92 foreach (
string itemIdentifier
in allItems)
94 string trimmedString = itemIdentifier.Trim();
98 foreach (ItemPrefab prefab
in ItemPrefab.Prefabs)
100 if (trimmedString == prefab.Identifier)
109 DebugConsole.ThrowError($
"Error loading {nameof(EntitySpawnerComponent)} - item prefab \"" + name +
"\" (identifier \"" + trimmedString +
"\") not found.");
115 public override void Update(
float deltaTime, Camera cam)
117 if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated)
119 SpawnCharacter(Vector2.Zero, onSpawn: (Character c) =>
121 preloadedCharacter = c;
122 c.DisabledByEvent = true;
124 preloadInitiated =
true;
128 base.Update(deltaTime, cam);
132 if (GameMain.NetworkMember is { IsClient: true }) {
return; }
134 float minTime = Math.Min(SpawnTimerRange.X, SpawnTimerRange.Y),
135 maxTime = Math.Max(SpawnTimerRange.X, SpawnTimerRange.Y);
137 if (minTime < 0 && maxTime < 0) {
return; }
139 spawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced);
141 spawnTimer += deltaTime;
143 if (spawnTimer > spawnTimerGoal)
146 spawnTimerGoal =
null;
151 public override void ReceiveSignal(Signal signal, Connection connection)
153 bool isNonZero = signal.value !=
"0";
154 bool isClient = GameMain.NetworkMember is { IsClient:
true };
156 switch (connection.Name)
159 CanSpawn = isNonZero;
161 case "toggle" when isNonZero:
162 CanSpawn = !CanSpawn;
164 case "trigger_in" when isNonZero && !isClient:
170 private RectangleF GetAreaRectangle(Vector2 size, Vector2 offset,
bool draw)
179 RectangleF rect =
new RectangleF(pos.X - size.X / 2f, pos.Y - size.Y / 2f, size.X, size.Y);
183 private bool CanSpawnMore()
185 if (!CanSpawn) {
return false; }
186 if (MaximumAmount > 0 && spawnedAmount >= MaximumAmount) {
return false; }
188 if (OnlySpawnWhenCrewInRange)
190 if (!
Character.CharacterList.Any(c => !c.IsDead && c.IsOnPlayerTeam && IsInRange(c.WorldPosition, crewArea:
true, rangePad:
false)))
196 if (MaximumAmountInArea <= 0) {
return true; }
199 if (!
string.IsNullOrWhiteSpace(SpeciesName))
201 amount =
Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName == SpeciesName && IsInRange(c.WorldPosition, crewArea:
false, rangePad:
true));
203 else if (!
string.IsNullOrWhiteSpace(ItemIdentifier))
205 amount =
Item.ItemList.Count(it => it.Submarine ==
item.
Submarine && it.Prefab.Identifier == ItemIdentifier && IsInRange(it.WorldPosition, crewArea:
false, rangePad:
true));
212 return amount < MaximumAmountInArea;
215 private bool IsInRange(Vector2 worldPos,
bool crewArea =
false,
bool rangePad =
false)
217 Vector2 offset = crewArea ? CrewAreaOffset : SpawnAreaOffset;
218 switch (crewArea ? CrewAreaShape : SpawnAreaShape)
220 case AreaShape.Circle:
222 float distance = (crewArea ? CrewAreaRadius : SpawnAreaRadius) + (rangePad ? MaximumAmountRangePadding : 0);
223 return Vector2.DistanceSquared(worldPos, center) < distance * distance;
225 case AreaShape.Rectangle:
226 RectangleF rect = GetAreaRectangle(crewArea ? CrewAreaBounds : SpawnAreaBounds, offset, draw:
false);
229 rect.Inflate(MaximumAmountRangePadding, MaximumAmountRangePadding);
232 return rect.Contains(worldPos);
240 if (!CanSpawnMore()) {
return; }
242 int minAmount = Math.Min((
int)SpawnAmountRange.X, (
int)SpawnAmountRange.Y),
243 maxAmount = Math.Max((
int)SpawnAmountRange.X, (
int)SpawnAmountRange.Y);
245 int amount = Rand.Range(minAmount, maxAmount, Rand.RandSync.Unsynced);
247 Vector2 offset = SpawnAreaOffset;
249 switch (SpawnAreaShape)
251 case AreaShape.Circle:
255 for (
int i = 0; i < Math.Max(1, amount); i++)
257 float angle = Rand.Range(-MathHelper.TwoPi, MathHelper.TwoPi);
258 float distance = Rand.Range(0, SpawnAreaRadius, Rand.RandSync.Unsynced);
259 Vector2 spawnPos =
new Vector2(x + distance * (
float)Math.Cos(angle), y + distance * (
float)Math.Sin(angle));
261 SpawnEntity(spawnPos);
265 case AreaShape.Rectangle:
267 RectangleF rect = GetAreaRectangle(SpawnAreaBounds, offset, draw:
false);
269 for (
int i = 0; i < Math.Max(1, amount); i++)
271 float minX = Math.Min(rect.Left, rect.Right),
272 maxX = Math.Max(rect.Left, rect.Right),
273 minY = Math.Min(rect.Top, rect.Bottom),
274 maxY = Math.Max(rect.Top, rect.Bottom);
276 Vector2 spawnPos =
new Vector2(Rand.Range(minX, maxX, Rand.RandSync.Unsynced), Rand.Range(minY, maxY, Rand.RandSync.Unsynced));
278 SpawnEntity(spawnPos);
284 void SpawnEntity(Vector2 pos)
286 if (!
string.IsNullOrWhiteSpace(SpeciesName))
288 if (preloadedCharacter !=
null)
290 preloadedCharacter.DisabledByEvent =
false;
291 preloadedCharacter.TeleportTo(pos);
292 preloadedCharacter =
null;
301 else if (!
string.IsNullOrWhiteSpace(ItemIdentifier))
303 Identifier[] allItems = ItemIdentifier.ToIdentifiers().ToArray();
304 Identifier itemIdentifier = allItems.GetRandomUnsynced();
305 ItemPrefab? prefab = ItemPrefab.Find(
null, itemIdentifier);
306 if (prefab is
null) {
return; }
313 Entity.Spawner?.AddItemToSpawnQueue(prefab, pos,
item.
Submarine);
319 private void SpawnCharacter(Vector2 pos, Action<Character>? onSpawn =
null)
321 if (!
string.IsNullOrWhiteSpace(SpeciesName))
323 Identifier[] allSpecies = SpeciesName.ToIdentifiers().ToArray();
324 Identifier species = allSpecies.GetRandomUnsynced();
325 Entity.Spawner?.AddCharacterToSpawnQueue(species, pos, onSpawn);
void SendSignal(string signal, string connectionName)
@ Character
Characters only