Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs
1 #nullable enable
2 
3 using System;
4 using System.Linq;
5 using System.Xml.Linq;
7 using Microsoft.Xna.Framework;
8 
10 {
11  internal partial class EntitySpawnerComponent : ItemComponent, IDrawableComponent
12  {
13  public enum AreaShape
14  {
15  Rectangle,
16  Circle
17  }
18 
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; }
21 
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; }
24 
25  [Editable, Serialize(true, IsPropertySaveable.Yes, "Only spawn if crew members are within certain area")]
26  public bool OnlySpawnWhenCrewInRange { get; set; }
27 
28  [Editable, Serialize(AreaShape.Rectangle, IsPropertySaveable.Yes, "Shape of the area where crew members need to stay")]
29  public AreaShape CrewAreaShape { get; set; }
30 
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; }
33 
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; }
36 
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; }
39 
40  [Editable, Serialize(AreaShape.Rectangle, IsPropertySaveable.Yes, "Shape of the area where enemies or items are spawned")]
41  public AreaShape SpawnAreaShape { get; set; }
42 
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; }
45 
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; }
48 
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; }
51 
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; }
54 
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; }
57 
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; }
60 
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; }
63 
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; }
66 
67  [Serialize(true, IsPropertySaveable.Yes, "")]
68  public bool CanSpawn { get; set; } = true;
69 
70  [Editable, Serialize(false, IsPropertySaveable.Yes, "")]
71  public bool PreloadCharacter { get; set; }
72 
73  private float spawnTimer;
74  private float? spawnTimerGoal;
75 
76  private int spawnedAmount = 0;
77 
78  private Character? preloadedCharacter;
79 
80  private bool preloadInitiated;
81 
82  public EntitySpawnerComponent(Item item, ContentXElement element) : base(item, element)
83  {
84  IsActive = true;
85  }
86 
87  public override void OnItemLoaded()
88  {
89  if (!string.IsNullOrWhiteSpace(ItemIdentifier))
90  {
91  string[] allItems = ItemIdentifier.Split(',');
92  foreach (string itemIdentifier in allItems)
93  {
94  string trimmedString = itemIdentifier.Trim();
95 
96  bool found = false;
97 
98  foreach (ItemPrefab prefab in ItemPrefab.Prefabs)
99  {
100  if (trimmedString == prefab.Identifier)
101  {
102  found = true;
103  break;
104  }
105  }
106 
107  if (!found)
108  {
109  DebugConsole.ThrowError($"Error loading {nameof(EntitySpawnerComponent)} - item prefab \"" + name + "\" (identifier \"" + trimmedString + "\") not found.");
110  }
111  }
112  }
113  }
114 
115  public override void Update(float deltaTime, Camera cam)
116  {
117  if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated)
118  {
119  SpawnCharacter(Vector2.Zero, onSpawn: (Character c) =>
120  {
121  preloadedCharacter = c;
122  c.DisabledByEvent = true;
123  });
124  preloadInitiated = true;
125  return;
126  }
127 
128  base.Update(deltaTime, cam);
129 
130  item.SendSignal(CanSpawn ? "1" : "0", "state_out");
131 
132  if (GameMain.NetworkMember is { IsClient: true }) { return; }
133 
134  float minTime = Math.Min(SpawnTimerRange.X, SpawnTimerRange.Y),
135  maxTime = Math.Max(SpawnTimerRange.X, SpawnTimerRange.Y);
136 
137  if (minTime < 0 && maxTime < 0) { return; }
138 
139  spawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced);
140 
141  spawnTimer += deltaTime;
142 
143  if (spawnTimer > spawnTimerGoal)
144  {
145  Spawn();
146  spawnTimerGoal = null;
147  spawnTimer = 0;
148  }
149  }
150 
151  public override void ReceiveSignal(Signal signal, Connection connection)
152  {
153  bool isNonZero = signal.value != "0";
154  bool isClient = GameMain.NetworkMember is { IsClient: true };
155 
156  switch (connection.Name)
157  {
158  case "set_state":
159  CanSpawn = isNonZero;
160  break;
161  case "toggle" when isNonZero:
162  CanSpawn = !CanSpawn;
163  break;
164  case "trigger_in" when isNonZero && !isClient:
165  Spawn();
166  break;
167  }
168  }
169 
170  private RectangleF GetAreaRectangle(Vector2 size, Vector2 offset, bool draw)
171  {
172  Vector2 pos = item.WorldPosition;
173  pos += offset;
174  if (draw)
175  {
176  pos.Y = -pos.Y;
177  }
178 
179  RectangleF rect = new RectangleF(pos.X - size.X / 2f, pos.Y - size.Y / 2f, size.X, size.Y);
180  return rect;
181  }
182 
183  private bool CanSpawnMore()
184  {
185  if (!CanSpawn) { return false; }
186  if (MaximumAmount > 0 && spawnedAmount >= MaximumAmount) { return false; }
187 
188  if (OnlySpawnWhenCrewInRange)
189  {
190  if (!Character.CharacterList.Any(c => !c.IsDead && c.IsOnPlayerTeam && IsInRange(c.WorldPosition, crewArea: true, rangePad: false)))
191  {
192  return false;
193  }
194  }
195 
196  if (MaximumAmountInArea <= 0) { return true; }
197 
198  int amount;
199  if (!string.IsNullOrWhiteSpace(SpeciesName))
200  {
201  amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName == SpeciesName && IsInRange(c.WorldPosition, crewArea: false, rangePad: true));
202  }
203  else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
204  {
205  amount = Item.ItemList.Count(it => it.Submarine == item.Submarine && it.Prefab.Identifier == ItemIdentifier && IsInRange(it.WorldPosition, crewArea: false, rangePad: true));
206  }
207  else
208  {
209  return false;
210  }
211 
212  return amount < MaximumAmountInArea;
213  }
214 
215  private bool IsInRange(Vector2 worldPos, bool crewArea = false, bool rangePad = false)
216  {
217  Vector2 offset = crewArea ? CrewAreaOffset : SpawnAreaOffset;
218  switch (crewArea ? CrewAreaShape : SpawnAreaShape)
219  {
220  case AreaShape.Circle:
221  Vector2 center = item.WorldPosition + offset;
222  float distance = (crewArea ? CrewAreaRadius : SpawnAreaRadius) + (rangePad ? MaximumAmountRangePadding : 0);
223  return Vector2.DistanceSquared(worldPos, center) < distance * distance;
224 
225  case AreaShape.Rectangle:
226  RectangleF rect = GetAreaRectangle(crewArea ? CrewAreaBounds : SpawnAreaBounds, offset, draw: false);
227  if (rangePad)
228  {
229  rect.Inflate(MaximumAmountRangePadding, MaximumAmountRangePadding);
230  }
231 
232  return rect.Contains(worldPos);
233  }
234 
235  return false;
236  }
237 
238  public void Spawn()
239  {
240  if (!CanSpawnMore()) { return; }
241 
242  int minAmount = Math.Min((int)SpawnAmountRange.X, (int)SpawnAmountRange.Y),
243  maxAmount = Math.Max((int)SpawnAmountRange.X, (int)SpawnAmountRange.Y);
244 
245  int amount = Rand.Range(minAmount, maxAmount, Rand.RandSync.Unsynced);
246 
247  Vector2 offset = SpawnAreaOffset;
248 
249  switch (SpawnAreaShape)
250  {
251  case AreaShape.Circle:
252  {
253  var (x, y) = item.WorldPosition + offset;
254 
255  for (int i = 0; i < Math.Max(1, amount); i++)
256  {
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));
260 
261  SpawnEntity(spawnPos);
262  }
263  break;
264  }
265  case AreaShape.Rectangle:
266  {
267  RectangleF rect = GetAreaRectangle(SpawnAreaBounds, offset, draw: false);
268 
269  for (int i = 0; i < Math.Max(1, amount); i++)
270  {
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);
275 
276  Vector2 spawnPos = new Vector2(Rand.Range(minX, maxX, Rand.RandSync.Unsynced), Rand.Range(minY, maxY, Rand.RandSync.Unsynced));
277 
278  SpawnEntity(spawnPos);
279  }
280  break;
281  }
282  }
283 
284  void SpawnEntity(Vector2 pos)
285  {
286  if (!string.IsNullOrWhiteSpace(SpeciesName))
287  {
288  if (preloadedCharacter != null)
289  {
290  preloadedCharacter.DisabledByEvent = false;
291  preloadedCharacter.TeleportTo(pos);
292  preloadedCharacter = null;
293  spawnedAmount++;
294  }
295  else
296  {
297  SpawnCharacter(pos);
298  spawnedAmount++;
299  }
300  }
301  else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
302  {
303  Identifier[] allItems = ItemIdentifier.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray();
304  Identifier itemIdentifier = allItems.GetRandomUnsynced();
305  ItemPrefab? prefab = ItemPrefab.Find(null, itemIdentifier);
306  if (prefab is null) { return; }
307 
308  if (item.Submarine is { } sub)
309  {
310  pos -= sub.Position;
311  }
312 
313  Entity.Spawner?.AddItemToSpawnQueue(prefab, pos, item.Submarine);
314  spawnedAmount++;
315  }
316  }
317  }
318 
319  private void SpawnCharacter(Vector2 pos, Action<Character>? onSpawn = null)
320  {
321  if (!string.IsNullOrWhiteSpace(SpeciesName))
322  {
323  Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray();
324  Identifier species = allSpecies.GetRandomUnsynced();
325  Entity.Spawner?.AddCharacterToSpawnQueue(species, pos, onSpawn);
326  }
327  }
328  }
329 }
Submarine Submarine
Definition: Entity.cs:53
static readonly List< Item > ItemList
void SendSignal(string signal, string connectionName)