5 using FarseerPhysics.Dynamics;
6 using Microsoft.Xna.Framework;
8 using System.Collections.Generic;
10 using System.Xml.Linq;
11 using Vector2 = Microsoft.Xna.Framework.Vector2;
12 using Vector4 = Microsoft.Xna.Framework.Vector4;
16 internal class ProducedItem
19 public float Probability {
get;
set; }
21 public readonly List<StatusEffect> StatusEffects =
new List<StatusEffect>();
23 public readonly
Item Producer;
25 public readonly ItemPrefab? Prefab;
27 public ProducedItem(Item producer, ItemPrefab prefab,
float probability)
31 Probability = probability;
34 public ProducedItem(Item producer, ContentXElement element)
36 SerializableProperty.DeserializeProperties(
this, element);
40 Identifier itemIdentifier = element.GetAttributeIdentifier(
"identifier", Identifier.Empty);
41 if (!itemIdentifier.IsEmpty)
43 Prefab = ItemPrefab.Find(
null, itemIdentifier);
46 LoadSubElements(element);
49 private void LoadSubElements(ContentXElement element)
51 if (!element.HasElements) {
return; }
53 foreach (var subElement
in element.Elements())
55 switch (subElement.Name.ToString().ToLowerInvariant())
59 StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name.Value);
60 if (effect.type !=
ActionType.OnProduceSpawned)
62 DebugConsole.ThrowError(
"Only OnProduceSpawned type can be used in <ProducedItem>.",
63 contentPackage: element.ContentPackage);
67 StatusEffects.Add(effect);
76 internal enum VineTileType
79 CrossJunction = 0b1111,
80 HorizontalLine = 0b1010,
81 VerticalLine = 0b0101,
83 VerticalLane = 0b1010,
84 HorizontalLane = 0b0101,
85 TurnTopRight = 0b1001,
87 TurnBottomLeft = 0b0110,
88 TurnBottomRight = 0b1100,
90 TSectionLeft = 0b0111,
91 TSectionBottom = 0b1110,
92 TSectionRight = 0b1101,
100 internal enum TileSide
109 internal struct FoliageConfig
111 public static FoliageConfig EmptyConfig =
new FoliageConfig { Variant = -1, Rotation = 0f, Scale = 1.0f };
112 public static readonly
int EmptyConfigValue = EmptyConfig.Serialize();
115 public float Rotation;
118 public readonly
int Serialize()
120 int variant = Math.Min(Variant + 1, 15);
121 int scale = (int) (Scale * 10f);
122 int rotation = (int) (Rotation / MathHelper.TwoPi * 10f);
124 return variant | (scale << 4) | (rotation << 8);
127 public static FoliageConfig Deserialize(
int value)
129 int variant = value & 0x00F;
130 int scale = (value & 0x0F0) >> 4;
131 int rotation = (value & 0xF00) >> 8;
133 return new FoliageConfig { Variant = variant - 1, Scale = scale / 10f, Rotation = rotation / 10f * MathHelper.TwoPi };
136 public static FoliageConfig CreateRandomConfig(
int maxVariants,
float minScale,
float maxScale, Random? random =
null)
138 int flowerVariant = Growable.RandomInt(0, maxVariants, random);
139 float flowerScale = (float)Growable.RandomDouble(minScale, maxScale, random);
140 float flowerRotation = (float)Growable.RandomDouble(0, MathHelper.TwoPi, random);
141 return new FoliageConfig { Variant = flowerVariant, Scale = flowerScale, Rotation = flowerRotation };
145 internal partial class VineTile
147 public TileSide Sides = TileSide.None;
148 public TileSide BlockedSides = TileSide.None;
150 public FoliageConfig FlowerConfig;
151 public FoliageConfig LeafConfig;
153 public int FailedGrowthAttempts;
155 public Vector2 Position;
157 private readonly
float diameter;
158 public Vector2 offset;
160 public VineTileType Type;
161 public readonly Dictionary<TileSide, Vector2> AdjacentPositions;
162 public static int Size = 32;
165 public float VineStep;
166 public float FlowerStep;
168 private float growthStep;
169 public float GrowthStep
174 const float limit = 1.0f;
176 VineStep = Math.Min((
float)Math.Pow(value, 2), limit);
179 FlowerStep = Math.Min((
float)Math.Pow(value - limit, 2), limit);
184 public Color HealthColor = Color.Transparent;
185 public float DecayDelay;
187 private readonly Growable? Parent;
189 public VineTile(Growable? parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig =
null, FoliageConfig? leafConfig =
null, Rectangle? rect =
null)
191 FlowerConfig = flowerConfig ?? FoliageConfig.EmptyConfig;
192 LeafConfig = leafConfig ?? FoliageConfig.EmptyConfig;
194 Rect = rect ?? CreatePlantRect(position);
197 diameter = Rect.Width / 2.0f;
199 AdjacentPositions =
new Dictionary<TileSide, Vector2>
201 { TileSide.Top,
new Vector2(Position.X, Position.Y + Rect.Height) },
202 { TileSide.Bottom,
new Vector2(Position.X, Position.Y - Rect.Height) },
203 { TileSide.Left,
new Vector2(Position.X - Rect.Width, Position.Y) },
204 { TileSide.Right,
new Vector2(Position.X + Rect.Width, Position.Y) }
208 public void UpdateScale(
float deltaTime)
210 bool decayed = Parent?.Decayed ??
false;
212 if (decayed && GrowthStep > 1.0f)
216 DecayDelay -= deltaTime;
220 GrowthStep -= 0.25f * deltaTime;
224 if (GrowthStep >= 2.0f || decayed) {
return; }
226 GrowthStep += deltaTime;
228 if (GrowthStep < 1.0f)
231 float offsetAmount = diameter * VineStep - diameter;
234 case VineTileType.StumpLeft:
235 offset.X = offsetAmount;
237 case VineTileType.StumpRight:
238 offset.X = -offsetAmount;
240 case VineTileType.StumpTop:
241 offset.Y = offsetAmount;
243 case VineTileType.Stem:
244 case VineTileType.StumpBottom:
245 offset.Y = -offsetAmount;
248 offset = Vector2.Zero;
254 offset = Vector2.Zero;
258 public Vector2 GetWorldPosition(Planter planter, Vector2 slotOffset)
260 return planter.Item.WorldPosition + slotOffset + Position;
263 public void UpdateType()
265 if (Type == VineTileType.Stem) {
return; }
267 Type = (VineTileType)Sides;
278 public TileSide GetRandomFreeSide(Random? random =
null)
280 const int maxSides = 4;
281 TileSide occupiedSides = Sides | BlockedSides;
282 int setBits = occupiedSides.Count();
283 if (setBits >= maxSides) {
return TileSide.None; }
285 int possible = maxSides - setBits;
286 int[] pool =
new int[possible];
288 for (
int i = 0, j = 0; i < maxSides; i++)
290 if (!occupiedSides.HasFlag((TileSide) (1 << i)))
300 value = pool[Growable.RandomInt(0, possible, random)];
304 var (x, y, z, w) = Parent.GrowthWeights;
305 float[] weights = { x, y, z, w };
307 value = pool.GetRandomByWeight(i => weights[i], Rand.RandSync.Unsynced);
310 return (TileSide) (1 << value);
313 public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4;
315 public bool IsSideBlocked(TileSide side) => BlockedSides.HasFlag(side) || Sides.HasFlag(side);
317 public static Rectangle CreatePlantRect(Vector2 pos) =>
new Rectangle((
int)pos.X - Size / 2, (
int)pos.Y + Size / 2, Size, Size);
320 internal static class GrowthSideExtension
323 public static int Count(
this TileSide side)
336 public static TileSide GetOppositeSide(
this TileSide side)
339 TileSide.Left => TileSide.Right,
340 TileSide.Right => TileSide.Left,
341 TileSide.Bottom => TileSide.Top,
342 TileSide.Top => TileSide.Bottom,
343 _ =>
throw new ArgumentException($
"Expected Left, Right, Bottom or Top, got {side}")
350 public readonly HashSet<Rectangle> FailedRectangles =
new HashSet<Rectangle>();
353 public float GrowthSpeed {
get;
set; }
355 [Serialize(100f,
IsPropertySaveable.Yes,
"How long the plant can go without watering.")]
356 public float MaxHealth {
get;
set; }
358 [Serialize(1f,
IsPropertySaveable.Yes,
"How much damage the plant takes while in water.")]
359 public float FloodTolerance {
get;
set; }
361 [Serialize(1f,
IsPropertySaveable.Yes,
"How much damage the plant takes while growing.")]
362 public float Hardiness {
get;
set; }
365 public float SeedRate {
get;
set; }
368 public float ProductRate {
get;
set; }
370 [Serialize(0.5f,
IsPropertySaveable.Yes,
"Probability of an attribute being randomly modified in a newly produced seed.")]
371 public float MutationProbability {
get;
set; }
374 public Color FlowerTint {
get;
set; }
377 public int FlowerQuantity {
get;
set; }
380 public float BaseFlowerScale {
get;
set; }
383 public float BaseLeafScale {
get;
set; }
386 public Color LeafTint {
get;
set; }
388 [Serialize(0.33f,
IsPropertySaveable.Yes,
"Chance of a leaf appearing behind a branch.")]
389 public float LeafProbability {
get;
set; }
392 public Color VineTint {
get;
set; }
394 [Serialize(32,
IsPropertySaveable.Yes,
"Maximum number of vine tiles the plant can grow.")]
395 public int MaximumVines {
get;
set; }
398 public float VineScale {
get;
set; }
401 public Color DeadTint {
get;
set; }
403 [Serialize(
"1,1,1,1",
IsPropertySaveable.Yes,
"Probability for the plant to grow in a direction.")]
404 public Vector4 GrowthWeights {
get;
set; }
407 public float FireVulnerability {
get;
set; }
409 private const float increasedDeathSpeed = 10f;
410 private bool accelerateDeath;
411 private float health;
412 private int flowerVariants;
413 private int leafVariants;
414 private int[] flowerTiles;
420 set => health = Math.Clamp(value, 0, MaxHealth);
423 public bool Decayed {
get;
set; }
424 public bool FullyGrown {
get;
set; }
426 private const int maxProductDelay = 10,
427 maxVineGrowthDelay = 10;
429 private int productDelay;
430 private int vineDelay;
431 private float fireCheckCooldown;
433 public readonly List<ProducedItem> ProducedItems =
new List<ProducedItem>();
434 public readonly List<VineTile> Vines =
new List<VineTile>();
435 private readonly ProducedItem ProducedSeed;
437 private static float MinFlowerScale = 0.5f, MaxFlowerScale = 1.0f, MinLeafScale = 0.5f, MaxLeafScale = 1.0f;
438 private const int VineChunkSize = 32;
440 public Growable(Item item, ContentXElement element) : base(item, element)
442 SerializableProperty.DeserializeProperties(
this, element);
446 if (element.HasElements)
448 foreach (var subElement
in element.Elements())
450 switch (subElement.Name.ToString().ToLowerInvariant())
453 ProducedItems.Add(
new ProducedItem(this.item, subElement));
456 LoadVines(subElement);
462 ProducedSeed =
new ProducedItem(this.item, this.item.Prefab, 1.0f);
463 flowerTiles =
new int[FlowerQuantity];
466 public override void OnItemLoaded()
469 if (flowerTiles.All(i => i == 0))
471 GenerateFlowerTiles();
475 private void GenerateFlowerTiles(Random? random =
null)
477 flowerTiles =
new int[FlowerQuantity];
478 List<int> pool =
new List<int>();
479 for (
int i = 0; i < MaximumVines - 1; i++) { pool.Add(i); }
481 for (
int i = 0; i < flowerTiles.Length; i++)
483 int index = RandomInt(0, pool.Count, random);
484 flowerTiles[i] = pool[index];
485 pool.RemoveAt(index);
489 partial
void LoadVines(ContentXElement element);
491 public void OnGrowthTick(Planter planter, PlantSlot slot)
493 if (Decayed) {
return; }
497 TryGenerateProduct(planter, slot);
502 GrowVines(planter, slot);
505 float multipler = planter.Fertilizer > 0 ? 0.5f : 1f;
507 Health -= (accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness) * multipler;
509 if (planter.Item.InWater)
511 Health -= FloodTolerance * multipler;
516 if (serverHealthUpdateTimer > serverHealthUpdateDelay)
518 item.CreateServerEvent(
this);
519 serverHealthUpdateTimer = 0;
523 serverHealthUpdateTimer++;
532 UpdateBranchHealth();
536 private void UpdateBranchHealth()
538 Color healthColor = Color.White * (1.0f -
Health / MaxHealth);
539 foreach (VineTile vine
in Vines)
541 vine.HealthColor = healthColor;
545 private void TryGenerateProduct(Planter planter, PlantSlot slot)
548 if (productDelay <= maxProductDelay) {
return; }
552 bool spawnProduct = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < ProductRate,
553 spawnSeed = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < SeedRate;
557 if (spawnProduct || spawnSeed)
559 VineTile vine = Vines.GetRandomUnsynced();
560 spawnPos = vine.GetWorldPosition(planter, slot.Offset);
567 if (spawnProduct && ProducedItems.Any())
569 SpawnItem(Item, ProducedItems.GetRandomByWeight(it => it.Probability, Rand.RandSync.Unsynced), spawnPos);
575 SpawnItem(Item, ProducedSeed, spawnPos);
578 static void SpawnItem(Item thisItem, ProducedItem producedItem, Vector2 pos)
580 if (producedItem.Prefab ==
null) {
return; }
582 GameAnalyticsManager.AddDesignEvent(
"MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ??
"null") +
":GardeningProduce:" + thisItem.Prefab.Identifier +
":" + producedItem.Prefab.Identifier);
584 Entity.Spawner?.AddItemToSpawnQueue(producedItem.Prefab, pos, onSpawned: it =>
586 foreach (StatusEffect effect in producedItem.StatusEffects)
588 it.ApplyStatusEffect(effect, ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
591 it.ApplyStatusEffects(
ActionType.OnProduceSpawned, 1.0f, isNetworkEvent:
true);
600 private bool CheckPlantState()
602 if (Decayed) {
return true; }
608 GameAnalyticsManager.AddDesignEvent(
"MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ??
"null") +
":GardeningDied:" + item.Prefab.Identifier);
613 foreach (VineTile vine
in Vines)
615 vine.DecayDelay = (float)RandomDouble(0f, 30f);
619 item.CreateServerEvent(
this);
624 if (Vines.Count >= MaximumVines && !FullyGrown)
628 item.CreateServerEvent(
this);
633 if (!FullyGrown && !accelerateDeath && Vines.Any() && Vines.All(tile => !tile.CanGrowMore()))
635 accelerateDeath =
true;
639 if (item.ParentInventory is CharacterInventory)
643 item.CreateServerEvent(
this);
651 public override void Update(
float deltaTime, Camera cam)
653 base.Update(deltaTime, cam);
655 UpdateFires(deltaTime);
658 foreach (VineTile vine
in Vines)
660 vine.UpdateScale(deltaTime);
667 private void UpdateFires(
float deltaTime)
669 if (!Decayed && item.CurrentHull?.FireSources is { } fireSources && FireVulnerability > 0f)
671 if (fireCheckCooldown <= 0)
673 foreach (FireSource source
in fireSources)
675 if (source.IsInDamageRange(item.WorldPosition, source.DamageRange))
677 Health -= FireVulnerability;
681 fireCheckCooldown = 5f;
685 fireCheckCooldown -= deltaTime;
690 private void GrowVines(Planter planter, PlantSlot slot)
692 if (FullyGrown) {
return; }
695 if (vineDelay <= maxVineGrowthDelay / GrowthSpeed) {
return; }
706 int count = Vines.Count;
708 TryGenerateBranches(planter, slot);
710 if (Vines.Count > count)
713 for (
int i = 0; i < Vines.Count; i += VineChunkSize)
715 item.CreateServerEvent(
this,
new EventData(offset: i));
723 private void GenerateStem()
725 VineTile stem =
new VineTile(
this, Vector2.Zero, VineTileType.Stem) { BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right };
729 private void TryGenerateBranches(Planter planter, PlantSlot slot, Random? random =
null, Random? flowerRandom =
null)
731 List<VineTile> newList =
new List<VineTile>(Vines);
732 foreach (VineTile oldVines
in newList)
734 if (oldVines.FailedGrowthAttempts > 8 || !oldVines.CanGrowMore()) {
continue; }
736 if (RandomInt(0, Vines.Count(tile => tile.CanGrowMore()), random) != 0) {
continue; }
738 TileSide side = oldVines.GetRandomFreeSide(random);
740 if (side == TileSide.None)
742 oldVines.FailedGrowthAttempts++;
746 if (GrowthWeights != Vector4.One)
748 var (x, y, z, w) = GrowthWeights;
749 float[] weights = { x, y, z, w };
750 int index = (int)Math.Log2((
int)side);
751 if (MathUtils.NearlyEqual(weights[index], 0f))
753 oldVines.FailedGrowthAttempts++;
758 Vector2 pos = oldVines.AdjacentPositions[side];
759 Rectangle rect = VineTile.CreatePlantRect(pos);
761 if (CollidesWithWorld(rect, planter, slot))
763 oldVines.BlockedSides |= side;
764 oldVines.FailedGrowthAttempts++;
768 FoliageConfig flowerConfig = FoliageConfig.EmptyConfig;
769 FoliageConfig leafConfig = FoliageConfig.EmptyConfig;
771 if (flowerTiles.Any(i => Vines.Count == i))
773 flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, MinFlowerScale, MaxFlowerScale, flowerRandom);
776 if (LeafProbability >= RandomDouble(0d, 1.0d, flowerRandom) && leafVariants > 0)
778 leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, MinLeafScale, MaxLeafScale, flowerRandom);
781 VineTile newVine =
new VineTile(
this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect);
783 foreach (VineTile otherVine
in Vines)
785 var (distX, distY) = pos - otherVine.Position;
786 int absDistX = (int)Math.Abs(distX), absDistY = (int)Math.Abs(distY);
789 if (absDistX > newVine.Rect.Width || absDistY > newVine.Rect.Height || absDistX > 0 && absDistY > 0) {
continue; }
793 TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom;
795 TileSide oppositeSide = connectingSide.GetOppositeSide();
797 if (otherVine.BlockedSides.HasFlag(connectingSide))
799 newVine.BlockedSides |= oppositeSide;
803 if (otherVine != oldVines)
805 otherVine.BlockedSides |= connectingSide;
806 newVine.BlockedSides |= oppositeSide;
810 otherVine.Sides |= connectingSide;
811 newVine.Sides |= oppositeSide;
817 foreach (VineTile vine
in Vines)
824 private bool CollidesWithWorld(Rectangle rect, Planter planter, PlantSlot slot)
826 if (Vines.Any(g => g.Rect.Contains(rect))) {
return true; }
829 worldRect.Location = planter.Item.WorldPosition.ToPoint() + slot.Offset.ToPoint() + worldRect.Location;
830 worldRect.Y -= worldRect.Height;
832 Rectangle planterRect = planter.Item.WorldRect;
833 planterRect.Y -= planterRect.Height;
835 if (planterRect.Intersects(worldRect))
838 if (!FailedRectangles.Contains(worldRect))
840 FailedRectangles.Add(worldRect);
846 Vector2 topLeft = ConvertUnits.ToSimUnits(
new Vector2(worldRect.Left, worldRect.Top)),
847 topRight = ConvertUnits.ToSimUnits(
new Vector2(worldRect.Right, worldRect.Top)),
848 bottomLeft = ConvertUnits.ToSimUnits(
new Vector2(worldRect.Left, worldRect.Bottom)),
849 bottomRight = ConvertUnits.ToSimUnits(
new Vector2(worldRect.Right, worldRect.Bottom));
852 bool hasCollision = planterRect.Intersects(worldRect) || LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft);
857 if (!FailedRectangles.Contains(worldRect))
859 FailedRectangles.Add(worldRect);
865 static bool LineCollides(Vector2 point1, Vector2 point2)
867 const Category category = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
868 return Submarine.PickBody(point1, point2, collisionCategory: category, customPredicate: f => !(f.UserData is Hull) && f.CollidesWith.HasFlag(Physics.CollisionItem)) !=
null;
872 public override XElement Save(XElement parentElement)
874 XElement element = base.Save(parentElement);
875 element.Add(
new XAttribute(
"flowertiles",
string.Join(
",", flowerTiles)));
876 element.Add(
new XAttribute(
"decayed", Decayed));
877 foreach (VineTile vine
in Vines)
879 XElement vineElement =
new XElement(
"Vine");
880 vineElement.Add(
new XAttribute(
"sides", (
int)vine.Sides));
881 vineElement.Add(
new XAttribute(
"blockedsides", (
int)vine.BlockedSides));
882 vineElement.Add(
new XAttribute(
"pos", XMLExtensions.Vector2ToString(vine.Position)));
883 vineElement.Add(
new XAttribute(
"tile", (
int)vine.Type));
884 vineElement.Add(
new XAttribute(
"failedattempts", vine.FailedGrowthAttempts));
886 vineElement.Add(
new XAttribute(
"growthscale", Decayed ? 1.0f : 2.0f));
888 vineElement.Add(
new XAttribute(
"growthscale", vine.GrowthStep));
890 vineElement.Add(
new XAttribute(
"flowerconfig", vine.FlowerConfig.Serialize()));
891 vineElement.Add(
new XAttribute(
"leafconfig", vine.LeafConfig.Serialize()));
893 element.Add(vineElement);
899 public override void Load(ContentXElement componentElement,
bool usePrefabValues, IdRemap idRemap,
bool isItemSwap)
901 base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
902 flowerTiles = componentElement.GetAttributeIntArray(
"flowertiles", Array.Empty<
int>())!;
903 Decayed = componentElement.GetAttributeBool(
"decayed",
false);
906 foreach (var element
in componentElement.Elements())
908 if (element.Name.ToString().Equals(
"vine", StringComparison.OrdinalIgnoreCase))
910 VineTileType type = (VineTileType)element.GetAttributeInt(
"tile", 0);
911 Vector2 pos = element.GetAttributeVector2(
"pos", Vector2.Zero);
912 TileSide sides = (TileSide)element.GetAttributeInt(
"sides", 0);
913 TileSide blockedSides = (TileSide)element.GetAttributeInt(
"blockedsides", 0);
914 int failedAttempts = element.GetAttributeInt(
"failedattempts", 0);
915 float growthscale = element.GetAttributeFloat(
"growthscale", 0f);
916 int flowerConfig = element.GetAttributeInt(
"flowerconfig", FoliageConfig.EmptyConfigValue);
917 int leafConfig = element.GetAttributeInt(
"leafconfig", FoliageConfig.EmptyConfigValue);
919 VineTile tile =
new VineTile(
this, pos, type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig))
921 Sides = sides, BlockedSides = blockedSides, FailedGrowthAttempts = failedAttempts, GrowthStep = growthscale
929 private bool CanGrowMore() => Vines.Any(tile => tile.CanGrowMore());
931 public static int RandomInt(
int min,
int max, Random? random =
null) => random?.Next(min, max) ?? Rand.Range(min, max);
932 public static double RandomDouble(
double min,
double max, Random? random =
null) => random?.NextDouble() * (max - min) + min ?? Rand.Range(min, max);
Interface for entities that the server can send events to the clients
ActionType
ActionTypes define when a StatusEffect is executed.