diff --git a/Content.Server/Botany/Components/AtmosphericGrowthComponent.cs b/Content.Server/Botany/Components/AtmosphericGrowthComponent.cs new file mode 100644 index 00000000000..8894e3f6862 --- /dev/null +++ b/Content.Server/Botany/Components/AtmosphericGrowthComponent.cs @@ -0,0 +1,35 @@ +using Content.Shared.Atmos; + +namespace Content.Server.Botany.Components; + +/// +/// Atmospheric-related requirements for proper entity growth. Used in botany. +/// +[RegisterComponent] +[DataDefinition] +public sealed partial class AtmosphericGrowthComponent : Component +{ + /// + /// Ideal temperature for plant growth in Kelvin. + /// + [DataField] + public float IdealHeat = Atmospherics.T20C; + + /// + /// Temperature tolerance range around . + /// + [DataField] + public float HeatTolerance = 10f; + + /// + /// Minimum pressure tolerance for plant growth. + /// + [DataField] + public float LowPressureTolerance = 81f; + + /// + /// Maximum pressure tolerance for plant growth. + /// + [DataField] + public float HighPressureTolerance = 121f; +} diff --git a/Content.Server/Botany/Components/BasicGrowthComponent.cs b/Content.Server/Botany/Components/BasicGrowthComponent.cs new file mode 100644 index 00000000000..aac178dfe7a --- /dev/null +++ b/Content.Server/Botany/Components/BasicGrowthComponent.cs @@ -0,0 +1,21 @@ +namespace Content.Server.Botany.Components; + +/// +/// Basic parameters for plant growth. +/// +[RegisterComponent] +[DataDefinition] +public sealed partial class BasicGrowthComponent : Component +{ + /// + /// Amount of water consumed per growth tick. + /// + [DataField] + public float WaterConsumption = 0.5f; + + /// + /// Amount of nutrients consumed per growth tick. + /// + [DataField] + public float NutrientConsumption = 0.75f; +} diff --git a/Content.Server/Botany/Components/BotanySwabComponent.cs b/Content.Server/Botany/Components/BotanySwabComponent.cs index 5fad5764b41..8652dbcfa9f 100644 --- a/Content.Server/Botany/Components/BotanySwabComponent.cs +++ b/Content.Server/Botany/Components/BotanySwabComponent.cs @@ -1,19 +1,19 @@ -using System.Threading; +namespace Content.Server.Botany.Components; -namespace Content.Server.Botany +/// +/// Anything that can be used to cross-pollinate plants. +/// +[RegisterComponent] +public sealed partial class BotanySwabComponent : Component { /// - /// Anything that can be used to cross-pollinate plants. + /// Delay between swab uses. /// - [RegisterComponent] - public sealed partial class BotanySwabComponent : Component - { - [DataField("swabDelay")] - public float SwabDelay = 2f; + [DataField] + public TimeSpan SwabDelay = TimeSpan.FromSeconds(2); - /// - /// SeedData from the first plant that got swabbed. - /// - public SeedData? SeedData; - } + /// + /// SeedData from the first plant that got swabbed. + /// + public SeedData? SeedData; } diff --git a/Content.Server/Botany/Components/ConsumeExudeGasGrowthComponent.cs b/Content.Server/Botany/Components/ConsumeExudeGasGrowthComponent.cs new file mode 100644 index 00000000000..96d92693f30 --- /dev/null +++ b/Content.Server/Botany/Components/ConsumeExudeGasGrowthComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared.Atmos; + +namespace Content.Server.Botany.Components; + +/// +/// Data for gas to consume/exude on plant growth. +/// +[RegisterComponent] +[DataDefinition] +public sealed partial class ConsumeExudeGasGrowthComponent : Component +{ + /// + /// Dictionary of gases and their consumption rates per growth tick. + /// + [DataField] public Dictionary ConsumeGasses = new(); + + /// + /// Dictionary of gases and their exude rates per growth tick. + /// + [DataField] public Dictionary ExudeGasses = new(); +} diff --git a/Content.Server/Botany/Components/GrowthComponentsHolder.cs b/Content.Server/Botany/Components/GrowthComponentsHolder.cs new file mode 100644 index 00000000000..d2c8ef65fd8 --- /dev/null +++ b/Content.Server/Botany/Components/GrowthComponentsHolder.cs @@ -0,0 +1,87 @@ +using System.Linq; +using System.Reflection; + +namespace Content.Server.Botany.Components; + +/// +/// TODO: Delete after plants transition to entities. +/// This is an intentionally evil approach kept only to simplify the +/// upcoming refactor: plants will become standalone entities that own these components. +/// Once that happens, this holder is no longer needed. +/// +[DataDefinition] +public sealed partial class GrowthComponentsHolder +{ + public static readonly PropertyInfo[] ComponentGetters = typeof(GrowthComponentsHolder).GetProperties(); + public static readonly Type[] GrowthComponentTypes = ComponentGetters.Select(x => x.PropertyType).ToArray(); + + /// + /// Extra-traits. + /// + [DataField] + public PlantTraitsComponent? PlantTraits { get; set; } + + /// + /// Plant characteristics. + /// + [DataField] + public PlantComponent? Plant { get; set; } + + /// + /// Basic properties for plant growth. + /// + [DataField] + public BasicGrowthComponent? BasicGrowth { get; set; } + + /// + /// What defence plant have against toxins? + /// + [DataField] + public PlantToxinsComponent? Toxins { get; set; } + + /// + /// Harvesting process-related data. + /// + [DataField] + public PlantHarvestComponent? Harvest { get; set; } + + /// + /// Atmos-related environment requirements for plant growth. + /// + [DataField] + public AtmosphericGrowthComponent? AtmosphericGrowth { get; set; } + + /// + /// What gases plant consume/exude upon growth. + /// + [DataField] + public ConsumeExudeGasGrowthComponent? ConsumeExudeGasGrowth { get; set; } + + /// + /// Weeds and pests related data for plant. + /// + [DataField] + public WeedPestGrowthComponent? WeedPestGrowth { get; set; } + + /// + /// Damage tolerance of plant. + /// + [DataField] + public UnviableGrowthComponent? UnviableGrowth { get; set; } + + /// + /// Populates any null properties with default component instances so that + /// systems can always apply a full set. Existing (YAML-provided) values are kept. + /// + public void EnsureGrowthComponents() + { + foreach (var prop in ComponentGetters) + { + if (prop.GetValue(this) == null) + { + var instance = Activator.CreateInstance(prop.PropertyType); // this is really cursed and should not be used in master, also this should be blocked by sandboxing. + prop.SetValue(this, instance); + } + } + } +} diff --git a/Content.Server/Botany/Components/PlantComponent.cs b/Content.Server/Botany/Components/PlantComponent.cs new file mode 100644 index 00000000000..066ec206da7 --- /dev/null +++ b/Content.Server/Botany/Components/PlantComponent.cs @@ -0,0 +1,51 @@ +namespace Content.Server.Botany.Components; + +/// +/// Component for storing core plant data. +/// +[RegisterComponent] +[DataDefinition] +public sealed partial class PlantComponent : Component +{ + /// + /// The plant's max health. + /// + [DataField] + public float Endurance = 100f; + + /// + /// How many produce are created on harvest. + /// + [DataField] + public int Yield; + + /// + /// The number of growth ticks this plant can be alive for. Plants take high damage levels when Age > Lifespan. + /// + [DataField] + public float Lifespan; + + /// + /// The number of growth ticks it takes for a plant to reach its final growth stage. + /// + [DataField] + public float Maturation; + + /// + /// The number of growth ticks it takes for a plant to be (re-)harvestable. Shouldn't be lower than Maturation. + /// + [DataField] + public float Production; + + /// + /// How many different sprites appear before the plant is fully grown. + /// + [DataField] + public int GrowthStages = 6; + + /// + /// A scalar for sprite size and chemical solution volume in the produce. Caps at 100. + /// + [DataField] + public float Potency = 1f; +} diff --git a/Content.Server/Botany/Components/PlantHarvestComponent.cs b/Content.Server/Botany/Components/PlantHarvestComponent.cs new file mode 100644 index 00000000000..2a3c918218c --- /dev/null +++ b/Content.Server/Botany/Components/PlantHarvestComponent.cs @@ -0,0 +1,48 @@ +namespace Content.Server.Botany.Components; + +/// +/// Data for plant harvesting process. +/// +[RegisterComponent] +[DataDefinition] +public sealed partial class PlantHarvestComponent : Component +{ + /// + /// Harvest repeat type. + /// + [DataField] + public HarvestType HarvestRepeat = HarvestType.NoRepeat; + + /// + /// Whether the plant is currently ready for harvest. + /// + [ViewVariables] + public bool ReadyForHarvest = false; + + /// + /// The age of the plant when last harvested. + /// + [ViewVariables] + public int LastHarvest = 0; +} + +/// +/// Harvest options for plants. +/// +public enum HarvestType +{ + /// + /// Plant is removed on harvest. + /// + NoRepeat, + + /// + /// Plant makes produce every Production ticks. + /// + Repeat, + + /// + /// Repeat, plus produce is dropped on the ground near the plant automatically. + /// + SelfHarvest +} diff --git a/Content.Server/Botany/Components/PlantHolderComponent.cs b/Content.Server/Botany/Components/PlantHolderComponent.cs index d65e1b702be..e72ec57d699 100644 --- a/Content.Server/Botany/Components/PlantHolderComponent.cs +++ b/Content.Server/Botany/Components/PlantHolderComponent.cs @@ -4,6 +4,9 @@ using Robust.Shared.Audio; namespace Content.Server.Botany.Components; +/// +/// Container for plant-holder and plant combined data. +/// [RegisterComponent] public sealed partial class PlantHolderComponent : Component { @@ -14,14 +17,8 @@ public sealed partial class PlantHolderComponent : Component public TimeSpan NextUpdate = TimeSpan.Zero; /// - /// Time between plant reagent consumption updates. + /// Number of missing gases required for plant growth. /// - [DataField] - public TimeSpan UpdateDelay = TimeSpan.FromSeconds(3); - - [DataField] - public int LastProduce; - [DataField] public int MissingGas; @@ -31,6 +28,12 @@ public sealed partial class PlantHolderComponent : Component [DataField] public TimeSpan CycleDelay = TimeSpan.FromSeconds(15f); + /// + /// Time between plant reagent consumption updates. + /// + [DataField] + public TimeSpan UpdateDelay = TimeSpan.FromSeconds(3); + /// /// Game time when the plant last did a growth update. /// @@ -43,6 +46,9 @@ public sealed partial class PlantHolderComponent : Component [DataField] public SoundSpecifier? WateringSound; + /// + /// Whether to update the sprite after the next update cycle. + /// [DataField] public bool UpdateSpriteAfterUpdate; @@ -53,33 +59,54 @@ public sealed partial class PlantHolderComponent : Component [DataField] public bool DrawWarnings = false; + /// + /// Current water level in the plant holder (0-100). + /// [DataField] public float WaterLevel = 100f; + /// + /// Current nutrient level in the plant holder (0-100). + /// [DataField] public float NutritionLevel = 100f; + /// + /// Current pest level in the plant holder (0-10). + /// [DataField] public float PestLevel; + /// + /// Current weed level in the plant holder (0-10). + /// [DataField] public float WeedLevel; + /// + /// Current toxin level in the plant holder (0-100). + /// [DataField] public float Toxins; + /// + /// Current age of the plant in growth cycles. + /// [DataField] public int Age; + /// + /// Number of growth cycles to skip due to poor conditions. + /// [DataField] public int SkipAging; + /// + /// Whether the plant is dead. + /// [DataField] public bool Dead; - [DataField] - public bool Harvest; - /// /// Set to true if this plant has been clipped by seed clippers. Used to prevent a single plant /// from repeatedly being clipped. @@ -93,18 +120,33 @@ public sealed partial class PlantHolderComponent : Component [DataField] public int YieldMod = 1; + /// + /// Multiplier for mutation chance and severity. + /// [DataField] public float MutationMod = 1f; + /// + /// Current mutation level (0-100). + /// [DataField] public float MutationLevel; + /// + /// Current health of the plant (0 to seed endurance). + /// [DataField] public float Health; + /// + /// Multiplier for weed growth rate. + /// [DataField] public float WeedCoefficient = 1f; + /// + /// Seed data for the currently planted seed. + /// [DataField] public SeedData? Seed; @@ -120,12 +162,6 @@ public sealed partial class PlantHolderComponent : Component [DataField] public bool ImproperPressure; - /// - /// Not currently used. - /// - [DataField] - public bool ImproperLight; - /// /// Set to true to force a plant update (visuals, component, etc.) regardless of the current /// update cycle time. Typically used when some interaction affects this plant. @@ -133,9 +169,15 @@ public sealed partial class PlantHolderComponent : Component [DataField] public bool ForceUpdate; + /// + /// Name of the solution container that holds the soil/nutrient solution. + /// [DataField] public string SoilSolutionName = "soil"; + /// + /// Reference to the soil solution container. + /// [ViewVariables] public Entity? SoilSolution = null; } diff --git a/Content.Server/Botany/Components/PlantToxinsComponent.cs b/Content.Server/Botany/Components/PlantToxinsComponent.cs new file mode 100644 index 00000000000..e02bdd196bc --- /dev/null +++ b/Content.Server/Botany/Components/PlantToxinsComponent.cs @@ -0,0 +1,21 @@ +namespace Content.Server.Botany.Components; + +/// +/// Data for plant resistance to toxins. +/// +[RegisterComponent] +[DataDefinition] +public sealed partial class PlantToxinsComponent : Component +{ + /// + /// Maximum toxin level the plant can tolerate before taking damage. + /// + [DataField] + public float ToxinsTolerance = 4f; + + /// + /// Divisor for calculating toxin uptake rate. Higher values mean slower toxin processing. + /// + [DataField] + public float ToxinUptakeDivisor = 10f; +} diff --git a/Content.Server/Botany/Components/PlantTraitsComponent.cs b/Content.Server/Botany/Components/PlantTraitsComponent.cs new file mode 100644 index 00000000000..b2c1253060e --- /dev/null +++ b/Content.Server/Botany/Components/PlantTraitsComponent.cs @@ -0,0 +1,49 @@ +using Robust.Shared.Prototypes; + +namespace Content.Server.Botany.Components; + +/// +/// Component for managing special plant traits and mutations. +/// +/// TODO: The logic for these component is quite hardcoded. +/// They require a separate a system that will use events or APIs from other growth systems. +[RegisterComponent] +[DataDefinition] +public sealed partial class PlantTraitsComponent : Component +{ + /// + /// If true, produce can't be put into the seed maker. + /// + [DataField] + public bool Seedless = false; + + /// + /// If true, a sharp tool is required to harvest this plant. + /// + [DataField] + public bool Ligneous = false; + + /// + /// If true, the plant can scream when harvested. + /// + [DataField] + public bool CanScream = false; + + /// + /// If true, the plant can turn into kudzu. + /// + [DataField] + public bool TurnIntoKudzu = false; + + /// + /// Which kind of kudzu this plant will turn into if it kuzuifies. + /// + [DataField] + public EntProtoId KudzuPrototype = "WeakKudzu"; + + /// + /// If false, rapidly decrease health while growing. Adds a bit of challenge to keep mutated plants alive via Unviable's frequency. + /// + [DataField] + public bool Viable = true; +} diff --git a/Content.Server/Botany/Components/ProduceComponent.cs b/Content.Server/Botany/Components/ProduceComponent.cs index db4ed62dd38..344fc7a89f6 100644 --- a/Content.Server/Botany/Components/ProduceComponent.cs +++ b/Content.Server/Botany/Components/ProduceComponent.cs @@ -1,24 +1,31 @@ using Content.Server.Botany.Systems; using Content.Shared.Botany.Components; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Prototypes; namespace Content.Server.Botany.Components; +/// +/// Produce-related data for plant and plant growth cycle. +/// [RegisterComponent] [Access(typeof(BotanySystem))] public sealed partial class ProduceComponent : SharedProduceComponent { - [DataField("targetSolution")] public string SolutionName { get; set; } = "food"; + /// + /// Name of the solution container that holds the produce's contents. + /// + [DataField("targetSolution")] + public string SolutionName { get; set; } = "food"; /// - /// Seed data used to create a when this produce has its seeds extracted. + /// Seed data used to create a when this produce has its seeds extracted. /// [DataField] public SeedData? Seed; /// - /// Seed data used to create a when this produce has its seeds extracted. + /// Prototype ID for the seed that can be extracted from this produce. /// - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? SeedId; + [DataField] + public ProtoId? SeedId; } diff --git a/Content.Server/Botany/Components/SeedComponent.cs b/Content.Server/Botany/Components/SeedComponent.cs index ffa1b7ef4e9..0f82d88720a 100644 --- a/Content.Server/Botany/Components/SeedComponent.cs +++ b/Content.Server/Botany/Components/SeedComponent.cs @@ -1,30 +1,32 @@ using Content.Server.Botany.Systems; using Content.Shared.Botany.Components; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Prototypes; -namespace Content.Server.Botany.Components +namespace Content.Server.Botany.Components; + +/// +/// Data container for plant seed. Contains all info (values for components) for new plant to grow from seed. +/// +[RegisterComponent, Access(typeof(BotanySystem))] +public sealed partial class SeedComponent : SharedSeedComponent { - [RegisterComponent, Access(typeof(BotanySystem))] - public sealed partial class SeedComponent : SharedSeedComponent - { - /// - /// Seed data containing information about the plant type & properties that this seed can grow seed. If - /// null, will instead attempt to get data from a seed prototype, if one is defined. See . - /// - [DataField("seed")] - public SeedData? Seed; + /// + /// Seed data containing information about the plant type & properties that this seed can grow seed. If + /// null, will instead attempt to get data from a seed prototype, if one is defined. See . + /// + [DataField] + public SeedData? Seed; - /// - /// If not null, overrides the plant's initial health. Otherwise, the plant's initial health is set to the Endurance value. - /// - [DataField] - public float? HealthOverride = null; + /// + /// If not null, overrides the plant's initial health. Otherwise, the plant's initial health is set to the Endurance value. + /// + [DataField] + public float? HealthOverride = null; - /// - /// Name of a base seed prototype that is used if is null. - /// - [DataField("seedId", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? SeedId; - } + /// + /// Name of a base seed prototype that is used if is null. + /// + [DataField] + public ProtoId? SeedId; } diff --git a/Content.Server/Botany/Components/UnviableGrowthComponent.cs b/Content.Server/Botany/Components/UnviableGrowthComponent.cs new file mode 100644 index 00000000000..745d8e1ab8e --- /dev/null +++ b/Content.Server/Botany/Components/UnviableGrowthComponent.cs @@ -0,0 +1,15 @@ +namespace Content.Server.Botany.Components; + +/// +/// Damage tolerance of plant. +/// +[RegisterComponent] +[DataDefinition] +public sealed partial class UnviableGrowthComponent : Component +{ + /// + /// Amount of damage dealt to the plant per growth tick with unviable. + /// + [DataField] + public float UnviableDamage = 6f; +} diff --git a/Content.Server/Botany/Components/WeedPestGrowthComponent.cs b/Content.Server/Botany/Components/WeedPestGrowthComponent.cs new file mode 100644 index 00000000000..ae80a30c7ef --- /dev/null +++ b/Content.Server/Botany/Components/WeedPestGrowthComponent.cs @@ -0,0 +1,52 @@ +namespace Content.Server.Botany.Components; + +/// +/// Data for weed and pest problems which can happen to plants - how well plant tolerates them, +/// chances to develop them, how big of a problem they will be. +/// +[RegisterComponent] +[DataDefinition] +public sealed partial class WeedPestGrowthComponent : Component +{ + /// + /// Maximum weed level the plant can tolerate before taking damage. + /// + [DataField] + public float WeedTolerance = 5f; + + /// + /// Maximum pest level the plant can tolerate before taking damage. + /// + [DataField] + public float PestTolerance = 5f; + + /// + /// Chance per tick for weeds to grow around this plant. + /// + [DataField] + public float WeedGrowthChance = 0.01f; + + /// + /// Amount of weed growth per successful weed growth tick. + /// + [DataField] + public float WeedGrowthAmount = 0.5f; + + /// + /// Weed level threshold at which the plant is considered overgrown and will transform into kudzu. + /// + [DataField] + public float WeedHighLevelThreshold = 10f; + + /// + /// Chance per tick for pests to damage this plant. + /// + [DataField] + public float PestDamageChance = 0.05f; + + /// + /// Amount of damage dealt to the plant per successful pest damage tick. + /// + [DataField] + public float PestDamageAmount = 1f; +} diff --git a/Content.Server/Botany/SeedPrototype.cs b/Content.Server/Botany/SeedPrototype.cs index a01d96a8781..8b58fdec0eb 100644 --- a/Content.Server/Botany/SeedPrototype.cs +++ b/Content.Server/Botany/SeedPrototype.cs @@ -1,10 +1,10 @@ -using Content.Shared.Atmos; +using Content.Server.Botany.Components; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Random; using Robust.Shared.Audio; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; namespace Content.Server.Botany; @@ -15,66 +15,29 @@ public sealed partial class SeedPrototype : SeedData, IPrototype [IdDataField] public string ID { get; private set; } = default!; } -public enum HarvestType : byte -{ - NoRepeat, - Repeat, - SelfHarvest -} - -/* - public enum PlantSpread : byte - { - NoSpread, - Creepers, - Vines, - } - - public enum PlantMutation : byte - { - NoMutation, - Mutable, - HighlyMutable, - } - - public enum PlantCarnivorous : byte - { - NotCarnivorous, - EatPests, - EatLivingBeings, - } - - public enum PlantJuicy : byte - { - NotJuicy, - Juicy, - Slippery, - } -*/ - [DataDefinition] public partial struct SeedChemQuantity { /// /// Minimum amount of chemical that is added to produce, regardless of the potency /// - [DataField("Min")] public FixedPoint2 Min = FixedPoint2.Epsilon; + [DataField] public FixedPoint2 Min = FixedPoint2.Epsilon; /// /// Maximum amount of chemical that can be produced after taking plant potency into account. /// - [DataField("Max")] public FixedPoint2 Max; + [DataField] public FixedPoint2 Max; /// /// When chemicals are added to produce, the potency of the seed is divided with this value. Final chemical amount is the result plus the `Min` value. /// Example: PotencyDivisor of 20 with seed potency of 55 results in 2.75, 55/20 = 2.75. If minimum is 1 then final result will be 3.75 of that chemical, 55/20+1 = 3.75. /// - [DataField("PotencyDivisor")] public float PotencyDivisor; + [DataField] public float PotencyDivisor; /// /// Inherent chemical is one that is NOT result of mutation or crossbreeding. These chemicals are removed if species mutation is executed. /// - [DataField("Inherent")] public bool Inherent = true; + [DataField] public bool Inherent = true; } // TODO Make Botany ECS and give it a proper API. I removed the limited access of this class because it's egregious how many systems needed access to it due to a lack of an actual API. @@ -89,34 +52,35 @@ public partial class SeedData #region Tracking /// - /// The name of this seed. Determines the name of seed packets. + /// The name of this seed. Determines the name of seed packets. /// - [DataField("name")] + [DataField] public string Name { get; private set; } = ""; /// - /// The noun for this type of seeds. E.g. for fungi this should probably be "spores" instead of "seeds". Also - /// used to determine the name of seed packets. + /// The noun for this type of seeds. E.g. for fungi this should probably be "spores" instead of "seeds". Also + /// used to determine the name of seed packets. /// - [DataField("noun")] + [DataField] public string Noun { get; private set; } = ""; /// - /// Name displayed when examining the hydroponics tray. Describes the actual plant, not the seed itself. + /// Name displayed when examining the hydroponics tray. Describes the actual plant, not the seed itself. /// - [DataField("displayName")] + [DataField] public string DisplayName { get; private set; } = ""; - [DataField("mysterious")] public bool Mysterious; + [DataField] public bool Mysterious; /// - /// If true, the properties of this seed cannot be modified. + /// If true, the properties of this seed cannot be modified. /// - [DataField("immutable")] public bool Immutable; + [DataField] + public bool Immutable; /// - /// If true, there is only a single reference to this seed and it's properties can be directly modified without - /// needing to clone the seed. + /// If true, there is only a single reference to this seed and its properties can be directly modified without + /// needing to clone the seed. /// [ViewVariables] public bool Unique = false; // seed-prototypes or yaml-defined seeds for entity prototypes will not generally be unique. @@ -124,91 +88,19 @@ public partial class SeedData #region Output /// - /// The entity prototype that is spawned when this type of seed is extracted from produce using a seed extractor. - /// - [DataField("packetPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string PacketPrototype = "SeedBase"; - - /// - /// The entity prototype this seed spawns when it gets harvested. + /// The entity prototype that is spawned when this type of seed is extracted from produce using a seed extractor. /// [DataField] - public List ProductPrototypes = new(); - - [DataField] public Dictionary Chemicals = new(); - - [DataField] public Dictionary ConsumeGasses = new(); - - [DataField] public Dictionary ExudeGasses = new(); - - #endregion - - #region Tolerances - - [DataField] public float NutrientConsumption = 0.75f; - - [DataField] public float WaterConsumption = 0.5f; - [DataField] public float IdealHeat = 293f; - [DataField] public float HeatTolerance = 10f; - [DataField] public float IdealLight = 7f; - [DataField] public float LightTolerance = 3f; - [DataField] public float ToxinsTolerance = 4f; - - [DataField] public float LowPressureTolerance = 81f; - - [DataField] public float HighPressureTolerance = 121f; - - [DataField] public float PestTolerance = 5f; - - [DataField] public float WeedTolerance = 5f; - - [DataField] public float WeedHighLevelThreshold = 10f; - - #endregion - - #region General traits - - [DataField] public float Endurance = 100f; - - [DataField] public int Yield; - [DataField] public float Lifespan; - [DataField] public float Maturation; - [DataField] public float Production; - [DataField] public int GrowthStages = 6; - - [DataField] public HarvestType HarvestRepeat = HarvestType.NoRepeat; - - [DataField] public float Potency = 1f; + public EntProtoId PacketPrototype = "SeedBase"; /// - /// If true, cannot be harvested for seeds. Balances hybrids and - /// mutations. + /// The entity prototypes that are spawned when this type of seed is harvested. /// - [DataField] public bool Seedless = false; + [DataField] + public List ProductPrototypes = []; - /// - /// If false, rapidly decrease health while growing. Used to kill off - /// plants with "bad" mutations. - /// - [DataField] public bool Viable = true; - - /// - /// If true, a sharp tool is required to harvest this plant. - /// - [DataField] public bool Ligneous; - - // No, I'm not removing these. - // if you re-add these, make sure that they get cloned. - //public PlantSpread Spread { get; set; } - //public PlantMutation Mutation { get; set; } - //public float AlterTemperature { get; set; } - //public PlantCarnivorous Carnivorous { get; set; } - //public bool Parasite { get; set; } - //public bool Hematophage { get; set; } - //public bool Thorny { get; set; } - //public bool Stinging { get; set; } - // public bool Teleporting { get; set; } - // public PlantJuicy Juicy { get; set; } + [DataField] + public Dictionary Chemicals = []; #endregion @@ -225,44 +117,51 @@ public partial class SeedData [DataField] public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("PlantScreams", AudioParams.Default.WithVolume(-10)); - [DataField("screaming")] public bool CanScream; - - [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string KudzuPrototype = "WeakKudzu"; - - [DataField] public bool TurnIntoKudzu; - [DataField] public string? SplatPrototype { get; set; } - #endregion /// /// The mutation effects that have been applied to this plant. /// - [DataField] public List Mutations { get; set; } = new(); + [DataField] + public List Mutations { get; set; } = []; /// - /// The seed prototypes this seed may mutate into when prompted to. + /// The seed prototypes this seed may mutate into when prompted to. /// [DataField] - public List> MutationPrototypes = new(); + public List> MutationPrototypes = []; /// - /// Log impact for when the seed is planted. + /// The growth components used by this seed. + /// TODO: Delete after plants transition to entities /// [DataField] - public LogImpact? PlantLogImpact = null; + public GrowthComponentsHolder GrowthComponents = new(); /// - /// Log impact for when the seed is harvested. + /// Log impact for harvest operations. /// [DataField] - public LogImpact? HarvestLogImpact = null; + public LogImpact? HarvestLogImpact; + + /// + /// Log impact for plant operations. + /// + [DataField] + public LogImpact? PlantLogImpact; public SeedData Clone() { - DebugTools.Assert(!Immutable, "There should be no need to clone an immutable seed."); + if (Immutable) + return this; + + var serializationManager = IoCManager.Resolve(); var newSeed = new SeedData { + GrowthComponents = serializationManager.CreateCopy(GrowthComponents, notNullableOverride: true), + HarvestLogImpact = HarvestLogImpact, + PlantLogImpact = PlantLogImpact, Name = Name, Noun = Noun, DisplayName = DisplayName, @@ -272,40 +171,10 @@ public partial class SeedData ProductPrototypes = new List(ProductPrototypes), MutationPrototypes = new List>(MutationPrototypes), Chemicals = new Dictionary(Chemicals), - ConsumeGasses = new Dictionary(ConsumeGasses), - ExudeGasses = new Dictionary(ExudeGasses), - - NutrientConsumption = NutrientConsumption, - WaterConsumption = WaterConsumption, - IdealHeat = IdealHeat, - HeatTolerance = HeatTolerance, - IdealLight = IdealLight, - LightTolerance = LightTolerance, - ToxinsTolerance = ToxinsTolerance, - LowPressureTolerance = LowPressureTolerance, - HighPressureTolerance = HighPressureTolerance, - PestTolerance = PestTolerance, - WeedTolerance = WeedTolerance, - - Endurance = Endurance, - Yield = Yield, - Lifespan = Lifespan, - Maturation = Maturation, - Production = Production, - GrowthStages = GrowthStages, - HarvestRepeat = HarvestRepeat, - Potency = Potency, - - Seedless = Seedless, - Viable = Viable, - Ligneous = Ligneous, PlantRsi = PlantRsi, PlantIconState = PlantIconState, - CanScream = CanScream, - TurnIntoKudzu = TurnIntoKudzu, - SplatPrototype = SplatPrototype, - Mutations = new List(), + Mutations = new List(Mutations), // Newly cloned seed is unique. No need to unnecessarily clone if repeatedly modified. Unique = true, @@ -321,8 +190,12 @@ public partial class SeedData /// public SeedData SpeciesChange(SeedData other) { + var serializationManager = IoCManager.Resolve(); var newSeed = new SeedData { + GrowthComponents = serializationManager.CreateCopy(other.GrowthComponents, notNullableOverride: true), + HarvestLogImpact = other.HarvestLogImpact, + PlantLogImpact = other.PlantLogImpact, Name = other.Name, Noun = other.Noun, DisplayName = other.DisplayName, @@ -333,41 +206,11 @@ public partial class SeedData MutationPrototypes = new List>(other.MutationPrototypes), Chemicals = new Dictionary(Chemicals), - ConsumeGasses = new Dictionary(ConsumeGasses), - ExudeGasses = new Dictionary(ExudeGasses), - - NutrientConsumption = NutrientConsumption, - WaterConsumption = WaterConsumption, - IdealHeat = IdealHeat, - HeatTolerance = HeatTolerance, - IdealLight = IdealLight, - LightTolerance = LightTolerance, - ToxinsTolerance = ToxinsTolerance, - LowPressureTolerance = LowPressureTolerance, - HighPressureTolerance = HighPressureTolerance, - PestTolerance = PestTolerance, - WeedTolerance = WeedTolerance, - - Endurance = Endurance, - Yield = Yield, - Lifespan = Lifespan, - Maturation = Maturation, - Production = Production, - GrowthStages = other.GrowthStages, - HarvestRepeat = HarvestRepeat, - Potency = Potency, Mutations = Mutations, - Seedless = Seedless, - Viable = Viable, - Ligneous = Ligneous, - PlantRsi = other.PlantRsi, PlantIconState = other.PlantIconState, - CanScream = CanScream, - TurnIntoKudzu = TurnIntoKudzu, - SplatPrototype = other.SplatPrototype, // Newly cloned seed is unique. No need to unnecessarily clone if repeatedly modified. Unique = true, diff --git a/Content.Server/Botany/Systems/AtmosphericGrowthSystem.cs b/Content.Server/Botany/Systems/AtmosphericGrowthSystem.cs new file mode 100644 index 00000000000..232f9fbe582 --- /dev/null +++ b/Content.Server/Botany/Systems/AtmosphericGrowthSystem.cs @@ -0,0 +1,55 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.Botany.Components; +using Content.Shared.Atmos; +using Robust.Shared.Random; + +namespace Content.Server.Botany.Systems; + +/// +/// Applies atmospheric temperature and pressure effects to plants during growth ticks. +/// Uses current tile gas mixture to penalize or clear warnings based on tolerances. +/// +public sealed class AtmosphericGrowthSystem : EntitySystem +{ + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnPlantGrow); + } + + private void OnPlantGrow(Entity ent, ref OnPlantGrowEvent args) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantHolderComponent? holder)) + return; + + var environment = _atmosphere.GetContainingMixture(uid, true, true) ?? GasMixture.SpaceGas; + if (MathF.Abs(environment.Temperature - component.IdealHeat) > component.HeatTolerance) + { + holder.Health -= _random.Next(1, 3); + holder.ImproperHeat = true; + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + } + else + { + holder.ImproperHeat = false; + } + + var pressure = environment.Pressure; + if (pressure < component.LowPressureTolerance || pressure > component.HighPressureTolerance) + { + holder.Health -= _random.Next(1, 3); + holder.ImproperPressure = true; + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + } + else + { + holder.ImproperPressure = false; + } + } +} diff --git a/Content.Server/Botany/Systems/BasicGrowthSystem.cs b/Content.Server/Botany/Systems/BasicGrowthSystem.cs new file mode 100644 index 00000000000..3ef0db6ce16 --- /dev/null +++ b/Content.Server/Botany/Systems/BasicGrowthSystem.cs @@ -0,0 +1,192 @@ +using Content.Server.Botany.Components; +using Content.Shared.Swab; +using Robust.Shared.Random; + +namespace Content.Server.Botany.Systems; + +/// +/// Handles baseline plant progression each growth tick: aging, resource consumption, +/// simple viability checks, and basic swab cross-pollination behavior. +/// +public sealed class BasicGrowthSystem : EntitySystem +{ + [Dependency] private readonly BotanySystem _botany = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + + // TODO: Multipliers should be taken from the hydroponics component. + /// + /// Multiplier for plant growth speed in hydroponics. + /// + public const float HydroponicsSpeedMultiplier = 1f; + + /// + /// Multiplier for resource consumption (water, nutrients) in hydroponics. + /// + public const float HydroponicsConsumptionMultiplier = 2f; + + public override void Initialize() + { + SubscribeLocalEvent(OnPlantGrow); + SubscribeLocalEvent(OnSwab); + } + + private void OnSwab(Entity ent, ref BotanySwabDoAfterEvent args) + { + var component = ent.Comp; + + if (args.Cancelled || args.Handled || args.Used == null) + return; + + if (!TryComp(args.Used.Value, out var swab) || swab.SeedData == null) + return; + + var swabComp = swab.SeedData.GrowthComponents.BasicGrowth; + if (swabComp == null) + { + swab.SeedData.GrowthComponents.BasicGrowth = new BasicGrowthComponent + { + WaterConsumption = component.WaterConsumption, + NutrientConsumption = component.NutrientConsumption + }; + } + else + { + if (_random.Prob(0.5f)) + swabComp.WaterConsumption = component.WaterConsumption; + if (_random.Prob(0.5f)) + swabComp.NutrientConsumption = component.NutrientConsumption; + } + } + + private void OnPlantGrow(Entity ent, ref OnPlantGrowEvent args) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantHolderComponent? holder) + || !TryComp(uid, out var traits)) + return; + + if (holder.Seed == null || holder.Dead) + return; + + // Check if the plant is viable. + if (!traits.Viable) + { + holder.Health -= _random.Next(5, 10) * HydroponicsSpeedMultiplier; + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + + return; + } + + // Advance plant age here. + if (holder.SkipAging > 0) + { + holder.SkipAging--; + } + else + { + if (_random.Prob(0.8f)) + holder.Age += (int)(1 * HydroponicsSpeedMultiplier); + + holder.UpdateSpriteAfterUpdate = true; + } + + if (holder.Age < 0) // Revert back to seed packet! + { + var packetSeed = holder.Seed; + // will put it in the trays hands if it has any, please do not try doing this. + _botany.SpawnSeedPacket(packetSeed, Transform(uid).Coordinates, uid); + _plantHolder.RemovePlant(uid, holder); + holder.ForceUpdate = true; + _plantHolder.Update(uid, holder); + return; + } + + if (component.WaterConsumption > 0 && holder.WaterLevel > 0 && _random.Prob(0.75f)) + { + holder.WaterLevel -= MathF.Max(0f, + component.WaterConsumption * HydroponicsConsumptionMultiplier * HydroponicsSpeedMultiplier); + + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + } + + if (component.NutrientConsumption > 0 && holder.NutritionLevel > 0 && _random.Prob(0.75f)) + { + holder.NutritionLevel -= MathF.Max(0f, + component.NutrientConsumption * HydroponicsConsumptionMultiplier * HydroponicsSpeedMultiplier); + + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + } + + var healthMod = _random.Next(1, 3) * HydroponicsSpeedMultiplier; + if (holder.SkipAging < 10) + { + // Make sure the plant is not thirsty. + if (holder.WaterLevel > 10) + { + holder.Health += Convert.ToInt32(_random.Prob(0.35f)) * healthMod; + } + else + { + AffectGrowth((uid, holder), -1); + holder.Health -= healthMod; + } + + if (holder.NutritionLevel > 5) + { + holder.Health += Convert.ToInt32(_random.Prob(0.35f)) * healthMod; + } + else + { + AffectGrowth((uid, holder), -1); + holder.Health -= healthMod; + } + + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + } + } + + /// + /// Affects the growth of a plant by modifying its age or production timing. + /// + public void AffectGrowth(Entity ent, int amount) + { + if (amount == 0) + return; + + var (uid, component) = ent; + + if (component.Seed == null) + return; + + if (!TryComp(uid, out PlantHarvestComponent? harvest) + || !TryComp(uid, out PlantComponent? plant)) + return; + + if (amount > 0) + { + if (component.Age < plant.Maturation) + component.Age += amount; + else if (!harvest.ReadyForHarvest && plant.Yield <= 0f) + harvest.LastHarvest -= amount; + } + else + { + if (component.Age < plant.Maturation) + component.SkipAging++; + else if (!harvest.ReadyForHarvest && plant.Yield <= 0f) + harvest.LastHarvest += amount; + } + } +} + +/// +/// Event of plant growing ticking. +/// +[ByRefEvent] +public readonly record struct OnPlantGrowEvent; diff --git a/Content.Server/Botany/Systems/BotanySwabSystem.cs b/Content.Server/Botany/Systems/BotanySwabSystem.cs index e8c7af92c27..d557d111de4 100644 --- a/Content.Server/Botany/Systems/BotanySwabSystem.cs +++ b/Content.Server/Botany/Systems/BotanySwabSystem.cs @@ -9,13 +9,14 @@ namespace Content.Server.Botany.Systems; public sealed class BotanySwabSystem : EntitySystem { - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly MutationSystem _mutationSystem = default!; + [Dependency] private readonly MutationSystem _mutation = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(OnDoAfter); @@ -44,7 +45,7 @@ public sealed class BotanySwabSystem : EntitySystem if (args.Target == null || !args.CanReach || !HasComp(args.Target)) return; - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, swab.SwabDelay, new BotanySwabDoAfterEvent(), uid, target: args.Target, used: uid) + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, swab.SwabDelay, new BotanySwabDoAfterEvent(), uid, target: args.Target, used: uid) { Broadcast = true, BreakOnMove = true, @@ -62,18 +63,25 @@ public sealed class BotanySwabSystem : EntitySystem if (swab.SeedData == null) { - // Pick up pollen - swab.SeedData = plant.Seed; - _popupSystem.PopupEntity(Loc.GetString("botany-swab-from"), args.Args.Target.Value, args.Args.User); + // Pick up pollen. + if (plant.Seed != null) + swab.SeedData = plant.Seed.Clone(); + + _popup.PopupEntity(Loc.GetString("botany-swab-from"), args.Args.Target.Value, args.Args.User); } else { var old = plant.Seed; if (old == null) return; - plant.Seed = _mutationSystem.Cross(swab.SeedData, old); // Cross-pollenate - swab.SeedData = old; // Transfer old plant pollen to swab - _popupSystem.PopupEntity(Loc.GetString("botany-swab-to"), args.Args.Target.Value, args.Args.User); + + // Cross-pollinate the plants. + plant.Seed = _mutation.Cross(swab.SeedData, old); + + // Transfer old plant pollen to swab. + swab.SeedData = old.Clone(); + + _popup.PopupEntity(Loc.GetString("botany-swab-to"), args.Args.Target.Value, args.Args.User); } args.Handled = true; diff --git a/Content.Server/Botany/Systems/BotanySystem.Produce.cs b/Content.Server/Botany/Systems/BotanySystem.Produce.cs index bba3c48b867..c63ebdad378 100644 --- a/Content.Server/Botany/Systems/BotanySystem.Produce.cs +++ b/Content.Server/Botany/Systems/BotanySystem.Produce.cs @@ -11,7 +11,7 @@ public sealed partial class BotanySystem public void ProduceGrown(EntityUid uid, ProduceComponent produce) { - if (!TryGetSeed(produce, out var seed)) + if (!TryGetSeed(produce, out var seed) || !TryGetPlant(seed, out var plant)) return; foreach (var mutation in seed.Mutations) @@ -27,11 +27,12 @@ public sealed partial class BotanySystem return; solutionContainer.RemoveAllSolution(); + foreach (var (chem, quantity) in seed.Chemicals) { var amount = quantity.Min; - if (quantity.PotencyDivisor > 0 && seed.Potency > 0) - amount += seed.Potency / quantity.PotencyDivisor; + if (quantity.PotencyDivisor > 0 && plant.Potency > 0) + amount += plant.Potency / quantity.PotencyDivisor; amount = FixedPoint2.Clamp(amount, quantity.Min, quantity.Max); solutionContainer.MaxVolume += amount; solutionContainer.AddReagent(chem, amount); diff --git a/Content.Server/Botany/Systems/BotanySystem.Seed.cs b/Content.Server/Botany/Systems/BotanySystem.Seed.cs index 6b26ce7119a..999966b4ac9 100644 --- a/Content.Server/Botany/Systems/BotanySystem.Seed.cs +++ b/Content.Server/Botany/Systems/BotanySystem.Seed.cs @@ -1,21 +1,19 @@ using Content.Server.Botany.Components; using Content.Server.Popups; -using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Administration.Logs; using Content.Shared.Botany; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.Hands.EntitySystems; +using Content.Shared.Kitchen.Components; using Content.Shared.Popups; using Content.Shared.Random; -using Content.Shared.Random.Helpers; using Robust.Server.GameObjects; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Random; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Shared.Administration.Logs; -using Content.Shared.Database; -using Content.Shared.Kitchen.Components; namespace Content.Server.Botany.Systems; @@ -48,9 +46,9 @@ public sealed partial class BotanySystem : EntitySystem } if (comp.SeedId != null - && _prototypeManager.TryIndex(comp.SeedId, out SeedPrototype? protoSeed)) + && _prototypeManager.TryIndex(comp.SeedId, out var protoSeed)) { - seed = protoSeed; + seed = protoSeed.Clone(); return true; } @@ -67,7 +65,7 @@ public sealed partial class BotanySystem : EntitySystem } if (comp.SeedId != null - && _prototypeManager.TryIndex(comp.SeedId, out SeedPrototype? protoSeed)) + && _prototypeManager.TryIndex(comp.SeedId, out var protoSeed)) { seed = protoSeed; return true; @@ -77,20 +75,34 @@ public sealed partial class BotanySystem : EntitySystem return false; } + /// TODO: Delete after plants transition to entities + public static bool TryGetPlant(SeedData? seed, [NotNullWhen(true)] out PlantComponent? plantComponent) + { + plantComponent = seed?.GrowthComponents.Plant; + return plantComponent != null; + } + + /// TODO: Delete after plants transition to entities + public static bool TryGetPlantTraits(SeedData? seed, [NotNullWhen(true)] out PlantTraitsComponent? traitsComponent) + { + traitsComponent = seed?.GrowthComponents.PlantTraits; + return traitsComponent != null; + } + private void OnExamined(EntityUid uid, SeedComponent component, ExaminedEvent args) { if (!args.IsInDetailsRange) return; - if (!TryGetSeed(component, out var seed)) + if (!TryGetSeed(component, out var seed) || !TryGetPlant(seed, out var plant)) return; using (args.PushGroup(nameof(SeedComponent), 1)) { var name = Loc.GetString(seed.DisplayName); args.PushMarkup(Loc.GetString($"seed-component-description", ("seedName", name))); - args.PushMarkup(Loc.GetString($"seed-component-plant-yield-text", ("seedYield", seed.Yield))); - args.PushMarkup(Loc.GetString($"seed-component-plant-potency-text", ("seedPotency", seed.Potency))); + args.PushMarkup(Loc.GetString($"seed-component-plant-yield-text", ("seedYield", plant.Yield))); + args.PushMarkup(Loc.GetString($"seed-component-plant-potency-text", ("seedPotency", plant.Potency))); } } @@ -103,7 +115,7 @@ public sealed partial class BotanySystem : EntitySystem { var seed = Spawn(proto.PacketPrototype, coords); var seedComp = EnsureComp(seed); - seedComp.Seed = proto; + seedComp.Seed = proto.Clone(); seedComp.HealthOverride = healthOverride; var name = Loc.GetString(proto.Name); @@ -116,7 +128,7 @@ public sealed partial class BotanySystem : EntitySystem return seed; } - public IEnumerable AutoHarvest(SeedData proto, EntityCoordinates position, int yieldMod = 1) + public IEnumerable AutoHarvest(SeedData proto, EntityCoordinates position, EntityUid plantEntity) { if (position.IsValid(EntityManager) && proto.ProductPrototypes.Count > 0) @@ -124,18 +136,18 @@ public sealed partial class BotanySystem : EntitySystem if (proto.HarvestLogImpact != null) _adminLogger.Add(LogType.Botany, proto.HarvestLogImpact.Value, $"Auto-harvested {Loc.GetString(proto.Name):seed} at Pos:{position}."); - return GenerateProduct(proto, position, yieldMod); + return GenerateProduct(proto, position, plantEntity); } - return Enumerable.Empty(); + return []; } - public IEnumerable Harvest(SeedData proto, EntityUid user, int yieldMod = 1) + public IEnumerable Harvest(SeedData proto, EntityUid user, EntityUid plantEntity) { - if (proto.ProductPrototypes.Count == 0 || proto.Yield <= 0) + if (!TryGetPlant(proto, out var plant) || proto.ProductPrototypes.Count == 0 || plant.Yield <= 0) { _popupSystem.PopupCursor(Loc.GetString("botany-harvest-fail-message"), user, PopupType.Medium); - return Enumerable.Empty(); + return []; } var name = Loc.GetString(proto.DisplayName); @@ -144,25 +156,32 @@ public sealed partial class BotanySystem : EntitySystem if (proto.HarvestLogImpact != null) _adminLogger.Add(LogType.Botany, proto.HarvestLogImpact.Value, $"{ToPrettyString(user):player} harvested {Loc.GetString(proto.Name):seed} at Pos:{Transform(user).Coordinates}."); - return GenerateProduct(proto, Transform(user).Coordinates, yieldMod); + return GenerateProduct(proto, Transform(user).Coordinates, plantEntity); } - public IEnumerable GenerateProduct(SeedData proto, EntityCoordinates position, int yieldMod = 1) + public IEnumerable GenerateProduct(SeedData proto, EntityCoordinates position, EntityUid plantEntity) { + if (!TryGetPlant(proto, out var plant)) + return []; + + var yieldMod = Comp(plantEntity).YieldMod; + var harvest = Comp(plantEntity); + var totalYield = 0; - if (proto.Yield > -1) + + if (plant.Yield > -1) { if (yieldMod < 0) - totalYield = proto.Yield; + totalYield = plant.Yield; else - totalYield = proto.Yield * yieldMod; + totalYield = plant.Yield * yieldMod; totalYield = Math.Max(1, totalYield); } var products = new List(); - if (totalYield > 1 || proto.HarvestRepeat != HarvestType.NoRepeat) + if (totalYield > 1 || harvest.HarvestRepeat != HarvestType.NoRepeat) proto.Unique = false; for (var i = 0; i < totalYield; i++) @@ -175,10 +194,11 @@ public sealed partial class BotanySystem : EntitySystem var produce = EnsureComp(entity); - produce.Seed = proto; + produce.Seed = proto.Clone(); + ProduceGrown(entity, produce); - _appearance.SetData(entity, ProduceVisuals.Potency, proto.Potency); + _appearance.SetData(entity, ProduceVisuals.Potency, plant.Potency); if (proto.Mysterious) { @@ -194,7 +214,10 @@ public sealed partial class BotanySystem : EntitySystem public bool CanHarvest(SeedData proto, EntityUid? held = null) { - return !proto.Ligneous || proto.Ligneous && held != null && HasComp(held); + if (!TryGetPlantTraits(proto, out var traits)) + return true; + + return !traits.Ligneous || traits.Ligneous && held != null && HasComp(held); } #endregion diff --git a/Content.Server/Botany/Systems/ConsumeExudeGasGrowthSystem.cs b/Content.Server/Botany/Systems/ConsumeExudeGasGrowthSystem.cs new file mode 100644 index 00000000000..a98cf831504 --- /dev/null +++ b/Content.Server/Botany/Systems/ConsumeExudeGasGrowthSystem.cs @@ -0,0 +1,64 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.Botany.Components; +using Content.Shared.Atmos; + +namespace Content.Server.Botany.Systems; + +/// +/// Consumes and emits configured gases around plants each growth tick, then merges +/// the adjusted gas mixture back into the environment. +/// +public sealed class ConsumeExudeGasGrowthSystem : EntitySystem +{ + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnPlantGrow); + } + + private void OnPlantGrow(Entity ent, ref OnPlantGrowEvent args) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantHolderComponent? holder) + || !TryComp(uid, out PlantComponent? plant)) + return; + + var environment = _atmosphere.GetContainingMixture(uid, true, true) ?? GasMixture.SpaceGas; + + // Consume Gasses. + holder.MissingGas = 0; + if (component.ConsumeGasses.Count > 0) + { + foreach (var (gas, amount) in component.ConsumeGasses) + { + if (environment.GetMoles(gas) < amount) + { + holder.MissingGas++; + continue; + } + + environment.AdjustMoles(gas, -amount); + } + + if (holder.MissingGas > 0) + { + holder.Health -= holder.MissingGas * BasicGrowthSystem.HydroponicsSpeedMultiplier; + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + } + } + + // Exude Gasses. + var exudeCount = component.ExudeGasses.Count; + if (exudeCount > 0) + { + foreach (var (gas, amount) in component.ExudeGasses) + { + environment.AdjustMoles(gas, + MathF.Max(1f, MathF.Round(amount * MathF.Round(plant.Potency) / exudeCount))); + } + } + } +} diff --git a/Content.Server/Botany/Systems/MutationSystem.cs b/Content.Server/Botany/Systems/MutationSystem.cs index 834fd9e8efb..6bda05b0ad7 100644 --- a/Content.Server/Botany/Systems/MutationSystem.cs +++ b/Content.Server/Botany/Systems/MutationSystem.cs @@ -1,18 +1,21 @@ +using Content.Server.Botany.Components; using Content.Shared.Atmos; using Content.Shared.EntityEffects; using Content.Shared.Random; using Robust.Shared.Prototypes; using Robust.Shared.Random; using System.Linq; +using Robust.Shared.Serialization.Manager; -namespace Content.Server.Botany; +namespace Content.Server.Botany.Systems; public sealed class MutationSystem : EntitySystem { - private static ProtoId RandomPlantMutations = "RandomPlantMutations"; + private static readonly ProtoId RandomPlantMutations = "RandomPlantMutations"; [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly ISerializationManager _serializationManager = default!; [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; private RandomPlantMutationListPrototype _randomMutations = default!; @@ -24,8 +27,6 @@ public sealed class MutationSystem : EntitySystem /// /// For each random mutation, see if it occurs on this plant this check. /// - /// - /// public void CheckRandomMutations(EntityUid plantHolder, ref SeedData seed, float severity) { foreach (var mutation in _randomMutations.mutations) @@ -54,40 +55,43 @@ public sealed class MutationSystem : EntitySystem } CheckRandomMutations(plantHolder, ref seed, severity); + EnsureGrowthComponents(plantHolder, seed); + } + + /// + /// Ensures that the plant has all the growth components specified in the seed data. + /// + private void EnsureGrowthComponents(EntityUid plantHolder, SeedData seed) + { + // Fill missing components in the seed with defaults. + seed.GrowthComponents.EnsureGrowthComponents(); + + foreach (var prop in GrowthComponentsHolder.ComponentGetters) + { + if (prop.GetValue(seed.GrowthComponents) is Component component && !EntityManager.HasComponent(plantHolder, component.GetType())) + { + var newComponent = _serializationManager.CreateCopy(component, notNullableOverride: true); + EntityManager.AddComponent(plantHolder, newComponent); + } + } } public SeedData Cross(SeedData a, SeedData b) { - SeedData result = b.Clone(); + if (b.Immutable) + return b; + + var result = b.Clone(); CrossChemicals(ref result.Chemicals, a.Chemicals); - CrossFloat(ref result.NutrientConsumption, a.NutrientConsumption); - CrossFloat(ref result.WaterConsumption, a.WaterConsumption); - CrossFloat(ref result.IdealHeat, a.IdealHeat); - CrossFloat(ref result.HeatTolerance, a.HeatTolerance); - CrossFloat(ref result.IdealLight, a.IdealLight); - CrossFloat(ref result.LightTolerance, a.LightTolerance); - CrossFloat(ref result.ToxinsTolerance, a.ToxinsTolerance); - CrossFloat(ref result.LowPressureTolerance, a.LowPressureTolerance); - CrossFloat(ref result.HighPressureTolerance, a.HighPressureTolerance); - CrossFloat(ref result.PestTolerance, a.PestTolerance); - CrossFloat(ref result.WeedTolerance, a.WeedTolerance); - - CrossFloat(ref result.Endurance, a.Endurance); - CrossInt(ref result.Yield, a.Yield); - CrossFloat(ref result.Lifespan, a.Lifespan); - CrossFloat(ref result.Maturation, a.Maturation); - CrossFloat(ref result.Production, a.Production); - CrossFloat(ref result.Potency, a.Potency); - - CrossBool(ref result.Seedless, a.Seedless); - CrossBool(ref result.Ligneous, a.Ligneous); - CrossBool(ref result.TurnIntoKudzu, a.TurnIntoKudzu); - CrossBool(ref result.CanScream, a.CanScream); - - CrossGasses(ref result.ExudeGasses, a.ExudeGasses); - CrossGasses(ref result.ConsumeGasses, a.ConsumeGasses); + if (BotanySystem.TryGetPlantTraits(a, out var sourceTraits) && BotanySystem.TryGetPlantTraits(result, out var resultTraits)) + { + CrossBool(ref resultTraits.Seedless, sourceTraits.Seedless); + CrossBool(ref resultTraits.Ligneous, sourceTraits.Ligneous); + CrossBool(ref resultTraits.CanScream, sourceTraits.CanScream); + CrossBool(ref resultTraits.TurnIntoKudzu, sourceTraits.TurnIntoKudzu); + } // LINQ Explanation // For the list of mutation effects on both plants, use a 50% chance to pick each one. @@ -98,7 +102,8 @@ public sealed class MutationSystem : EntitySystem // effective hybrid crossings. if (a.Name != result.Name && Random(0.7f)) { - result.Seedless = true; + if (BotanySystem.TryGetPlantTraits(result, out var traits)) + traits.Seedless = true; } return result; @@ -110,9 +115,9 @@ public sealed class MutationSystem : EntitySystem foreach (var otherChem in other) { // if both have same chemical, randomly pick potency ratio from the two. - if (val.ContainsKey(otherChem.Key)) + if (val.TryGetValue(otherChem.Key, out var value)) { - val[otherChem.Key] = Random(0.5f) ? otherChem.Value : val[otherChem.Key]; + val[otherChem.Key] = Random(0.5f) ? otherChem.Value : value; } // if target plant doesn't have this chemical, has 50% chance to add it. else @@ -148,9 +153,9 @@ public sealed class MutationSystem : EntitySystem foreach (var otherGas in other) { // if both have same gas, randomly pick ammount from the two. - if (val.ContainsKey(otherGas.Key)) + if (val.TryGetValue(otherGas.Key, out var value)) { - val[otherGas.Key] = Random(0.5f) ? otherGas.Value : val[otherGas.Key]; + val[otherGas.Key] = Random(0.5f) ? otherGas.Value : value; } // if target plant doesn't have this gas, has 50% chance to add it. else diff --git a/Content.Server/Botany/Systems/PlantHarvestSystem.cs b/Content.Server/Botany/Systems/PlantHarvestSystem.cs new file mode 100644 index 00000000000..d56914e869c --- /dev/null +++ b/Content.Server/Botany/Systems/PlantHarvestSystem.cs @@ -0,0 +1,151 @@ +using Content.Server.Botany.Components; +using Content.Server.Popups; +using Content.Shared.Interaction; +using Robust.Shared.Audio.Systems; + +namespace Content.Server.Botany.Systems; + +/// +/// Manages harvest readiness and execution for plants, including repeat/self-harvest +/// logic and produce spawning, responding to growth and interaction events. +/// +public sealed class HarvestSystem : EntitySystem +{ + [Dependency] private readonly BotanySystem _botany = default!; + [Dependency] private readonly PlantHolderSystem _holder = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnPlantGrow); + SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent(OnInteractUsing); + } + + private void OnPlantGrow(Entity ent, ref OnPlantGrowEvent args) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantHolderComponent? holder) + || !TryComp(uid, out PlantComponent? plant)) + return; + + if (component is { ReadyForHarvest: true, HarvestRepeat: HarvestType.SelfHarvest }) + AutoHarvest((ent, ent, holder)); + + // Check if plant is ready for harvest. + var timeLastHarvest = holder.Age - component.LastHarvest; + if (timeLastHarvest > plant.Production && !component.ReadyForHarvest) + { + component.ReadyForHarvest = true; + component.LastHarvest = holder.Age; + holder.UpdateSpriteAfterUpdate = true; + } + } + + private void OnInteractUsing(Entity ent, ref InteractUsingEvent args) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantTraitsComponent? traits) + || !traits.Ligneous + || !TryComp(uid, out PlantHolderComponent? holder) + || holder.Seed == null) + return; + + if (!component.ReadyForHarvest || holder.Dead || holder.Seed == null) + return; + + var canHarvestUsing = _botany.CanHarvest(holder.Seed, args.Used); + HandleInteraction((ent, ent, holder), args.User, !canHarvestUsing); + } + + private void OnInteractHand(Entity ent, ref InteractHandEvent args) + { + if (!TryComp(ent, out PlantHolderComponent? holder) + || !TryComp(ent, out PlantTraitsComponent? traits)) + return; + + HandleInteraction((ent, ent, holder), args.User, traits.Ligneous); + } + + private void HandleInteraction( + Entity ent, + EntityUid user, + bool missingRequiredTool + ) + { + if (missingRequiredTool) + { + _popup.PopupCursor(Loc.GetString("plant-holder-component-ligneous-cant-harvest-message"), user); + return; + } + + var (_, harvest, holder) = ent; + if (!harvest.ReadyForHarvest || holder.Dead || holder.Seed == null) + return; + + // Perform harvest. + DoHarvest(ent, user); + } + + public void DoHarvest(Entity ent, EntityUid user) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantHolderComponent? holder) + || !TryComp(uid, out PlantTraitsComponent? traits)) + return; + + if (holder.Dead) + { + // Remove dead plant. + _holder.RemovePlant(uid, holder); + AfterHarvest(ent); + return; + } + + if (!component.ReadyForHarvest) + return; + + // Spawn products. + if (holder.Seed != null) + _botany.Harvest(holder.Seed, user, ent); + + // Handle harvest type. + if (component.HarvestRepeat == HarvestType.NoRepeat) + _holder.RemovePlant(uid, holder); + + AfterHarvest(ent, holder, traits); + } + + private void AfterHarvest(Entity ent, PlantHolderComponent? holder = null, PlantTraitsComponent? traits = null) + { + var (uid, component) = ent; + if (!Resolve(uid, ref traits, ref holder)) + return; + + component.ReadyForHarvest = false; + component.LastHarvest = holder.Age; + + // Play scream sound if applicable. + if (traits.CanScream && holder.Seed != null) + _audio.PlayPvs(holder.Seed.ScreamSound, uid); + + // Update sprite. + _holder.UpdateSprite(uid, holder); + } + + /// + /// Auto-harvests a plant. + /// + public void AutoHarvest(Entity ent) + { + if (!ent.Comp1.ReadyForHarvest || ent.Comp2.Seed == null) + return; + + _botany.AutoHarvest(ent.Comp2.Seed, Transform(ent.Owner).Coordinates, ent); + AfterHarvest(ent); + } +} diff --git a/Content.Server/Botany/Systems/PlantHolderSystem.cs b/Content.Server/Botany/Systems/PlantHolderSystem.cs index d5f331c157f..ba9782eef90 100644 --- a/Content.Server/Botany/Systems/PlantHolderSystem.cs +++ b/Content.Server/Botany/Systems/PlantHolderSystem.cs @@ -1,19 +1,19 @@ -using Content.Server.Atmos.EntitySystems; +using System.Linq; using Content.Server.Botany.Components; -using Content.Server.Hands.Systems; using Content.Server.Popups; using Content.Shared.Administration.Logs; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Atmos; using Content.Shared.Botany; using Content.Shared.Burial.Components; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reagent; -using Content.Shared.Coordinates.Helpers; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Database; +using Content.Shared.EntityEffects; using Content.Shared.Examine; using Content.Shared.FixedPoint; -using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; +using Content.Shared.Labels.Components; using Content.Shared.Popups; using Content.Shared.Random; using Content.Shared.Tag; @@ -22,47 +22,36 @@ using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Timing; -using Content.Shared.Chemistry.Reaction; -using Content.Shared.Containers.ItemSlots; -using Content.Shared.Database; -using Content.Shared.EntityEffects; -using Content.Shared.Kitchen.Components; -using Content.Shared.Labels.Components; namespace Content.Server.Botany.Systems; public sealed class PlantHolderSystem : EntitySystem { - [Dependency] private readonly AtmosphereSystem _atmosphere = default!; - [Dependency] private readonly BotanySystem _botany = default!; - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly MutationSystem _mutation = default!; [Dependency] private readonly AppearanceSystem _appearance = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly HandsSystem _hands = default!; - [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly BotanySystem _botany = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly TagSystem _tagSystem = default!; - [Dependency] private readonly RandomHelperSystem _randomHelper = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly ISerializationManager _copier = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly MutationSystem _mutation = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly RandomHelperSystem _randomHelper = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!; - - public const float HydroponicsSpeedMultiplier = 1f; - public const float HydroponicsConsumptionMultiplier = 2f; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly TagSystem _tag = default!; private static readonly ProtoId HoeTag = "Hoe"; private static readonly ProtoId PlantSampleTakerTag = "PlantSampleTaker"; public override void Initialize() { - base.Initialize(); SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnSolutionTransferred); } @@ -75,6 +64,7 @@ public sealed class PlantHolderSystem : EntitySystem { if (plantHolder.NextUpdate > _gameTiming.CurTime) continue; + plantHolder.NextUpdate = _gameTiming.CurTime + plantHolder.UpdateDelay; Update(uid, plantHolder); @@ -88,16 +78,24 @@ public sealed class PlantHolderSystem : EntitySystem if (component.Seed == null) return 0; - var result = Math.Max(1, (int)(component.Age * component.Seed.GrowthStages / component.Seed.Maturation)); + if (!TryComp(uid, out var plant)) + return 0; + + var result = Math.Max(1, (int)(component.Age * plant.GrowthStages / plant.Maturation)); return result; } private void OnExamine(Entity entity, ref ExaminedEvent args) { - if (!args.IsInDetailsRange) + var (uid, component) = entity; + + PlantComponent? plant = null; + PlantTraitsComponent? traits = null; + if (!Resolve(uid, ref plant, ref traits)) return; - var (_, component) = entity; + if (!args.IsInDetailsRange) + return; using (args.PushGroup(nameof(PlantHolderComponent))) { @@ -112,28 +110,27 @@ public sealed class PlantHolderSystem : EntitySystem ("seedName", displayName), ("toBeForm", displayName.EndsWith('s') ? "are" : "is"))); - if (component.Health <= component.Seed.Endurance / 2) + if (component.Health <= plant.Endurance / 2) { args.PushMarkup(Loc.GetString( "plant-holder-component-something-already-growing-low-health-message", ("healthState", - Loc.GetString(component.Age > component.Seed.Lifespan + Loc.GetString(component.Age > plant.Lifespan ? "plant-holder-component-plant-old-adjective" : "plant-holder-component-plant-unhealthy-adjective")))); } // For future reference, mutations should only appear on examine if they apply to a plant, not to produce. - - if (component.Seed.Ligneous) + if (traits.Ligneous) args.PushMarkup(Loc.GetString("mutation-plant-ligneous")); - if (component.Seed.TurnIntoKudzu) + if (traits.TurnIntoKudzu) args.PushMarkup(Loc.GetString("mutation-plant-kudzu")); - if (component.Seed.CanScream) + if (traits.CanScream) args.PushMarkup(Loc.GetString("mutation-plant-scream")); - if (component.Seed.Viable == false) + if (!traits.Viable) args.PushMarkup(Loc.GetString("mutation-plant-unviable")); } else @@ -157,9 +154,6 @@ public sealed class PlantHolderSystem : EntitySystem if (component.Toxins > 40f) args.PushMarkup(Loc.GetString("plant-holder-component-toxins-high-warning")); - if (component.ImproperLight) - args.PushMarkup(Loc.GetString("plant-holder-component-light-improper-warning")); - if (component.ImproperHeat) args.PushMarkup(Loc.GetString("plant-holder-component-heat-improper-warning")); @@ -174,13 +168,13 @@ public sealed class PlantHolderSystem : EntitySystem private void OnInteractUsing(Entity entity, ref InteractUsingEvent args) { - var (uid, component) = entity; + var (uid, plantHolder) = entity; if (TryComp(args.Used, out SeedComponent? seeds)) { - if (component.Seed == null) + if (plantHolder.Seed == null) { - if (!_botany.TryGetSeed(seeds, out var seed)) + if (!_botany.TryGetSeed(seeds, out var seed) || !BotanySystem.TryGetPlant(seed, out var seedPlant)) return; args.Handled = true; @@ -188,20 +182,31 @@ public sealed class PlantHolderSystem : EntitySystem var noun = Loc.GetString(seed.Noun); _popup.PopupCursor(Loc.GetString("plant-holder-component-plant-success-message", ("seedName", name), - ("seedNoun", noun)), args.User, PopupType.Medium); + ("seedNoun", noun)), + args.User, + PopupType.Medium); - component.Seed = seed; - component.Dead = false; - component.Age = 1; - if (seeds.HealthOverride != null) + plantHolder.Seed = seed.Clone(); + plantHolder.Dead = false; + plantHolder.Age = 1; + + plantHolder.Health = seeds.HealthOverride ?? seedPlant.Endurance; + + plantHolder.LastCycle = _gameTiming.CurTime; + + // Ensure no existing growth components before adding new ones + RemoveAllGrowthComponents(uid); + + // Fill missing components with defaults + seed.GrowthComponents.EnsureGrowthComponents(); + + foreach (var prop in GrowthComponentsHolder.ComponentGetters) { - component.Health = seeds.HealthOverride.Value; + if (prop.GetValue(seed.GrowthComponents) is Component growthComp) + { + EntityManager.AddComponent(uid, _copier.CreateCopy(growthComp, notNullableOverride: true), overwrite: true); + } } - else - { - component.Health = component.Seed.Endurance; - } - component.LastCycle = _gameTiming.CurTime; if (TryComp(args.Used, out var paperLabel)) { @@ -209,8 +214,7 @@ public sealed class PlantHolderSystem : EntitySystem } QueueDel(args.Used); - CheckLevelSanity(uid, component); - UpdateSprite(uid, component); + UpdateSprite(uid, plantHolder); if (seed.PlantLogImpact != null) _adminLogger.Add(LogType.Botany, seed.PlantLogImpact.Value, $"{ToPrettyString(args.User):player} planted {Loc.GetString(seed.Name):seed} at Pos:{Transform(uid).Coordinates}."); @@ -219,22 +223,29 @@ public sealed class PlantHolderSystem : EntitySystem } args.Handled = true; - _popup.PopupCursor(Loc.GetString("plant-holder-component-already-seeded-message", - ("name", Comp(uid).EntityName)), args.User, PopupType.Medium); + _popup.PopupCursor( + Loc.GetString("plant-holder-component-already-seeded-message", ("name", MetaData(uid).EntityName)), + args.User, + PopupType.Medium); return; } - if (_tagSystem.HasTag(args.Used, HoeTag)) + if (_tag.HasTag(args.Used, HoeTag)) { args.Handled = true; - if (component.WeedLevel > 0) + if (plantHolder.WeedLevel > 0) { - _popup.PopupCursor(Loc.GetString("plant-holder-component-remove-weeds-message", - ("name", Comp(uid).EntityName)), args.User, PopupType.Medium); - _popup.PopupEntity(Loc.GetString("plant-holder-component-remove-weeds-others-message", - ("otherName", Comp(args.User).EntityName)), uid, Filter.PvsExcept(args.User), true); - component.WeedLevel = 0; - UpdateSprite(uid, component); + _popup.PopupCursor( + Loc.GetString("plant-holder-component-remove-weeds-message", ("name", MetaData(uid).EntityName)), + args.User, + PopupType.Medium); + _popup.PopupEntity( + Loc.GetString("plant-holder-component-remove-weeds-others-message", ("otherName", MetaData(args.User).EntityName)), + uid, + Filter.PvsExcept(args.User), + true); + plantHolder.WeedLevel = 0; + UpdateSprite(uid, plantHolder); } else { @@ -247,39 +258,45 @@ public sealed class PlantHolderSystem : EntitySystem if (HasComp(args.Used)) { args.Handled = true; - if (component.Seed != null) + if (plantHolder.Seed != null) { - _popup.PopupCursor(Loc.GetString("plant-holder-component-remove-plant-message", - ("name", Comp(uid).EntityName)), args.User, PopupType.Medium); - _popup.PopupEntity(Loc.GetString("plant-holder-component-remove-plant-others-message", - ("name", Comp(args.User).EntityName)), uid, Filter.PvsExcept(args.User), true); - RemovePlant(uid, component); + _popup.PopupCursor( + Loc.GetString("plant-holder-component-remove-plant-message", ("name", MetaData(uid).EntityName)), + args.User, + PopupType.Medium); + _popup.PopupEntity( + Loc.GetString("plant-holder-component-remove-plant-others-message", ("name", MetaData(args.User).EntityName)), + uid, + Filter.PvsExcept(args.User), + true); + RemovePlant(uid, plantHolder); } else { - _popup.PopupCursor(Loc.GetString("plant-holder-component-no-plant-message", - ("name", Comp(uid).EntityName)), args.User); + _popup.PopupCursor( + Loc.GetString("plant-holder-component-no-plant-message", ("name", MetaData(uid).EntityName)), + args.User); } return; } - if (_tagSystem.HasTag(args.Used, PlantSampleTakerTag)) + if (_tag.HasTag(args.Used, PlantSampleTakerTag)) { args.Handled = true; - if (component.Seed == null) + if (plantHolder.Seed == null) { _popup.PopupCursor(Loc.GetString("plant-holder-component-nothing-to-sample-message"), args.User); return; } - if (component.Sampled) + if (plantHolder.Sampled) { _popup.PopupCursor(Loc.GetString("plant-holder-component-already-sampled-message"), args.User); return; } - if (component.Dead) + if (plantHolder.Dead) { _popup.PopupCursor(Loc.GetString("plant-holder-component-dead-plant-message"), args.User); return; @@ -291,40 +308,48 @@ public sealed class PlantHolderSystem : EntitySystem return; } - component.Health -= (_random.Next(3, 5) * 10); + plantHolder.Health -= _random.Next(3, 5) * 10; float? healthOverride; - if (component.Harvest) + if (TryComp(uid, out var harvest) && harvest.ReadyForHarvest) { healthOverride = null; } else { - healthOverride = component.Health; + healthOverride = plantHolder.Health; } - var packetSeed = component.Seed; - var seed = _botany.SpawnSeedPacket(packetSeed, Transform(args.User).Coordinates, args.User, healthOverride); - _randomHelper.RandomOffset(seed, 0.25f); - var displayName = Loc.GetString(component.Seed.DisplayName); - _popup.PopupCursor(Loc.GetString("plant-holder-component-take-sample-message", - ("seedName", displayName)), args.User); - DoScream(entity.Owner, component.Seed); + var packetSeed = plantHolder.Seed; + if (packetSeed != null) + { + // Copy growth components from the plant to the seed before creating seed packet + var holder = new GrowthComponentsHolder(); - if (_random.Prob(0.3f)) - component.Sampled = true; + foreach (var prop in typeof(GrowthComponentsHolder).GetProperties()) + { + if (EntityManager.TryGetComponent(uid, prop.PropertyType, out var growthComponent)) + { + var copiedComponent = _copier.CreateCopy((Component)growthComponent, notNullableOverride: true); + prop.SetValue(holder, copiedComponent); + } + } - // Just in case. - CheckLevelSanity(uid, component); - ForceUpdateByExternalCause(uid, component); + packetSeed.GrowthComponents = holder; - return; - } + var seed = _botany.SpawnSeedPacket(packetSeed, Transform(args.User).Coordinates, args.User, healthOverride); + _randomHelper.RandomOffset(seed, 0.25f); + var displayName = Loc.GetString(plantHolder.Seed.DisplayName); + _popup.PopupCursor(Loc.GetString("plant-holder-component-take-sample-message", + ("seedName", displayName)), + args.User); + + if (_random.Prob(0.3f)) + plantHolder.Sampled = true; + + ForceUpdateByExternalCause(uid, plantHolder); + } - if (HasComp(args.Used)) - { - args.Handled = true; - DoHarvest(uid, args.User, component); return; } @@ -333,30 +358,35 @@ public sealed class PlantHolderSystem : EntitySystem args.Handled = true; _popup.PopupCursor(Loc.GetString("plant-holder-component-compost-message", ("owner", uid), - ("usingItem", args.Used)), args.User, PopupType.Medium); + ("usingItem", args.Used)), + args.User, + PopupType.Medium); _popup.PopupEntity(Loc.GetString("plant-holder-component-compost-others-message", ("user", Identity.Entity(args.User, EntityManager)), ("usingItem", args.Used), - ("owner", uid)), uid, Filter.PvsExcept(args.User), true); + ("owner", uid)), + uid, + Filter.PvsExcept(args.User), + true); - if (_solutionContainerSystem.TryGetSolution(args.Used, produce.SolutionName, out var soln2, out var solution2)) + if (_solutionContainer.TryGetSolution(args.Used, produce.SolutionName, out var soln2, out var solution2)) { - if (_solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution, out var solution1)) + if (_solutionContainer.ResolveSolution(uid, plantHolder.SoilSolutionName, ref plantHolder.SoilSolution, out var solution1)) { // We try to fit as much of the composted plant's contained solution into the hydroponics tray as we can, // since the plant will be consumed anyway. var fillAmount = FixedPoint2.Min(solution2.Volume, solution1.AvailableVolume); - _solutionContainerSystem.TryAddSolution(component.SoilSolution.Value, _solutionContainerSystem.SplitSolution(soln2.Value, fillAmount)); + _solutionContainer.TryAddSolution(plantHolder.SoilSolution.Value, _solutionContainer.SplitSolution(soln2.Value, fillAmount)); - ForceUpdateByExternalCause(uid, component); + ForceUpdateByExternalCause(uid, plantHolder); } } var seed = produce.Seed; - if (seed != null) + if (seed != null && BotanySystem.TryGetPlant(seed, out var seedPlant)) { - var nutrientBonus = seed.Potency / 2.5f; - AdjustNutrient(uid, nutrientBonus, component); + var nutrientBonus = seedPlant.Potency / 2.5f; + AdjustNutrient(uid, nutrientBonus, plantHolder); } QueueDel(args.Used); } @@ -366,16 +396,6 @@ public sealed class PlantHolderSystem : EntitySystem { _audio.PlayPvs(ent.Comp.WateringSound, ent.Owner); } - private void OnInteractHand(Entity entity, ref InteractHandEvent args) - { - DoHarvest(entity, args.User, entity.Comp); - } - - public void WeedInvasion() - { - // TODO - } - public void Update(EntityUid uid, PlantHolderComponent? component = null) { @@ -386,18 +406,26 @@ public sealed class PlantHolderSystem : EntitySystem var curTime = _gameTiming.CurTime; + // ForceUpdate is used for external triggers like swabbing if (component.ForceUpdate) component.ForceUpdate = false; - else if (curTime < (component.LastCycle + component.CycleDelay)) + else if (curTime < component.LastCycle + component.CycleDelay) { if (component.UpdateSpriteAfterUpdate) UpdateSprite(uid, component); + return; } component.LastCycle = curTime; - // Process mutations + if (component is { Seed: not null, Dead: false }) + { + var plantGrow = new OnPlantGrowEvent(); + RaiseLocalEvent(uid, ref plantGrow); + } + + // Process mutations. All plants can mutate, so this stays here. if (component.MutationLevel > 0) { Mutate(uid, Math.Min(component.MutationLevel, 25), component); @@ -405,40 +433,6 @@ public sealed class PlantHolderSystem : EntitySystem component.MutationLevel = 0; } - // Weeds like water and nutrients! They may appear even if there's not a seed planted. - if (component.WaterLevel > 10 && component.NutritionLevel > 5) - { - var chance = 0f; - if (component.Seed == null) - chance = 0.05f; - else if (component.Seed.TurnIntoKudzu) - chance = 1f; - else - chance = 0.01f; - - if (_random.Prob(chance)) - component.WeedLevel += 1 + HydroponicsSpeedMultiplier * component.WeedCoefficient; - - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - - if (component.Seed != null && component.Seed.TurnIntoKudzu - && component.WeedLevel >= component.Seed.WeedHighLevelThreshold) - { - Spawn(component.Seed.KudzuPrototype, Transform(uid).Coordinates.SnapToGrid(EntityManager)); - component.Seed.TurnIntoKudzu = false; - component.Health = 0; - } - - // There's a chance for a weed explosion to happen if weeds take over. - // Plants that are themselves weeds (WeedTolerance > 8) are unaffected. - if (component.WeedLevel >= 10 && _random.Prob(0.1f)) - { - if (component.Seed == null || component.WeedLevel >= component.Seed.WeedTolerance + 2) - WeedInvasion(); - } - // If we have no seed planted, or the plant is dead, stop processing here. if (component.Seed == null || component.Dead) { @@ -448,337 +442,12 @@ public sealed class PlantHolderSystem : EntitySystem return; } - // There's a small chance the pest population increases. - // Can only happen when there's a live seed planted. - if (_random.Prob(0.01f)) - { - component.PestLevel += 0.5f * HydroponicsSpeedMultiplier; - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - - // Advance plant age here. - if (component.SkipAging > 0) - component.SkipAging--; - else - { - if (_random.Prob(0.8f)) - component.Age += (int)(1 * HydroponicsSpeedMultiplier); - - component.UpdateSpriteAfterUpdate = true; - } - - // Nutrient consumption. - if (component.Seed.NutrientConsumption > 0 && component.NutritionLevel > 0 && _random.Prob(0.75f)) - { - component.NutritionLevel -= MathF.Max(0f, component.Seed.NutrientConsumption * HydroponicsSpeedMultiplier); - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - - // Water consumption. - if (component.Seed.WaterConsumption > 0 && component.WaterLevel > 0 && _random.Prob(0.75f)) - { - component.WaterLevel -= MathF.Max(0f, - component.Seed.WaterConsumption * HydroponicsConsumptionMultiplier * HydroponicsSpeedMultiplier); - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - - var healthMod = _random.Next(1, 3) * HydroponicsSpeedMultiplier; - - // Make sure genetics are viable. - if (!component.Seed.Viable) - { - AffectGrowth(uid, -1, component); - component.Health -= 6 * healthMod; - } - - // Prevents the plant from aging when lacking resources. - // Limits the effect on aging so that when resources are added, the plant starts growing in a reasonable amount of time. - if (component.SkipAging < 10) - { - // Make sure the plant is not starving. - if (component.NutritionLevel > 5) - { - component.Health += Convert.ToInt32(_random.Prob(0.35f)) * healthMod; - } - else - { - AffectGrowth(uid, -1, component); - component.Health -= healthMod; - } - - // Make sure the plant is not thirsty. - if (component.WaterLevel > 10) - { - component.Health += Convert.ToInt32(_random.Prob(0.35f)) * healthMod; - } - else - { - AffectGrowth(uid, -1, component); - component.Health -= healthMod; - } - - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - - var environment = _atmosphere.GetContainingMixture(uid, true, true) ?? GasMixture.SpaceGas; - - component.MissingGas = 0; - if (component.Seed.ConsumeGasses.Count > 0) - { - foreach (var (gas, amount) in component.Seed.ConsumeGasses) - { - if (environment.GetMoles(gas) < amount) - { - component.MissingGas++; - continue; - } - - environment.AdjustMoles(gas, -amount); - } - - if (component.MissingGas > 0) - { - component.Health -= component.MissingGas * HydroponicsSpeedMultiplier; - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - } - - // SeedPrototype pressure resistance. - var pressure = environment.Pressure; - if (pressure < component.Seed.LowPressureTolerance || pressure > component.Seed.HighPressureTolerance) - { - component.Health -= healthMod; - component.ImproperPressure = true; - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - else - { - component.ImproperPressure = false; - } - - // SeedPrototype ideal temperature. - if (MathF.Abs(environment.Temperature - component.Seed.IdealHeat) > component.Seed.HeatTolerance) - { - component.Health -= healthMod; - component.ImproperHeat = true; - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - else - { - component.ImproperHeat = false; - } - - // Gas production. - var exudeCount = component.Seed.ExudeGasses.Count; - if (exudeCount > 0) - { - foreach (var (gas, amount) in component.Seed.ExudeGasses) - { - environment.AdjustMoles(gas, - MathF.Max(1f, MathF.Round(amount * MathF.Round(component.Seed.Potency) / exudeCount))); - } - } - - // Toxin levels beyond the plant's tolerance cause damage. - // They are, however, slowly reduced over time. - if (component.Toxins > 0) - { - var toxinUptake = MathF.Max(1, MathF.Round(component.Toxins / 10f)); - if (component.Toxins > component.Seed.ToxinsTolerance) - { - component.Health -= toxinUptake; - } - - component.Toxins -= toxinUptake; - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - - // Weed levels. - if (component.PestLevel > 0) - { - // TODO: Carnivorous plants? - if (component.PestLevel > component.Seed.PestTolerance) - { - component.Health -= HydroponicsSpeedMultiplier; - } - - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - - // Weed levels. - if (component.WeedLevel > 0) - { - // TODO: Parasitic plants. - if (component.WeedLevel >= component.Seed.WeedTolerance) - { - component.Health -= HydroponicsSpeedMultiplier; - } - - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - - if (component.Age > component.Seed.Lifespan) - { - component.Health -= _random.Next(3, 5) * HydroponicsSpeedMultiplier; - if (component.DrawWarnings) - component.UpdateSpriteAfterUpdate = true; - } - else if (component.Age < 0) // Revert back to seed packet! - { - var packetSeed = component.Seed; - // will put it in the trays hands if it has any, please do not try doing this - _botany.SpawnSeedPacket(packetSeed, Transform(uid).Coordinates, uid); - RemovePlant(uid, component); - component.ForceUpdate = true; - Update(uid, component); - return; - } - CheckHealth(uid, component); - if (component.Harvest && component.Seed.HarvestRepeat == HarvestType.SelfHarvest) - AutoHarvest(uid, component); - - // If enough time has passed since the plant was harvested, we're ready to harvest again! - if (!component.Dead && component.Seed.ProductPrototypes.Count > 0) - { - if (component.Age > component.Seed.Production) - { - if (component.Age - component.LastProduce > component.Seed.Production && !component.Harvest) - { - component.Harvest = true; - component.LastProduce = component.Age; - } - } - else - { - if (component.Harvest) - { - component.Harvest = false; - component.LastProduce = component.Age; - } - } - } - - CheckLevelSanity(uid, component); - if (component.UpdateSpriteAfterUpdate) UpdateSprite(uid, component); } - //TODO: kill this bullshit - public void CheckLevelSanity(EntityUid uid, PlantHolderComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (component.Seed != null) - component.Health = MathHelper.Clamp(component.Health, 0, component.Seed.Endurance); - else - { - component.Health = 0f; - component.Dead = false; - } - - component.MutationLevel = MathHelper.Clamp(component.MutationLevel, 0f, 100f); - component.NutritionLevel = MathHelper.Clamp(component.NutritionLevel, 0f, 100f); - component.WaterLevel = MathHelper.Clamp(component.WaterLevel, 0f, 100f); - component.PestLevel = MathHelper.Clamp(component.PestLevel, 0f, 10f); - component.WeedLevel = MathHelper.Clamp(component.WeedLevel, 0f, 10f); - component.Toxins = MathHelper.Clamp(component.Toxins, 0f, 100f); - component.YieldMod = MathHelper.Clamp(component.YieldMod, 0, 2); - component.MutationMod = MathHelper.Clamp(component.MutationMod, 0f, 3f); - } - - public bool DoHarvest(EntityUid plantholder, EntityUid user, PlantHolderComponent? component = null) - { - if (!Resolve(plantholder, ref component)) - return false; - - if (component.Seed == null || Deleted(user)) - return false; - - - if (component.Harvest && !component.Dead) - { - if (_hands.TryGetActiveItem(user, out var activeItem)) - { - if (!_botany.CanHarvest(component.Seed, activeItem)) - { - _popup.PopupCursor(Loc.GetString("plant-holder-component-ligneous-cant-harvest-message"), user); - return false; - } - } - else if (!_botany.CanHarvest(component.Seed)) - { - return false; - } - - _botany.Harvest(component.Seed, user, component.YieldMod); - AfterHarvest(plantholder, component); - return true; - } - - if (!component.Dead) - return false; - - RemovePlant(plantholder, component); - AfterHarvest(plantholder, component); - return true; - } - - /// - /// Force do scream on PlantHolder (like plant is screaming) using seed's ScreamSound specifier (collection or soundPath) - /// - /// - public bool DoScream(EntityUid plantholder, SeedData? seed = null) - { - if (seed == null || seed.CanScream == false) - return false; - - _audio.PlayPvs(seed.ScreamSound, plantholder); - return true; - } - - public void AutoHarvest(EntityUid uid, PlantHolderComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (component.Seed == null || !component.Harvest) - return; - - _botany.AutoHarvest(component.Seed, Transform(uid).Coordinates); - AfterHarvest(uid, component); - } - - private void AfterHarvest(EntityUid uid, PlantHolderComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - component.Harvest = false; - component.LastProduce = component.Age; - - DoScream(uid, component.Seed); - - if (component.Seed?.HarvestRepeat == HarvestType.NoRepeat) - RemovePlant(uid, component); - - CheckLevelSanity(uid, component); - UpdateSprite(uid, component); - } - public void CheckHealth(EntityUid uid, PlantHolderComponent? component = null) { if (!Resolve(uid, ref component)) @@ -790,69 +459,51 @@ public sealed class PlantHolderSystem : EntitySystem } } - public void Die(EntityUid uid, PlantHolderComponent? component = null) + public void Die(EntityUid uid, PlantHolderComponent component) { - if (!Resolve(uid, ref component)) + PlantHarvestComponent? harvest = null; + if (!Resolve(uid, ref harvest)) return; component.Dead = true; - component.Harvest = false; + harvest.ReadyForHarvest = false; component.MutationLevel = 0; component.YieldMod = 1; component.MutationMod = 1; - component.ImproperLight = false; - component.ImproperHeat = false; component.ImproperPressure = false; - component.WeedLevel += 1 * HydroponicsSpeedMultiplier; + component.WeedLevel += 1 * BasicGrowthSystem.HydroponicsSpeedMultiplier; component.PestLevel = 0; + UpdateSprite(uid, component); } public void RemovePlant(EntityUid uid, PlantHolderComponent? component = null) { - if (!Resolve(uid, ref component)) + PlantHarvestComponent? harvest = null; + if (!Resolve(uid, ref component, ref harvest)) return; + if (component.Seed == null) + return; + + // Remove all growth components before planting new seed + RemoveAllGrowthComponents(uid); + component.YieldMod = 1; component.MutationMod = 1; component.PestLevel = 0; component.Seed = null; component.Dead = false; component.Age = 0; - component.LastProduce = 0; + harvest.LastHarvest = 0; component.Sampled = false; - component.Harvest = false; - component.ImproperLight = false; + harvest.ReadyForHarvest = false; component.ImproperPressure = false; component.ImproperHeat = false; UpdateSprite(uid, component); } - public void AffectGrowth(EntityUid uid, int amount, PlantHolderComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (component.Seed == null) - return; - - if (amount > 0) - { - if (component.Age < component.Seed.Maturation) - component.Age += amount; - else if (!component.Harvest && component.Seed.Yield <= 0f) - component.LastProduce -= amount; - } - else - { - if (component.Age < component.Seed.Maturation) - component.SkipAging++; - else if (!component.Harvest && component.Seed.Yield <= 0f) - component.LastProduce += amount; - } - } - public void AdjustNutrient(EntityUid uid, float amount, PlantHolderComponent? component = null) { if (!Resolve(uid, ref component)) @@ -880,7 +531,7 @@ public sealed class PlantHolderSystem : EntitySystem if (!Resolve(uid, ref component)) return; - if (!_solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution, out var solution)) + if (!_solutionContainer.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution, out var solution)) return; if (solution.Volume > 0 && component.MutationLevel < 25) @@ -888,13 +539,11 @@ public sealed class PlantHolderSystem : EntitySystem foreach (var entry in component.SoilSolution.Value.Comp.Solution.Contents) { var reagentProto = _prototype.Index(entry.Reagent.Prototype); - _entityEffects.ApplyEffects(uid, reagentProto.PlantMetabolisms.ToArray(), entry.Quantity.Float()); + _entityEffects.ApplyEffects(uid, [.. reagentProto.PlantMetabolisms], entry.Quantity.Float()); } - _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, FixedPoint2.New(1)); + _solutionContainer.RemoveEachReagent(component.SoilSolution.Value, FixedPoint2.New(1)); } - - CheckLevelSanity(uid, component); } private void Mutate(EntityUid uid, float severity, PlantHolderComponent? component = null) @@ -903,27 +552,34 @@ public sealed class PlantHolderSystem : EntitySystem return; if (component.Seed != null) - { - EnsureUniqueSeed(uid, component); _mutation.MutateSeed(uid, ref component.Seed, severity); - } } public void UpdateSprite(EntityUid uid, PlantHolderComponent? component = null) { - if (!Resolve(uid, ref component)) + PlantHarvestComponent? harvest = null; + PlantComponent? plant = null; + Resolve(uid, ref harvest, ref plant, ref component, false); + + if (!TryComp(uid, out var app) + || component == null) return; + component.UpdateSpriteAfterUpdate = false; - if (!TryComp(uid, out var app)) - return; - - if (component.Seed != null) + // If no seed, clear visuals regardless of traits. + if (component.Seed == null) + { + _appearance.SetData(uid, PlantHolderVisuals.PlantState, string.Empty, app); + _appearance.SetData(uid, PlantHolderVisuals.HealthLight, false, app); + _appearance.SetData(uid, PlantHolderVisuals.HarvestLight, false, app); + } + else if (harvest != null && plant != null) { if (component.DrawWarnings) { - _appearance.SetData(uid, PlantHolderVisuals.HealthLight, component.Health <= component.Seed.Endurance / 2f); + _appearance.SetData(uid, PlantHolderVisuals.HealthLight, component.Health <= plant.Endurance / 2f); } if (component.Dead) @@ -931,53 +587,40 @@ public sealed class PlantHolderSystem : EntitySystem _appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app); _appearance.SetData(uid, PlantHolderVisuals.PlantState, "dead", app); } - else if (component.Harvest) + else if (harvest.ReadyForHarvest) { _appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app); _appearance.SetData(uid, PlantHolderVisuals.PlantState, "harvest", app); } - else if (component.Age < component.Seed.Maturation) + else if (component.Age < plant.Maturation) { var growthStage = GetCurrentGrowthStage((uid, component)); _appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app); _appearance.SetData(uid, PlantHolderVisuals.PlantState, $"stage-{growthStage}", app); - component.LastProduce = component.Age; + harvest.LastHarvest = component.Age; } else { _appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app); - _appearance.SetData(uid, PlantHolderVisuals.PlantState, $"stage-{component.Seed.GrowthStages}", app); + _appearance.SetData(uid, PlantHolderVisuals.PlantState, $"stage-{plant.GrowthStages}", app); } } - else - { - _appearance.SetData(uid, PlantHolderVisuals.PlantState, "", app); - _appearance.SetData(uid, PlantHolderVisuals.HealthLight, false, app); - } if (!component.DrawWarnings) return; + // TODO: dehardcode those alert levels. + // Not obvious where they go, as plant holder have alerts, sure, but some plants could have + // very different consumption rates so it would make sense to have different thresholds _appearance.SetData(uid, PlantHolderVisuals.WaterLight, component.WaterLevel <= 15, app); _appearance.SetData(uid, PlantHolderVisuals.NutritionLight, component.NutritionLevel <= 8, app); - _appearance.SetData(uid, PlantHolderVisuals.AlertLight, - component.WeedLevel >= 5 || component.PestLevel >= 5 || component.Toxins >= 40 || component.ImproperHeat || - component.ImproperLight || component.ImproperPressure || component.MissingGas > 0, app); - _appearance.SetData(uid, PlantHolderVisuals.HarvestLight, component.Harvest, app); - } - - /// - /// Check if the currently contained seed is unique. If it is not, clone it so that we have a unique seed. - /// Necessary to avoid modifying global seeds. - /// - public void EnsureUniqueSeed(EntityUid uid, PlantHolderComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (component.Seed is { Unique: false }) - component.Seed = component.Seed.Clone(); + _appearance.SetData(uid, + PlantHolderVisuals.AlertLight, + component.WeedLevel >= 5 || component.PestLevel >= 5 || component.Toxins >= 40 || component.ImproperHeat + || component.ImproperPressure || component.MissingGas > 0, + app); + _appearance.SetData(uid, PlantHolderVisuals.HarvestLight, harvest is { ReadyForHarvest: true }, app); } public void ForceUpdateByExternalCause(EntityUid uid, PlantHolderComponent? component = null) @@ -989,4 +632,19 @@ public sealed class PlantHolderSystem : EntitySystem component.ForceUpdate = true; Update(uid, component); } + + /// + /// Removes all growth-related components from a plant. + /// TODO: Delete after plants transition to entities + /// + private void RemoveAllGrowthComponents(EntityUid uid) + { + foreach (var comp in EntityManager.GetComponents(uid)) + { + if (GrowthComponentsHolder.GrowthComponentTypes.Contains(comp.GetType())) + { + RemComp(uid, comp); + } + } + } } diff --git a/Content.Server/Botany/Systems/PlantSystem.cs b/Content.Server/Botany/Systems/PlantSystem.cs new file mode 100644 index 00000000000..23120c4a130 --- /dev/null +++ b/Content.Server/Botany/Systems/PlantSystem.cs @@ -0,0 +1,43 @@ +using Content.Server.Botany.Components; +using Robust.Shared.Random; + +namespace Content.Server.Botany.Systems; + +/// +/// Handles plant behavior and growth processing. +/// +public sealed class PlantSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnPlantGrow); + } + + private void OnPlantGrow(Entity ent, ref OnPlantGrowEvent args) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantHolderComponent? holder)) + return; + + // Check if plant is too old. + if (holder.Age > component.Lifespan) + { + holder.Health -= _random.Next(3, 5) * BasicGrowthSystem.HydroponicsSpeedMultiplier; + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + } + } + + /// + /// Adjusts the potency of a plant component. + /// + public void AdjustPotency(Entity ent, float delta) + { + var plant = ent.Comp; + plant.Potency = Math.Max(plant.Potency + delta, 1); + Dirty(ent); + } +} diff --git a/Content.Server/Botany/Systems/PlantToxinsSystem.cs b/Content.Server/Botany/Systems/PlantToxinsSystem.cs new file mode 100644 index 00000000000..7c03da028fd --- /dev/null +++ b/Content.Server/Botany/Systems/PlantToxinsSystem.cs @@ -0,0 +1,37 @@ +using Content.Server.Botany.Components; + +namespace Content.Server.Botany.Systems; + +/// +/// Handles toxin accumulation and tolerance for plants, applying health damage +/// and decrementing toxins based on per-tick uptake. +/// +public sealed class ToxinsSystem : EntitySystem +{ + public override void Initialize() + { + SubscribeLocalEvent(OnPlantGrow); + } + + private void OnPlantGrow(Entity ent, ref OnPlantGrowEvent args) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantHolderComponent? holder )) + return; + + if (holder.Toxins < 0) + return; + + var toxinUptake = MathF.Max(1, MathF.Round(holder.Toxins / component.ToxinUptakeDivisor)); + if (holder.Toxins > component.ToxinsTolerance) + holder.Health -= toxinUptake; + + // there is a possibility that it will remove more toxin than amount of damage it took on plant health (and killed it). + // TODO: get min out of health left and toxin uptake - would work better, probably. + holder.Toxins -= toxinUptake; + + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + } +} diff --git a/Content.Server/Botany/Systems/SeedExtractorSystem.cs b/Content.Server/Botany/Systems/SeedExtractorSystem.cs index c7e20983a7a..fa030f50e30 100644 --- a/Content.Server/Botany/Systems/SeedExtractorSystem.cs +++ b/Content.Server/Botany/Systems/SeedExtractorSystem.cs @@ -27,7 +27,11 @@ public sealed class SeedExtractorSystem : EntitySystem if (!TryComp(args.Used, out ProduceComponent? produce)) return; - if (!_botanySystem.TryGetSeed(produce, out var seed) || seed.Seedless) + + if (!_botanySystem.TryGetSeed(produce, out var seed)) + return; + + if (!BotanySystem.TryGetPlantTraits(seed, out var traits) || traits.Seedless) { _popupSystem.PopupCursor(Loc.GetString("seed-extractor-component-no-seeds", ("name", args.Used)), args.User, PopupType.MediumCaution); diff --git a/Content.Server/Botany/Systems/UnviableGrowthSystem.cs b/Content.Server/Botany/Systems/UnviableGrowthSystem.cs new file mode 100644 index 00000000000..64ed3f32ad2 --- /dev/null +++ b/Content.Server/Botany/Systems/UnviableGrowthSystem.cs @@ -0,0 +1,28 @@ +using Content.Server.Botany.Components; + +namespace Content.Server.Botany.Systems; + +/// +/// Applies a death chance and damage to unviable plants each growth tick, updating visuals when necessary. +/// +public sealed class UnviableGrowthSystem : EntitySystem +{ + public override void Initialize() + { + SubscribeLocalEvent(OnPlantGrow); + } + + private void OnPlantGrow(Entity ent, ref OnPlantGrowEvent args) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantHolderComponent? holder) + || !BotanySystem.TryGetPlantTraits(holder.Seed, out var traits) + || traits.Viable) + return; + + holder.Health -= component.UnviableDamage; + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + } +} diff --git a/Content.Server/Botany/Systems/WeedPestGrowthSystem.cs b/Content.Server/Botany/Systems/WeedPestGrowthSystem.cs new file mode 100644 index 00000000000..0f0d8f00be4 --- /dev/null +++ b/Content.Server/Botany/Systems/WeedPestGrowthSystem.cs @@ -0,0 +1,80 @@ +using Content.Server.Botany.Components; +using Content.Shared.Coordinates.Helpers; +using Robust.Shared.Random; + +namespace Content.Server.Botany.Systems; + +/// +/// Manages weed growth and pest damage per growth tick, and handles tray-level +/// weed spawning and kudzu transformation based on conditions. +/// +public sealed class WeedPestGrowthSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnPlantGrow); + SubscribeLocalEvent(OnTrayUpdate); + } + + private void OnPlantGrow(Entity ent, ref OnPlantGrowEvent args) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantHolderComponent? holder)) + return; + + // Weed growth logic. + if (_random.Prob(component.WeedGrowthChance)) + { + holder.WeedLevel += component.WeedGrowthAmount; + if (holder.DrawWarnings) + holder.UpdateSpriteAfterUpdate = true; + } + + // Pest damage logic. + if (_random.Prob(component.PestDamageChance)) + holder.Health -= component.PestDamageAmount; + } + + /// + /// Handles weed growth and kudzu transformation for plant holder trays. + /// + private void OnTrayUpdate(Entity ent, ref OnPlantGrowEvent args) + { + var (uid, component) = ent; + + if (!TryComp(uid, out PlantTraitsComponent? traits) + || !TryComp(uid, out WeedPestGrowthComponent? weed)) + return; + + // Weeds like water and nutrients! They may appear even if there's not a seed planted. + if (component is { WaterLevel: > 10, NutritionLevel: > 5 }) + { + float chance; + if (component.Seed == null) + chance = 0.05f; + else if (traits.TurnIntoKudzu) + chance = 1f; + else + chance = 0.01f; + + if (_random.Prob(chance)) + component.WeedLevel += 1 + component.WeedCoefficient * BasicGrowthSystem.HydroponicsSpeedMultiplier; + + if (component.DrawWarnings) + component.UpdateSpriteAfterUpdate = true; + } + + // Handle kudzu transformation. + if (component is { Seed: not null } + && traits.TurnIntoKudzu + && component.WeedLevel >= weed.WeedHighLevelThreshold) + { + Spawn(traits.KudzuPrototype, Transform(uid).Coordinates.SnapToGrid(EntityManager)); + traits.TurnIntoKudzu = false; + component.Health = 0; + } + } +} diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs index ebe5c83181c..2ac191ad413 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs @@ -5,15 +5,21 @@ using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; +/// +/// Entity effect that sets plant potency. +/// public sealed partial class PlantAdjustPotencyEntityEffectSystem : EntityEffectSystem { - [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + [Dependency] private readonly PlantSystem _plant = default!; + protected override void Effect(Entity entity, ref EntityEffectEvent args) { if (entity.Comp.Seed == null || entity.Comp.Dead) return; - _plantHolder.EnsureUniqueSeed(entity, entity.Comp); - entity.Comp.Seed.Potency = Math.Max(entity.Comp.Seed.Potency + args.Effect.Amount, 1); + if (!TryComp(entity, out var plant)) + return; + + _plant.AdjustPotency((entity.Owner, plant), args.Effect.Amount); } } diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs index b0faa6255e1..571ff827b26 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs @@ -5,15 +5,18 @@ using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; +/// +/// Entity effect that increments plant age / growth cycle. +/// public sealed partial class PlantAffectGrowthEntityEffectSystem : EntityEffectSystem { - [Dependency] private readonly PlantHolderSystem _plantHolder = default!; + [Dependency] private readonly BasicGrowthSystem _plantGrowth = default!; protected override void Effect(Entity entity, ref EntityEffectEvent args) { if (entity.Comp.Seed == null || entity.Comp.Dead) return; - _plantHolder.AffectGrowth(entity, (int)args.Effect.Amount, entity); + _plantGrowth.AffectGrowth(entity, (int)args.Effect.Amount); } } diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs index 3d82f74b116..f98a378be2c 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Server.Botany.Components; using Content.Shared.EntityEffects; using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; @@ -19,37 +20,60 @@ public sealed partial class PlantChangeStatEntityEffectSystem : EntityEffectSyst if (entity.Comp.Seed == null || entity.Comp.Dead) return; - var effect = args.Effect; - var member = entity.Comp.Seed.GetType().GetField(args.Effect.TargetValue); + var targetValue = args.Effect.TargetValue; - if (member == null) + // Scan live plant growth components and mutate the first matching field. + foreach (var growthComp in EntityManager.GetComponents(entity.Owner)) { - Log.Error($"{ effect.GetType().Name } Error: Member { args.Effect.TargetValue} not found on { entity.Comp.Seed.GetType().Name }. Did you misspell it?"); - return; + + var componentType = growthComp.GetType(); + if(!GrowthComponentsHolder.GrowthComponentTypes.Contains(componentType)) + continue; + + var field = componentType.GetField(targetValue); + + if (field == null) + continue; + + var currentValue = field.GetValue(growthComp); + if (currentValue == null) + continue; + + if (TryGetValue(currentValue, out var floatVal)) + { + MutateFloat(ref floatVal, args.Effect.MinValue, args.Effect.MaxValue, args.Effect.Steps); + field.SetValue(growthComp, floatVal); + return; + } + + if (TryGetValue(currentValue, out var intVal)) + { + MutateInt(ref intVal, (int)args.Effect.MinValue, (int)args.Effect.MaxValue, args.Effect.Steps); + field.SetValue(growthComp, intVal); + return; + } + + if (TryGetValue(currentValue, out var boolVal)) + { + field.SetValue(growthComp, !boolVal); + return; + } } - var currentValObj = member.GetValue(entity.Comp.Seed); - if (currentValObj == null) - return; + // Field not found in any component. + Log.Error($"{nameof(PlantChangeStat)} Error: Field '{targetValue}' not found in any plant component. Did you misspell it?"); + } - if (member.FieldType == typeof(float)) + private bool TryGetValue(object value, out T? result) + { + result = default; + if (value is T val) { - var floatVal = (float)currentValObj; - MutateFloat(ref floatVal, args.Effect.MinValue, args.Effect.MaxValue, args.Effect.Steps); - member.SetValue(entity.Comp.Seed, floatVal); - } - else if (member.FieldType == typeof(int)) - { - var intVal = (int)currentValObj; - MutateInt(ref intVal, (int)args.Effect.MinValue, (int)args.Effect.MaxValue, args.Effect.Steps); - member.SetValue(entity.Comp.Seed, intVal); - } - else if (member.FieldType == typeof(bool)) - { - var boolVal = (bool)currentValObj; - boolVal = !boolVal; - member.SetValue(entity.Comp.Seed, boolVal); + result = val; + return true; } + + return false; } // Mutate reference 'val' between 'min' and 'max' by pretending the value @@ -65,14 +89,14 @@ public sealed partial class PlantChangeStatEntityEffectSystem : EntityEffectSyst // Starting number of bits that are high, between 0 and bits. // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded. - int valInt = (int)MathF.Round((val - min) / (max - min) * bits); + var valInt = (int)MathF.Round((val - min) / (max - min) * bits); // val may be outside the range of min/max due to starting prototype values, so clamp. valInt = Math.Clamp(valInt, 0, bits); // Probability that the bit flip increases n. // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it. // In other words, it tends to go to the middle. - float probIncrease = 1 - (float)valInt / bits; + var probIncrease = 1 - (float)valInt / bits; int valIntMutated; if (_random.Prob(probIncrease)) { @@ -84,7 +108,7 @@ public sealed partial class PlantChangeStatEntityEffectSystem : EntityEffectSyst } // Set value based on mutated thermometer code. - float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max); + var valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max); val = valMutated; } @@ -98,14 +122,14 @@ public sealed partial class PlantChangeStatEntityEffectSystem : EntityEffectSyst // Starting number of bits that are high, between 0 and bits. // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded. - int valInt = (int)MathF.Round((val - min) / (max - min) * bits); + var valInt = (int)MathF.Round((val - min) / (max - min) * bits); // val may be outside the range of min/max due to starting prototype values, so clamp. valInt = Math.Clamp(valInt, 0, bits); // Probability that the bit flip increases n. // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it. // In other words, it tends to go to the middle. - float probIncrease = 1 - (float)valInt / bits; + var probIncrease = 1 - (float)valInt / bits; int valMutated; if (_random.Prob(probIncrease)) { diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs index 710bce24dde..c9d3bf7e221 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs @@ -5,6 +5,9 @@ using Robust.Shared.Random; namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; +/// +/// Entity effect that reverts aging of plant. +/// public sealed partial class PlantCryoxadoneEntityEffectSystem : EntityEffectSystem { [Dependency] private readonly IRobustRandom _random = default!; @@ -14,17 +17,16 @@ public sealed partial class PlantCryoxadoneEntityEffectSystem : EntityEffectSyst if (entity.Comp.Seed == null || entity.Comp.Dead) return; - var deviation = 0; - var seed = entity.Comp.Seed; - if (seed == null) + if (!TryComp(entity, out var plant) || !TryComp(entity, out var harvest)) return; - if (entity.Comp.Age > seed.Maturation) - deviation = (int) Math.Max(seed.Maturation - 1, entity.Comp.Age - _random.Next(7, 10)); - else - deviation = (int) (seed.Maturation / seed.GrowthStages); + + var deviation = entity.Comp.Age > plant.Maturation + ? (int)Math.Max(plant.Maturation - 1, entity.Comp.Age - _random.Next(7, 10)) + : (int)(plant.Maturation / plant.GrowthStages); + entity.Comp.Age -= deviation; - entity.Comp.LastProduce = entity.Comp.Age; entity.Comp.SkipAging++; entity.Comp.ForceUpdate = true; + harvest.LastHarvest = entity.Comp.Age; } } diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs index 1661c501be1..667e31cc684 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs @@ -7,6 +7,9 @@ using Content.Shared.Popups; namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; +/// +/// Entity effect that removes ability to get seeds from plant using seed maker. +/// public sealed partial class PlantDestroySeedsEntityEffectSystem : EntityEffectSystem { [Dependency] private readonly PlantHolderSystem _plantHolder = default!; @@ -17,15 +20,14 @@ public sealed partial class PlantDestroySeedsEntityEffectSystem : EntityEffectSy if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) return; - if (entity.Comp.Seed.Seedless) + if (!TryComp(entity, out var traits) || traits.Seedless) return; - _plantHolder.EnsureUniqueSeed(entity, entity.Comp); _popup.PopupEntity( Loc.GetString("botany-plant-seedsdestroyed"), entity, PopupType.SmallCaution ); - entity.Comp.Seed.Seedless = true; + traits.Seedless = true; } } diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs index f6aebde465d..34575b2124c 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs @@ -1,31 +1,29 @@ using Content.Server.Botany.Components; -using Content.Server.Botany.Systems; using Content.Shared.EntityEffects; using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; using Robust.Shared.Random; namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; +/// +/// Entity effect that enhances plant longevity and endurance. +/// public sealed partial class PlantDiethylamineEntityEffectSystem : EntityEffectSystem { [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly PlantHolderSystem _plantHolder = default!; protected override void Effect(Entity entity, ref EntityEffectEvent args) { if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) return; - if (_random.Prob(0.1f)) - { - _plantHolder.EnsureUniqueSeed(entity, entity); - entity.Comp.Seed!.Lifespan++; - } + if (!TryComp(entity, out var plant)) + return; if (_random.Prob(0.1f)) - { - _plantHolder.EnsureUniqueSeed(entity, entity); - entity.Comp.Seed!.Endurance++; - } + plant.Lifespan++; + + if (_random.Prob(0.1f)) + plant.Endurance++; } } diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs index 8a073392e1c..fd28a1ea46f 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs @@ -4,6 +4,9 @@ using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; +/// +/// Entity effect that mutates plant to lose health with time. +/// public sealed partial class PlantPhalanximineEntityEffectSystem : EntityEffectSystem { protected override void Effect(Entity entity, ref EntityEffectEvent args) @@ -11,6 +14,7 @@ public sealed partial class PlantPhalanximineEntityEffectSystem : EntityEffectSy if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) return; - entity.Comp.Seed.Viable = true; + if (TryComp(entity, out var traits)) + traits.Viable = true; } } diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs index 4d724be2443..dc1081ca516 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs @@ -6,6 +6,9 @@ using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes; namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; +/// +/// Entity effect that restores ability to get seeds from plant seed maker. +/// public sealed partial class PlantRestoreSeedsEntityEffectSystem : EntityEffectSystem { [Dependency] private readonly PlantHolderSystem _plantHolder = default!; @@ -16,11 +19,10 @@ public sealed partial class PlantRestoreSeedsEntityEffectSystem : EntityEffectSy if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable) return; - if (!entity.Comp.Seed.Seedless) + if (!TryComp(entity, out var traits) || !traits.Seedless) return; - _plantHolder.EnsureUniqueSeed(entity, entity.Comp); _popup.PopupEntity(Loc.GetString("botany-plant-seedsrestored"), entity); - entity.Comp.Seed.Seedless = false; + traits.Seedless = false; } } diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs index 68ea3319ef4..79ed860343b 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs @@ -14,28 +14,26 @@ namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes; public sealed partial class RobustHarvestEntityEffectSystem : EntityEffectSystem { [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly PlantHolderSystem _plantHolder = default!; protected override void Effect(Entity entity, ref EntityEffectEvent args) { if (entity.Comp.Seed == null || entity.Comp.Dead) return; - if (entity.Comp.Seed.Potency < args.Effect.PotencyLimit) - { - _plantHolder.EnsureUniqueSeed(entity, entity.Comp); - entity.Comp.Seed.Potency = Math.Min(entity.Comp.Seed.Potency + args.Effect.PotencyIncrease, args.Effect.PotencyLimit); + if (!TryComp(entity, out var plant) || !TryComp(entity, out var traits)) + return; - if (entity.Comp.Seed.Potency > args.Effect.PotencySeedlessThreshold) - { - entity.Comp.Seed.Seedless = true; - } - } - else if (entity.Comp.Seed.Yield > 1 && _random.Prob(0.1f)) + if (plant.Potency < args.Effect.PotencyLimit) { - // Too much of a good thing reduces yield - _plantHolder.EnsureUniqueSeed(entity, entity.Comp); - entity.Comp.Seed.Yield--; + plant.Potency = Math.Min(plant.Potency + args.Effect.PotencyIncrease, args.Effect.PotencyLimit); + + if (plant.Potency > args.Effect.PotencySeedlessThreshold) + traits.Seedless = true; + } + else if (plant.Yield > 1 && _random.Prob(0.1f)) + { + // Too much of a good thing reduces yield. + plant.Yield--; } } } diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs index e2376ba186c..70f1f52bf39 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs @@ -1,4 +1,3 @@ -using System.Linq; using Content.Server.Botany.Components; using Content.Shared.Atmos; using Content.Shared.EntityEffects; @@ -7,6 +6,9 @@ using Robust.Shared.Random; namespace Content.Server.EntityEffects.Effects.Botany; +/// +/// Plant mutation entity effect that forces plant to exude gas while living. +/// public sealed partial class PlantMutateExudeGasesEntityEffectSystem : EntityEffectSystem { [Dependency] private readonly IRobustRandom _random = default!; @@ -16,11 +18,12 @@ public sealed partial class PlantMutateExudeGasesEntityEffectSystem : EntityEffe if (entity.Comp.Seed == null) return; - var gasses = entity.Comp.Seed.ExudeGasses; + var gasComponent = EnsureComp(entity); + var gasses = gasComponent.ExudeGasses; - // Add a random amount of a random gas to this gas dictionary + // Add a random amount of a random gas to this gas dictionary. float amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue); - var gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList()); + var gas = _random.Pick(Enum.GetValues()); if (!gasses.TryAdd(gas, amount)) { @@ -29,6 +32,9 @@ public sealed partial class PlantMutateExudeGasesEntityEffectSystem : EntityEffe } } +/// +/// Plant mutation entity effect that forces plant to consume gas while living. +/// public sealed partial class PlantMutateConsumeGasesEntityEffectSystem : EntityEffectSystem { [Dependency] private readonly IRobustRandom _random = default!; @@ -38,11 +44,12 @@ public sealed partial class PlantMutateConsumeGasesEntityEffectSystem : EntityEf if (entity.Comp.Seed == null) return; - var gasses = entity.Comp.Seed.ConsumeGasses; + var gasComponent = EnsureComp(entity); + var gasses = gasComponent.ConsumeGasses; - // Add a random amount of a random gas to this gas dictionary + // Add a random amount of a random gas to this gas dictionary. var amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue); - var gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList()); + var gas = _random.Pick(Enum.GetValues()); if (!gasses.TryAdd(gas, amount)) { diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs index 95d7f97bbe3..c96b80588c4 100644 --- a/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs +++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs @@ -1,10 +1,12 @@ -using Content.Server.Botany; using Content.Server.Botany.Components; using Content.Shared.EntityEffects; using Content.Shared.EntityEffects.Effects.Botany; namespace Content.Server.EntityEffects.Effects.Botany; +/// +/// Plant mutation entity effect that changes repeatability of plant harvesting (without re-planting). +/// public sealed partial class PlantMutateHarvestEntityEffectSystem : EntityEffectSystem { protected override void Effect(Entity entity, ref EntityEffectEvent args) @@ -12,13 +14,14 @@ public sealed partial class PlantMutateHarvestEntityEffectSystem : EntityEffectS if (entity.Comp.Seed == null) return; - switch (entity.Comp.Seed.HarvestRepeat) + var harvest = EnsureComp(entity); + switch (harvest.HarvestRepeat) { case HarvestType.NoRepeat: - entity.Comp.Seed.HarvestRepeat = HarvestType.Repeat; + harvest.HarvestRepeat = HarvestType.Repeat; break; case HarvestType.Repeat: - entity.Comp.Seed.HarvestRepeat = HarvestType.SelfHarvest; + harvest.HarvestRepeat = HarvestType.SelfHarvest; break; } } diff --git a/Content.Shared/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffect.cs b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffect.cs index c617c05b334..692bd0c400f 100644 --- a/Content.Shared/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffect.cs +++ b/Content.Shared/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffect.cs @@ -1,3 +1,5 @@ +using Robust.Shared.Prototypes; + namespace Content.Shared.EntityEffects.Effects.Botany; /// @@ -10,6 +12,15 @@ public sealed partial class PlantMutateConsumeGases : EntityEffectBase + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + return Loc.GetString("entity-effect-guidebook-plant-mutate-consume-gasses", + ("chance", Probability), + ("minValue", MinValue), + ("maxValue", MaxValue)); + } } public sealed partial class PlantMutateExudeGases : EntityEffectBase @@ -19,4 +30,13 @@ public sealed partial class PlantMutateExudeGases : EntityEffectBase + public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + return Loc.GetString("entity-effect-guidebook-plant-mutate-exude-gasses", + ("chance", Probability), + ("minValue", MinValue), + ("maxValue", MaxValue)); + } } diff --git a/Resources/Locale/en-US/guidebook/entity-effects/effects.ftl b/Resources/Locale/en-US/guidebook/entity-effects/effects.ftl index fccc6291a8f..4acd2cdcfde 100644 --- a/Resources/Locale/en-US/guidebook/entity-effects/effects.ftl +++ b/Resources/Locale/en-US/guidebook/entity-effects/effects.ftl @@ -513,6 +513,18 @@ entity-effect-guidebook-plant-seeds-remove = *[other] remove the } seeds of the plant +entity-effect-guidebook-plant-mutate-exude-gasses = + { $chance -> + [1] Mutates + *[other] mutate + } the plant to exude gases between {$minValue} and {$maxValue} moles + +entity-effect-guidebook-plant-mutate-consume-gasses = + { $chance -> + [1] Mutates + *[other] mutate + } the plant to consume gases between {$minValue} and {$maxValue} moles + entity-effect-guidebook-plant-mutate-chemicals = { $chance -> [1] Mutates diff --git a/Resources/Maps/Salvage/vegan-meatball.yml b/Resources/Maps/Salvage/vegan-meatball.yml index 824e3d0b7ea..f6b045d26a4 100644 --- a/Resources/Maps/Salvage/vegan-meatball.yml +++ b/Resources/Maps/Salvage/vegan-meatball.yml @@ -485,23 +485,13 @@ entities: age: 1 health: 100 seed: - splatPrototype: null - lifespan: 75 - endurance: 100 - weedHighLevelThreshold: 10 - weedTolerance: 5 - pestTolerance: 5 - highPressureTolerance: 121 - lowPressureTolerance: 81 - toxinsTolerance: 4 - lightTolerance: 3 - idealLight: 9 - heatTolerance: 10 - idealHeat: 298 - waterConsumption: 0.4 - nutrientConsumption: 0.75 - exudeGasses: {} - consumeGasses: {} + growthComponents: + plant: + lifespan: 75 + basicGrowth: + waterConsumption: 0.4 + atmosphericGrowth: + idealHeat: 298 chemicals: THC: Inherent: True @@ -558,23 +548,13 @@ entities: age: 1 health: 100 seed: - splatPrototype: null - lifespan: 25 - endurance: 100 - weedHighLevelThreshold: 10 - weedTolerance: 5 - pestTolerance: 5 - highPressureTolerance: 121 - lowPressureTolerance: 81 - toxinsTolerance: 4 - lightTolerance: 3 - idealLight: 7 - heatTolerance: 10 - idealHeat: 293 - waterConsumption: 0.6 - nutrientConsumption: 0.75 - exudeGasses: {} - consumeGasses: {} + growthComponents: + plant: + lifespan: 25 + basicGrowth: + waterConsumption: 0.6 + atmosphericGrowth: + idealHeat: 293 chemicals: Stellibinin: Inherent: True diff --git a/Resources/Prototypes/Hydroponics/seeds.yml b/Resources/Prototypes/Hydroponics/seeds.yml index 9a2b4eafae9..d90ff84e6d4 100644 --- a/Resources/Prototypes/Hydroponics/seeds.yml +++ b/Resources/Prototypes/Hydroponics/seeds.yml @@ -9,22 +9,24 @@ - WheatBushel mutationPrototypes: - meatwheat - lifespan: 25 - maturation: 6 - production: 3 - yield: 3 - potency: 5 - idealLight: 8 - nutrientConsumption: 0.40 + growthComponents: + plant: + lifespan: 25 + maturation: 6 + production: 3 + yield: 3 + potency: 5 + basicGrowth: + nutrientConsumption: 0.4 chemicals: Nutriment: - Min: 1 - Max: 20 - PotencyDivisor: 20 + min: 1 + max: 20 + potencyDivisor: 20 Flour: - Min: 5 - Max: 20 - PotencyDivisor: 20 + min: 5 + max: 20 + potencyDivisor: 20 - type: seed id: meatwheat @@ -35,22 +37,24 @@ packetPrototype: MeatwheatSeeds productPrototypes: - MeatwheatBushel - lifespan: 25 - maturation: 6 - production: 3 - yield: 3 - potency: 5 - idealLight: 8 - nutrientConsumption: 0.40 + growthComponents: + plant: + lifespan: 25 + maturation: 6 + production: 3 + yield: 3 + potency: 5 + basicGrowth: + nutrientConsumption: 0.4 chemicals: Nutriment: - Min: 1 - Max: 20 - PotencyDivisor: 20 + min: 1 + max: 20 + potencyDivisor: 20 UncookedAnimalProteins: - Min: 5 - Max: 20 - PotencyDivisor: 20 + min: 5 + max: 20 + potencyDivisor: 20 - type: seed id: oat @@ -61,22 +65,24 @@ packetPrototype: OatSeeds productPrototypes: - OatBushel - lifespan: 25 - maturation: 6 - production: 3 - yield: 3 - potency: 5 - idealLight: 8 - nutrientConsumption: 0.40 + growthComponents: + plant: + lifespan: 25 + maturation: 6 + production: 3 + yield: 3 + potency: 5 + basicGrowth: + nutrientConsumption: 0.4 chemicals: Nutriment: - Min: 1 - Max: 20 - PotencyDivisor: 20 + min: 1 + max: 20 + potencyDivisor: 20 Oats: - Min: 5 - Max: 20 - PotencyDivisor: 20 + min: 5 + max: 20 + potencyDivisor: 20 - type: seed id: banana @@ -89,23 +95,27 @@ - FoodBanana mutationPrototypes: - mimana - harvestRepeat: Repeat - lifespan: 50 - maturation: 6 - production: 6 - yield: 2 - idealLight: 9 - waterConsumption: 0.60 - idealHeat: 298 + growthComponents: + plant: + lifespan: 50 + maturation: 6 + production: 6 + yield: 2 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.6 + atmosphericGrowth: + idealHeat: 298 chemicals: Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 Nutriment: - Min: 1 - Max: 2 - PotencyDivisor: 50 + min: 1 + max: 2 + potencyDivisor: 50 - type: seed id: mimana @@ -116,23 +126,27 @@ packetPrototype: MimanaSeeds productPrototypes: - FoodMimana - harvestRepeat: Repeat - lifespan: 50 - maturation: 6 - production: 6 - yield: 2 - idealLight: 9 - waterConsumption: 0.60 - idealHeat: 298 + growthComponents: + plant: + lifespan: 50 + maturation: 6 + production: 6 + yield: 2 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.6 + atmosphericGrowth: + idealHeat: 298 chemicals: MuteToxin: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Nutriment: - Min: 1 - Max: 2 - PotencyDivisor: 50 + min: 1 + max: 2 + potencyDivisor: 50 - type: seed id: carrots @@ -143,26 +157,29 @@ packetPrototype: CarrotSeeds productPrototypes: - FoodCarrot - lifespan: 25 - maturation: 10 - production: 3 - yield: 3 - potency: 10 - growthStages: 3 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 25 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 3 + basicGrowth: + waterConsumption: 0.6 chemicals: JuiceCarrot: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Oculine: - Min: 2 - Max: 6 - PotencyDivisor: 20 + min: 2 + max: 6 + potencyDivisor: 20 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: laughinPea @@ -175,29 +192,32 @@ - FoodLaughinPeaPod mutationPrototypes: - worldPea - lifespan: 25 - growthStages: 3 - maturation: 7 - production: 5 - yield: 3 - potency: 20 - idealLight: 8 - harvestRepeat: Repeat - nutrientConsumption: 0.6 - waterConsumption: 0.6 + growthComponents: + plant: + lifespan: 25 + growthStages: 3 + maturation: 7 + production: 5 + yield: 3 + potency: 20 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.6 chemicals: Nutriment: - Min: 1 - Max: 3 - PotencyDivisor: 7 + min: 1 + max: 3 + potencyDivisor: 7 Sugar: - Min: 1 - Max: 10 - PotencyDivisor: 5 + min: 1 + max: 10 + potencyDivisor: 5 Laughter: - Min: 1 - Max: 10 - PotencyDivisor: 5 + min: 1 + max: 10 + potencyDivisor: 5 - type: seed id: lemon @@ -212,22 +232,24 @@ - lemoon - lime - orange - harvestRepeat: Repeat - lifespan: 55 - maturation: 6 - production: 6 - yield: 3 - potency: 10 - idealLight: 8 + growthComponents: + plant: + lifespan: 55 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + harvest: + harvestRepeat: Repeat chemicals: Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: lemoon @@ -238,22 +260,24 @@ packetPrototype: LemoonSeeds productPrototypes: - FoodLemoon - harvestRepeat: Repeat - lifespan: 90 - maturation: 8 - production: 6 - yield: 4 - potency: 1 - idealLight: 8 + growthComponents: + plant: + lifespan: 90 + maturation: 8 + production: 6 + yield: 4 + potency: 1 + harvest: + harvestRepeat: Repeat chemicals: Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 Milk: - Min: 8 - Max: 20 - PotencyDivisor: 5 + min: 8 + max: 20 + potencyDivisor: 5 - type: seed id: lime @@ -267,22 +291,24 @@ mutationPrototypes: - orange - lemon - harvestRepeat: Repeat - lifespan: 55 - maturation: 6 - production: 6 - yield: 3 - potency: 10 - idealLight: 8 + growthComponents: + plant: + lifespan: 55 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + harvest: + harvestRepeat: Repeat chemicals: Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: orange @@ -297,22 +323,24 @@ - extradimensionalOrange - lemon - lime - harvestRepeat: Repeat - lifespan: 55 - maturation: 6 - production: 6 - yield: 3 - potency: 10 - idealLight: 8 + growthComponents: + plant: + lifespan: 55 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + harvest: + harvestRepeat: Repeat chemicals: Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: extradimensionalOrange @@ -323,26 +351,28 @@ packetPrototype: ExtradimensionalOrangeSeeds productPrototypes: - FoodExtradimensionalOrange - harvestRepeat: Repeat - lifespan: 55 - maturation: 6 - production: 6 - yield: 3 - potency: 10 - idealLight: 8 + growthComponents: + plant: + lifespan: 55 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + harvest: + harvestRepeat: Repeat chemicals: Haloperidol: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: pineapple @@ -353,27 +383,29 @@ packetPrototype: PineappleSeeds productPrototypes: - FoodPineapple - harvestRepeat: Repeat - lifespan: 55 - maturation: 6 - production: 6 - yield: 3 - potency: 10 - idealLight: 8 - growthStages: 3 + growthComponents: + plant: + lifespan: 55 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + growthStages: 3 + harvest: + harvestRepeat: Repeat chemicals: Nutriment: - Min: 1 - Max: 20 - PotencyDivisor: 20 + min: 1 + max: 20 + potencyDivisor: 20 Water: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 Vitamin: - Min: 1 - Max: 2 - PotencyDivisor: 50 + min: 1 + max: 2 + potencyDivisor: 50 - type: seed id: potato @@ -384,22 +416,25 @@ packetPrototype: PotatoSeeds productPrototypes: - FoodPotato - lifespan: 30 - maturation: 10 - production: 3 - yield: 3 - potency: 10 - growthStages: 4 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 30 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 4 + basicGrowth: + waterConsumption: 0.6 chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: sugarcane @@ -412,19 +447,23 @@ - Sugarcane mutationPrototypes: - papercane - harvestRepeat: Repeat - lifespan: 60 - maturation: 6 - production: 6 - yield: 3 - potency: 10 - growthStages: 3 - idealHeat: 298 + growthComponents: + plant: + lifespan: 60 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + growthStages: 3 + harvest: + harvestRepeat: Repeat + atmosphericGrowth: + idealHeat: 298 chemicals: Sugar: - Min: 4 - Max: 5 - PotencyDivisor: 5 + min: 4 + max: 5 + potencyDivisor: 5 - type: seed id: teaPlant @@ -435,21 +474,25 @@ packetPrototype: TeaPlantSeeds productPrototypes: - LeavesTea - harvestRepeat: Repeat - lifespan: 75 - maturation: 5 - production: 3 - yield: 2 - potency: 20 - growthStages: 5 - waterConsumption: 0.6 - idealLight: 9 - idealHeat: 298 + growthComponents: + plant: + lifespan: 75 + maturation: 5 + production: 3 + yield: 2 + potency: 20 + growthStages: 5 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.6 + atmosphericGrowth: + idealHeat: 298 chemicals: Vitamin: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 - type: seed id: papercane @@ -460,14 +503,18 @@ packetPrototype: PapercaneSeeds productPrototypes: - Papercane - harvestRepeat: Repeat - lifespan: 60 - maturation: 6 - production: 6 - yield: 3 - potency: 10 - growthStages: 3 - idealHeat: 298 + growthComponents: + plant: + lifespan: 60 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + growthStages: 3 + harvest: + harvestRepeat: Repeat + atmosphericGrowth: + idealHeat: 298 - type: seed id: towercap @@ -480,17 +527,21 @@ - Log mutationPrototypes: - steelcap - lifespan: 80 - maturation: 15 - ligneous: true - production: 3 - yield: 5 - potency: 1 - growthStages: 3 - waterConsumption: 0.60 - nutrientConsumption: 0.50 - lightTolerance: 6 - idealHeat: 288 + growthComponents: + plant: + lifespan: 80 + maturation: 15 + production: 3 + yield: 5 + potency: 1 + growthStages: 3 + plantTraits: + ligneous: true + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.5 + atmosphericGrowth: + idealHeat: 288 - type: seed id: steelcap @@ -501,17 +552,21 @@ packetPrototype: SteelcapSeeds productPrototypes: - SteelLog - lifespan: 80 - maturation: 15 - ligneous: true - production: 3 - yield: 3 - potency: 1 - growthStages: 3 - waterConsumption: 0.60 - nutrientConsumption: 0.80 - lightTolerance: 6 - idealHeat: 288 + growthComponents: + plant: + lifespan: 80 + maturation: 15 + production: 3 + yield: 3 + potency: 1 + growthStages: 3 + plantTraits: + ligneous: true + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.8 + atmosphericGrowth: + idealHeat: 288 - type: seed id: tomato @@ -525,30 +580,33 @@ mutationPrototypes: - blueTomato - bloodTomato - harvestRepeat: Repeat - lifespan: 25 - maturation: 8 - production: 6 - yield: 2 - potency: 10 - waterConsumption: 0.60 - nutrientConsumption: 0.40 - idealLight: 8 - idealHeat: 298 - splatPrototype: PuddleSplatter + growthComponents: + plant: + lifespan: 25 + maturation: 8 + production: 6 + yield: 2 + potency: 10 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.4 + atmosphericGrowth: + idealHeat: 298 chemicals: Nutriment: - Min: 1 - Max: 7 - PotencyDivisor: 14 + min: 1 + max: 7 + potencyDivisor: 14 Vitamin: - Min: 1 - Max: 3 - PotencyDivisor: 33 + min: 1 + max: 3 + potencyDivisor: 33 Water: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: blueTomato @@ -559,30 +617,33 @@ packetPrototype: BlueTomatoSeeds productPrototypes: - FoodBlueTomato - harvestRepeat: Repeat - lifespan: 25 - maturation: 8 - production: 6 - yield: 2 - potency: 10 - waterConsumption: 0.60 - nutrientConsumption: 0.70 - idealLight: 8 - idealHeat: 298 - splatPrototype: PuddleSplatter + growthComponents: + plant: + lifespan: 25 + maturation: 8 + production: 6 + yield: 2 + potency: 10 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.7 + atmosphericGrowth: + idealHeat: 298 chemicals: Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 SpaceLube: - Min: 5 - Max: 15 - PotencyDivisor: 10 + min: 5 + max: 15 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: bloodTomato @@ -595,26 +656,29 @@ - FoodBloodTomato mutationPrototypes: - killerTomato - harvestRepeat: Repeat - lifespan: 60 - maturation: 8 - production: 6 - yield: 2 - potency: 10 - waterConsumption: 0.60 - nutrientConsumption: 0.70 - idealLight: 8 - idealHeat: 298 - splatPrototype: PuddleSplatter + growthComponents: + plant: + lifespan: 60 + maturation: 8 + production: 6 + yield: 2 + potency: 10 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.7 + atmosphericGrowth: + idealHeat: 298 chemicals: Blood: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: killerTomato @@ -627,27 +691,30 @@ packetPrototype: KillerTomatoSeeds productPrototypes: - MobTomatoKiller - harvestRepeat: Repeat - lifespan: 25 - maturation: 15 - production: 6 - yield: 2 - potency: 10 - waterConsumption: 0.60 - nutrientConsumption: 0.70 - idealLight: 8 - idealHeat: 298 - growthStages: 2 - splatPrototype: PuddleSplatter + growthComponents: + plant: + lifespan: 25 + maturation: 15 + production: 6 + yield: 2 + potency: 10 + growthStages: 2 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.7 + atmosphericGrowth: + idealHeat: 298 chemicals: Blood: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 JuiceTomato: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: eggplant @@ -660,24 +727,27 @@ - FoodEggplant mutationPrototypes: - eggy - harvestRepeat: Repeat - lifespan: 25 - maturation: 6 - production: 6 - yield: 2 - potency: 20 - growthStages: 3 - idealLight: 9 - idealHeat: 298 + growthComponents: + plant: + lifespan: 25 + maturation: 6 + production: 6 + yield: 2 + potency: 20 + growthStages: 3 + harvest: + harvestRepeat: Repeat + atmosphericGrowth: + idealHeat: 298 chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: cabbage @@ -688,21 +758,23 @@ packetPrototype: CabbageSeeds productPrototypes: - FoodCabbage - lifespan: 50 - maturation: 7 - production: 5 - yield: 3 - potency: 10 - growthStages: 1 + growthComponents: + plant: + lifespan: 50 + maturation: 7 + production: 5 + yield: 3 + potency: 10 + growthStages: 1 chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: garlic @@ -713,25 +785,27 @@ packetPrototype: GarlicSeeds productPrototypes: - FoodGarlic - lifespan: 25 - maturation: 8 - production: 5 - yield: 3 - potency: 25 - growthStages: 3 + growthComponents: + plant: + lifespan: 25 + maturation: 8 + production: 5 + yield: 3 + potency: 25 + growthStages: 3 chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 Allicin: - Min: 1 - Max: 8 - PotencyDivisor: 25 + min: 1 + max: 8 + potencyDivisor: 25 - type: seed id: apple @@ -744,22 +818,24 @@ - FoodApple mutationPrototypes: - goldenApple - harvestRepeat: Repeat - lifespan: 55 - maturation: 6 - production: 6 - yield: 3 - potency: 10 - idealLight: 6 + growthComponents: + plant: + lifespan: 55 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + harvest: + harvestRepeat: Repeat chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: goldenApple @@ -770,28 +846,30 @@ packetPrototype: GoldenAppleSeeds productPrototypes: - FoodGoldenApple - harvestRepeat: Repeat - lifespan: 55 - maturation: 6 - production: 6 - yield: 3 - potency: 10 - idealLight: 6 - waterConsumption: 0.75 - nutrientConsumption: 0.75 + growthComponents: + plant: + lifespan: 55 + maturation: 6 + production: 6 + yield: 3 + potency: 10 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.75 chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 DoctorsDelight: - Min: 3 - Max: 13 - PotencyDivisor: 10 + min: 3 + max: 13 + potencyDivisor: 10 - type: seed id: corn @@ -802,24 +880,27 @@ packetPrototype: CornSeeds productPrototypes: - FoodCorn - lifespan: 25 - maturation: 8 - production: 6 - yield: 2 - potency: 20 - growthStages: 3 - idealLight: 8 - waterConsumption: 0.60 - idealHeat: 298 + growthComponents: + plant: + lifespan: 25 + maturation: 8 + production: 6 + yield: 2 + potency: 20 + growthStages: 3 + basicGrowth: + waterConsumption: 0.6 + atmosphericGrowth: + idealHeat: 298 chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 20 + min: 1 + max: 10 + potencyDivisor: 20 Cornmeal: - Min: 5 - Max: 15 - PotencyDivisor: 10 + min: 5 + max: 15 + potencyDivisor: 10 - type: seed id: onion @@ -833,28 +914,31 @@ mutationPrototypes: - onionred - bloonion - lifespan: 25 - maturation: 8 - production: 6 - yield: 2 - potency: 20 - growthStages: 3 - idealLight: 8 - waterConsumption: 0.60 - idealHeat: 298 + growthComponents: + plant: + lifespan: 25 + maturation: 8 + production: 6 + yield: 2 + potency: 20 + growthStages: 3 + basicGrowth: + waterConsumption: 0.6 + atmosphericGrowth: + idealHeat: 298 chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 Allicin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: onionred @@ -865,28 +949,31 @@ packetPrototype: OnionRedSeeds productPrototypes: - FoodOnionRed - lifespan: 25 - maturation: 8 - production: 6 - yield: 2 - potency: 20 - growthStages: 3 - idealLight: 8 - waterConsumption: 0.60 - idealHeat: 298 + growthComponents: + plant: + lifespan: 25 + maturation: 8 + production: 6 + yield: 2 + potency: 20 + growthStages: 3 + basicGrowth: + waterConsumption: 0.6 + atmosphericGrowth: + idealHeat: 298 chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 Allicin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: chanterelle @@ -897,21 +984,24 @@ packetPrototype: ChanterelleSeeds productPrototypes: - FoodMushroom - lifespan: 35 - maturation: 10 - production: 7 - yield: 5 - potency: 1 - growthStages: 3 - lightTolerance: 6 - waterConsumption: 0.60 - nutrientConsumption: 0.50 - idealHeat: 288 + growthComponents: + plant: + lifespan: 35 + maturation: 10 + production: 7 + yield: 5 + potency: 1 + growthStages: 3 + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.5 + atmosphericGrowth: + idealHeat: 288 chemicals: Nutriment: - Min: 1 - Max: 25 - PotencyDivisor: 25 + min: 1 + max: 25 + potencyDivisor: 25 - type: seed id: eggy @@ -922,20 +1012,24 @@ packetPrototype: EggySeeds productPrototypes: - FoodEgg - harvestRepeat: Repeat - lifespan: 75 - maturation: 6 - production: 12 - yield: 2 - potency: 20 - nutrientConsumption: 0.50 - idealLight: 9 - idealHeat: 298 + growthComponents: + plant: + lifespan: 75 + maturation: 6 + production: 12 + yield: 2 + potency: 20 + harvest: + harvestRepeat: Repeat + basicGrowth: + nutrientConsumption: 0.5 + atmosphericGrowth: + idealHeat: 298 chemicals: Egg: - Min: 4 - Max: 12 - PotencyDivisor: 10 + min: 4 + max: 12 + potencyDivisor: 10 - type: seed id: cannabis @@ -948,21 +1042,25 @@ - LeavesCannabis mutationPrototypes: - rainbowCannabis - harvestRepeat: Repeat - lifespan: 75 - maturation: 8 - production: 12 - yield: 2 - potency: 20 - growthStages: 3 - waterConsumption: 0.40 - idealLight: 9 - idealHeat: 298 + growthComponents: + plant: + lifespan: 75 + maturation: 8 + production: 12 + yield: 2 + potency: 20 + growthStages: 3 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.4 + atmosphericGrowth: + idealHeat: 298 chemicals: THC: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 - type: seed id: rainbowCannabis @@ -973,41 +1071,45 @@ packetPrototype: RainbowCannabisSeeds productPrototypes: - LeavesCannabisRainbow - harvestRepeat: Repeat - lifespan: 75 - maturation: 8 - production: 12 - yield: 2 - potency: 20 - growthStages: 3 - waterConsumption: 0.40 - idealLight: 9 - idealHeat: 298 + growthComponents: + plant: + lifespan: 75 + maturation: 8 + production: 12 + yield: 2 + potency: 20 + growthStages: 3 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.4 + atmosphericGrowth: + idealHeat: 298 chemicals: SpaceDrugs: - Min: 1 - Max: 15 - PotencyDivisor: 10 + min: 1 + max: 15 + potencyDivisor: 10 Lipolicide: - Min: 1 - Max: 15 - PotencyDivisor: 10 + min: 1 + max: 15 + potencyDivisor: 10 MindbreakerToxin: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Happiness: - Min: 1 - Max: 5 -# PotencyDivisor: 20 + min: 1 + max: 5 +# potencyDivisor: 20 # ColorfulReagent: -# Min: 0 -# Max: 5 -# PotencyDivisor: 20 +# min: 0 +# max: 5 +# potencyDivisor: 20 Psicodine: - Min: 0 - Max: 5 - PotencyDivisor: 33 + min: 0 + max: 5 + potencyDivisor: 33 - type: seed id: tobacco @@ -1018,21 +1120,25 @@ packetPrototype: TobaccoSeeds productPrototypes: - LeavesTobacco - harvestRepeat: Repeat - lifespan: 75 - maturation: 5 - production: 5 - yield: 2 - potency: 20 - growthStages: 3 - waterConsumption: 0.40 - idealLight: 9 - idealHeat: 298 + growthComponents: + plant: + lifespan: 75 + maturation: 5 + production: 5 + yield: 2 + potency: 20 + growthStages: 3 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.4 + atmosphericGrowth: + idealHeat: 298 chemicals: Nicotine: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 - type: seed id: nettle @@ -1045,20 +1151,23 @@ - Nettle mutationPrototypes: - deathNettle - lifespan: 25 - maturation: 8 - production: 6 - yield: 2 - potency: 20 - growthStages: 5 - idealLight: 8 - waterConsumption: 0.60 - idealHeat: 298 + growthComponents: + plant: + lifespan: 25 + maturation: 8 + production: 6 + yield: 2 + potency: 20 + growthStages: 5 + basicGrowth: + waterConsumption: 0.6 + atmosphericGrowth: + idealHeat: 298 chemicals: Histamine: - Min: 1 - Max: 25 - PotencyDivisor: 4 + min: 1 + max: 25 + potencyDivisor: 4 - type: seed id: deathNettle @@ -1071,25 +1180,28 @@ harvestLogImpact: High productPrototypes: - DeathNettle - lifespan: 25 - maturation: 8 - production: 6 - yield: 2 - potency: 20 - growthStages: 5 - idealLight: 8 - waterConsumption: 0.70 - nutrientConsumption: 0.80 - idealHeat: 298 + growthComponents: + plant: + lifespan: 25 + maturation: 8 + production: 6 + yield: 2 + potency: 20 + growthStages: 5 + basicGrowth: + waterConsumption: 0.7 + nutrientConsumption: 0.8 + atmosphericGrowth: + idealHeat: 298 chemicals: SulfuricAcid: - Min: 1 - Max: 15 - PotencyDivisor: 6 + min: 1 + max: 15 + potencyDivisor: 6 FluorosulfuricAcid: - Min: 1 - Max: 15 - PotencyDivisor: 6 + min: 1 + max: 15 + potencyDivisor: 6 - type: seed id: chili @@ -1102,27 +1214,30 @@ - FoodChiliPepper mutationPrototypes: - chilly - harvestRepeat: Repeat - lifespan: 25 - maturation: 6 - production: 6 - yield: 2 - potency: 20 - idealLight: 9 - idealHeat: 298 + growthComponents: + plant: + lifespan: 25 + maturation: 6 + production: 6 + yield: 2 + potency: 20 + harvest: + harvestRepeat: Repeat + atmosphericGrowth: + idealHeat: 298 chemicals: CapsaicinOil: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Nutriment: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: chilly @@ -1133,27 +1248,30 @@ packetPrototype: ChillySeeds productPrototypes: - FoodChillyPepper - harvestRepeat: Repeat - lifespan: 25 - maturation: 6 - production: 6 - yield: 2 - potency: 20 - idealLight: 9 - idealHeat: 298 + growthComponents: + plant: + lifespan: 25 + maturation: 6 + production: 6 + yield: 2 + potency: 20 + harvest: + harvestRepeat: Repeat + atmosphericGrowth: + idealHeat: 298 chemicals: FrostOil: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Nutriment: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: poppy @@ -1166,22 +1284,25 @@ - FoodPoppy mutationPrototypes: - lily - lifespan: 25 - maturation: 10 - production: 3 - yield: 3 - potency: 10 - growthStages: 3 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 25 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 3 + basicGrowth: + waterConsumption: 0.6 chemicals: Nutriment: - Min: 1 - Max: 2 - PotencyDivisor: 50 + min: 1 + max: 2 + potencyDivisor: 50 Bicaridine: - Min: 1 - Max: 20 - PotencyDivisor: 5 + min: 1 + max: 20 + potencyDivisor: 5 - type: seed id: aloe @@ -1192,22 +1313,25 @@ packetPrototype: AloeSeeds productPrototypes: - FoodAloe - lifespan: 25 - maturation: 10 - production: 3 - yield: 3 - potency: 10 - growthStages: 5 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 25 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 5 + basicGrowth: + waterConsumption: 0.6 chemicals: Aloe: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Dermaline: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 - type: seed id: lily @@ -1220,22 +1344,25 @@ - FoodLily mutationPrototypes: - spacemansTrumpet - lifespan: 25 - maturation: 10 - production: 3 - yield: 3 - potency: 10 - growthStages: 3 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 25 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 3 + basicGrowth: + waterConsumption: 0.6 chemicals: Nutriment: - Min: 1 - Max: 2 - PotencyDivisor: 50 + min: 1 + max: 2 + potencyDivisor: 50 Bicaridine: - Min: 1 - Max: 20 - PotencyDivisor: 5 + min: 1 + max: 20 + potencyDivisor: 5 - type: seed id: lingzhi @@ -1246,22 +1373,25 @@ packetPrototype: LingzhiSeeds productPrototypes: - FoodLingzhi - lifespan: 25 - maturation: 10 - production: 3 - yield: 3 - potency: 10 - growthStages: 3 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 25 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 3 + basicGrowth: + waterConsumption: 0.6 chemicals: Ultravasculine: - Min: 1 - Max: 20 - PotencyDivisor: 5 + min: 1 + max: 20 + potencyDivisor: 5 Epinephrine: - Min: 1 - Max: 20 - PotencyDivisor: 5 + min: 1 + max: 20 + potencyDivisor: 5 - type: seed id: ambrosiaVulgaris @@ -1274,30 +1404,33 @@ - FoodAmbrosiaVulgaris mutationPrototypes: - ambrosiaDeus - lifespan: 25 - maturation: 10 - production: 3 - yield: 3 - potency: 10 - growthStages: 6 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 25 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 6 + basicGrowth: + waterConsumption: 0.6 chemicals: Nutriment: - Min: 1 - Max: 2 - PotencyDivisor: 10 + min: 1 + max: 2 + potencyDivisor: 10 Bicaridine: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Kelotane: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Vitamin: - Min: 1 - Max: 2 - PotencyDivisor: 50 + min: 1 + max: 2 + potencyDivisor: 50 - type: seed id: ambrosiaDeus @@ -1308,26 +1441,29 @@ packetPrototype: AmbrosiaDeusSeeds productPrototypes: - FoodAmbrosiaDeus - lifespan: 25 - maturation: 10 - production: 3 - yield: 3 - potency: 10 - growthStages: 6 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 25 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 6 + basicGrowth: + waterConsumption: 0.6 chemicals: Nutriment: - Min: 1 - Max: 2 - PotencyDivisor: 10 + min: 1 + max: 2 + potencyDivisor: 10 Omnizine: # Don't kill me - Min: 1 - Max: 3 - PotencyDivisor: 35 + min: 1 + max: 3 + potencyDivisor: 35 SpaceDrugs: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 - type: seed id: galaxythistle @@ -1340,18 +1476,21 @@ - FoodGalaxythistle mutationPrototypes: - glasstle - lifespan: 25 - maturation: 10 - production: 3 - yield: 3 - potency: 10 - growthStages: 3 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 25 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 3 + basicGrowth: + waterConsumption: 0.6 chemicals: Stellibinin: - Min: 1 - Max: 25 - PotencyDivisor: 4 + min: 1 + max: 25 + potencyDivisor: 4 - type: seed id: glasstle @@ -1362,22 +1501,23 @@ packetPrototype: GlasstleSeeds productPrototypes: - FoodGlasstle - lifespan: 25 - maturation: 10 - production: 3 - yield: 3 - potency: 10 - growthStages: 3 - waterConsumption: 0.5 + growthComponents: + plant: + lifespan: 25 + maturation: 10 + production: 3 + yield: 3 + potency: 10 + growthStages: 3 chemicals: Razorium: - Min: 1 - Max: 25 - PotencyDivisor: 4 + min: 1 + max: 25 + potencyDivisor: 4 Desoxyephedrine: # meff!! - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 - type: seed id: flyAmanita @@ -1388,23 +1528,26 @@ packetPrototype: FlyAmanitaSeeds productPrototypes: - FoodFlyAmanita - lifespan: 25 - maturation: 12 - production: 3 - yield: 3 - potency: 10 - growthStages: 2 - waterConsumption: 0.60 - nutrientConsumption: 0.50 + growthComponents: + plant: + lifespan: 25 + maturation: 12 + production: 3 + yield: 3 + potency: 10 + growthStages: 2 + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.5 chemicals: Amatoxin: - Min: 1 - Max: 10 - PotencyDivisor: 12 + min: 1 + max: 10 + potencyDivisor: 12 Nutriment: ## yumby :) - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 - type: seed id: gatfruit @@ -1418,22 +1561,23 @@ mutationPrototypes: - fakeCapfruit - realCapfruit - lifespan: 65 - maturation: 25 - production: 25 - yield: 1 - potency: 10 - growthStages: 2 - idealLight: 6 + growthComponents: + plant: + lifespan: 65 + maturation: 25 + production: 25 + yield: 1 + potency: 10 + growthStages: 2 chemicals: Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Sulfur: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 - type: seed id: fakeCapfruit @@ -1444,22 +1588,23 @@ packetPrototype: FakeCapfruitSeeds productPrototypes: - FoodFakeCapfruit - lifespan: 65 - maturation: 25 - production: 25 - yield: 1 - potency: 10 - growthStages: 2 - idealLight: 6 + growthComponents: + plant: + lifespan: 65 + maturation: 25 + production: 25 + yield: 1 + potency: 10 + growthStages: 2 chemicals: Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Sulfur: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 - type: seed id: realCapfruit @@ -1470,22 +1615,23 @@ packetPrototype: RealCapfruitSeeds productPrototypes: - FoodRealCapfruit - lifespan: 65 - maturation: 25 - production: 25 - yield: 1 - potency: 10 - growthStages: 2 - idealLight: 6 + growthComponents: + plant: + lifespan: 65 + maturation: 25 + production: 25 + yield: 1 + potency: 10 + growthStages: 2 chemicals: Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Sulfur: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 - type: seed id: rice @@ -1496,24 +1642,26 @@ packetPrototype: RiceSeeds productPrototypes: - RiceBushel - lifespan: 25 - maturation: 6 - production: 3 - yield: 3 - potency: 5 - growthStages: 4 - idealLight: 5 - nutrientConsumption: 0.40 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 25 + maturation: 6 + production: 3 + yield: 3 + potency: 5 + growthStages: 4 + basicGrowth: + nutrientConsumption: 0.4 + waterConsumption: 0.6 chemicals: Nutriment: - Min: 1 - Max: 20 - PotencyDivisor: 20 + min: 1 + max: 20 + potencyDivisor: 20 Rice: - Min: 5 - Max: 20 - PotencyDivisor: 20 + min: 5 + max: 20 + potencyDivisor: 20 - type: seed id: soybeans @@ -1526,19 +1674,21 @@ - FoodSoybeans mutationPrototypes: - koibean - growthStages: 4 - lifespan: 25 - maturation: 6 - production: 6 - yield: 3 - potency: 5 - idealLight: 7 - nutrientConsumption: 0.40 + growthComponents: + plant: + growthStages: 4 + lifespan: 25 + maturation: 6 + production: 6 + yield: 3 + potency: 5 + basicGrowth: + nutrientConsumption: 0.4 chemicals: Nutriment: - Min: 1 - Max: 2 - PotencyDivisor: 50 + min: 1 + max: 2 + potencyDivisor: 50 - type: seed id: spacemansTrumpet @@ -1549,22 +1699,25 @@ packetPrototype: SpacemansTrumpetSeeds productPrototypes: - FoodSpacemansTrumpet - growthStages: 4 - lifespan: 20 - maturation: 14 - production: 3 - yield: 2 - potency: 10 - waterConsumption: 0.60 + growthComponents: + plant: + growthStages: 4 + lifespan: 20 + maturation: 14 + production: 3 + yield: 2 + potency: 10 + basicGrowth: + waterConsumption: 0.6 chemicals: Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 50 + min: 1 + max: 5 + potencyDivisor: 50 PolypyryliumOligomers: - Min: 1 - Max: 15 - PotencyDivisor: 5 + min: 1 + max: 15 + potencyDivisor: 5 - type: seed id: koibean @@ -1575,23 +1728,25 @@ packetPrototype: KoibeanSeeds productPrototypes: - FoodKoibean - growthStages: 4 - lifespan: 25 - maturation: 6 - production: 6 - yield: 3 - potency: 5 - idealLight: 7 - nutrientConsumption: 0.40 + growthComponents: + plant: + growthStages: 4 + lifespan: 25 + maturation: 6 + production: 6 + yield: 3 + potency: 5 + basicGrowth: + nutrientConsumption: 0.4 chemicals: Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 CarpoToxin: - Min: 1 - Max: 4 - PotencyDivisor: 30 + min: 1 + max: 4 + potencyDivisor: 30 - type: seed id: grape @@ -1602,21 +1757,23 @@ packetPrototype: GrapeSeeds productPrototypes: - FoodGrape - lifespan: 50 - maturation: 6 - production: 5 - yield: 3 - potency: 10 - growthStages: 2 + growthComponents: + plant: + lifespan: 50 + maturation: 6 + production: 5 + yield: 3 + potency: 10 + growthStages: 2 chemicals: Nutriment: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 - type: seed id: watermelon @@ -1629,25 +1786,26 @@ - FoodWatermelon mutationPrototypes: - holymelon - lifespan: 55 - maturation: 12 - production: 3 - yield: 1 - potency: 1 - idealLight: 8 + growthComponents: + plant: + lifespan: 55 + maturation: 12 + production: 3 + yield: 1 + potency: 1 chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Water: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 - type: seed id: holymelon @@ -1658,25 +1816,26 @@ packetPrototype: HolymelonSeeds productPrototypes: - FoodHolymelon - lifespan: 55 - maturation: 12 - production: 3 - yield: 1 - potency: 1 - idealLight: 8 + growthComponents: + plant: + lifespan: 55 + maturation: 12 + production: 3 + yield: 1 + potency: 1 chemicals: Nutriment: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Holywater: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10 Vitamin: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 - type: seed id: cocoa @@ -1687,24 +1846,28 @@ packetPrototype: CocoaSeeds productPrototypes: - FoodCocoaPod - harvestRepeat: Repeat - lifespan: 50 - maturation: 6 - production: 6 - yield: 6 - idealLight: 7 - waterConsumption: 1 - nutrientConsumption: 0.8 - idealHeat: 298 + growthComponents: + plant: + lifespan: 50 + maturation: 6 + production: 6 + yield: 6 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 1.0 + nutrientConsumption: 0.8 + atmosphericGrowth: + idealHeat: 298 chemicals: Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 25 + min: 1 + max: 4 + potencyDivisor: 25 Nutriment: - Min: 1 - Max: 2 - PotencyDivisor: 50 + min: 1 + max: 2 + potencyDivisor: 50 - type: seed id: berries @@ -1715,22 +1878,25 @@ packetPrototype: BerrySeeds productPrototypes: - FoodBerries - harvestRepeat: Repeat - lifespan: 50 - maturation: 6 - production: 6 - yield: 4 - idealLight: 7 - nutrientConsumption: 0.6 + growthComponents: + plant: + lifespan: 50 + maturation: 6 + production: 6 + yield: 4 + harvest: + harvestRepeat: Repeat + basicGrowth: + nutrientConsumption: 0.6 chemicals: Nutriment: - Min: 2 - Max: 5 - PotencyDivisor: 30 + min: 2 + max: 5 + potencyDivisor: 30 Vitamin: - Min: 1 - Max: 4 - PotencyDivisor: 40 + min: 1 + max: 4 + potencyDivisor: 40 - type: seed id: bungo @@ -1741,25 +1907,29 @@ packetPrototype: BungoSeeds productPrototypes: - FoodBungo - harvestRepeat: Repeat - lifespan: 50 - maturation: 8 - production: 6 - potency: 10 - yield: 3 - idealLight: 8 - idealHeat: 298 - growthStages: 4 - waterConsumption: 0.6 + growthComponents: + plant: + lifespan: 50 + maturation: 8 + production: 6 + potency: 10 + yield: 3 + growthStages: 4 + harvest: + harvestRepeat: Repeat + basicGrowth: + waterConsumption: 0.6 + atmosphericGrowth: + idealHeat: 298 chemicals: Nutriment: - Min: 5 - Max: 10 - PotencyDivisor: 20 + min: 5 + max: 10 + potencyDivisor: 20 Enzyme: - Min: 5 - Max: 10 - PotencyDivisor: 20 + min: 5 + max: 10 + potencyDivisor: 20 - type: seed id: pea @@ -1772,25 +1942,27 @@ - FoodPeaPod mutationPrototypes: - laughinPea - lifespan: 25 - growthStages: 3 - maturation: 8 - production: 6 - yield: 3 - potency: 25 - idealLight: 8 - harvestRepeat: Repeat - nutrientConsumption: 0.5 - waterConsumption: 0.5 + growthComponents: + plant: + lifespan: 25 + growthStages: 3 + maturation: 8 + production: 6 + yield: 3 + potency: 25 + harvest: + harvestRepeat: Repeat + basicGrowth: + nutrientConsumption: 0.5 chemicals: Nutriment: - Min: 1 - Max: 3 - PotencyDivisor: 33 + min: 1 + max: 3 + potencyDivisor: 33 Vitamin: - Min: 1 - Max: 2 - PotencyDivisor: 50 + min: 1 + max: 2 + potencyDivisor: 50 - type: seed id: worldPea @@ -1801,29 +1973,31 @@ packetPrototype: PeaSeeds productPrototypes: - FoodWorldPeas - lifespan: 25 - growthStages: 3 - maturation: 20 - production: 6 - yield: 3 - potency: 25 - idealLight: 8 - harvestRepeat: Repeat - nutrientConsumption: 0.5 - waterConsumption: 0.5 + growthComponents: + plant: + lifespan: 25 + growthStages: 3 + maturation: 20 + production: 6 + yield: 3 + potency: 25 + harvest: + harvestRepeat: Repeat + basicGrowth: + nutrientConsumption: 0.5 chemicals: Happiness: - Min: 1 - Max: 3 - PotencyDivisor: 25 + min: 1 + max: 3 + potencyDivisor: 25 Nutriment: - Min: 1 - Max: 3 - PotencyDivisor: 20 + min: 1 + max: 3 + potencyDivisor: 20 Pax: - Min: 1 - Max: 2 - PotencyDivisor: 50 + min: 1 + max: 2 + potencyDivisor: 50 - type: seed id: pumpkin @@ -1836,22 +2010,25 @@ - FoodPumpkin mutationPrototypes: - bluePumpkin - lifespan: 55 - maturation: 10 - production: 4 - yield: 2 - potency: 10 - idealHeat: 288 - growthStages: 3 + growthComponents: + plant: + lifespan: 55 + maturation: 10 + production: 4 + yield: 2 + potency: 10 + growthStages: 3 + atmosphericGrowth: + idealHeat: 288 chemicals: PumpkinFlesh: - Min: 1 - Max: 20 - PotencyDivisor: 5 + min: 1 + max: 20 + potencyDivisor: 5 Vitamin: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 - type: seed id: bluePumpkin @@ -1862,26 +2039,29 @@ packetPrototype: BluePumpkinSeeds productPrototypes: - FoodBluePumpkin - lifespan: 55 - maturation: 10 - production: 4 - yield: 2 - potency: 10 - idealHeat: 288 - growthStages: 3 + growthComponents: + plant: + lifespan: 55 + maturation: 10 + production: 4 + yield: 2 + potency: 10 + growthStages: 3 + atmosphericGrowth: + idealHeat: 288 chemicals: Ammonia: - Min: 1 - Max: 15 - PotencyDivisor: 3 + min: 1 + max: 15 + potencyDivisor: 3 Chlorine: - Min: 1 - Max: 5 - PotencyDivisor: 5 + min: 1 + max: 5 + potencyDivisor: 5 Vitamin: - Min: 1 - Max: 10 - PotencyDivisor: 3 + min: 1 + max: 10 + potencyDivisor: 3 - type: seed id: cotton @@ -1894,19 +2074,21 @@ - CottonBol mutationPrototypes: - pyrotton - lifespan: 25 - maturation: 8 - production: 3 - yield: 3 - potency: 5 - idealLight: 8 - growthStages: 3 - waterConsumption: 0.60 + growthComponents: + plant: + lifespan: 25 + maturation: 8 + production: 3 + yield: 3 + potency: 5 + growthStages: 3 + basicGrowth: + waterConsumption: 0.6 chemicals: Fiber: - Min: 5 - Max: 10 - PotencyDivisor: 20 + min: 5 + max: 10 + potencyDivisor: 20 - type: seed id: pyrotton @@ -1917,23 +2099,25 @@ packetPrototype: PyrottonSeeds productPrototypes: - PyrottonBol - lifespan: 25 - maturation: 8 - production: 3 - yield: 2 - potency: 5 - idealLight: 8 - growthStages: 3 - waterConsumption: 0.80 + growthComponents: + plant: + lifespan: 25 + maturation: 8 + production: 3 + yield: 2 + potency: 5 + growthStages: 3 + basicGrowth: + waterConsumption: 0.8 chemicals: Fiber: - Min: 5 - Max: 10 - PotencyDivisor: 20 + min: 5 + max: 10 + potencyDivisor: 20 Phlogiston: - Min: 4 - Max: 8 - PotencyDivisor: 30 + min: 4 + max: 8 + potencyDivisor: 30 - type: seed id: cherry @@ -1944,22 +2128,24 @@ packetPrototype: CherrySeeds productPrototypes: - FoodCherry - harvestRepeat: Repeat - lifespan: 55 - maturation: 6 - production: 6 - yield: 5 - potency: 10 - idealLight: 6 + growthComponents: + plant: + lifespan: 55 + maturation: 6 + production: 6 + yield: 5 + potency: 10 + harvest: + harvestRepeat: Repeat chemicals: Nutriment: - Min: 1 - Max: 3 - PotencyDivisor: 30 + min: 1 + max: 3 + potencyDivisor: 30 Vitamin: - Min: 1 - Max: 3 - PotencyDivisor: 40 + min: 1 + max: 3 + potencyDivisor: 40 - type: seed id: anomalyBerry @@ -1970,27 +2156,30 @@ packetPrototype: AnomalyBerrySeeds productPrototypes: - FoodAnomalyBerry - lifespan: 25 - maturation: 12 - production: 3 - yield: 3 - potency: 10 - growthStages: 2 - waterConsumption: 0.60 - nutrientConsumption: 0.50 + growthComponents: + plant: + lifespan: 25 + maturation: 12 + production: 3 + yield: 3 + potency: 10 + growthStages: 2 + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.5 chemicals: Artifexium: - Min: 1 - Max: 1 - PotencyDivisor: 4 + min: 1 + max: 1 + potencyDivisor: 4 Nutriment: - Min: 1 - Max: 2 - PotencyDivisor: 30 + min: 1 + max: 2 + potencyDivisor: 30 Vitamin: - Min: 1 - Max: 2 - PotencyDivisor: 40 + min: 1 + max: 2 + potencyDivisor: 40 - type: seed id: bloonion @@ -2001,28 +2190,31 @@ packetPrototype: BloonionSeeds productPrototypes: - FoodBloonion - lifespan: 25 - maturation: 15 - production: 3 - yield: 3 - potency: 10 - growthStages: 4 - waterConsumption: 0.60 - nutrientConsumption: 0.50 + growthComponents: + plant: + lifespan: 25 + maturation: 15 + production: 3 + yield: 3 + potency: 10 + growthStages: 4 + basicGrowth: + waterConsumption: 0.6 + nutrientConsumption: 0.5 chemicals: Potassium: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Phosphorus: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Sugar: - Min: 1 - Max: 5 - PotencyDivisor: 20 + min: 1 + max: 5 + potencyDivisor: 20 Allicin: - Min: 1 - Max: 10 - PotencyDivisor: 10 + min: 1 + max: 10 + potencyDivisor: 10