954 lines
35 KiB
C#
954 lines
35 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. Value of 1 means a vine attempts to grow every 10 seconds while 2 and 0.5 mean every 5 and 20 seconds respectively.")]
|
|
public float GrowthSpeed { get; set; }
|
|
|
|
[Serialize(100f, IsPropertySaveable.Yes, "How much water the plant can hold. Affects how long the plant can survive without water.")]
|
|
public float MaxWater { get; set; }
|
|
|
|
[Serialize(1f, IsPropertySaveable.Yes, "How much extra water the plant uses per second while it is submerged in a flooded hull.")]
|
|
public float ExtraWaterUsedPerSecondWhileFlooded { get; set; }
|
|
|
|
[Serialize(1f, IsPropertySaveable.Yes, "How much water the plant consumes passively per second.")]
|
|
public float WaterUsedPerSecond { get; set; }
|
|
|
|
[Serialize(0.01f, IsPropertySaveable.Yes, "Percentage chance of a seed item being produced on growth ticks (every 10 seconds without a multiplier). 0.01 means 1% chance. Not used in vanilla plants.")]
|
|
public float SeedSpawnChance { get; set; }
|
|
|
|
[Serialize(0.01f, IsPropertySaveable.Yes, "How often a product item is produced on growth ticks (every 10 seconds without a multiplier). 0.01 means 1% chance.")]
|
|
public float ProductSpawnChance { get; set; }
|
|
|
|
[Serialize(0.5f, IsPropertySaveable.Yes, "Completely unused property that was added on the first design pass but due to the first pass being too complex was never used and now it is used by mods so it cannot be removed.")]
|
|
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.")]
|
|
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 water is lost due to fires every 10 seconds.")]
|
|
public float FireVulnerability { get; set; }
|
|
|
|
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, "Modifier to the percentage of product and seed items produced before the plant is fully grown based on how many vines have been grown. 0 would mean no products or seeds are produced while 0.5 would mean half of the normal amount.")]
|
|
public Vector2 LinearProductAndSeedMultiplierBeforeFullyGrown { 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, MaxWater);
|
|
}
|
|
|
|
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);
|
|
|
|
// backwards compatibility
|
|
MaxWater = element.GetAttributeFloat("maxhealth", MaxWater);
|
|
WaterUsedPerSecond = element.GetAttributeFloat("hardiness", WaterUsedPerSecond);
|
|
ExtraWaterUsedPerSecondWhileFlooded = element.GetAttributeFloat("floodtolerance", ExtraWaterUsedPerSecondWhileFlooded);
|
|
ProductSpawnChance = element.GetAttributeFloat("productrate", ProductSpawnChance);
|
|
SeedSpawnChance = element.GetAttributeFloat("seedrate", SeedSpawnChance);
|
|
|
|
Health = MaxWater;
|
|
|
|
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; }
|
|
|
|
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 ? WaterUsedPerSecond * increasedDeathSpeed : WaterUsedPerSecond) * multipler;
|
|
|
|
if (planter.Item.InWater)
|
|
{
|
|
Health -= ExtraWaterUsedPerSecondWhileFlooded * 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 / MaxWater);
|
|
foreach (VineTile vine in Vines)
|
|
{
|
|
vine.HealthColor = healthColor;
|
|
}
|
|
}
|
|
|
|
private void TryGenerateProduct(Planter planter, PlantSlot slot)
|
|
{
|
|
productDelay++;
|
|
if (productDelay <= maxProductDelay) { return; }
|
|
productDelay = 0;
|
|
|
|
float spawnChanceMultiplier = 1f;
|
|
|
|
if (!FullyGrown)
|
|
{
|
|
if (LinearProductAndSeedMultiplierBeforeFullyGrown.NearlyEquals(Vector2.Zero)) { return; }
|
|
|
|
float growthProgress = Vines.Count / (float)MaximumVines;
|
|
|
|
spawnChanceMultiplier = MathHelper.Lerp(LinearProductAndSeedMultiplierBeforeFullyGrown.X, LinearProductAndSeedMultiplierBeforeFullyGrown.Y, growthProgress);
|
|
|
|
if (MathUtils.NearlyEqual(spawnChanceMultiplier, 0f)) { return; }
|
|
}
|
|
|
|
|
|
bool spawnProduct = Rand.Range(0f, 1f) < (ProductSpawnChance * spawnChanceMultiplier),
|
|
spawnSeed = Rand.Range(0f, 1f) < (SeedSpawnChance * spawnChanceMultiplier);
|
|
|
|
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 = 10f;
|
|
}
|
|
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);
|
|
}
|
|
} |