351 lines
15 KiB
C#
351 lines
15 KiB
C#
#nullable enable
|
|
|
|
using System;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
using Barotrauma.Extensions;
|
|
using Microsoft.Xna.Framework;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
internal partial class EntitySpawnerComponent : ItemComponent, IDrawableComponent
|
|
{
|
|
public enum AreaShape
|
|
{
|
|
Rectangle,
|
|
Circle
|
|
}
|
|
|
|
[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.")]
|
|
public string? ItemIdentifier { get; set; }
|
|
|
|
[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.")]
|
|
public string? SpeciesName { get; set; }
|
|
|
|
[Editable, Serialize(true, IsPropertySaveable.Yes, "Only spawn if crew members are within certain area")]
|
|
public bool OnlySpawnWhenCrewInRange { get; set; }
|
|
|
|
[Editable, Serialize(AreaShape.Rectangle, IsPropertySaveable.Yes, "Shape of the area where crew members need to stay")]
|
|
public AreaShape CrewAreaShape { get; set; }
|
|
|
|
[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")]
|
|
public Vector2 CrewAreaBounds { get; set; }
|
|
|
|
[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")]
|
|
public float CrewAreaRadius { get; set; }
|
|
|
|
[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")]
|
|
public Vector2 CrewAreaOffset { get; set; }
|
|
|
|
[Editable, Serialize(AreaShape.Rectangle, IsPropertySaveable.Yes, "Shape of the area where enemies or items are spawned")]
|
|
public AreaShape SpawnAreaShape { get; set; }
|
|
|
|
[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")]
|
|
public Vector2 SpawnAreaBounds { get; set; }
|
|
|
|
[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")]
|
|
public float SpawnAreaRadius { get; set; }
|
|
|
|
[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")]
|
|
public Vector2 SpawnAreaOffset { get; set; }
|
|
|
|
[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.")]
|
|
public Vector2 SpawnTimerRange { get; set; }
|
|
|
|
[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")]
|
|
public Vector2 SpawnAmountRange { get; set; }
|
|
|
|
[Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, IsPropertySaveable.Yes, "Total maximum amount of items or creatures that can be spawned. 0 = unrestricted.")]
|
|
public int MaximumAmount { get; set; }
|
|
|
|
[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.")]
|
|
public int MaximumAmountInArea { get; set; }
|
|
|
|
[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")]
|
|
public float MaximumAmountRangePadding { get; set; }
|
|
|
|
[Serialize(true, IsPropertySaveable.Yes, "")]
|
|
public bool CanSpawn { get; set; } = true;
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, "")]
|
|
public bool PreloadCharacter { get; set; }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, "Should the \"spawn monsters\" setting affect this item in the PvP mode?")]
|
|
public bool AffectedByPvPSpawnMonstersSetting { get; set; }
|
|
|
|
/// <summary>
|
|
/// Implemented as a property and checked on the fly instead of disabling the component,
|
|
/// because the signals sent by the component might be necessary even if it can't spawn anything.
|
|
/// </summary>
|
|
private bool DisabledByByPvPSpawnMonstersSetting =>
|
|
!SpeciesName.IsNullOrEmpty() &&
|
|
AffectedByPvPSpawnMonstersSetting &&
|
|
GameMain.GameSession?.GameMode is PvPMode &&
|
|
GameMain.NetworkMember is { ServerSettings.PvPSpawnMonsters: false };
|
|
|
|
private float spawnTimer;
|
|
private float? spawnTimerGoal;
|
|
|
|
private int spawnedAmount = 0;
|
|
|
|
private Character? preloadedCharacter;
|
|
|
|
private bool preloadInitiated;
|
|
|
|
public EntitySpawnerComponent(Item item, ContentXElement element) : base(item, element)
|
|
{
|
|
IsActive = true;
|
|
}
|
|
|
|
public override void OnItemLoaded()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(ItemIdentifier))
|
|
{
|
|
string[] allItems = ItemIdentifier.Split(',');
|
|
foreach (string itemIdentifier in allItems)
|
|
{
|
|
string trimmedString = itemIdentifier.Trim();
|
|
|
|
bool found = false;
|
|
|
|
foreach (ItemPrefab prefab in ItemPrefab.Prefabs)
|
|
{
|
|
if (trimmedString == prefab.Identifier)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
DebugConsole.ThrowError($"Error loading {nameof(EntitySpawnerComponent)} - item prefab \"" + name + "\" (identifier \"" + trimmedString + "\") not found.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
if (DisabledByByPvPSpawnMonstersSetting)
|
|
{
|
|
CanSpawn = false;
|
|
//in most cases we could probably just disable the component here and return,
|
|
//but the state_out signal might be needed for something even if the spawning is disabled
|
|
}
|
|
else
|
|
{
|
|
if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated)
|
|
{
|
|
SpawnCharacter(Vector2.Zero, onSpawn: (Character c) =>
|
|
{
|
|
preloadedCharacter = c;
|
|
c.DisabledByEvent = true;
|
|
});
|
|
preloadInitiated = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
base.Update(deltaTime, cam);
|
|
|
|
item.SendSignal(CanSpawn ? "1" : "0", "state_out");
|
|
|
|
if (GameMain.NetworkMember is { IsClient: true }) { return; }
|
|
|
|
float minTime = Math.Min(SpawnTimerRange.X, SpawnTimerRange.Y),
|
|
maxTime = Math.Max(SpawnTimerRange.X, SpawnTimerRange.Y);
|
|
|
|
if (minTime < 0 && maxTime < 0) { return; }
|
|
|
|
spawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced);
|
|
|
|
spawnTimer += deltaTime;
|
|
|
|
if (spawnTimer > spawnTimerGoal)
|
|
{
|
|
Spawn();
|
|
spawnTimerGoal = null;
|
|
spawnTimer = 0;
|
|
}
|
|
}
|
|
|
|
public override void ReceiveSignal(Signal signal, Connection connection)
|
|
{
|
|
bool isNonZero = signal.value != "0";
|
|
bool isClient = GameMain.NetworkMember is { IsClient: true };
|
|
|
|
switch (connection.Name)
|
|
{
|
|
case "set_state":
|
|
CanSpawn = isNonZero;
|
|
break;
|
|
case "toggle" when isNonZero:
|
|
CanSpawn = !CanSpawn;
|
|
break;
|
|
case "trigger_in" when isNonZero && !isClient:
|
|
Spawn();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private RectangleF GetAreaRectangle(Vector2 size, Vector2 offset, bool draw)
|
|
{
|
|
Vector2 pos = item.WorldPosition;
|
|
pos += offset;
|
|
if (draw)
|
|
{
|
|
pos.Y = -pos.Y;
|
|
}
|
|
|
|
RectangleF rect = new RectangleF(pos.X - size.X / 2f, pos.Y - size.Y / 2f, size.X, size.Y);
|
|
return rect;
|
|
}
|
|
|
|
private bool CanSpawnMore()
|
|
{
|
|
if (!CanSpawn || DisabledByByPvPSpawnMonstersSetting) { return false; }
|
|
if (MaximumAmount > 0 && spawnedAmount >= MaximumAmount) { return false; }
|
|
|
|
if (OnlySpawnWhenCrewInRange)
|
|
{
|
|
if (!Character.CharacterList.Any(c => !c.IsDead && c.IsOnPlayerTeam && IsInRange(c.WorldPosition, crewArea: true, rangePad: false)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (MaximumAmountInArea <= 0) { return true; }
|
|
|
|
int amount;
|
|
if (!string.IsNullOrWhiteSpace(SpeciesName))
|
|
{
|
|
amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName == SpeciesName && IsInRange(c.WorldPosition, crewArea: false, rangePad: true));
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
|
|
{
|
|
amount = Item.ItemList.Count(it => it.Submarine == item.Submarine && it.Prefab.Identifier == ItemIdentifier && IsInRange(it.WorldPosition, crewArea: false, rangePad: true));
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return amount < MaximumAmountInArea;
|
|
}
|
|
|
|
private bool IsInRange(Vector2 worldPos, bool crewArea = false, bool rangePad = false)
|
|
{
|
|
Vector2 offset = crewArea ? CrewAreaOffset : SpawnAreaOffset;
|
|
switch (crewArea ? CrewAreaShape : SpawnAreaShape)
|
|
{
|
|
case AreaShape.Circle:
|
|
Vector2 center = item.WorldPosition + offset;
|
|
float distance = (crewArea ? CrewAreaRadius : SpawnAreaRadius) + (rangePad ? MaximumAmountRangePadding : 0);
|
|
return Vector2.DistanceSquared(worldPos, center) < distance * distance;
|
|
|
|
case AreaShape.Rectangle:
|
|
RectangleF rect = GetAreaRectangle(crewArea ? CrewAreaBounds : SpawnAreaBounds, offset, draw: false);
|
|
if (rangePad)
|
|
{
|
|
rect.Inflate(MaximumAmountRangePadding, MaximumAmountRangePadding);
|
|
}
|
|
|
|
return rect.Contains(worldPos);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void Spawn()
|
|
{
|
|
if (!CanSpawnMore()) { return; }
|
|
|
|
int minAmount = Math.Min((int)SpawnAmountRange.X, (int)SpawnAmountRange.Y),
|
|
maxAmount = Math.Max((int)SpawnAmountRange.X, (int)SpawnAmountRange.Y);
|
|
|
|
int amount = Rand.Range(minAmount, maxAmount, Rand.RandSync.Unsynced);
|
|
|
|
Vector2 offset = SpawnAreaOffset;
|
|
|
|
switch (SpawnAreaShape)
|
|
{
|
|
case AreaShape.Circle:
|
|
{
|
|
var (x, y) = item.WorldPosition + offset;
|
|
|
|
for (int i = 0; i < Math.Max(1, amount); i++)
|
|
{
|
|
float angle = Rand.Range(-MathHelper.TwoPi, MathHelper.TwoPi);
|
|
float distance = Rand.Range(0, SpawnAreaRadius, Rand.RandSync.Unsynced);
|
|
Vector2 spawnPos = new Vector2(x + distance * (float)Math.Cos(angle), y + distance * (float)Math.Sin(angle));
|
|
|
|
SpawnEntity(spawnPos);
|
|
}
|
|
break;
|
|
}
|
|
case AreaShape.Rectangle:
|
|
{
|
|
RectangleF rect = GetAreaRectangle(SpawnAreaBounds, offset, draw: false);
|
|
|
|
for (int i = 0; i < Math.Max(1, amount); i++)
|
|
{
|
|
float minX = Math.Min(rect.Left, rect.Right),
|
|
maxX = Math.Max(rect.Left, rect.Right),
|
|
minY = Math.Min(rect.Top, rect.Bottom),
|
|
maxY = Math.Max(rect.Top, rect.Bottom);
|
|
|
|
Vector2 spawnPos = new Vector2(Rand.Range(minX, maxX, Rand.RandSync.Unsynced), Rand.Range(minY, maxY, Rand.RandSync.Unsynced));
|
|
|
|
SpawnEntity(spawnPos);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SpawnEntity(Vector2 pos)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(SpeciesName))
|
|
{
|
|
if (preloadedCharacter != null)
|
|
{
|
|
preloadedCharacter.DisabledByEvent = false;
|
|
preloadedCharacter.TeleportTo(pos);
|
|
preloadedCharacter = null;
|
|
spawnedAmount++;
|
|
}
|
|
else
|
|
{
|
|
SpawnCharacter(pos);
|
|
spawnedAmount++;
|
|
}
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
|
|
{
|
|
Identifier[] allItems = ItemIdentifier.ToIdentifiers().ToArray();
|
|
Identifier itemIdentifier = allItems.GetRandomUnsynced();
|
|
ItemPrefab? prefab = ItemPrefab.Find(null, itemIdentifier);
|
|
if (prefab is null) { return; }
|
|
|
|
if (item.Submarine is { } sub)
|
|
{
|
|
pos -= sub.Position;
|
|
}
|
|
|
|
Entity.Spawner?.AddItemToSpawnQueue(prefab, pos, item.Submarine);
|
|
spawnedAmount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SpawnCharacter(Vector2 pos, Action<Character>? onSpawn = null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(SpeciesName))
|
|
{
|
|
Identifier[] allSpecies = SpeciesName.ToIdentifiers().ToArray();
|
|
Identifier species = allSpecies.GetRandomUnsynced();
|
|
Entity.Spawner?.AddCharacterToSpawnQueue(species, pos, onSpawn);
|
|
}
|
|
}
|
|
}
|
|
} |