934 lines
33 KiB
C#
934 lines
33 KiB
C#
#nullable enable
|
|
using Barotrauma.Extensions;
|
|
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
using Vector2 = Microsoft.Xna.Framework.Vector2;
|
|
using Vector4 = Microsoft.Xna.Framework.Vector4;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
internal class ProducedItem
|
|
{
|
|
[Serialize(0f, IsPropertySaveable.Yes)]
|
|
public float Probability { get; set; }
|
|
|
|
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
|
|
|
|
public readonly Item Producer;
|
|
|
|
public readonly ItemPrefab? Prefab;
|
|
|
|
public ProducedItem(Item producer, ItemPrefab prefab, float probability)
|
|
{
|
|
Producer = producer;
|
|
Prefab = prefab;
|
|
Probability = probability;
|
|
}
|
|
|
|
public ProducedItem(Item producer, ContentXElement element)
|
|
{
|
|
SerializableProperty.DeserializeProperties(this, element);
|
|
|
|
Producer = producer;
|
|
|
|
Identifier itemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
|
|
if (!itemIdentifier.IsEmpty)
|
|
{
|
|
Prefab = ItemPrefab.Find(null, itemIdentifier);
|
|
}
|
|
|
|
LoadSubElements(element);
|
|
}
|
|
|
|
private void LoadSubElements(ContentXElement element)
|
|
{
|
|
if (!element.HasElements) { return; }
|
|
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "statuseffect":
|
|
{
|
|
StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name.Value);
|
|
if (effect.type != ActionType.OnProduceSpawned)
|
|
{
|
|
DebugConsole.ThrowError("Only OnProduceSpawned type can be used in <ProducedItem>.",
|
|
contentPackage: element.ContentPackage);
|
|
continue;
|
|
}
|
|
|
|
StatusEffects.Add(effect);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ReSharper disable UnusedMember.Global
|
|
internal enum VineTileType
|
|
{
|
|
Stem = 0b0000,
|
|
CrossJunction = 0b1111,
|
|
HorizontalLine = 0b1010,
|
|
VerticalLine = 0b0101,
|
|
/*backwards compatibility, the vertical and horizontal "lane" used to be backwards*/
|
|
VerticalLane = 0b1010,
|
|
HorizontalLane = 0b0101,
|
|
TurnTopRight = 0b1001,
|
|
TurnTopLeft = 0b0011,
|
|
TurnBottomLeft = 0b0110,
|
|
TurnBottomRight = 0b1100,
|
|
TSectionTop = 0b1011,
|
|
TSectionLeft = 0b0111,
|
|
TSectionBottom = 0b1110,
|
|
TSectionRight = 0b1101,
|
|
StumpTop = 0b0001,
|
|
StumpLeft = 0b0010,
|
|
StumpBottom = 0b0100,
|
|
StumpRight = 0b1000
|
|
}
|
|
|
|
[Flags]
|
|
internal enum TileSide
|
|
{
|
|
None = 0,
|
|
Top = 1 << 0,
|
|
Left = 1 << 1,
|
|
Bottom = 1 << 2,
|
|
Right = 1 << 3
|
|
}
|
|
|
|
internal struct FoliageConfig
|
|
{
|
|
public static FoliageConfig EmptyConfig = new FoliageConfig { Variant = -1, Rotation = 0f, Scale = 1.0f };
|
|
public static readonly int EmptyConfigValue = EmptyConfig.Serialize();
|
|
|
|
public int Variant;
|
|
public float Rotation;
|
|
public float Scale;
|
|
|
|
public readonly int Serialize()
|
|
{
|
|
int variant = Math.Min(Variant + 1, 15);
|
|
int scale = (int) (Scale * 10f);
|
|
int rotation = (int) (Rotation / MathHelper.TwoPi * 10f);
|
|
|
|
return variant | (scale << 4) | (rotation << 8);
|
|
}
|
|
|
|
public static FoliageConfig Deserialize(int value)
|
|
{
|
|
int variant = value & 0x00F;
|
|
int scale = (value & 0x0F0) >> 4;
|
|
int rotation = (value & 0xF00) >> 8;
|
|
|
|
return new FoliageConfig { Variant = variant - 1, Scale = scale / 10f, Rotation = rotation / 10f * MathHelper.TwoPi };
|
|
}
|
|
|
|
public static FoliageConfig CreateRandomConfig(int maxVariants, float minScale, float maxScale, Random? random = null)
|
|
{
|
|
int flowerVariant = Growable.RandomInt(0, maxVariants, random);
|
|
float flowerScale = (float)Growable.RandomDouble(minScale, maxScale, random);
|
|
float flowerRotation = (float)Growable.RandomDouble(0, MathHelper.TwoPi, random);
|
|
return new FoliageConfig { Variant = flowerVariant, Scale = flowerScale, Rotation = flowerRotation };
|
|
}
|
|
}
|
|
|
|
internal partial class VineTile
|
|
{
|
|
public TileSide Sides = TileSide.None;
|
|
public TileSide BlockedSides = TileSide.None;
|
|
|
|
public FoliageConfig FlowerConfig;
|
|
public FoliageConfig LeafConfig;
|
|
|
|
public int FailedGrowthAttempts;
|
|
public Rectangle Rect;
|
|
public Vector2 Position;
|
|
|
|
private readonly float diameter;
|
|
public Vector2 offset;
|
|
|
|
public VineTileType Type;
|
|
public readonly Dictionary<TileSide, Vector2> AdjacentPositions;
|
|
public static int Size = 32;
|
|
|
|
|
|
public float VineStep;
|
|
public float FlowerStep;
|
|
|
|
private float growthStep;
|
|
public float GrowthStep
|
|
{
|
|
get => growthStep;
|
|
set
|
|
{
|
|
const float limit = 1.0f;
|
|
growthStep = value;
|
|
VineStep = Math.Min((float)Math.Pow(value, 2), limit);
|
|
if (value > limit)
|
|
{
|
|
FlowerStep = Math.Min((float)Math.Pow(value - limit, 2), limit);
|
|
}
|
|
}
|
|
}
|
|
|
|
public Color HealthColor = Color.Transparent;
|
|
public float DecayDelay;
|
|
|
|
private readonly Growable? Parent;
|
|
|
|
public VineTile(Growable? parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null)
|
|
{
|
|
FlowerConfig = flowerConfig ?? FoliageConfig.EmptyConfig;
|
|
LeafConfig = leafConfig ?? FoliageConfig.EmptyConfig;
|
|
Position = position;
|
|
Rect = rect ?? CreatePlantRect(position);
|
|
Parent = parent;
|
|
Type = type;
|
|
diameter = Rect.Width / 2.0f;
|
|
|
|
AdjacentPositions = new Dictionary<TileSide, Vector2>
|
|
{
|
|
{ TileSide.Top, new Vector2(Position.X, Position.Y + Rect.Height) },
|
|
{ TileSide.Bottom, new Vector2(Position.X, Position.Y - Rect.Height) },
|
|
{ TileSide.Left, new Vector2(Position.X - Rect.Width, Position.Y) },
|
|
{ TileSide.Right, new Vector2(Position.X + Rect.Width, Position.Y) }
|
|
};
|
|
}
|
|
|
|
public void UpdateScale(float deltaTime)
|
|
{
|
|
bool decayed = Parent?.Decayed ?? false;
|
|
|
|
if (decayed && GrowthStep > 1.0f)
|
|
{
|
|
if (DecayDelay > 0)
|
|
{
|
|
DecayDelay -= deltaTime;
|
|
}
|
|
else
|
|
{
|
|
GrowthStep -= 0.25f * deltaTime;
|
|
}
|
|
}
|
|
|
|
if (GrowthStep >= 2.0f || decayed) { return; }
|
|
|
|
GrowthStep += deltaTime;
|
|
|
|
if (GrowthStep < 1.0f)
|
|
{
|
|
// I don't know how or why this works
|
|
float offsetAmount = diameter * VineStep - diameter;
|
|
switch (Type)
|
|
{
|
|
case VineTileType.StumpLeft:
|
|
offset.X = offsetAmount;
|
|
break;
|
|
case VineTileType.StumpRight:
|
|
offset.X = -offsetAmount;
|
|
break;
|
|
case VineTileType.StumpTop:
|
|
offset.Y = offsetAmount;
|
|
break;
|
|
case VineTileType.Stem:
|
|
case VineTileType.StumpBottom:
|
|
offset.Y = -offsetAmount;
|
|
break;
|
|
default:
|
|
offset = Vector2.Zero;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
offset = Vector2.Zero;
|
|
}
|
|
}
|
|
|
|
public Vector2 GetWorldPosition(Planter planter, Vector2 slotOffset)
|
|
{
|
|
return planter.Item.WorldPosition + slotOffset + Position;
|
|
}
|
|
|
|
public void UpdateType()
|
|
{
|
|
if (Type == VineTileType.Stem) { return; }
|
|
|
|
Type = (VineTileType)Sides;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a random side that is not occupied.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// There is probably a much better way of doing this than allocating memory with an array
|
|
/// but this felt like the most reliable approach I could come up with.
|
|
/// </remarks>
|
|
/// <returns></returns>
|
|
public TileSide GetRandomFreeSide(Random? random = null)
|
|
{
|
|
const int maxSides = 4;
|
|
TileSide occupiedSides = Sides | BlockedSides;
|
|
int setBits = occupiedSides.Count();
|
|
if (setBits >= maxSides) { return TileSide.None; }
|
|
|
|
int possible = maxSides - setBits;
|
|
int[] pool = new int[possible];
|
|
|
|
for (int i = 0, j = 0; i < maxSides; i++)
|
|
{
|
|
if (!occupiedSides.HasFlag((TileSide) (1 << i)))
|
|
{
|
|
pool[j] = i;
|
|
j++;
|
|
}
|
|
}
|
|
|
|
int value;
|
|
if (Parent == null)
|
|
{
|
|
value = pool[Growable.RandomInt(0, possible, random)];
|
|
}
|
|
else
|
|
{
|
|
var (x, y, z, w) = Parent.GrowthWeights;
|
|
float[] weights = { x, y, z, w };
|
|
|
|
value = pool.GetRandomByWeight(i => weights[i], Rand.RandSync.Unsynced);
|
|
}
|
|
|
|
return (TileSide) (1 << value);
|
|
}
|
|
|
|
public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4;
|
|
|
|
public bool IsSideBlocked(TileSide side) => BlockedSides.HasFlag(side) || Sides.HasFlag(side);
|
|
|
|
public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int)pos.X - Size / 2, (int)pos.Y + Size / 2, Size, Size);
|
|
}
|
|
|
|
internal static class GrowthSideExtension
|
|
{
|
|
// K&R algorithm for counting how many bits are set in a bit field
|
|
public static int Count(this TileSide side)
|
|
{
|
|
int n = (int)side;
|
|
int count = 0;
|
|
while (n != 0)
|
|
{
|
|
count += n & 1;
|
|
n >>= 1;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
public static TileSide GetOppositeSide(this TileSide side)
|
|
=> side switch
|
|
{
|
|
TileSide.Left => TileSide.Right,
|
|
TileSide.Right => TileSide.Left,
|
|
TileSide.Bottom => TileSide.Top,
|
|
TileSide.Top => TileSide.Bottom,
|
|
_ => throw new ArgumentException($"Expected Left, Right, Bottom or Top, got {side}")
|
|
};
|
|
}
|
|
|
|
internal partial class Growable : ItemComponent, IServerSerializable
|
|
{
|
|
// used for debugging where a vine failed to grow
|
|
public readonly HashSet<Rectangle> FailedRectangles = new HashSet<Rectangle>();
|
|
|
|
[Serialize(1f, IsPropertySaveable.Yes, "How fast the plant grows.")]
|
|
public float GrowthSpeed { get; set; }
|
|
|
|
[Serialize(100f, IsPropertySaveable.Yes, "How long the plant can go without watering.")]
|
|
public float MaxHealth { get; set; }
|
|
|
|
[Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while in water.")]
|
|
public float FloodTolerance { get; set; }
|
|
|
|
[Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while growing.")]
|
|
public float Hardiness { get; set; }
|
|
|
|
[Serialize(0.01f, IsPropertySaveable.Yes, "How often a seed is produced.")]
|
|
public float SeedRate { get; set; }
|
|
|
|
[Serialize(0.01f, IsPropertySaveable.Yes, "How often a product item is produced.")]
|
|
public float ProductRate { get; set; }
|
|
|
|
[Serialize(0.5f, IsPropertySaveable.Yes, "Probability of an attribute being randomly modified in a newly produced seed.")]
|
|
public float MutationProbability { get; set; }
|
|
|
|
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the flowers.")]
|
|
public Color FlowerTint { get; set; }
|
|
|
|
[Serialize(3, IsPropertySaveable.Yes, "Number of flowers drawn when fully grown")]
|
|
public int FlowerQuantity { get; set; }
|
|
|
|
[Serialize(0.25f, IsPropertySaveable.Yes, "Size of the flower sprites.")]
|
|
public float BaseFlowerScale { get; set; }
|
|
|
|
[Serialize(0.5f, IsPropertySaveable.Yes, "Size of the leaf sprites.")]
|
|
public float BaseLeafScale { get; set; }
|
|
|
|
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the leaves.")]
|
|
public Color LeafTint { get; set; }
|
|
|
|
[Serialize(0.33f, IsPropertySaveable.Yes, "Chance of a leaf appearing behind a branch.")]
|
|
public float LeafProbability { get; set; }
|
|
|
|
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the vines.")]
|
|
public Color VineTint { get; set; }
|
|
|
|
[Serialize(32, IsPropertySaveable.Yes, "Maximum number of vine tiles the plant can grow.")]
|
|
public int MaximumVines { get; set; }
|
|
|
|
[Serialize(0.25f, IsPropertySaveable.Yes, "Size of the vine sprites.")]
|
|
public float VineScale { get; set; }
|
|
|
|
[Serialize("0.26,0.27,0.29,1.0", IsPropertySaveable.Yes, "Tint of a dead plant.")]
|
|
public Color DeadTint { get; set; }
|
|
|
|
[Serialize("1,1,1,1", IsPropertySaveable.Yes, "Probability for the plant to grow in a direction.")]
|
|
public Vector4 GrowthWeights { get; set; }
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.Yes, "How much damage is taken from fires.")]
|
|
public float FireVulnerability { get; set; }
|
|
|
|
private const float increasedDeathSpeed = 10f;
|
|
private bool accelerateDeath;
|
|
private float health;
|
|
private int flowerVariants;
|
|
private int leafVariants;
|
|
private int[] flowerTiles;
|
|
|
|
[Serialize(100.0f, IsPropertySaveable.Yes)]
|
|
public float Health
|
|
{
|
|
get => health;
|
|
set => health = Math.Clamp(value, 0, MaxHealth);
|
|
}
|
|
|
|
public bool Decayed { get; set; }
|
|
public bool FullyGrown { get; set; }
|
|
|
|
private const int maxProductDelay = 10,
|
|
maxVineGrowthDelay = 10;
|
|
|
|
private int productDelay;
|
|
private int vineDelay;
|
|
private float fireCheckCooldown;
|
|
|
|
public readonly List<ProducedItem> ProducedItems = new List<ProducedItem>();
|
|
public readonly List<VineTile> Vines = new List<VineTile>();
|
|
private readonly ProducedItem ProducedSeed;
|
|
|
|
private static float MinFlowerScale = 0.5f, MaxFlowerScale = 1.0f, MinLeafScale = 0.5f, MaxLeafScale = 1.0f;
|
|
private const int VineChunkSize = 32;
|
|
|
|
public Growable(Item item, ContentXElement element) : base(item, element)
|
|
{
|
|
SerializableProperty.DeserializeProperties(this, element);
|
|
|
|
Health = MaxHealth;
|
|
|
|
if (element.HasElements)
|
|
{
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "produceditem":
|
|
ProducedItems.Add(new ProducedItem(this.item, subElement));
|
|
break;
|
|
case "vinesprites":
|
|
LoadVines(subElement);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
ProducedSeed = new ProducedItem(this.item, this.item.Prefab, 1.0f);
|
|
flowerTiles = new int[FlowerQuantity];
|
|
}
|
|
|
|
public override void OnItemLoaded()
|
|
{
|
|
base.OnItemLoaded();
|
|
if (flowerTiles.All(i => i == 0))
|
|
{
|
|
GenerateFlowerTiles();
|
|
}
|
|
}
|
|
|
|
private void GenerateFlowerTiles(Random? random = null)
|
|
{
|
|
flowerTiles = new int[FlowerQuantity];
|
|
List<int> pool = new List<int>();
|
|
for (int i = 0; i < MaximumVines - 1; i++) { pool.Add(i); }
|
|
|
|
for (int i = 0; i < flowerTiles.Length; i++)
|
|
{
|
|
int index = RandomInt(0, pool.Count, random);
|
|
flowerTiles[i] = pool[index];
|
|
pool.RemoveAt(index);
|
|
}
|
|
}
|
|
|
|
partial void LoadVines(ContentXElement element);
|
|
|
|
public void OnGrowthTick(Planter planter, PlantSlot slot)
|
|
{
|
|
if (Decayed) { return; }
|
|
|
|
if (FullyGrown)
|
|
{
|
|
TryGenerateProduct(planter, slot);
|
|
}
|
|
|
|
if (Health > 0)
|
|
{
|
|
GrowVines(planter, slot);
|
|
|
|
// fertilizer makes the plant tick faster, compensate by halving water requirement
|
|
float multipler = planter.Fertilizer > 0 ? 0.5f : 1f;
|
|
|
|
Health -= (accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness) * multipler;
|
|
|
|
if (planter.Item.InWater)
|
|
{
|
|
Health -= FloodTolerance * multipler;
|
|
}
|
|
#if SERVER
|
|
if (FullyGrown)
|
|
{
|
|
if (serverHealthUpdateTimer > serverHealthUpdateDelay)
|
|
{
|
|
item.CreateServerEvent(this);
|
|
serverHealthUpdateTimer = 0;
|
|
}
|
|
else
|
|
{
|
|
serverHealthUpdateTimer++;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
CheckPlantState();
|
|
|
|
#if CLIENT
|
|
UpdateBranchHealth();
|
|
#endif
|
|
}
|
|
|
|
private void UpdateBranchHealth()
|
|
{
|
|
Color healthColor = Color.White * (1.0f - Health / MaxHealth);
|
|
foreach (VineTile vine in Vines)
|
|
{
|
|
vine.HealthColor = healthColor;
|
|
}
|
|
}
|
|
|
|
private void TryGenerateProduct(Planter planter, PlantSlot slot)
|
|
{
|
|
productDelay++;
|
|
if (productDelay <= maxProductDelay) { return; }
|
|
|
|
productDelay = 0;
|
|
|
|
bool spawnProduct = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < ProductRate,
|
|
spawnSeed = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < SeedRate;
|
|
|
|
Vector2 spawnPos;
|
|
|
|
if (spawnProduct || spawnSeed)
|
|
{
|
|
VineTile vine = Vines.GetRandomUnsynced();
|
|
spawnPos = vine.GetWorldPosition(planter, slot.Offset);
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (spawnProduct && ProducedItems.Any())
|
|
{
|
|
SpawnItem(Item, ProducedItems.GetRandomByWeight(it => it.Probability, Rand.RandSync.Unsynced), spawnPos);
|
|
return;
|
|
}
|
|
|
|
if (spawnSeed)
|
|
{
|
|
SpawnItem(Item, ProducedSeed, spawnPos);
|
|
}
|
|
|
|
static void SpawnItem(Item thisItem, ProducedItem producedItem, Vector2 pos)
|
|
{
|
|
if (producedItem.Prefab == null) { return; }
|
|
|
|
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningProduce:" + thisItem.Prefab.Identifier + ":" + producedItem.Prefab.Identifier);
|
|
|
|
Entity.Spawner?.AddItemToSpawnQueue(producedItem.Prefab, pos, onSpawned: it =>
|
|
{
|
|
foreach (StatusEffect effect in producedItem.StatusEffects)
|
|
{
|
|
it.ApplyStatusEffect(effect, ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
|
|
}
|
|
|
|
it.ApplyStatusEffects(ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates plant's state to fully grown or dead depending on its conditions.
|
|
/// </summary>
|
|
/// <returns>True if the plant has finished growing.</returns>
|
|
private bool CheckPlantState()
|
|
{
|
|
if (Decayed) { return true; }
|
|
|
|
if (Health <= 0)
|
|
{
|
|
if (!Decayed)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningDied:" + item.Prefab.Identifier);
|
|
}
|
|
|
|
Decayed = true;
|
|
#if CLIENT
|
|
foreach (VineTile vine in Vines)
|
|
{
|
|
vine.DecayDelay = (float)RandomDouble(0f, 30f);
|
|
}
|
|
#endif
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
if (Vines.Count >= MaximumVines && !FullyGrown)
|
|
{
|
|
FullyGrown = true;
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
if (!FullyGrown && !accelerateDeath && Vines.Any() && Vines.All(tile => !tile.CanGrowMore()))
|
|
{
|
|
accelerateDeath = true;
|
|
}
|
|
|
|
// if the player somehow finds a way to extract the seed out of a planter kill the plant
|
|
if (item.ParentInventory is CharacterInventory)
|
|
{
|
|
Decayed = true;
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
base.Update(deltaTime, cam);
|
|
|
|
UpdateFires(deltaTime);
|
|
|
|
#if CLIENT
|
|
foreach (VineTile vine in Vines)
|
|
{
|
|
vine.UpdateScale(deltaTime);
|
|
}
|
|
#endif
|
|
|
|
CheckPlantState();
|
|
}
|
|
|
|
private void UpdateFires(float deltaTime)
|
|
{
|
|
if (!Decayed && item.CurrentHull?.FireSources is { } fireSources && FireVulnerability > 0f)
|
|
{
|
|
if (fireCheckCooldown <= 0)
|
|
{
|
|
foreach (FireSource source in fireSources)
|
|
{
|
|
if (source.IsInDamageRange(item.WorldPosition, source.DamageRange))
|
|
{
|
|
Health -= FireVulnerability;
|
|
}
|
|
}
|
|
|
|
fireCheckCooldown = 5f;
|
|
}
|
|
else
|
|
{
|
|
fireCheckCooldown -= deltaTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void GrowVines(Planter planter, PlantSlot slot)
|
|
{
|
|
if (FullyGrown) { return; }
|
|
|
|
vineDelay++;
|
|
if (vineDelay <= maxVineGrowthDelay / GrowthSpeed) { return; }
|
|
|
|
vineDelay = 0;
|
|
|
|
if (!Vines.Any())
|
|
{
|
|
// generate first stem
|
|
GenerateStem();
|
|
return;
|
|
}
|
|
|
|
int count = Vines.Count;
|
|
|
|
TryGenerateBranches(planter, slot);
|
|
|
|
if (Vines.Count > count)
|
|
{
|
|
#if SERVER
|
|
for (int i = 0; i < Vines.Count; i += VineChunkSize)
|
|
{
|
|
item.CreateServerEvent(this, new EventData(offset: i));
|
|
}
|
|
#elif CLIENT
|
|
ResetPlanterSize();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private void GenerateStem()
|
|
{
|
|
VineTile stem = new VineTile(this, Vector2.Zero, VineTileType.Stem) { BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right };
|
|
Vines.Add(stem);
|
|
}
|
|
|
|
private void TryGenerateBranches(Planter planter, PlantSlot slot, Random? random = null, Random? flowerRandom = null)
|
|
{
|
|
List<VineTile> newList = new List<VineTile>(Vines);
|
|
foreach (VineTile oldVines in newList)
|
|
{
|
|
if (oldVines.FailedGrowthAttempts > 8 || !oldVines.CanGrowMore()) { continue; }
|
|
|
|
if (RandomInt(0, Vines.Count(tile => tile.CanGrowMore()), random) != 0) { continue; }
|
|
|
|
TileSide side = oldVines.GetRandomFreeSide(random);
|
|
|
|
if (side == TileSide.None)
|
|
{
|
|
oldVines.FailedGrowthAttempts++;
|
|
continue;
|
|
}
|
|
|
|
if (GrowthWeights != Vector4.One)
|
|
{
|
|
var (x, y, z, w) = GrowthWeights;
|
|
float[] weights = { x, y, z, w };
|
|
int index = (int)Math.Log2((int)side);
|
|
if (MathUtils.NearlyEqual(weights[index], 0f))
|
|
{
|
|
oldVines.FailedGrowthAttempts++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
Vector2 pos = oldVines.AdjacentPositions[side];
|
|
Rectangle rect = VineTile.CreatePlantRect(pos);
|
|
|
|
if (CollidesWithWorld(rect, planter, slot))
|
|
{
|
|
oldVines.BlockedSides |= side;
|
|
oldVines.FailedGrowthAttempts++;
|
|
continue;
|
|
}
|
|
|
|
FoliageConfig flowerConfig = FoliageConfig.EmptyConfig;
|
|
FoliageConfig leafConfig = FoliageConfig.EmptyConfig;
|
|
|
|
if (flowerTiles.Any(i => Vines.Count == i))
|
|
{
|
|
flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, MinFlowerScale, MaxFlowerScale, flowerRandom);
|
|
}
|
|
|
|
if (LeafProbability >= RandomDouble(0d, 1.0d, flowerRandom) && leafVariants > 0)
|
|
{
|
|
leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, MinLeafScale, MaxLeafScale, flowerRandom);
|
|
}
|
|
|
|
VineTile newVine = new VineTile(this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect);
|
|
|
|
foreach (VineTile otherVine in Vines)
|
|
{
|
|
var (distX, distY) = pos - otherVine.Position;
|
|
int absDistX = (int)Math.Abs(distX), absDistY = (int)Math.Abs(distY);
|
|
|
|
// check if the tile is within the with or height distance from us but ignore diagonals
|
|
if (absDistX > newVine.Rect.Width || absDistY > newVine.Rect.Height || absDistX > 0 && absDistY > 0) { continue; }
|
|
|
|
// determines what side the tile is relative to the new tile by comparing the X/Y distance values
|
|
// if the X value is bigger than Y it's to the left or right of us and then check if X is negative or positive to determine if it's right or left
|
|
TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom;
|
|
|
|
TileSide oppositeSide = connectingSide.GetOppositeSide();
|
|
|
|
if (otherVine.BlockedSides.HasFlag(connectingSide))
|
|
{
|
|
newVine.BlockedSides |= oppositeSide;
|
|
continue;
|
|
}
|
|
|
|
if (otherVine != oldVines)
|
|
{
|
|
otherVine.BlockedSides |= connectingSide;
|
|
newVine.BlockedSides |= oppositeSide;
|
|
}
|
|
else
|
|
{
|
|
otherVine.Sides |= connectingSide;
|
|
newVine.Sides |= oppositeSide;
|
|
}
|
|
}
|
|
|
|
Vines.Add(newVine);
|
|
|
|
foreach (VineTile vine in Vines)
|
|
{
|
|
vine.UpdateType();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool CollidesWithWorld(Rectangle rect, Planter planter, PlantSlot slot)
|
|
{
|
|
if (Vines.Any(g => g.Rect.Contains(rect))) { return true; }
|
|
|
|
Rectangle worldRect = rect;
|
|
worldRect.Location = planter.Item.WorldPosition.ToPoint() + slot.Offset.ToPoint() + worldRect.Location;
|
|
worldRect.Y -= worldRect.Height;
|
|
|
|
Rectangle planterRect = planter.Item.WorldRect;
|
|
planterRect.Y -= planterRect.Height;
|
|
|
|
if (planterRect.Intersects(worldRect))
|
|
{
|
|
#if DEBUG
|
|
if (!FailedRectangles.Contains(worldRect))
|
|
{
|
|
FailedRectangles.Add(worldRect);
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
Vector2 topLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Top)),
|
|
topRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Top)),
|
|
bottomLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Bottom)),
|
|
bottomRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Bottom));
|
|
|
|
// ray casting a cross on the corners didn't seem to work so we are ray casting along the perimeter
|
|
bool hasCollision = planterRect.Intersects(worldRect) || LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft);
|
|
|
|
#if DEBUG
|
|
if (hasCollision)
|
|
{
|
|
if (!FailedRectangles.Contains(worldRect))
|
|
{
|
|
FailedRectangles.Add(worldRect);
|
|
}
|
|
}
|
|
#endif
|
|
return hasCollision;
|
|
|
|
static bool LineCollides(Vector2 point1, Vector2 point2)
|
|
{
|
|
const Category category = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
|
|
return Submarine.PickBody(point1, point2, collisionCategory: category, customPredicate: f => !(f.UserData is Hull) && f.CollidesWith.HasFlag(Physics.CollisionItem)) != null;
|
|
}
|
|
}
|
|
|
|
public override XElement Save(XElement parentElement)
|
|
{
|
|
XElement element = base.Save(parentElement);
|
|
element.Add(new XAttribute("flowertiles", string.Join(",", flowerTiles)));
|
|
element.Add(new XAttribute("decayed", Decayed));
|
|
foreach (VineTile vine in Vines)
|
|
{
|
|
XElement vineElement = new XElement("Vine");
|
|
vineElement.Add(new XAttribute("sides", (int)vine.Sides));
|
|
vineElement.Add(new XAttribute("blockedsides", (int)vine.BlockedSides));
|
|
vineElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(vine.Position)));
|
|
vineElement.Add(new XAttribute("tile", (int)vine.Type));
|
|
vineElement.Add(new XAttribute("failedattempts", vine.FailedGrowthAttempts));
|
|
#if SERVER
|
|
vineElement.Add(new XAttribute("growthscale", Decayed ? 1.0f : 2.0f));
|
|
#else
|
|
vineElement.Add(new XAttribute("growthscale", vine.GrowthStep));
|
|
#endif
|
|
vineElement.Add(new XAttribute("flowerconfig", vine.FlowerConfig.Serialize()));
|
|
vineElement.Add(new XAttribute("leafconfig", vine.LeafConfig.Serialize()));
|
|
|
|
element.Add(vineElement);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
|
{
|
|
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
|
|
flowerTiles = componentElement.GetAttributeIntArray("flowertiles", Array.Empty<int>())!;
|
|
Decayed = componentElement.GetAttributeBool("decayed", false);
|
|
|
|
Vines.Clear();
|
|
foreach (var element in componentElement.Elements())
|
|
{
|
|
if (element.Name.ToString().Equals("vine", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
VineTileType type = (VineTileType)element.GetAttributeInt("tile", 0);
|
|
Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero);
|
|
TileSide sides = (TileSide)element.GetAttributeInt("sides", 0);
|
|
TileSide blockedSides = (TileSide)element.GetAttributeInt("blockedsides", 0);
|
|
int failedAttempts = element.GetAttributeInt("failedattempts", 0);
|
|
float growthscale = element.GetAttributeFloat("growthscale", 0f);
|
|
int flowerConfig = element.GetAttributeInt("flowerconfig", FoliageConfig.EmptyConfigValue);
|
|
int leafConfig = element.GetAttributeInt("leafconfig", FoliageConfig.EmptyConfigValue);
|
|
|
|
VineTile tile = new VineTile(this, pos, type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig))
|
|
{
|
|
Sides = sides, BlockedSides = blockedSides, FailedGrowthAttempts = failedAttempts, GrowthStep = growthscale
|
|
};
|
|
|
|
Vines.Add(tile);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool CanGrowMore() => Vines.Any(tile => tile.CanGrowMore());
|
|
|
|
public static int RandomInt(int min, int max, Random? random = null) => random?.Next(min, max) ?? Rand.Range(min, max);
|
|
public static double RandomDouble(double min, double max, Random? random = null) => random?.NextDouble() * (max - min) + min ?? Rand.Range(min, max);
|
|
}
|
|
} |