Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Growable.cs
1 #nullable enable
4 using FarseerPhysics;
5 using FarseerPhysics.Dynamics;
6 using Microsoft.Xna.Framework;
7 using System;
8 using System.Collections.Generic;
9 using System.Linq;
10 using System.Xml.Linq;
11 using Vector2 = Microsoft.Xna.Framework.Vector2;
12 using Vector4 = Microsoft.Xna.Framework.Vector4;
13 
15 {
16  internal class ProducedItem
17  {
18  [Serialize(0f, IsPropertySaveable.Yes)]
19  public float Probability { get; set; }
20 
21  public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
22 
23  public readonly Item Producer;
24 
25  public readonly ItemPrefab? Prefab;
26 
27  public ProducedItem(Item producer, ItemPrefab prefab, float probability)
28  {
29  Producer = producer;
30  Prefab = prefab;
31  Probability = probability;
32  }
33 
34  public ProducedItem(Item producer, ContentXElement element)
35  {
36  SerializableProperty.DeserializeProperties(this, element);
37 
38  Producer = producer;
39 
40  Identifier itemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
41  if (!itemIdentifier.IsEmpty)
42  {
43  Prefab = ItemPrefab.Find(null, itemIdentifier);
44  }
45 
46  LoadSubElements(element);
47  }
48 
49  private void LoadSubElements(ContentXElement element)
50  {
51  if (!element.HasElements) { return; }
52 
53  foreach (var subElement in element.Elements())
54  {
55  switch (subElement.Name.ToString().ToLowerInvariant())
56  {
57  case "statuseffect":
58  {
59  StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name.Value);
60  if (effect.type != ActionType.OnProduceSpawned)
61  {
62  DebugConsole.ThrowError("Only OnProduceSpawned type can be used in <ProducedItem>.",
63  contentPackage: element.ContentPackage);
64  continue;
65  }
66 
67  StatusEffects.Add(effect);
68  break;
69  }
70  }
71  }
72  }
73  }
74 
75  // ReSharper disable UnusedMember.Global
76  internal enum VineTileType
77  {
78  Stem = 0b0000,
79  CrossJunction = 0b1111,
80  HorizontalLine = 0b1010,
81  VerticalLine = 0b0101,
82  /*backwards compatibility, the vertical and horizontal "lane" used to be backwards*/
83  VerticalLane = 0b1010,
84  HorizontalLane = 0b0101,
85  TurnTopRight = 0b1001,
86  TurnTopLeft = 0b0011,
87  TurnBottomLeft = 0b0110,
88  TurnBottomRight = 0b1100,
89  TSectionTop = 0b1011,
90  TSectionLeft = 0b0111,
91  TSectionBottom = 0b1110,
92  TSectionRight = 0b1101,
93  StumpTop = 0b0001,
94  StumpLeft = 0b0010,
95  StumpBottom = 0b0100,
96  StumpRight = 0b1000
97  }
98 
99  [Flags]
100  internal enum TileSide
101  {
102  None = 0,
103  Top = 1 << 0,
104  Left = 1 << 1,
105  Bottom = 1 << 2,
106  Right = 1 << 3
107  }
108 
109  internal struct FoliageConfig
110  {
111  public static FoliageConfig EmptyConfig = new FoliageConfig { Variant = -1, Rotation = 0f, Scale = 1.0f };
112  public static readonly int EmptyConfigValue = EmptyConfig.Serialize();
113 
114  public int Variant;
115  public float Rotation;
116  public float Scale;
117 
118  public readonly int Serialize()
119  {
120  int variant = Math.Min(Variant + 1, 15);
121  int scale = (int) (Scale * 10f);
122  int rotation = (int) (Rotation / MathHelper.TwoPi * 10f);
123 
124  return variant | (scale << 4) | (rotation << 8);
125  }
126 
127  public static FoliageConfig Deserialize(int value)
128  {
129  int variant = value & 0x00F;
130  int scale = (value & 0x0F0) >> 4;
131  int rotation = (value & 0xF00) >> 8;
132 
133  return new FoliageConfig { Variant = variant - 1, Scale = scale / 10f, Rotation = rotation / 10f * MathHelper.TwoPi };
134  }
135 
136  public static FoliageConfig CreateRandomConfig(int maxVariants, float minScale, float maxScale, Random? random = null)
137  {
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 };
142  }
143  }
144 
145  internal partial class VineTile
146  {
147  public TileSide Sides = TileSide.None;
148  public TileSide BlockedSides = TileSide.None;
149 
150  public FoliageConfig FlowerConfig;
151  public FoliageConfig LeafConfig;
152 
153  public int FailedGrowthAttempts;
154  public Rectangle Rect;
155  public Vector2 Position;
156 
157  private readonly float diameter;
158  public Vector2 offset;
159 
160  public VineTileType Type;
161  public readonly Dictionary<TileSide, Vector2> AdjacentPositions;
162  public static int Size = 32;
163 
164 
165  public float VineStep;
166  public float FlowerStep;
167 
168  private float growthStep;
169  public float GrowthStep
170  {
171  get => growthStep;
172  set
173  {
174  const float limit = 1.0f;
175  growthStep = value;
176  VineStep = Math.Min((float)Math.Pow(value, 2), limit);
177  if (value > limit)
178  {
179  FlowerStep = Math.Min((float)Math.Pow(value - limit, 2), limit);
180  }
181  }
182  }
183 
184  public Color HealthColor = Color.Transparent;
185  public float DecayDelay;
186 
187  private readonly Growable? Parent;
188 
189  public VineTile(Growable? parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null)
190  {
191  FlowerConfig = flowerConfig ?? FoliageConfig.EmptyConfig;
192  LeafConfig = leafConfig ?? FoliageConfig.EmptyConfig;
193  Position = position;
194  Rect = rect ?? CreatePlantRect(position);
195  Parent = parent;
196  Type = type;
197  diameter = Rect.Width / 2.0f;
198 
199  AdjacentPositions = new Dictionary<TileSide, Vector2>
200  {
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) }
205  };
206  }
207 
208  public void UpdateScale(float deltaTime)
209  {
210  bool decayed = Parent?.Decayed ?? false;
211 
212  if (decayed && GrowthStep > 1.0f)
213  {
214  if (DecayDelay > 0)
215  {
216  DecayDelay -= deltaTime;
217  }
218  else
219  {
220  GrowthStep -= 0.25f * deltaTime;
221  }
222  }
223 
224  if (GrowthStep >= 2.0f || decayed) { return; }
225 
226  GrowthStep += deltaTime;
227 
228  if (GrowthStep < 1.0f)
229  {
230  // I don't know how or why this works
231  float offsetAmount = diameter * VineStep - diameter;
232  switch (Type)
233  {
234  case VineTileType.StumpLeft:
235  offset.X = offsetAmount;
236  break;
237  case VineTileType.StumpRight:
238  offset.X = -offsetAmount;
239  break;
240  case VineTileType.StumpTop:
241  offset.Y = offsetAmount;
242  break;
243  case VineTileType.Stem:
244  case VineTileType.StumpBottom:
245  offset.Y = -offsetAmount;
246  break;
247  default:
248  offset = Vector2.Zero;
249  break;
250  }
251  }
252  else
253  {
254  offset = Vector2.Zero;
255  }
256  }
257 
258  public Vector2 GetWorldPosition(Planter planter, Vector2 slotOffset)
259  {
260  return planter.Item.WorldPosition + slotOffset + Position;
261  }
262 
263  public void UpdateType()
264  {
265  if (Type == VineTileType.Stem) { return; }
266 
267  Type = (VineTileType)Sides;
268  }
269 
278  public TileSide GetRandomFreeSide(Random? random = null)
279  {
280  const int maxSides = 4;
281  TileSide occupiedSides = Sides | BlockedSides;
282  int setBits = occupiedSides.Count();
283  if (setBits >= maxSides) { return TileSide.None; }
284 
285  int possible = maxSides - setBits;
286  int[] pool = new int[possible];
287 
288  for (int i = 0, j = 0; i < maxSides; i++)
289  {
290  if (!occupiedSides.HasFlag((TileSide) (1 << i)))
291  {
292  pool[j] = i;
293  j++;
294  }
295  }
296 
297  int value;
298  if (Parent == null)
299  {
300  value = pool[Growable.RandomInt(0, possible, random)];
301  }
302  else
303  {
304  var (x, y, z, w) = Parent.GrowthWeights;
305  float[] weights = { x, y, z, w };
306 
307  value = pool.GetRandomByWeight(i => weights[i], Rand.RandSync.Unsynced);
308  }
309 
310  return (TileSide) (1 << value);
311  }
312 
313  public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4;
314 
315  public bool IsSideBlocked(TileSide side) => BlockedSides.HasFlag(side) || Sides.HasFlag(side);
316 
317  public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int)pos.X - Size / 2, (int)pos.Y + Size / 2, Size, Size);
318  }
319 
320  internal static class GrowthSideExtension
321  {
322  // K&R algorithm for counting how many bits are set in a bit field
323  public static int Count(this TileSide side)
324  {
325  int n = (int)side;
326  int count = 0;
327  while (n != 0)
328  {
329  count += n & 1;
330  n >>= 1;
331  }
332 
333  return count;
334  }
335 
336  public static TileSide GetOppositeSide(this TileSide side)
337  => side switch
338  {
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}")
344  };
345  }
346 
347  internal partial class Growable : ItemComponent, IServerSerializable
348  {
349  // used for debugging where a vine failed to grow
350  public readonly HashSet<Rectangle> FailedRectangles = new HashSet<Rectangle>();
351 
352  [Serialize(1f, IsPropertySaveable.Yes, "How fast the plant grows.")]
353  public float GrowthSpeed { get; set; }
354 
355  [Serialize(100f, IsPropertySaveable.Yes, "How long the plant can go without watering.")]
356  public float MaxHealth { get; set; }
357 
358  [Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while in water.")]
359  public float FloodTolerance { get; set; }
360 
361  [Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while growing.")]
362  public float Hardiness { get; set; }
363 
364  [Serialize(0.01f, IsPropertySaveable.Yes, "How often a seed is produced.")]
365  public float SeedRate { get; set; }
366 
367  [Serialize(0.01f, IsPropertySaveable.Yes, "How often a product item is produced.")]
368  public float ProductRate { get; set; }
369 
370  [Serialize(0.5f, IsPropertySaveable.Yes, "Probability of an attribute being randomly modified in a newly produced seed.")]
371  public float MutationProbability { get; set; }
372 
373  [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the flowers.")]
374  public Color FlowerTint { get; set; }
375 
376  [Serialize(3, IsPropertySaveable.Yes, "Number of flowers drawn when fully grown")]
377  public int FlowerQuantity { get; set; }
378 
379  [Serialize(0.25f, IsPropertySaveable.Yes, "Size of the flower sprites.")]
380  public float BaseFlowerScale { get; set; }
381 
382  [Serialize(0.5f, IsPropertySaveable.Yes, "Size of the leaf sprites.")]
383  public float BaseLeafScale { get; set; }
384 
385  [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the leaves.")]
386  public Color LeafTint { get; set; }
387 
388  [Serialize(0.33f, IsPropertySaveable.Yes, "Chance of a leaf appearing behind a branch.")]
389  public float LeafProbability { get; set; }
390 
391  [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the vines.")]
392  public Color VineTint { get; set; }
393 
394  [Serialize(32, IsPropertySaveable.Yes, "Maximum number of vine tiles the plant can grow.")]
395  public int MaximumVines { get; set; }
396 
397  [Serialize(0.25f, IsPropertySaveable.Yes, "Size of the vine sprites.")]
398  public float VineScale { get; set; }
399 
400  [Serialize("0.26,0.27,0.29,1.0", IsPropertySaveable.Yes, "Tint of a dead plant.")]
401  public Color DeadTint { get; set; }
402 
403  [Serialize("1,1,1,1", IsPropertySaveable.Yes, "Probability for the plant to grow in a direction.")]
404  public Vector4 GrowthWeights { get; set; }
405 
406  [Serialize(0.0f, IsPropertySaveable.Yes, "How much damage is taken from fires.")]
407  public float FireVulnerability { get; set; }
408 
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;
415 
416  [Serialize(100.0f, IsPropertySaveable.Yes)]
417  public float Health
418  {
419  get => health;
420  set => health = Math.Clamp(value, 0, MaxHealth);
421  }
422 
423  public bool Decayed { get; set; }
424  public bool FullyGrown { get; set; }
425 
426  private const int maxProductDelay = 10,
427  maxVineGrowthDelay = 10;
428 
429  private int productDelay;
430  private int vineDelay;
431  private float fireCheckCooldown;
432 
433  public readonly List<ProducedItem> ProducedItems = new List<ProducedItem>();
434  public readonly List<VineTile> Vines = new List<VineTile>();
435  private readonly ProducedItem ProducedSeed;
436 
437  private static float MinFlowerScale = 0.5f, MaxFlowerScale = 1.0f, MinLeafScale = 0.5f, MaxLeafScale = 1.0f;
438  private const int VineChunkSize = 32;
439 
440  public Growable(Item item, ContentXElement element) : base(item, element)
441  {
442  SerializableProperty.DeserializeProperties(this, element);
443 
444  Health = MaxHealth;
445 
446  if (element.HasElements)
447  {
448  foreach (var subElement in element.Elements())
449  {
450  switch (subElement.Name.ToString().ToLowerInvariant())
451  {
452  case "produceditem":
453  ProducedItems.Add(new ProducedItem(this.item, subElement));
454  break;
455  case "vinesprites":
456  LoadVines(subElement);
457  break;
458  }
459  }
460  }
461 
462  ProducedSeed = new ProducedItem(this.item, this.item.Prefab, 1.0f);
463  flowerTiles = new int[FlowerQuantity];
464  }
465 
466  public override void OnItemLoaded()
467  {
468  base.OnItemLoaded();
469  if (flowerTiles.All(i => i == 0))
470  {
471  GenerateFlowerTiles();
472  }
473  }
474 
475  private void GenerateFlowerTiles(Random? random = null)
476  {
477  flowerTiles = new int[FlowerQuantity];
478  List<int> pool = new List<int>();
479  for (int i = 0; i < MaximumVines - 1; i++) { pool.Add(i); }
480 
481  for (int i = 0; i < flowerTiles.Length; i++)
482  {
483  int index = RandomInt(0, pool.Count, random);
484  flowerTiles[i] = pool[index];
485  pool.RemoveAt(index);
486  }
487  }
488 
489  partial void LoadVines(ContentXElement element);
490 
491  public void OnGrowthTick(Planter planter, PlantSlot slot)
492  {
493  if (Decayed) { return; }
494 
495  if (FullyGrown)
496  {
497  TryGenerateProduct(planter, slot);
498  }
499 
500  if (Health > 0)
501  {
502  GrowVines(planter, slot);
503 
504  // fertilizer makes the plant tick faster, compensate by halving water requirement
505  float multipler = planter.Fertilizer > 0 ? 0.5f : 1f;
506 
507  Health -= (accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness) * multipler;
508 
509  if (planter.Item.InWater)
510  {
511  Health -= FloodTolerance * multipler;
512  }
513 #if SERVER
514  if (FullyGrown)
515  {
516  if (serverHealthUpdateTimer > serverHealthUpdateDelay)
517  {
518  item.CreateServerEvent(this);
519  serverHealthUpdateTimer = 0;
520  }
521  else
522  {
523  serverHealthUpdateTimer++;
524  }
525  }
526 #endif
527  }
528 
529  CheckPlantState();
530 
531 #if CLIENT
532  UpdateBranchHealth();
533 #endif
534  }
535 
536  private void UpdateBranchHealth()
537  {
538  Color healthColor = Color.White * (1.0f - Health / MaxHealth);
539  foreach (VineTile vine in Vines)
540  {
541  vine.HealthColor = healthColor;
542  }
543  }
544 
545  private void TryGenerateProduct(Planter planter, PlantSlot slot)
546  {
547  productDelay++;
548  if (productDelay <= maxProductDelay) { return; }
549 
550  productDelay = 0;
551 
552  bool spawnProduct = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < ProductRate,
553  spawnSeed = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < SeedRate;
554 
555  Vector2 spawnPos;
556 
557  if (spawnProduct || spawnSeed)
558  {
559  VineTile vine = Vines.GetRandomUnsynced();
560  spawnPos = vine.GetWorldPosition(planter, slot.Offset);
561  }
562  else
563  {
564  return;
565  }
566 
567  if (spawnProduct && ProducedItems.Any())
568  {
569  SpawnItem(Item, ProducedItems.GetRandomByWeight(it => it.Probability, Rand.RandSync.Unsynced), spawnPos);
570  return;
571  }
572 
573  if (spawnSeed)
574  {
575  SpawnItem(Item, ProducedSeed, spawnPos);
576  }
577 
578  static void SpawnItem(Item thisItem, ProducedItem producedItem, Vector2 pos)
579  {
580  if (producedItem.Prefab == null) { return; }
581 
582  GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningProduce:" + thisItem.Prefab.Identifier + ":" + producedItem.Prefab.Identifier);
583 
584  Entity.Spawner?.AddItemToSpawnQueue(producedItem.Prefab, pos, onSpawned: it =>
585  {
586  foreach (StatusEffect effect in producedItem.StatusEffects)
587  {
588  it.ApplyStatusEffect(effect, ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
589  }
590 
591  it.ApplyStatusEffects(ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
592  });
593  }
594  }
595 
600  private bool CheckPlantState()
601  {
602  if (Decayed) { return true; }
603 
604  if (Health <= 0)
605  {
606  if (!Decayed)
607  {
608  GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningDied:" + item.Prefab.Identifier);
609  }
610 
611  Decayed = true;
612 #if CLIENT
613  foreach (VineTile vine in Vines)
614  {
615  vine.DecayDelay = (float)RandomDouble(0f, 30f);
616  }
617 #endif
618 #if SERVER
619  item.CreateServerEvent(this);
620 #endif
621  return true;
622  }
623 
624  if (Vines.Count >= MaximumVines && !FullyGrown)
625  {
626  FullyGrown = true;
627 #if SERVER
628  item.CreateServerEvent(this);
629 #endif
630  return true;
631  }
632 
633  if (!FullyGrown && !accelerateDeath && Vines.Any() && Vines.All(tile => !tile.CanGrowMore()))
634  {
635  accelerateDeath = true;
636  }
637 
638  // if the player somehow finds a way to extract the seed out of a planter kill the plant
639  if (item.ParentInventory is CharacterInventory)
640  {
641  Decayed = true;
642 #if SERVER
643  item.CreateServerEvent(this);
644 #endif
645  return true;
646  }
647 
648  return false;
649  }
650 
651  public override void Update(float deltaTime, Camera cam)
652  {
653  base.Update(deltaTime, cam);
654 
655  UpdateFires(deltaTime);
656 
657 #if CLIENT
658  foreach (VineTile vine in Vines)
659  {
660  vine.UpdateScale(deltaTime);
661  }
662 #endif
663 
664  CheckPlantState();
665  }
666 
667  private void UpdateFires(float deltaTime)
668  {
669  if (!Decayed && item.CurrentHull?.FireSources is { } fireSources && FireVulnerability > 0f)
670  {
671  if (fireCheckCooldown <= 0)
672  {
673  foreach (FireSource source in fireSources)
674  {
675  if (source.IsInDamageRange(item.WorldPosition, source.DamageRange))
676  {
677  Health -= FireVulnerability;
678  }
679  }
680 
681  fireCheckCooldown = 5f;
682  }
683  else
684  {
685  fireCheckCooldown -= deltaTime;
686  }
687  }
688  }
689 
690  private void GrowVines(Planter planter, PlantSlot slot)
691  {
692  if (FullyGrown) { return; }
693 
694  vineDelay++;
695  if (vineDelay <= maxVineGrowthDelay / GrowthSpeed) { return; }
696 
697  vineDelay = 0;
698 
699  if (!Vines.Any())
700  {
701  // generate first stem
702  GenerateStem();
703  return;
704  }
705 
706  int count = Vines.Count;
707 
708  TryGenerateBranches(planter, slot);
709 
710  if (Vines.Count > count)
711  {
712 #if SERVER
713  for (int i = 0; i < Vines.Count; i += VineChunkSize)
714  {
715  item.CreateServerEvent(this, new EventData(offset: i));
716  }
717 #elif CLIENT
718  ResetPlanterSize();
719 #endif
720  }
721  }
722 
723  private void GenerateStem()
724  {
725  VineTile stem = new VineTile(this, Vector2.Zero, VineTileType.Stem) { BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right };
726  Vines.Add(stem);
727  }
728 
729  private void TryGenerateBranches(Planter planter, PlantSlot slot, Random? random = null, Random? flowerRandom = null)
730  {
731  List<VineTile> newList = new List<VineTile>(Vines);
732  foreach (VineTile oldVines in newList)
733  {
734  if (oldVines.FailedGrowthAttempts > 8 || !oldVines.CanGrowMore()) { continue; }
735 
736  if (RandomInt(0, Vines.Count(tile => tile.CanGrowMore()), random) != 0) { continue; }
737 
738  TileSide side = oldVines.GetRandomFreeSide(random);
739 
740  if (side == TileSide.None)
741  {
742  oldVines.FailedGrowthAttempts++;
743  continue;
744  }
745 
746  if (GrowthWeights != Vector4.One)
747  {
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))
752  {
753  oldVines.FailedGrowthAttempts++;
754  continue;
755  }
756  }
757 
758  Vector2 pos = oldVines.AdjacentPositions[side];
759  Rectangle rect = VineTile.CreatePlantRect(pos);
760 
761  if (CollidesWithWorld(rect, planter, slot))
762  {
763  oldVines.BlockedSides |= side;
764  oldVines.FailedGrowthAttempts++;
765  continue;
766  }
767 
768  FoliageConfig flowerConfig = FoliageConfig.EmptyConfig;
769  FoliageConfig leafConfig = FoliageConfig.EmptyConfig;
770 
771  if (flowerTiles.Any(i => Vines.Count == i))
772  {
773  flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, MinFlowerScale, MaxFlowerScale, flowerRandom);
774  }
775 
776  if (LeafProbability >= RandomDouble(0d, 1.0d, flowerRandom) && leafVariants > 0)
777  {
778  leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, MinLeafScale, MaxLeafScale, flowerRandom);
779  }
780 
781  VineTile newVine = new VineTile(this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect);
782 
783  foreach (VineTile otherVine in Vines)
784  {
785  var (distX, distY) = pos - otherVine.Position;
786  int absDistX = (int)Math.Abs(distX), absDistY = (int)Math.Abs(distY);
787 
788  // check if the tile is within the with or height distance from us but ignore diagonals
789  if (absDistX > newVine.Rect.Width || absDistY > newVine.Rect.Height || absDistX > 0 && absDistY > 0) { continue; }
790 
791  // determines what side the tile is relative to the new tile by comparing the X/Y distance values
792  // 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
793  TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom;
794 
795  TileSide oppositeSide = connectingSide.GetOppositeSide();
796 
797  if (otherVine.BlockedSides.HasFlag(connectingSide))
798  {
799  newVine.BlockedSides |= oppositeSide;
800  continue;
801  }
802 
803  if (otherVine != oldVines)
804  {
805  otherVine.BlockedSides |= connectingSide;
806  newVine.BlockedSides |= oppositeSide;
807  }
808  else
809  {
810  otherVine.Sides |= connectingSide;
811  newVine.Sides |= oppositeSide;
812  }
813  }
814 
815  Vines.Add(newVine);
816 
817  foreach (VineTile vine in Vines)
818  {
819  vine.UpdateType();
820  }
821  }
822  }
823 
824  private bool CollidesWithWorld(Rectangle rect, Planter planter, PlantSlot slot)
825  {
826  if (Vines.Any(g => g.Rect.Contains(rect))) { return true; }
827 
828  Rectangle worldRect = rect;
829  worldRect.Location = planter.Item.WorldPosition.ToPoint() + slot.Offset.ToPoint() + worldRect.Location;
830  worldRect.Y -= worldRect.Height;
831 
832  Rectangle planterRect = planter.Item.WorldRect;
833  planterRect.Y -= planterRect.Height;
834 
835  if (planterRect.Intersects(worldRect))
836  {
837 #if DEBUG
838  if (!FailedRectangles.Contains(worldRect))
839  {
840  FailedRectangles.Add(worldRect);
841  }
842 #endif
843  return true;
844  }
845 
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));
850 
851  // ray casting a cross on the corners didn't seem to work so we are ray casting along the perimeter
852  bool hasCollision = planterRect.Intersects(worldRect) || LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft);
853 
854 #if DEBUG
855  if (hasCollision)
856  {
857  if (!FailedRectangles.Contains(worldRect))
858  {
859  FailedRectangles.Add(worldRect);
860  }
861  }
862 #endif
863  return hasCollision;
864 
865  static bool LineCollides(Vector2 point1, Vector2 point2)
866  {
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;
869  }
870  }
871 
872  public override XElement Save(XElement parentElement)
873  {
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)
878  {
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));
885 #if SERVER
886  vineElement.Add(new XAttribute("growthscale", Decayed ? 1.0f : 2.0f));
887 #else
888  vineElement.Add(new XAttribute("growthscale", vine.GrowthStep));
889 #endif
890  vineElement.Add(new XAttribute("flowerconfig", vine.FlowerConfig.Serialize()));
891  vineElement.Add(new XAttribute("leafconfig", vine.LeafConfig.Serialize()));
892 
893  element.Add(vineElement);
894  }
895 
896  return element;
897  }
898 
899  public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
900  {
901  base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
902  flowerTiles = componentElement.GetAttributeIntArray("flowertiles", Array.Empty<int>())!;
903  Decayed = componentElement.GetAttributeBool("decayed", false);
904 
905  Vines.Clear();
906  foreach (var element in componentElement.Elements())
907  {
908  if (element.Name.ToString().Equals("vine", StringComparison.OrdinalIgnoreCase))
909  {
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);
918 
919  VineTile tile = new VineTile(this, pos, type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig))
920  {
921  Sides = sides, BlockedSides = blockedSides, FailedGrowthAttempts = failedAttempts, GrowthStep = growthscale
922  };
923 
924  Vines.Add(tile);
925  }
926  }
927  }
928 
929  private bool CanGrowMore() => Vines.Any(tile => tile.CanGrowMore());
930 
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);
933  }
934 }
Interface for entities that the server can send events to the clients
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19