#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 StatusEffects = new List(); 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 .", 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 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.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; } /// /// Returns a random side that is not occupied. /// /// /// 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. /// /// 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 FailedRectangles = new HashSet(); [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 ProducedItems = new List(); public readonly List Vines = new List(); 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 pool = new List(); 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); }); } } /// /// Updates plant's state to fully grown or dead depending on its conditions. /// /// True if the plant has finished growing. 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 newList = new List(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())!; 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); } }